Files
captable/pkg/tax/cfsf/router.go
zeekay fe3c061e4a feat(tax): G-15 IRS IRIS + FIRE + CFSF + TCC e-file adapters
G-15 — IRS FIRE + IRIS e-file adapter for 1099 series, with CFSF state
routing and per-issuer TCC management.

pkg/tax/iris — IRIS A2A adapter (mandatory for TY 2024+):
- Client with JWT bearer auth (OAuth2 password-grant exchange against
  IRS Identity), env switch (Production / AATS sandbox), per-call
  re-auth on 401, exponential-backoff + jitter retry with Retry-After
  honoring, X-IRS-TCC header on every submission
- SubmitForm / GetSubmissionStatus / ListSubmissions /
  CorrectSubmission cover all 8 form types: 1099-DIV / B / INT / MISC /
  NEC / OID / K / R
- Typed Form1099*Data per form covering every IRS-published box
- IRIS schema XML marshaling (header + per-payee Form1099* element with
  per-form payload element name; FormTypeCd discriminator; correction
  envelope with PaymentYearTypeCd=G and OriginalReceiptID)
- Validation: TCC format, EIN 9-digit, TIN 9-digit per payee, payee
  data type compatible with FormType, max 100,000 payees per
  submission (Pub 5717 §3.1), original_receipt_id required for
  corrections
- Acknowledgment + Status parsing for both XML and JSON wire formats

pkg/tax/fire — FIRE legacy adapter (pre-TY-2024 corrections):
- Client with multipart upload, env switch (Production / Test)
- Fixed-width 750-byte records per Publication 1220 Part C —
  T (Transmitter), A (Payer), B (Payee), C (End-of-A), F (End-of-File),
  K (State Totals) with positional layout: numeric right-justified
  zero-padded, alphanumeric left-justified blank-padded, dollar
  amounts in cents
- Per-payee amount codes mapped to positions 55..187 (12-position
  fields, codes '1'..'9' / 'A'..'I' = 18 codes total) and per-amount-
  code totals carried on the C and K records
- All 8 form codes: 1099-DIV (1), 1099-INT (6), 1099-B (B),
  1099-MISC (A), 1099-NEC (NE), 1099-OID (D), 1099-K (MC), 1099-R (9)
- CFSF state-totals block on B records (positions 663-748) and per-
  state K records with CombinedFedStateCode

pkg/tax/cfsf — Combined Federal/State Filing routing:
- StateProfile registry covering 32 CFSF participants + DC + WV with
  their IRS-assigned numeric codes (CA=06, NY=n/a, MA=25, etc.)
  plus 19 non-CFSF states with the per-state DOR portal references
  (PortalDORWebsite / PortalFSET / PortalNone for no-income-tax
  states)
- RouteToStates(submission, []State) -> []*StateFiling distinguishing
  cfsf (federal filing auto-routes), direct (operator hand-off to
  named portal), and none (no broad state income tax)
- IRISWrapper adapts *iris.FormSubmission to FormSubmissionForRouting
  by reading StateCd / StateTaxWithheld / StateIncome from each
  payee's typed Form1099*Data

pkg/tax/tcc — Transmitter Control Code registry:
- Per-(issuer, system, env) scoping: same issuer may hold separate
  IRIS / FIRE TCCs and separate Sandbox / Production TCCs
- Agency split: AgencyIssuer (issuer files own returns) vs
  AgencyTransmitter (Lux files as agent under transmitter TCC)
- Validation: TCC 5-char, EIN 9-digit, agency requires entity id when
  transmitter

pkg/tax — integration hook (submit.go):
- tax.SubmitForm + tax.SubmitForm1099B route by default to IRIS for
  TY >= 2024 (IRISMandatoryTaxYear per IRS final regs), fall back to
  FIRE only when ForceLegacyFIRE is set or TaxYear < 2024
- SubmissionOptions binds the chosen IRIS / FIRE clients plus a
  TCCRegistry for per-issuer TCC resolution
- Builders translate captable Form1099DIV / Form1099B into the
  transport's typed submission shape

Coverage: go build / vet / -race clean across the full module. 68
tests in the four new packages (16 IRIS happy paths × form types +
validation suite + correction round trip + 429 retry + re-auth + XML
ack parsing; 8 FIRE happy paths × form codes + record-layout tests +
CFSF K-record test + rejection path; 9 CFSF routing tests covering
the spec-required CA/MA/OH/OK/IN CFSF participants + NY/IL direct +
TX/FL no-tax; 9 TCC scoping + validation tests), all passing.

Source-of-design: Public-Spec
Source-ref: https://www.irs.gov/e-file-providers/iris-online-portal
Source-ref: IRS Publication 5717 (IRIS A2A Specifications)
Source-ref: IRS Publication 5718 (IRIS Electronic Filing Application Guide)
Source-ref: IRS Publication 1220 (FIRE Specifications)
Source-ref: https://www.irs.gov/pub/irs-pdf/p1220.pdf
Clean-Team-Member: dev-agent (P0 G-15 implementation)
Signed-off-by: dev <[email protected]>
2026-05-26 18:24:44 -07:00

91 lines
3.0 KiB
Go

// CFSF routing engine — given a federal 1099 submission, produce the
// per-state filing decisions (CFSF-satisfied / direct-file / no-tax
// state).
//
// Source-of-design: Public-Spec
// Source-ref: IRS Publication 1220 Part A §10
package cfsf
import (
"fmt"
"sort"
)
// RouteToStates produces the per-state filing decisions for a federal
// 1099 submission. Each requested state appears in the output:
//
// - CFSF participant: routing_type="cfsf", CFSFCode populated; the
// federal IRIS/FIRE filing carries the per-payee state slice and
// the IRS auto-routes to the state. No direct-file action needed.
//
// - Non-CFSF state with a DOR portal: routing_type="direct", Portal
// and PortalURL populated; the operator must hand-file (CSV upload
// or FSET) at the named portal.
//
// - Non-CFSF state with no broad state income tax (FL/TX/NV/etc.):
// routing_type="none"; no action.
//
// If the requested states slice is empty, the function infers states
// from the submission's StateBreakdown — any state that appears in the
// federal payee data is routed automatically.
func RouteToStates(fs FormSubmissionForRouting, states []State) ([]*StateFiling, error) {
if fs == nil {
return nil, fmt.Errorf("cfsf: submission is nil")
}
breakdown := fs.StateBreakdown()
if len(states) == 0 {
states = make([]State, 0, len(breakdown))
for s := range breakdown {
states = append(states, s)
}
sort.Slice(states, func(i, j int) bool { return states[i] < states[j] })
}
out := make([]*StateFiling, 0, len(states))
for _, s := range states {
filing := buildFiling(s, breakdown[s])
out = append(out, filing)
}
return out, nil
}
// buildFiling produces a single StateFiling from a state code + the
// per-payee state breakdown for that state.
func buildFiling(s State, amounts []StateAmount) *StateFiling {
prof, known := GetProfile(s)
filing := &StateFiling{
State: s,
PayeeCount: len(amounts),
}
for _, a := range amounts {
filing.TotalStateTaxWithheld += a.TaxWithheld
filing.TotalStateIncome += a.Income
}
if !known {
filing.RoutingType = "direct"
filing.Portal = PortalDORWebsite
filing.Notes = fmt.Sprintf("state %s not in CFSF registry — verify per-state portal manually", s)
return filing
}
switch {
case prof.ParticipatesInCFSF:
filing.RoutingType = "cfsf"
filing.CFSFCode = prof.CFSFCode
filing.Notes = fmt.Sprintf("federal filing auto-routes to %s under CFSF (state code %s)", s, prof.CFSFCode)
case prof.FallbackPortal == PortalNone:
filing.RoutingType = "none"
filing.Notes = prof.FallbackURL
default:
filing.RoutingType = "direct"
filing.Portal = prof.FallbackPortal
filing.PortalURL = prof.FallbackURL
if prof.FormThreshold > 0 && filing.PayeeCount < prof.FormThreshold {
filing.Notes = fmt.Sprintf("below %s e-file threshold (%d forms); paper filing permitted",
s, prof.FormThreshold)
} else {
filing.Notes = fmt.Sprintf("file directly at %s DOR portal (%s)", s, string(prof.FallbackPortal))
}
}
return filing
}