Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-07-09-phase-f-recompute-itr-observability

Decisioncanonicalverified 2026-07-09

DECISION.2026-07-09.PHASE-F-RECOMPUTE-ITR-OBSERVABILITY

Decision

Close three follow-on items from the 2026-07-09 audit:

  1. recomputeExpenseReview + recomputePortfolioReview — parity callables mirroring the existing recomputeTaxReview. Both are idempotent, overwrite the review doc, and let the client force a refresh after a mapper fix or a manual re-categorise without waiting for the next onDocumentConfirmed cascade.
  2. ITR form auto-inference persisted on the tax review. The orchestrator writes itr_form: "ITR-1" | "ITR-2" on every tax_reviews/{ay} recompute. Discriminants:
    • Any capital gains (STCG / LTCG / VDA) → ITR-2.
    • Any foreign income (salary or dividend) → ITR-2.
    • Total income > ₹50L → ITR-2.
    • Otherwise → ITR-1. The client inferItrForm helper reads the persisted value when present and falls back to re-running the shared inferrer on older reviews that predate the field.
  3. Admin unmapped_paise observability tile. The KPI recompute samples up to 200 recent tax_reviews, counts how many carry income_summary.unmapped_paise > 0, and sums the total. Two new fields on admin_kpis/current: unmapped_paise_total and unmapped_users_count. Both go to zero when every mapper's emitted entry_type is either in ENTRY_TYPE_TO_BUCKET or on the audit-only whitelist. A non-zero signal here is a contract bug (a mapper is silently orphaning revenue-affecting rows) — fix at the mapper level. CI's mapper-contract.test.ts should have caught it before deploy; this metric is defence-in-depth.

Why

Phase A–E landed the correctness spine of the document lifecycle. Three items from the original audit remained loose:

  • Tax pillar had a recomputeTaxReview callable but Expense and Portfolio did not — a mapper fix couldn't refresh those reviews without the caller re-confirming a doc.
  • ITR-1 vs ITR-2 was inferred client-side at CA-hire time only. If the classification was wrong (or drifted between the review and the hire), the CA-handoff packet would be built against the wrong form. Persisting the value on the review makes the tax engine the authority.
  • unmapped_paise became a clean silent-orphan signal in Phase C. Without an admin surface, a future mapper rename would ship silently until a user noticed their tax review under-counted.

Impact

Contracts (packages/shared/src/contracts):

  • expense.ts — new RecomputeExpenseReviewInputSchema / RecomputeExpenseReviewOutputSchema.
  • portfolio.ts — new RecomputePortfolioReviewInputSchema / RecomputePortfolioReviewOutputSchema.

Callable registry auto-picked both up (144 → 146).

Backend (apps/functions/src):

  • expense/recompute.ts — new. 512 MiB / 300 s.
  • portfolio/recompute.ts — new. 512 MiB / 120 s.
  • index.ts — exports both.
  • admin/recompute-kpis.ts — samples tax_reviews for unmapped_paise; writes unmapped_paise_total + unmapped_users_count onto admin_kpis/current.

Rules (packages/shared/src/rules/v1):

  • income-summary.ts — new inferItrForm(summary) helper. ITR-1 by default; ITR-2 when CG / foreign / total > ₹50L.
  • orchestrator.ts — stamps itr_form on the persisted review.

Schemas:

  • tax-review.ts — new itr_form: "ITR-1" | "ITR-2" optional field.
  • admin-kpi.ts — new unmapped_paise_total + unmapped_users_count fields.
  • Client store/tax-review.ts + utils/itr-form.ts — reads the persisted itr_form when present.

Tests:

  • yarn typecheck — 7 workspaces green.
  • yarn test — 424 functions + 122 shared + 185 mobile = 731 tests pass (no regressions; shared tax-computer tests unchanged).
  • yarn lint — 0 errors.
  • yarn format:check — clean.

Status

Active.

Sources

Every project of mine is written down like this.

Read the résumé