# Recon V2 — Generic Rule-Engine Reconciliation Plan

**Supersedes:** the per-type matcher design tracked in `RECON_PHASE_TRACKER.md` (commit `52d5575`)
**Reference material:** `D:\moment\recon\Recon_updated_single_generalized` (rule-engine sample: `bank_momentcurrent 1.py`, `CARDCHARGESFILE 1.py`, `rule_engine_instruction.txt`)
**Started:** 2026-07-11
**Last Updated:** 2026-07-12
**Status legend:** ⬜ Pending · 🟡 In Progress · ✅ Done · ❌ Dropped

**Goal:** One generic, data-driven reconciliation engine. Onboarding a new customer of **any business type** (hospital, retail, restaurant, …) means configuring sources, field mappings, and match rules through the admin UI — no new Python files, no code deploys, no manual SQL.

---

## 1. Guiding Principles

1. **Rules are data.** Match behaviour lives in DB rows (rule sets), interpreted by one generic engine. The 7 hardcoded matchers (`matchers/*.py`) are replaced, not extended.
2. **Business-agnostic vocabulary.** No "HIS", "hospital", or "unit" in schema, engine, or rule documents. Those are per-tenant *labels*. The engine only knows **source roles**:
   - `internal_system` — customer's system of record (HIS for a hospital, POS for retail, PMS for a hotel)
   - `gateway` — MomentsPay transaction export
   - `settlement_bank` — acquirer/bank settlement file (MPR)
   - `processor` — intermediate processor (Pine, PayU, Razorpay, Amex)
3. **Normalization is declarative.** Every transform the reference doc does with manual SQL (LPAD approval codes, last-4 card, strip quotes/commas, unit-name mapping, date reformat) becomes a named normalization step in a source template. Zero manual pre-processing per run.
4. **One-to-one consumption matching.** Each row is matched at most once per stage, via an ordered tier cascade. Every match records *which rule, which tier* — auditable.
5. **DB is the system of record; Excel is a rendering.** Match results persist to `recon_matches`. Reports are generated from DB, enabling drill-down UI, reruns, and a future exceptions workflow.
6. **Tenant isolation by construction.** Matching runs in pandas, in-memory, per session. No shared MySQL staging tables, no dynamic SQL (rejects the reference implementation's `card`/`axis`/`his` global tables and f-string SQL).
7. **Parity before deletion.** The existing 7 matchers become golden tests. The generic engine must reproduce their matched/unmatched sets on fixture files before `matchers/*.py` is deleted.

---

## 2. What We Take From the Reference vs. What We Reject

| Reference concept | Verdict | Notes |
|---|---|---|
| `rule_engine` table: comparison_pairs, join_condition, priority, enabled | ✅ Adopt (reshaped) | Core idea: rules as data |
| Rule chaining (stage output feeds later stage) | ✅ Adopt | Enables N-way recon from 2-way primitives |
| `merchant_config` separated from rules | ✅ Adopt (reshaped) | Becomes business profile + report layout config |
| Manual SQL preprocessing (STEP 2 of instruction doc) | ❌ Reject | Becomes declarative normalization steps |
| Dynamic SQL over global MySQL tables (`card`, `axis`, `his`) | ❌ Reject | SQL-injection surface, no tenant isolation |
| LEFT JOIN + row-by-row Python re-evaluation | ❌ Reject | Redundant, slow, cartesian-duplication bugs |
| Excel cells as system of record (summary at rows 26–29 etc.) | ❌ Reject | Results persist in DB; layout is report config |
| OpenAI column-type inference (always returns VARCHAR) | ❌ Reject | Ingestion is deterministic |

**Added beyond both implementations:** match tiers with amount tolerance & date windows (T+n settlement), reusable global file-format templates, persisted match results, dry-run preview, future many-to-one aggregation matching.

---

## 3. Target Architecture

```
┌────────────────────────────────────────────────────────────────────┐
│ gateway_web (LiveView)                                             │
│  OnboardingLive: business profile + labels + field-mapping wizard  │
│  RuleSetLive:    rule builder + dry-run preview                    │
│  ReconWizardLive: select business → upload → run → results (kept)  │
└──────────────────────────┬─────────────────────────────────────────┘
                           │
┌──────────────────────────▼─────────────────────────────────────────┐
│ mw_recon (Elixir)                                                  │
│  Orchestrator (kept) — loads rule set, sends rules + files to      │
│  engine, persists recon_matches, triggers report                   │
└──────┬──────────────────────────────────┬──────────────────────────┘
       │ /+/recon/engine/                 │ /+/recon/report/
┌──────▼───────────────────┐   ┌──────────▼───────────────────────┐
│ recon_engine_py          │   │ recon_report_py                  │
│ ONE generic interpreter: │   │ Reads persisted results;         │
│  normalize → tier-match  │   │ sheets/summary driven by         │
│  → stage-chain           │   │ rule-set report config           │
└──────────────────────────┘   └──────────────────────────────────┘

infra_repo (MySQL):
  recon_source_templates   file formats: canonical fields, column aliases,
                           normalization pipeline, header detection  (global, reusable)
  recon_business_profiles  per tenant: business_type, label map, business_group
  recon_rule_sets          per tenant/business_group: ordered stages + report config
  recon_rules              stage: input sources (template ref or prior stage output),
                           tier cascade, tolerances, filters, output names
  recon_sessions           (kept) + rule_set_id
  recon_run_files          (kept) + source_template_id
  recon_matches            NEW: session, rule, tier, side-A ref, side-B ref, status
  recon_location_maps      (kept) value-mapping (unit/store/outlet names → codes)
```

### 3.1 Label configuration (business-agnostic UI)

Per-tenant `recon_business_profiles.labels` (JSON), set at onboarding, consumed by all LiveViews:

```json
{
  "business_type": "hospital",
  "internal_system": "HIS",
  "location": "Unit",
  "location_plural": "Units"
}
```

Retail example: `{"business_type": "retail", "internal_system": "POS", "location": "Store"}`.
Schema/engine/rules never contain these words — only the UI renders them.

### 3.2 Rule document shape (illustrative)

Today's `bank_card_vs_momentspay` expressed as data:

```json
{
  "stage": "bank_card_vs_gateway",
  "side_a": {"source": "settlement_bank_card"},
  "side_b": {"source": "gateway"},
  "tiers": [
    {"name": "exact-4key",
     "keys": [["card_last4","card_last4"], ["terminal_id","terminal_id"],
              ["amount","amount"], ["approval_code","approval_code"]]},
    {"name": "3key",
     "keys": [["card_last4","card_last4"], ["terminal_id","terminal_id"],
              ["amount","amount"]]},
    {"name": "tolerant", "enabled": false,
     "keys": [["card_last4","card_last4"], ["terminal_id","terminal_id"]],
     "amount_tolerance": {"type": "abs", "value": 1.0},
     "date_window_days": 2}
  ],
  "output": {"matched_sheet": "BankCard-GW-Matched",
             "unmatched_sheet": "BankCard-GW-Unmatched"}
}
```

A later stage references `{"source": "stage:bank_card_vs_gateway:matched"}` for 3-way recon (internal_system vs matched bank rows) — the reference doc's chaining, made explicit.

---

## 4. Phase Plan

| Phase | Name | Depends on | Status |
|-------|------|-----------|--------|
| **R0** | Rule model, schemas & vocabulary migration | — | ✅ Done |
| **R1** | Generic Python engine + parity tests | R0 | ✅ Done |
| **R2** | Orchestrator/API rewire + persisted results | R1 | ✅ Done |
| **R3** | Report generator from DB | R2 | ✅ Done |
| **R4** | Onboarding & field-mapping UI | R0 | ✅ Done (browser walkthrough folded into R6) |
| **R5** | Rule-set builder UI + dry-run (+ seeds, menu) | R2, R4 | ✅ Done |
| **R6** | Cutover: seed all tenants, delete legacy matchers | R1–R5 | ✅ Done (manual browser walkthrough pending) |
| **R7** | Aggregation matching, exceptions workflow, OQ-01…07 answers | R6 | ✅ Done (aging + scheduled recon + OQ-03 remain on backlog) |

---

### Phase R0 — Rule Model, Schemas & Vocabulary Migration

**Goal:** The data model can describe every recon we do today, plus the Sahyadri 17-rule example from the reference doc, on paper — before any engine code.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Design rule-document JSON schema (tiers, tolerances, chaining, filters) | `docs/contracts/recon_rule_schema.json` | ✅ | Draft-07; stages/tiers/filters/carry_fields/report config |
| Design source-template JSON schema (fields, aliases, normalization steps, header detection) | `docs/contracts/recon_source_template_schema.json` | ✅ | 9 normalization steps + derived fields (coalesce/sum/concat) |
| Express all 7 current recon types as rule documents (paper validation) | `docs/contracts/recon_rule_examples/` | ✅ | `card_3way` (3 types), `upi_3way` (3 types), `amex_2way` (1 type) + 7 templates |
| Express Sahyadri 17-rule reference set as rule documents | `docs/contracts/recon_rule_examples/sahyadri_full.ruleset.json` | ✅ | All 17 rules as stages; manual rules 9–12 chain off `unmatched_a` (improvement over reference) |
| Migration: `recon_source_templates`, `recon_business_profiles`, `recon_rule_sets`, `recon_matches` | `migrations/20260711000001_create_recon_v2_tables.exs` | ✅ | Also adds `rule_set_id` to sessions, `source_template_id` to run files. Note: no separate `recon_rules` table — stages live inside the rule-set definition JSON. Applied to dev DB ✔ |
| Migration: rename `hospital_group` → `business_group` (+ all code refs: schema, context, LiveViews, seeds) | `migrations/20260711000002_rename_hospital_group_to_business_group.exs` | ✅ | Applied to dev DB ✔. Uses raw `CHANGE COLUMN` — local MariaDB 10.4 (XAMPP) lacks `RENAME COLUMN` |
| Ecto schemas + changesets with JSON validation (`ex_json_schema`) | `infra_repo/lib/infra_repo/schemas/recon_{source_template,rule_set,business_profile,match}.ex` | ✅ | `Recon.ContractValidator` validates definitions against `priv/recon_contracts/` schemas |
| Contexts: `ReconSourceTemplates`, `ReconRuleSets`, `ReconBusinessProfiles`, `ReconMatches` | `infra_repo/lib/infra_repo/` | ✅ | Incl. `clone_rule_set` (for R5), bulk `insert_results` + summaries (for R2/R3) |
| Contract validation script (exit-criteria check) | `dev/validate_recon_contracts.exs` | ✅ | `mix run --no-start dev/validate_recon_contracts.exs` → 11 documents, 0 invalid |

**Exit criteria:** every existing recon type + the 17-rule reference set round-trips through changeset validation as pure data.

---

### Phase R1 — Generic Python Engine

**Goal:** One interpreter replaces all matchers, proven equivalent on fixtures.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Normalization interpreter (9 steps + coalesce/sum/concat derives) | `services/recon_engine_py/engine/normalize.py` | ✅ | New `engine/` package; legacy `normaliser.py` untouched until R6 (legacy matchers still import it) |
| Source loader: template-driven parse (xlsx/csv), auto header detection, skip-rows, canonical field projection, 200K row guard | `services/recon_engine_py/engine/loader.py` | ✅ | Original file columns kept alongside canonical fields for D8 payloads |
| Tiered matcher with one-to-one consumption (each row matched once; records tier) | `services/recon_engine_py/engine/matcher.py` | ✅ | Occurrence-indexed merge — no cartesian duplication. Keys canonicalized to strings (numbers → integer paise) so '500'/500/'500.00' all match |
| Amount tolerance + date-window matching | `services/recon_engine_py/engine/matcher.py` | ✅ | Candidate join → tolerance filter → greedy smallest-difference one-to-one |
| Stage chaining (stage outputs as later inputs) + filters + carry_fields | `services/recon_engine_py/engine/pipeline.py` | ✅ | Also returns `matched_b_rows` aligned 1:1 for recon_matches side_b_payload |
| Generic `main.py`: V2 payload (rule_set + templates + files) alongside legacy branch | `services/recon_engine_py/main.py` | ✅ | Legacy `recon_type` branch removed at R6 |
| Fixtures per recon type | `services/recon_engine_py/tests/test_parity.py` | ✅ | Synthetic fixtures built in-test (match + fallback + unmatched rows per type) |
| Golden-parity tests: legacy matcher vs engine, identical matched/unmatched sets | `services/recon_engine_py/tests/` | ✅ | **34/34 pass** — all 5 legacy matchers (7 types) + unit tests. Run: `.venv\Scripts\python -m pytest tests` |

**Exit criteria:** parity green for all 7 types ✔ (5 legacy matchers cover them); chained 3-way fixture passes ✔ (`test_pipeline.py::test_chained_stages_carry_fields_flow_through`).

**Known deliberate divergence from legacy (documented, tested):** on duplicate keys the legacy LEFT JOIN matches one bank row to many gateway rows (then dedups); the engine matches strictly one-to-one (D5). Parity fixtures use unique keys; the divergence is covered by `test_matcher.py::test_one_to_one_consumption_no_cartesian_duplication`.

---

### Phase R2 — Orchestrator/API Rewire + Persisted Results

**Goal:** Sessions run rule sets; results live in DB.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Orchestrator V2 branch: sessions with `rule_set_id` run the generic engine | `mw_recon/orchestrator.ex` | ✅ | `dispatch_run` branches on `session.rule_set_id`; PubSub status flow unchanged; V2 completes without report until R3 |
| Persist match results to `recon_matches` | `mw_recon/result_store.ex` | ✅ | New module; idempotent per session (delete + chunked insert); maps tier, both payloads, amount_a/b/diff, location |
| `EngineClient.run_v2`: rule_set + resolved templates + files keyed by template name | `mw_recon/engine_client.ex` | ✅ | Missing template/upload errors are user-readable; location_map falls back to legacy config until R4 |
| Session/upload contract: `rule_set_id` on sessions, `role` = template name + `source_template_id` on run files | `infra_repo` schemas + `mw_recon.ex` | ✅ | Legacy role enum removed from `ReconRunFile`; `MwRecon.file_slots_for_rule_set/1` drives upload slots from the rule set |
| Results read from `recon_matches` | `infra_repo/recon_matches.ex` | ✅ | `list_for_session` (stage/status filters), `summarise_session`, `summarise_by_location` — consumed by R3 report + UI. (Correction from R4: a REST ReconController DOES exist in `gateway_api` — but that app is not in the umbrella whitelist and `gateway_web` has no `/api/v1` routes, so the wizard's upload POST 404s at runtime; resolved by the R6 wizard rewire) |
| Persistence smoke test vs dev DB | `dev/smoke_recon_v2_persist.exs` | ✅ | `mix run --no-start dev/smoke_recon_v2_persist.exs` → SMOKE PASSED |

**Bugs found & fixed while wiring R2:**
1. Umbrella root `mix.exs` `apps:` whitelist never included `:mw_recon` — the app was compiled but **never loaded/started at runtime**, so the committed wizard's "start run" would have crashed. Added to the list, and `gateway_web` now declares the `mw_recon` dependency (also removes the long-standing compile warnings).
2. `migration_primary_key: [type: :bigint, autogenerate: true]` emits PKs **without AUTO_INCREMENT** on MySQL/MariaDB (inserts return `id: nil`). Migration `20260711000003` durably fixes all recon tables — including legacy `recon_sessions`/`recon_run_files`, which the old hand-run fix script (`dev/fix_recon_mysql_ids.exs`) had missed.

---

### Phase R3 — Report Generator From DB

**Goal:** Excel is a rendering of persisted results; layout is config.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Report payload = query of `recon_matches` + rule-set report config | `mw_recon/report_builder.ex` (new) + `report_client.ex` `generate_v2` | ✅ | Decodes persisted payloads; all definition stages included even with zero rows |
| Sheet writer driven by rule-set output config (no hardcoded sheet names) | `services/recon_report_py/report_v2.py` (new) | ✅ | Legacy `writer.py` untouched (helpers reused); duplicate sheet names deduped; column union with priority ordering |
| Location summary uses tenant labels | `report_v2.py` + `report_builder.ex` | ✅ | "Unit"/"Store" from `recon_business_profiles.labels`; zero business vocabulary in the writer |
| Summary sheet: per-stage totals, per-location, per-tier breakdown | `report_v2.py` | ✅ | Aggregation computed from rows in one place (Python); tier breakdown toggleable via `report.include_tier_breakdown` |
| Orchestrator: `reporting` step rendered FROM persisted rows + `regenerate_report/1` | `mw_recon/orchestrator.ex` | ✅ | Reports regenerable any time without engine re-run (D2 payoff) |
| Verification | `recon_report_py/tests/` + `dev/smoke_recon_v2_report.exs` + `dev/render_report_payload.py` | ✅ | 8/8 pytest; cross-language seam smoke: Elixir builds payload from dev-DB rows → real Python writer renders it (labels, tiers, sheets all asserted) |

---

### Phase R4 — Onboarding & Field-Mapping UI

**Goal:** A non-technical admin onboards a new business of any type without engineering involvement.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Business profile step: business type, labels (internal system name, location term), business_group | `gateway_web/live/recon_onboarding_live.ex` (new; routed at `/recon/onboarding`) | ✅ | Upserts `recon_business_profiles`; label placeholders show HIS/POS/PMS, Unit/Store |
| Field-mapping wizard: upload sample file → parsed headers + sample rows shown → map to canonical fields | same + `engine/preview.py` (new) + `EngineClient.preview_file/4` | ✅ | New `SampleFileUpload` hook pushes base64 straight to LiveView (no broken REST hop); engine preview auto-detects the header row under bank-portal junk rows |
| Auto-suggest mappings from existing template aliases (fuzzy header match) | `mw_recon/template_suggester.ex` (new) | ✅ | Template aliases + builtin aliases + Jaro ≥ 0.86; verified: `Approv_Code`→approval_code, `Txn Ref No.(RRN)`→rrn, `GROSS AMT`→amount all exact |
| Per-field normalization step picker (last4, lpad, date format, value map) | same | ✅ | 9 presets expanded into contract-schema steps; preset defaults follow the chosen canonical field |
| Save as new source template (global, D9) | onboarding LiveView | ✅ | Definition built from mappings → contract validation fires in the changeset; per-tenant *binding* happens via rule sets (R5), so no separate binding record needed |
| Location/value map editor (replaces manual `UPDATE ... CASE WHEN` from reference doc) | onboarding LiveView + `infra_repo/recon_location_maps.ex` (new) | ✅ | Tenant-scoped via migration `20260712000001`; `EngineClient` now prefers the tenant map (legacy config map is fallback until R6) |
| Verification | pytest + `dev/smoke_recon_v2_locations.exs` | ✅ | 37/37 engine tests (incl. 3 preview tests); location-map smoke vs dev DB passed; umbrella compiles clean |

**Exit criteria:** onboard a fictional retail customer end-to-end (POS + bank + gateway) with zero code changes — *code path complete; the browser walkthrough needs the app + CloudI running and is folded into R6 verification.*

**Bug found & fixed while wiring R4:** the `ReconFileUpload` JS hook existed as a file but was **never registered** in `assets/js/hooks/index.js` — the committed wizard's file input did nothing in the browser. Both hooks are now registered.

---

### Phase R5 — Rule-Set Builder UI + Dry-Run

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Rule-set builder: stage/tier visualization + validated JSON editor | `gateway_web/live/recon_rule_set_live.ex` (new; `/admin/recon/rules`) | ✅ | Read view: stages → sources → tiers (keys, tolerances, T+n windows) rendered structurally for non-technical users. Edits via JSON editor with Validate button: JSON → contract schema → template-reference check, all before save. A drag-style graphical tier editor stays on the backlog |
| Chaining picker: stage outputs selectable as inputs | same | ✅ | Chained sources render as "output of <stage> → matched"; editable in JSON; validated |
| Dry-run: run rule set against sample files, show match counts per tier before saving | same + `EngineClient.dry_run/4` (new) | ✅ | One upload slot per referenced template (slot-aware `SampleFileUpload`); engine runs the full rule set, nothing persisted; per-stage matched/unmatched + tier counts table |
| Clone rule set | same + `ReconRuleSets.clone_rule_set/3` | ✅ | Duplicate-within-tenant button (auto `_copy` name); cross-tenant clone exists at context level, super-admin UI on backlog |
| **Seed V2 data (user request): templates, profiles, rule sets, location maps** | `infra_repo/priv/repo/seeds_recon_v2.exs` (new; also required by `seeds.exs`) | ✅ | Idempotent. Seeds: 7 global templates, 3 business profiles (Sahyadri/Rela/Dr. Mohan's), 9 rule sets (incl. 17-stage `sahyadri_full`), tenant-1 location map (22 entries migrated from legacy configs). Contract examples mirrored into `priv/recon_contracts/examples/` so seeds are deploy-safe |
| **Recon menu items (user request)** | `gateway_web/layouts.ex` | ✅ | New "Reconciliation" nav section: Run Recon, History, Rule Sets, Onboarding, Legacy Config — each page now has a distinct `active_nav` |

---

### Phase R6 — Cutover

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Seed source templates + business profiles + rule sets for Sahyadri, Rela, Dr. Mohan's | `infra_repo/priv/repo/seeds_recon_v2.exs` | ✅ | Done early, in R5 (user request) |
| Parity gate before deletion | `services/recon_engine_py/tests/` | ✅ | Passed in R1 on synthetic fixtures (34/34) — no real production sample files were available; the parity tests were converted to standalone **golden tests** carrying the legacy-verified expected values forward |
| Delete `matchers/*.py`, `MATCHER_MAP`, `recon_type` enum validation | engine + schemas | ✅ | Also removed: legacy branches in both Python services, legacy `Orchestrator.run`, `EngineClient.run`, `ReportClient.generate`, and `MwRecon.recon_types/required_file_roles/file_slots_for`. Legacy payloads now get an explicit error. Sessions without `rule_set_id` fail with a clear message |
| Update `ReconWizardLive` to be rule-set driven | `gateway_web/live/recon_wizard_live.ex` | ✅ | Step 1 selects a rule set; slots from `file_slots_for_rule_set`; upload POSTs to NEW same-origin `GatewayWebWeb.ReconApiController` (`/admin/api/recon/...`) — fixing the wizard's broken `/api/v1` path; report download served the same way. `gateway_api`'s external REST controller also rewritten to V2 (rule_set_id) |
| Docs update; mark `RECON_PHASE_TRACKER.md` superseded | `docs/` | ✅ | Superseded banner added |

**Remaining before calling the system live:** a manual browser walkthrough with the app + CloudI running — onboard a business, dry-run a rule set, run the wizard end-to-end with real files, download the report. All code paths beneath it are unit/smoke-tested.

---

### Phase R7 — Backlog + Open-Question Answers (2026-07-12)

The legacy tracker's open questions OQ-01…OQ-07 were answered by the team and
implemented here alongside the backlog items.

| Task | Location | Status | Notes |
|------|----------|--------|-------|
| Many-to-one aggregation matching (`match_mode: many_to_one`) | `engine/matcher.py` + rule schema | ✅ | Side-A rows grouped by tier keys; group amount-sum must equal one side-B row (± tolerance). One settlement line ↔ N transactions. 5 new tests |
| Exceptions workflow | migration `20260712000002` + `ReconMatches` + `ReconSessionDetailLive` (`/admin/recon/history/:id`) | ✅ | Unmatched rows → Resolve / Ignore with note (who + when recorded); reopen supported; open-exception count badge; History rows link to Details |
| OQ-01: report retention 30 days | `MwRecon.ReportRetention` GenServer + `ReconSessions.purge_old_reports/1` | ✅ | Daily sweep clears only the XLSX blob — session + recon_matches remain, report regenerable anytime. `config :mw_recon, :report_retention_days` (default 30) |
| OQ-02: role gating (finance_admin + superadmin) | `GatewayWebWeb.ReconAuth` + router `live_session :recon` + API plug | ✅ | Allowed roles configurable: `config :gateway_web, :recon_allowed_roles` (default superadmin/super_admin/finance_admin). ⚠ `finance_admin` is a NEW role string — existing `entity_admin` users are locked out by design |
| OQ-04: configurable summary layout | — | ✅ (already) | V2 report never used fixed cell positions — layout is driven by the rule set's `report` config + tenant labels |
| OQ-05: date detected from file + mismatch alert | `engine/pipeline.py` `_date_warnings` + wizard/dry-run UI | ✅ | Dominant `txn_date` per file compared to session recon date → yellow warning banner on results & dry run; multi-date warning when no date selected |
| OQ-06: cross-tenant history for superadmin | `ReconSessions.list_all_sessions` + `ReconHistoryLive` | ✅ | Superadmin sees all tenants (with tenant column); tenant admins unchanged |
| OQ-07: Razorpay | `razorpay_processor.template.json` (seeded) | ✅ | Confirmed: just another source template — zero engine changes needed |
| OQ-03: DR MOHAN entity/cluster/branch grouping | — | ⬜ Open | Study item: likely expressible as `location` value-maps + `business_group` tenants; needs a look at the DRMOHAN cluster scripts before deciding |
| Aging report (T+n buckets) | — | ⬜ Backlog | Needs paid-date vs txn-date columns in settlement files |
| Scheduled/automated recon (SFTP/email arrival) | — | ⬜ Backlog | Depends on adapter_file SFTP wiring |

---

## 5. Decision Log

| # | Date | Decision | Rationale |
|---|------|----------|-----------|
| D1 | 2026-07-11 | Engine matches in pandas per-session, not SQL over staged MySQL tables | Tenant isolation, no dynamic-SQL injection surface, no manual table renames |
| D2 | 2026-07-11 | Match results persist in `recon_matches`; Excel is a rendering | Enables drill-down, rerun, audits, exceptions workflow |
| D3 | 2026-07-11 | Business-agnostic source roles + per-tenant label config | Support hospital/retail/restaurant without schema or engine changes |
| D4 | 2026-07-11 | Field-mapping UI is part of onboarding (R4), before rule builder (R5) | Onboarding depends on templates existing |
| D5 | 2026-07-11 | One-to-one consumption with tier cascade; every match records rule + tier | Fixes reference's cartesian-duplication; auditable for finance |
| D6 | 2026-07-11 | Parity gate: legacy matchers deleted only after golden tests pass | Safe migration |
| D7 | 2026-07-11 | In-memory pandas confirmed; max expected file size ~200K rows (typical: few thousand) | Q1/Q2 answered — well within pandas limits; engine adds a hard row-count guard with a clear error |
| D8 | 2026-07-11 | `recon_matches` stores **full row payloads** (side-A and side-B JSON) plus rule/tier metadata | Q3 answered — needed for reports and for showing amount differences / which columns were received vs. not matched |
| D9 | 2026-07-11 | Global source templates shared across tenants from day one | Q4 answered — define "HDFC Card MPR" once, bind per tenant |
| D10 | 2026-07-11 | Clean break from the committed per-type REST/UI contract — no backward compatibility needed | Q5 answered — not live yet; removes transition-shim work from R2/R6 |
| D11 | 2026-07-12 | Report XLSX blobs retained 30 days, then purged (session + matches kept; regenerable) | OQ-01 |
| D12 | 2026-07-12 | Recon gated to `finance_admin` + superadmin roles (config-overridable) | OQ-02 |
| D13 | 2026-07-12 | Summary layout stays config-driven (no fixed cells) | OQ-04 — V2 design already satisfied it |
| D14 | 2026-07-12 | Recon date detected from files; mismatch with selected date raises a UI warning, not a failure | OQ-05 |
| D15 | 2026-07-12 | Superadmins see recon history across all tenants | OQ-06 |
| D16 | 2026-07-12 | Razorpay = new source template only; same engine | OQ-07 — validated by seeding `razorpay_processor` with zero code changes |

---

## 6. Open Questions

All initial questions (Q1–Q5) were answered on 2026-07-11 and promoted to decisions D7–D10 above.

| # | Question | Owner | Status |
|---|----------|-------|--------|
| Q1 | Confirm D1 (pandas engine) and D2 (DB-persisted results) | prem | ✅ Answered → D7, D8 |
| Q2 | Expected max file sizes? | prem | ✅ Answered → D7 (max ~200K rows) |
| Q3 | `recon_matches`: full row payloads or references? | team | ✅ Answered → D8 (full payloads) |
| Q4 | Global source templates shared across tenants from day one? | team | ✅ Answered → D9 (yes) |
| Q5 | Backward compatibility with committed REST/UI needed? | team | ✅ Answered → D10 (clean break) |

---

*This is a living document — update task statuses, decisions, and open questions as implementation proceeds. Bump "Last Updated" on every edit.*
