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]>
201 lines
5.9 KiB
Go
201 lines
5.9 KiB
Go
// Package tcc — Transmitter Control Code registry. Each issuer that
|
|
// files its own information returns has its own IRS-issued TCC; Lux
|
|
// may also act as agent under its own TCC. The registry resolves
|
|
// (issuer, env) -> TCC and tracks per-TCC scoping rules.
|
|
//
|
|
// Source-of-design: Public-Spec
|
|
// Source-ref: IRS Publication 5717 §4 — Transmitter Control Code
|
|
// Source-ref: IRS Publication 1220 Part B §2 — TCC Application Process
|
|
package tcc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// Env identifies the IRS environment a TCC is registered against.
|
|
// Sandboxes (IRIS AATS, FIRE Test) require their own TCC distinct
|
|
// from the production TCC.
|
|
type Env string
|
|
|
|
const (
|
|
// EnvProduction is the production TCC scope.
|
|
EnvProduction Env = "production"
|
|
|
|
// EnvSandbox is the sandbox TCC scope (IRIS AATS, FIRE Test).
|
|
EnvSandbox Env = "sandbox"
|
|
)
|
|
|
|
// System identifies the IRS e-file system the TCC is registered
|
|
// against — IRIS or FIRE. The same legal entity may hold separate
|
|
// IRIS and FIRE TCCs.
|
|
type System string
|
|
|
|
const (
|
|
// SystemIRIS is the IRIS A2A scope.
|
|
SystemIRIS System = "iris"
|
|
|
|
// SystemFIRE is the FIRE scope.
|
|
SystemFIRE System = "fire"
|
|
)
|
|
|
|
// Agency identifies whether the TCC is used by the issuer directly or
|
|
// by a third-party transmitter acting on the issuer's behalf.
|
|
type Agency string
|
|
|
|
const (
|
|
// AgencyIssuer = the TCC belongs to the issuer; the issuer
|
|
// transmits its own returns.
|
|
AgencyIssuer Agency = "issuer"
|
|
|
|
// AgencyTransmitter = the TCC belongs to a third-party
|
|
// transmitter (e.g., Lux Industries Inc. transmitting on behalf
|
|
// of a captable customer).
|
|
AgencyTransmitter Agency = "transmitter"
|
|
)
|
|
|
|
// Entry is one registry row — a single (issuer, system, env) -> TCC
|
|
// mapping.
|
|
type Entry struct {
|
|
// IssuerID is the captable-level issuer identifier (the company
|
|
// id under which returns are filed).
|
|
IssuerID string
|
|
|
|
// System is the e-file system the TCC is registered against.
|
|
System System
|
|
|
|
// Env is the IRS environment the TCC is registered for.
|
|
Env Env
|
|
|
|
// TCC is the IRS-assigned 5-character Transmitter Control Code.
|
|
// Loaded from KMS, never logged.
|
|
TCC string
|
|
|
|
// EIN is the EIN bound to the TCC on the IRIS / FIRE
|
|
// Application. The IRS rejects submissions where the
|
|
// transmitter EIN on the wire does not match the EIN of record
|
|
// for the TCC.
|
|
EIN string
|
|
|
|
// Agency identifies whether this TCC is the issuer's own or a
|
|
// transmitter's.
|
|
Agency Agency
|
|
|
|
// TransmitterEntityID identifies the transmitter entity when
|
|
// Agency == AgencyTransmitter (e.g., "lux-industries-inc").
|
|
// Empty for AgencyIssuer.
|
|
TransmitterEntityID string
|
|
}
|
|
|
|
// Registry is the in-memory TCC registry. The production registry is
|
|
// backed by KMS-encrypted storage outside the captable library; this
|
|
// in-memory implementation is the canonical contract that consumers
|
|
// (broker/treasury) instantiate.
|
|
type Registry struct {
|
|
mu sync.RWMutex
|
|
entries map[registryKey]*Entry
|
|
}
|
|
|
|
// registryKey scopes a registry lookup to (issuer, system, env).
|
|
type registryKey struct {
|
|
IssuerID string
|
|
System System
|
|
Env Env
|
|
}
|
|
|
|
// NewRegistry constructs an empty Registry. Callers Register
|
|
// entries from KMS at process boot.
|
|
func NewRegistry() *Registry {
|
|
return &Registry{entries: make(map[registryKey]*Entry)}
|
|
}
|
|
|
|
// Errors surfaced by the registry.
|
|
var (
|
|
// ErrNotRegistered is returned by Resolve when no TCC is registered
|
|
// for the (issuer, system, env) tuple.
|
|
ErrNotRegistered = errors.New("tcc: no TCC registered for the (issuer, system, env) tuple")
|
|
|
|
// ErrInvalidEntry is returned by Register on validation failure.
|
|
ErrInvalidEntry = errors.New("tcc: invalid entry")
|
|
)
|
|
|
|
// Register stores a new (issuer, system, env) -> TCC entry. Returns
|
|
// ErrInvalidEntry on validation failure.
|
|
func (r *Registry) Register(e *Entry) error {
|
|
if err := validateEntry(e); err != nil {
|
|
return fmt.Errorf("%w: %v", ErrInvalidEntry, err)
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.entries[registryKey{e.IssuerID, e.System, e.Env}] = e
|
|
return nil
|
|
}
|
|
|
|
// Resolve returns the TCC entry for the (issuer, system, env) tuple.
|
|
// Returns ErrNotRegistered if no entry is present.
|
|
func (r *Registry) Resolve(issuerID string, system System, env Env) (*Entry, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
e, ok := r.entries[registryKey{issuerID, system, env}]
|
|
if !ok {
|
|
return nil, ErrNotRegistered
|
|
}
|
|
cp := *e
|
|
return &cp, nil
|
|
}
|
|
|
|
// List returns a copy of all registered entries (no particular order).
|
|
func (r *Registry) List() []*Entry {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]*Entry, 0, len(r.entries))
|
|
for _, e := range r.entries {
|
|
cp := *e
|
|
out = append(out, &cp)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Unregister removes the (issuer, system, env) entry. Returns
|
|
// ErrNotRegistered if no entry was present.
|
|
func (r *Registry) Unregister(issuerID string, system System, env Env) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
key := registryKey{issuerID, system, env}
|
|
if _, ok := r.entries[key]; !ok {
|
|
return ErrNotRegistered
|
|
}
|
|
delete(r.entries, key)
|
|
return nil
|
|
}
|
|
|
|
// validateEntry runs field-level validation on a registry entry.
|
|
func validateEntry(e *Entry) error {
|
|
if e == nil {
|
|
return fmt.Errorf("entry is nil")
|
|
}
|
|
if e.IssuerID == "" {
|
|
return fmt.Errorf("issuer_id is required")
|
|
}
|
|
if e.System != SystemIRIS && e.System != SystemFIRE {
|
|
return fmt.Errorf("system must be one of iris/fire, got %q", e.System)
|
|
}
|
|
if e.Env != EnvProduction && e.Env != EnvSandbox {
|
|
return fmt.Errorf("env must be one of production/sandbox, got %q", e.Env)
|
|
}
|
|
if len(e.TCC) != 5 {
|
|
return fmt.Errorf("tcc must be 5 chars, got %d", len(e.TCC))
|
|
}
|
|
if len(e.EIN) != 9 {
|
|
return fmt.Errorf("ein must be 9 digits, got %d", len(e.EIN))
|
|
}
|
|
if e.Agency != AgencyIssuer && e.Agency != AgencyTransmitter {
|
|
return fmt.Errorf("agency must be one of issuer/transmitter, got %q", e.Agency)
|
|
}
|
|
if e.Agency == AgencyTransmitter && e.TransmitterEntityID == "" {
|
|
return fmt.Errorf("transmitter_entity_id is required when agency=transmitter")
|
|
}
|
|
return nil
|
|
}
|