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-DEDUPAIS 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'sentry_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, untestedsubsetMatches, this one is extracted and covered, per thededupSpanEntriesprecedent). persist-confirmed-document.tsgains 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, andpersist-confirmed-document.ts's existingreconcileProfessionalReceiptsAgainstRemittances.
Every project of mine is written down like this.
Read the résumé