mirror of
https://github.com/luxfi/captable.git
synced 2026-10-30 17:01:02 +00:00
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]>
139 lines
4.7 KiB
Go
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))
|
|
}
|
|
}
|