# Admin · Finance — Parity table

Demo (canonical): `portals/admin/finance/{ShopBalances,TransferRequests}.tsx` + `lib/finance.ts`, `store/store.ts`, `store/selectors.ts`, `types.ts`.
Ours: `backend/controllers/{WithdrawalController,FinanceController}.php`, `common/models/base/Withdrawal.php`, `frontend/controllers/AgentsWalletController.php`, `backend/views/withdrawal/*`.

| Demo behavior | Demo ref | Our ref | Status | Note |
|---|---|---|---|---|
| **Shop Balances screen** (per-shop running ledger) | ShopBalances.tsx (whole) | — | missing | Entire admin screen absent on our side |
| Running balance = collectable − outstanding fees, signed | finance.ts:588-619 | — | missing | No charge-ledger / running-balance computation |
| Collectable earnings = Σ unconsumed eligible bookings | finance.ts:596-602 | ShopEarning rows exist; no aggregate | missing | Per-booking only; no "unconsumed collectable" selector |
| Outstanding fees = Σ unpaid settlement charges | finance.ts:603-616 | — | missing | No `charges` ledger table |
| Credit limit (per-shop / global carry threshold) | finance.ts:569 | — | missing | No threshold field or config |
| Auto-bill when carried ≥ threshold | store.ts reconcileBilling | — | missing | No auto-invoicing engine |
| Held-earnings netting into invoice amount-due | finance.ts earningsApplied | — | missing | — |
| "Issue invoice now" gated by shopIssuableNow>0 | selectors.ts:183 / ShopBalances.tsx:99 | — | missing | — |
| Filter Owing/All, sort most-negative | ShopBalances.tsx:46 | — | missing | — |
| Age-in-negative (oldest unpaid fee days) | finance.ts:634 | — | missing | — |
| **Transfer Requests list** | TransferRequests.tsx:23 | backend/views/withdrawal/index.php | done | Tailwind table, richer filters + KPIs + export |
| TR id, shop avatar, status chips | TransferRequests.tsx:27-95 | withdrawal/index.php:84-92,273-290 | done | Chip colours mirror demo semantics |
| Fee columns split: Marketing / Payment-Processing / Fee-VAT | TransferRequests.tsx:54-75 | withdrawal/index.php:85 (single `total_navagoo_fees`) | partial | One fees column; processing fee + VAT not surfaced |
| Net payout column | TransferRequests.tsx:76-81 | net_transferable_amount, index.php:88 | done | Equivalent net |
| Amount paid column | TransferRequests.tsx:82-90 | amount_transferred, index.php:92 | done | — |
| Net payout formula (stamped components) | TransferRequests.tsx:188-205 | AgentsWalletController.php:386-459 | partial | Summed once from ShopEarning; no per-component stamping |
| **Settle modal** | SettleModal | backend/views/withdrawal/_settlement_form.php | partial | Full-page/AJAX form, legacy (non-Tailwind) chrome |
| FormulaStrip breakdown on settle | TransferRequests.tsx:185-206 | — | missing | No fee breakdown shown when settling |
| Upload invoice document | TransferRequests.tsx:217-226 | DocumentUploadWidget navagoo_invoice | done | — |
| Upload bank-transfer confirmation | TransferRequests.tsx:245-256 | DocumentUploadWidget transfer_receipt | done | — |
| "Generate invoice via API" placeholder | TransferRequests.tsx:229-243 | — | missing | Not present (acceptable — placeholder only) |
| Settle disabled until BOTH docs uploaded | TransferRequests.tsx:166 | _settlement_form.php (not hard-gated) | partial | Server-side validation only; button not gated |
| Amount-paid mismatch warning vs net payout | TransferRequests.tsx:159,247-256 | — | missing | No reconciliation check |
| Settle flips charges→paid + generates settlement invoice + links invoices | store.ts:745-838 | WithdrawalController.php:214-228 (file upload only) | missing | No invoice entity generated/linked; no ledger flip |
| Settled-at / amountPaid / docs recorded on settle | store.ts:802-815 | WithdrawalController.php:160-211 | done | settlement_date, amount_transferred, doc paths |
| Associated-invoices section (expandable InvoiceDocument) | TransferRequests.tsx:271-318 | — | missing | No linked-invoice display |
| Status transitions | requested→settled | NEW/REQUESTED/IN_PROGRESS/SETTLED | done | Ours is a superset |
| Propagate settlement to agents/earnings + transactions | — (none) | WithdrawalController.php:222-224 | done | Ours-extra (no demo counterpart) |
| Permissions / role gating | — (none) | WithdrawalController.php:27-43 | done | Ours-extra |

Area score: 38%

## Verified verdict (adversarial)

Verifier read BOTH the demo refs and our code line-by-line. Most analyst rows hold up;
two were over-credited and are downgraded with evidence. The headline 38% was slightly
optimistic — the settlement workflow is materially thinner than the analyst's "done"
rows imply (no document gating, no required validation, no invoice/ledger entity).

### Confirmed (verified true)

| Row | Verdict | Evidence |
|---|---|---|
| Transfer Requests list | done ✓ | `backend/views/withdrawal/index.php:217-357` — full Tailwind table, all columns, filters that the data provider honours, KPI tiles, CSV/XLSX `ExportMenu`. Genuine superset of demo `TransferRequests.tsx:23`. |
| Status chips (4 statuses) | done ✓ | `index.php:50-66` maps model constants `SETTLEMENT_STATUS_NEW=-1 / REQUESTED=0 / IN_PROGRESS=1 / SETTLED=2` (`common/models/base/Withdrawal.php:55-58`) to distinct chips. Superset of demo `requested→settled`. |
| Net payout column | done ✓ | `index.php:277` renders `net_transferable_amount`. Matches demo `netPayout` (`TransferRequests.tsx:76-81`). |
| Amount paid column | done ✓ | `index.php:290` shows `amount_transferred` or `—`; demo `TransferRequests.tsx:82-90` shows `—` when null too. |
| Upload invoice + bank confirmation | done ✓ | `_settlement_form.php:140-169` — `navagoo_invoice` + `transfer_receipt` + `other_documents` widgets. |
| Record settled-at / amountPaid / docs | done ✓ (caveat) | `WithdrawalController.php:174-211` stamps `settlement_date`, doc paths; `amount_transferred` via `loadAll`. CAVEAT: `amount_transferred` is NOT auto-seeded to net payout (demo `TransferRequests.tsx:156` seeds `amountPaid ?? netPayout`); ours starts blank. |
| Propagate settlement to agents/earnings + transactions | done ✓ (ours-extra) | `WithdrawalController.php:480-503` (`synchronizeRelatedSettlementStatus`) + `:512+` (`pushSpecialistSettlementTransactions`). No demo counterpart. |
| Permissions / role gating | done ✓ (weak, ours-extra) | `WithdrawalController.php:27-45`. CAVEAT: only the `manager` role is permission-checked; the `else` branch returns `true` for every other role unconditionally — coarse, but present. |

### Downgraded (analyst over-credited)

| Row | Was | Now | Evidence |
|---|---|---|---|
| Settle disabled until BOTH documents uploaded | partial | **missing** | Demo hard-gates: `TransferRequests.tsx:200` `disabled={!invoiceUploaded || !bankUploaded}`. Ours has NEITHER client gating NOR server enforcement: `_settlement_form.php:185-192` is a plain `Html::button` (never disabled), and `common/models/base/Withdrawal.php:123` marks `navagoo_invoice`/`transfer_receipt` as `file` with `skipOnEmpty => true` (optional, not `required`). A settlement can be executed with zero documents attached. The analyst's "server-side validation only" is inaccurate — there is no doc-required validation at all. |
| Settle modal | partial | partial (confirmed) | `_settlement_form.php` is legacy AdminLTE-era chrome (inline Bootstrap-style CSS, `fas fa-*` FontAwesome, `.form-control`), NOT the Tailwind aurora chrome. Confirmed partial — flagged here because the index it launches from is fully Tailwind, so the settle flow is a visual regression on click-through. |

### Confirmed-missing (no change, spot-checked)

- Shop Balances screen / running-balance ledger: `backend/controllers/FinanceController.php` exists but is a **duplicate Withdrawal CRUD** (renders `withdrawal`-style index/view/create/update on the same `WithdrawalSearch`); it is NOT a per-shop running-ledger view. Demo `ShopBalances.tsx` + `lib/finance.ts:586-635` (`shopRunningBalance`, `shopCarriedBalance`, `effectiveCarryThreshold`, `oldestUnpaidFeeAgeDays`) have no equivalent. Confirmed missing.
- Charge ledger / outstanding-fees / credit-limit / auto-bill / held-earnings netting / issuable-gated "Issue invoice now" / Owing-All filter / age-in-negative: all confirmed absent.
- FormulaStrip on settle (`TransferRequests.tsx:211-225`): confirmed absent in `_settlement_form.php`.
- Amount-paid mismatch warning (`TransferRequests.tsx:170,300-308`): confirmed absent; `amount_transferred` is only a `number` validator, no reconciliation against `net_transferable_amount`.
- Settle flips charges→paid + generates/links settlement invoice (`store.ts:744-826`): confirmed absent; ours only uploads files + flips related Earnings/ShopEarning/Payment statuses — no invoice entity is generated or linked.
- Associated-invoices expandable section (`TransferRequests.tsx:271-318`): confirmed absent (no invoice entity exists).

### Adjusted area score

One row dropped from partial→missing (doc gating) and the settle-modal/settled-recording
"done" rows carry real caveats (legacy chrome, no amount seeding, no reconciliation). The
Transfer Requests *list* surface is strong (genuine superset), but the *settlement workflow*
and the *entire Shop Balances half* of the area are thin-to-absent.

**Adjusted score: 34%** (down from analyst's 38%).

