|
| 1 | +# Bank Transaction ID — Design Spec |
| 2 | + |
| 3 | +**Date:** 2026-05-29 |
| 4 | +**Status:** Approved (design), pending implementation plan |
| 5 | + |
| 6 | +## Problem |
| 7 | + |
| 8 | +Transactions need to record the **bank's own transaction reference** (UPI / NEFT |
| 9 | +UTR / cheque number) — a human-supplied identifier distinct from the app's |
| 10 | +internal `transaction_id`. |
| 11 | + |
| 12 | +While scoping this, we found an existing defect: the user-facing **"Submit a |
| 13 | +payment"** form has a required "Transaction ID" text input (`name="transaction_id"`, |
| 14 | +placeholder `e.g., TXN-001`) that writes the user's free-text value **straight |
| 15 | +into `transactions.transaction_id`** — the column that is supposed to |
| 16 | +auto-generate as `YYYYMMDD-NNN` via the `set_transaction_id()` trigger. Because |
| 17 | +the field is never empty, the trigger's auto-generation is bypassed. |
| 18 | + |
| 19 | +Result today: |
| 20 | +- Admin-created transactions → canonical `YYYYMMDD-NNN` IDs. |
| 21 | +- User-submitted (then approved) transactions → non-canonical IDs like `TXN-001`. |
| 22 | + |
| 23 | +## Goal |
| 24 | + |
| 25 | +Introduce a dedicated, optional `bank_transaction_id` column captured on **every** |
| 26 | +transaction (admin-created, user-submitted, and editable later), surfaced in the |
| 27 | +transactions table / dashboard. Restore `transaction_id` to always |
| 28 | +auto-generating cleanly. |
| 29 | + |
| 30 | +## Decisions (confirmed with user) |
| 31 | + |
| 32 | +- **Field rules:** optional everywhere, **not** unique. Blank is allowed (cash / |
| 33 | + historical / adjustment rows). |
| 34 | +- **Existing data:** backfill non-canonical `transaction_id` values into the new |
| 35 | + column; **leave the old `transaction_id` values untouched** (do not re-number |
| 36 | + live data). |
| 37 | +- **Scope:** all entry points — admin create form, user submit form, admin |
| 38 | + pending approve/edit, and the existing edit-transaction flow. Show it in the |
| 39 | + transactions table. |
| 40 | + |
| 41 | +## Affected surface (verified file paths) |
| 42 | + |
| 43 | +- DB: `scripts/prod/migrations/001_init_schema.sql` (reference only), |
| 44 | + new migration `scripts/prod/migrations/033_add_bank_transaction_id.sql` |
| 45 | +- View: `scripts/prod/migrations/003_views.sql` (`dashboard_transactions`) |
| 46 | +- Actions: `src/lib/actions/transactions.ts` |
| 47 | + (`createTransaction`, `updateTransaction`), |
| 48 | + `src/lib/actions/payments.ts` (`submitPayment`, `approvePayment`) |
| 49 | +- Forms / UI: |
| 50 | + - `src/app/(app)/admin/transactions/new/new-transaction-form.tsx` |
| 51 | + - `src/app/(app)/admin/transactions/[transaction_id]/edit-transaction-form.tsx` |
| 52 | + - `src/app/(app)/dashboard/submit-payment-form.tsx` |
| 53 | + - `src/app/(app)/admin/pending/pending-payment-row.tsx` |
| 54 | + - `src/components/transactions-table.tsx` |
| 55 | + |
| 56 | +## Design |
| 57 | + |
| 58 | +### 1. Migration `033_add_bank_transaction_id.sql` |
| 59 | + |
| 60 | +```sql |
| 61 | +-- New optional reference column on both tables |
| 62 | +alter table public.transactions add column if not exists bank_transaction_id text; |
| 63 | +alter table public.pending_payments add column if not exists bank_transaction_id text; |
| 64 | + |
| 65 | +-- Submitted payments no longer need a user-supplied canonical ID; |
| 66 | +-- transaction_id is generated by the trigger at approval time. |
| 67 | +alter table public.pending_payments alter column transaction_id drop not null; |
| 68 | + |
| 69 | +-- Backfill: move non-canonical transaction_id values into the new column. |
| 70 | +-- Canonical format is YYYYMMDD-NNN (8 digits, dash, 3 digits). |
| 71 | +update public.transactions |
| 72 | + set bank_transaction_id = transaction_id |
| 73 | + where bank_transaction_id is null |
| 74 | + and transaction_id !~ '^\d{8}-\d{3}$'; |
| 75 | + |
| 76 | +-- pending_payments.transaction_id is always user-typed today: copy it across, |
| 77 | +-- then clear non-canonical values so future approvals auto-generate cleanly. |
| 78 | +update public.pending_payments |
| 79 | + set bank_transaction_id = coalesce(bank_transaction_id, transaction_id) |
| 80 | + where transaction_id is not null; |
| 81 | + |
| 82 | +update public.pending_payments |
| 83 | + set transaction_id = null |
| 84 | + where transaction_id is not null |
| 85 | + and transaction_id !~ '^\d{8}-\d{3}$'; |
| 86 | +``` |
| 87 | + |
| 88 | +Note: `transactions.transaction_id` keeps its `unique not null` constraint and |
| 89 | +the `set_transaction_id()` trigger — both already fill it on insert when null. |
| 90 | +We do **not** re-number existing rows. |
| 91 | + |
| 92 | +### 2. View `dashboard_transactions` |
| 93 | + |
| 94 | +Add `t.bank_transaction_id` to the select list. No other view changes. |
| 95 | + |
| 96 | +### 3. Server actions |
| 97 | + |
| 98 | +- **`createTransaction`** — read `bank_transaction_id` from FormData |
| 99 | + (`(value as string)?.trim() || null`), add to the insert payload. Continue |
| 100 | + leaving `transaction_id` unset (trigger fills it). |
| 101 | +- **`updateTransaction`** — read and include `bank_transaction_id` in the update |
| 102 | + payload. |
| 103 | +- **`submitPayment`** — read `bank_transaction_id` instead of `transaction_id`. |
| 104 | + Stop inserting `transaction_id` entirely (column is now nullable). Insert |
| 105 | + `bank_transaction_id`. The field is optional → no longer enforce required. |
| 106 | +- **`approvePayment`** — carry `bank_transaction_id` from the pending row (admin |
| 107 | + may edit it). Insert the transaction with `transaction_id` left null so the |
| 108 | + trigger generates the canonical ID. Mirror the final `bank_transaction_id` |
| 109 | + back onto the pending row alongside the existing audit fields. Drop the |
| 110 | + `finalTxnId = rawTxnId || payment.transaction_id` logic for `transaction_id`. |
| 111 | + |
| 112 | +### 4. Forms / UI |
| 113 | + |
| 114 | +- **`new-transaction-form.tsx`** — add an optional "Bank Transaction ID" text |
| 115 | + input (`name="bank_transaction_id"`, placeholder e.g. `UPI/NEFT ref`) near the |
| 116 | + description. Keep the "Transaction ID is auto-generated as YYYYMMDD-NNN" note. |
| 117 | +- **`edit-transaction-form.tsx`** — same input, `defaultValue` pre-filled from |
| 118 | + the transaction's `bank_transaction_id`. |
| 119 | +- **`submit-payment-form.tsx`** — relabel the existing "Transaction ID" field to |
| 120 | + "Bank Transaction ID (optional)", change `name` to `bank_transaction_id`, |
| 121 | + remove `required`, update placeholder. |
| 122 | +- **`pending-payment-row.tsx`** — display `payment.bank_transaction_id` (read |
| 123 | + mode) and provide an editable input `name="bank_transaction_id"` |
| 124 | + (`defaultValue={payment.bank_transaction_id}`) in edit mode. The raw |
| 125 | + `transaction_id` is no longer user-editable here. |
| 126 | +- **`transactions-table.tsx`** — render `bank_transaction_id` beneath the |
| 127 | + `transaction_id` cell when present (`font-mono text-xs text-muted`). |
| 128 | + |
| 129 | +### 5. Types |
| 130 | + |
| 131 | +Extend the row/record TypeScript types that flow from the actions/views into the |
| 132 | +table and forms to include `bank_transaction_id: string | null` |
| 133 | +(`dashboard_transactions` row type, the edit form's transaction prop, the |
| 134 | +pending payment row type). |
| 135 | + |
| 136 | +## Out of scope |
| 137 | + |
| 138 | +- Re-numbering existing `transaction_id` values. |
| 139 | +- Uniqueness / format validation on `bank_transaction_id`. |
| 140 | +- Any change to the donation / loan-interest specific flows beyond passing the |
| 141 | + new field through the shared actions. |
| 142 | + |
| 143 | +## Testing |
| 144 | + |
| 145 | +- Unit: `createTransaction` / `updateTransaction` include `bank_transaction_id` |
| 146 | + in their payloads; blank input → `null`. |
| 147 | +- Unit: `submitPayment` writes `bank_transaction_id` and does **not** set |
| 148 | + `transaction_id`; `approvePayment` leaves `transaction_id` null (trigger fills) |
| 149 | + and carries the bank ref. |
| 150 | +- Manual: create / edit / submit / approve a transaction with and without a bank |
| 151 | + ref; confirm canonical `transaction_id` and the bank ref render in the table. |
| 152 | +- `npm run build`, `npm run lint`, `npm test` must pass. |
0 commit comments