Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-07-09-mapper-rollup-contract

Decisioncanonicalverified 2026-07-09

DECISION.2026-07-09.MAPPER-ROLLUP-CONTRACT

Decision

Fix the silent-orphan mapper contract between triggers/firestore/ledger-mappers/*.ts and packages/shared/src/rules/v1/income-summary.ts. Add a hard rule and a contract test:

Every entry_type a mapper emits must be either

  1. a key in ENTRY_TYPE_TO_BUCKET (routed to a real tax bucket), or
  2. on the AUDIT_ONLY_ENTRY_TYPES whitelist / an audit prefix.

Anything else is a build-time failure.

The mapper-contract Jest test drives every registered mapper with a fixture matching its schema, captures every emitted entry_type, and fails if any classifies as unknown. This closes the failure mode where a mapper's rename or a new form's field emits a value that silently vanishes into unmapped_paise at review time.

Why

The 2026-07-09 fresh audit found the mapper→consumer contract broken in 10 places:

  • bank-interest-cert emitted bank_interest_income + tds_bank_interest → both silent. Bank-interest-cert income and TDS never reached the tax review.
  • home-loan-interest-cert emitted section_24b_interest → the bucket table only knew section_24b_self_occupied / section_24b_let_out. §24(b) deduction was always zero.
  • mf-cas emitted dividend_income → silent. MF dividends dropped.
  • form-16a emitted tds_non_salary → silent. Non-salary TDS (§194J, §194H, §194I) dropped.
  • medical-insurance-premium emitted section_80d_preventive_checkup → silent. ₹5k preventive sub-cap not credited.
  • mf-cas mf_stcg_realised / mf_ltcg_realised → silent in the tax bucket table (they reached the CG rollup via a parallel path, but the tax review's own STCG / LTCG buckets missed them).
  • crypto-statement tds_194s_vda → silent. §194S crypto TDS dropped.
  • professional-receipt had no mapper at all → freelance income contributed zero to every review.

The audit-only side had its own problem: unmapped_paise was polluted by intentionally-audit entries (Form 12BA perquisites, TIS heads, per-transaction bank-statement rows, salary-slip monthlies that would double-count Form 16). That noise buried the true-orphan signal.

Impact

Ledger contract changes in packages/shared/src/rules/v1/income-summary.ts:

  • Bucket table extended: tds_non_salary, section_194ib_tds, mf_stcg_realised, mf_ltcg_realised, tds_194s_vda, professional_receipt_income.
  • New AUDIT_ONLY_ENTRY_TYPES set + AUDIT_ONLY_PREFIXES (form_12ba_, form_12bb_, tis_) — enumerates entry_types that are documentation rows, not tax-affecting.
  • summariseIncome now excludes audit-only rows from unmapped_paise, so unmapped_paise > 0 means a true silent orphan in the contract.
  • New export classifyEntryType(entryType) returns "bucket" | "audit_only" | "unknown" — used by the contract test.

Mapper edits (fixes to silent orphans):

  • bank-interest-cert → routes through info_code_savings_interest and info_code_tds_194a (same buckets AIS uses).
  • home-loan-interest-cert → emits section_24b_self_occupied as the V1 default. Comment notes a V2 property-type discriminator will route let-out interest.
  • mf-cas → emits info_code_dividend_mf for dividends.
  • form-16a → drops the non_salary_payment_gross emit entirely (would double-count vs 26AS/AIS). Keeps tds_non_salary as the single row.
  • medical-insurance-premium → routes preventive-checkup under the same section_80d_self / section_80d_parents entry_type as the base premium, so chapterVIABreakdown caps the combined amount at the §80D limit.
  • New professional-receipt mapper — emits base receipt amount as professional_receipt_income → other_income_paise. GST is subtracted when only the total is available.

New file: apps/functions/src/__tests__/mapper-contract.test.ts — one test per registered mapper, 29 total, all green.

Status

Active.

Sources

Every project of mine is written down like this.

Read the résumé