Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-08-10-ais-vs-evidence-doc-income-dedup

Decisioncanonicalverified 2026-08-10

DECISION.2026-08-10.AIS-VS-EVIDENCE-DOC-INCOME-DEDUP

AIS vs bank-interest-cert / broker-dividend-log / mf-cas — closing the residual ~₹27k/year income double-count

Decision

2026-08-06-expense-portfolio-double-counting-round-3 fixed the Torc/Randstad professional-receipt-vs-remittance double count and left one gap open under "Found, NOT fixed": AIS (the tax department's own aggregate of interest/dividend income) can double-count against bank-interest-cert, broker-dividend-log, and mf-cas when a user uploads both AIS and the more precise source document for the same real income. broker-dividend-log.ts's own code comment already flagged this as a known V1 gap. Closed it with a second cross-document reconciliation pass, reconcileAisAgainstEvidenceDocuments, added to persist-confirmed-document.ts alongside the existing reconcileProfessionalReceiptsAgainstRemittances — both run on every confirm, in either upload order.

Why this is simpler than the Torc fix

The Torc fix needed a date window because the two mappers (professional-receipt.ts, foreign-remittance.ts) emit different entry_types for the same event. Here, bank-interest-cert.ts and mf-cas.ts already say, in their own comments, that they deliberately reuse the same entry_type AIS's classify() would produce for the same income ("Merge into the same bank-interest bucket AIS uses"; "Route through the same AIS-derived MF dividend bucket") — so matching is "same entry_type family, same AY", no date window (none of these four mappers stamp txn_date; they're annual-total envelope rows).

One wrinkle: AIS's classify() splits bank interest into info_code_savings_interest vs info_code_fd_interest by description text, but bank-interest-cert.ts always emits info_code_savings_interest regardless of the real account type. Both AIS interest buckets are folded into one "bank_interest" family so a real duplicate isn't missed just because AIS classified it as FD.

Matching + tolerance

Pure matcher: findAisIncomeDuplicates in apps/functions/src/_lib/reconcile-income-sources.ts. For each AIS row (target), subset-sum over same-AY, same-family evidence rows (bank-interest-cert / broker-dividend-log / mf-cas), bounded to 16 candidates. Tolerance is 1%, tighter than Torc's 3% — Torc's remittances are multi-leg wire transfers that lose a small amount to bank/forex charges, whereas AIS and the certificate/broker/AMC statement are both annual roll-ups reported to the rupee and should agree almost exactly; 1% absorbs rounding drift without masking genuinely distinct income. On a match, the AIS row is demoted to entry_class: "other" — AIS is a tax-department aggregation prone to lag/rounding; the certificate/broker/AMC statement is the primary source and is kept. TDS entry_types (info_code_tds_194a) and capital-gains entry_types are explicitly out of scope — this fix targets the documented income double-count only, not tax-credit reconciliation.

The wrapper (reconcileAisAgainstEvidenceDocuments in persist-confirmed-document.ts) follows the same shape as reconcileProfessionalReceiptsAgainstRemittances: query all ledger rows for the AY, hand rows to the pure matcher, batch-demote matched existing rows, and map demotions onto the current confirm's own new rows.

Verified

  • apps/functions/src/__tests__/reconcile-income-sources.test.ts — 11 new unit tests on the pure matcher: exact match, AIS-FD-vs-cert-savings family fold, broker-dividend match, mf-cas match with no cross-family bleed, multi-document subset-sum match, tolerance boundary (in/out of band), distinct multi-bank income left alone, TDS rows ignored, already-demoted rows skipped, and empty-input no-ops.
  • yarn workspace chitragupt-functions typecheck — clean.
  • yarn workspace chitragupt-functions lint — 0 errors, same pre-existing warning set as before this change.
  • yarn workspace chitragupt-functions test — 557 passed (546 pre-existing baseline + 11 new), 1 pre-existing skip, 0 failures.

Impact

  • pillar-tax — no canonical value changed; closes an implementation gap already documented against that pillar's already-canonical income definition.
  • income-summary.ts's entry_class === "other" exclude-everywhere handling (added in the round-3 fix) covers these demotions with no further change needed there.
  • New file: apps/functions/src/_lib/reconcile-income-sources.ts (pure matcher + types, unit-tested directly — unlike the Torc pass's inline, untested subsetMatches, this one is extracted and covered, per the dedupSpanEntries precedent).
  • persist-confirmed-document.ts gains a third cross-document reconciliation pass (span dedup, Torc reconcile, AIS reconcile) — all three run on every confirm, in either upload order.
  • Extends 2026-08-06-expense-portfolio-double-counting-round-3 (closes its "Found, NOT fixed" AIS gap); does not supersede it.

Status

Active.

Sources

  • This session, 2026-08-10. Direct reading of ledger-mappers/ais.ts, bank-interest-cert.ts, broker-dividend-log.ts, mf-cas.ts, and persist-confirmed-document.ts's existing reconcileProfessionalReceiptsAgainstRemittances.

Every project of mine is written down like this.

Read the résumé