Files
zeekay e7471084cb 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

139 lines
4.7 KiB
Go

package tcc
import (
"errors"
"testing"
)
func TestRegister_HappyPath(t *testing.T) {
r := NewRegistry()
err := r.Register(&Entry{
IssuerID: "acme",
System: SystemIRIS,
Env: EnvProduction,
TCC: "ABC12",
EIN: "987654321",
Agency: AgencyIssuer,
})
if err != nil {
t.Fatalf("Register: %v", err)
}
got, err := r.Resolve("acme", SystemIRIS, EnvProduction)
if err != nil {
t.Fatalf("Resolve: %v", err)
}
if got.TCC != "ABC12" {
t.Fatalf("TCC = %q, want ABC12", got.TCC)
}
}
func TestResolve_NotRegistered(t *testing.T) {
r := NewRegistry()
_, err := r.Resolve("ghost", SystemIRIS, EnvProduction)
if !errors.Is(err, ErrNotRegistered) {
t.Fatalf("err = %v, want ErrNotRegistered", err)
}
}
func TestRegister_Scoping(t *testing.T) {
r := NewRegistry()
// Same issuer with separate sandbox + prod TCCs (different IRS-
// assigned codes).
must := func(e *Entry) {
t.Helper()
if err := r.Register(e); err != nil {
t.Fatalf("Register: %v", err)
}
}
must(&Entry{IssuerID: "acme", System: SystemIRIS, Env: EnvSandbox, TCC: "SBX01", EIN: "987654321", Agency: AgencyIssuer})
must(&Entry{IssuerID: "acme", System: SystemIRIS, Env: EnvProduction, TCC: "PRD01", EIN: "987654321", Agency: AgencyIssuer})
must(&Entry{IssuerID: "acme", System: SystemFIRE, Env: EnvProduction, TCC: "FRE01", EIN: "987654321", Agency: AgencyIssuer})
resolveExpect := func(system System, env Env, want string) {
t.Helper()
got, err := r.Resolve("acme", system, env)
if err != nil {
t.Fatalf("Resolve(%s,%s): %v", system, env, err)
}
if got.TCC != want {
t.Fatalf("TCC = %q, want %q", got.TCC, want)
}
}
resolveExpect(SystemIRIS, EnvSandbox, "SBX01")
resolveExpect(SystemIRIS, EnvProduction, "PRD01")
resolveExpect(SystemFIRE, EnvProduction, "FRE01")
}
func TestRegister_TransmitterAgency(t *testing.T) {
r := NewRegistry()
err := r.Register(&Entry{
IssuerID: "small-co",
System: SystemIRIS,
Env: EnvProduction,
TCC: "LUX01",
EIN: "200000001",
Agency: AgencyTransmitter,
TransmitterEntityID: "lux-industries-inc",
})
if err != nil {
t.Fatalf("Register: %v", err)
}
got, _ := r.Resolve("small-co", SystemIRIS, EnvProduction)
if got.Agency != AgencyTransmitter {
t.Fatalf("Agency = %q, want transmitter", got.Agency)
}
if got.TransmitterEntityID != "lux-industries-inc" {
t.Fatalf("TransmitterEntityID = %q", got.TransmitterEntityID)
}
}
func TestRegister_ValidationFailures(t *testing.T) {
r := NewRegistry()
cases := []struct {
name string
e *Entry
want string
}{
{"missing issuer", &Entry{System: SystemIRIS, Env: EnvProduction, TCC: "ABC12", EIN: "123456789", Agency: AgencyIssuer}, "issuer_id"},
{"bad system", &Entry{IssuerID: "x", System: System("xx"), Env: EnvProduction, TCC: "ABC12", EIN: "123456789", Agency: AgencyIssuer}, "system"},
{"bad env", &Entry{IssuerID: "x", System: SystemIRIS, Env: Env("zz"), TCC: "ABC12", EIN: "123456789", Agency: AgencyIssuer}, "env"},
{"short tcc", &Entry{IssuerID: "x", System: SystemIRIS, Env: EnvProduction, TCC: "AB", EIN: "123456789", Agency: AgencyIssuer}, "tcc"},
{"short ein", &Entry{IssuerID: "x", System: SystemIRIS, Env: EnvProduction, TCC: "ABC12", EIN: "12", Agency: AgencyIssuer}, "ein"},
{"bad agency", &Entry{IssuerID: "x", System: SystemIRIS, Env: EnvProduction, TCC: "ABC12", EIN: "123456789", Agency: Agency("partner")}, "agency"},
{"transmitter without entity id", &Entry{IssuerID: "x", System: SystemIRIS, Env: EnvProduction, TCC: "ABC12", EIN: "123456789", Agency: AgencyTransmitter}, "transmitter_entity_id"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := r.Register(tc.e)
if err == nil {
t.Fatalf("expected error containing %q", tc.want)
}
if !errors.Is(err, ErrInvalidEntry) {
t.Fatalf("err = %v, want ErrInvalidEntry", err)
}
})
}
}
func TestUnregister(t *testing.T) {
r := NewRegistry()
_ = r.Register(&Entry{IssuerID: "x", System: SystemIRIS, Env: EnvProduction, TCC: "ABC12", EIN: "987654321", Agency: AgencyIssuer})
if err := r.Unregister("x", SystemIRIS, EnvProduction); err != nil {
t.Fatalf("Unregister: %v", err)
}
if err := r.Unregister("x", SystemIRIS, EnvProduction); !errors.Is(err, ErrNotRegistered) {
t.Fatalf("err = %v, want ErrNotRegistered", err)
}
}
func TestList(t *testing.T) {
r := NewRegistry()
_ = r.Register(&Entry{IssuerID: "a", System: SystemIRIS, Env: EnvProduction, TCC: "AAA01", EIN: "111111111", Agency: AgencyIssuer})
_ = r.Register(&Entry{IssuerID: "b", System: SystemFIRE, Env: EnvProduction, TCC: "BBB01", EIN: "222222222", Agency: AgencyIssuer})
got := r.List()
if len(got) != 2 {
t.Fatalf("len(List()) = %d, want 2", len(got))
}
}