# Admin · Finance — Logic & Flows parity

Scope: two admin-side finance surfaces in the dev demo:
- **Shop Balances** (`portals/admin/finance/ShopBalances.tsx`) — a per-shop running-balance ledger view with a manual "Issue invoice now" action. **NEW in dev — no equivalent on our side.**
- **Transfer Requests** (`portals/admin/finance/TransferRequests.tsx`) — admin reviews a shop's payout request and settles it. Maps to OUR `Withdrawal` flow.

---

## 1. Shop Balances (demo) — NEW, MISSING on our side

### What the demo computes
Per shop, a row of:
- `collectableEarnings` — `lib/finance.ts:588 shopRunningBalance()` = Σ over eligible, terminal, *unconsumed* bookings of `(amountCollected − refundValue + tips)`.
- `outstandingFees` — Σ `chargeAmount + vatAmount` over unpaid, non-pending, `net_from_settlement` charges not yet attached to a transfer (`finance.ts:600-616`).
- `runningBalance` = `collectableEarnings − outstandingFees`. **Positive = Navagoo owes shop; negative = shop owes Navagoo** (`finance.ts:617`).
- `carried` — `shopCarriedBalance()` = `max(0, −runningBalance)` (`finance.ts:622-631`).
- `issuable` — `selectors.ts:183 shopIssuableNow()` = Σ outstanding fees not yet invoiced AND not in a transfer; gates the "Issue invoice now" button.
- `threshold` — `effectiveCarryThreshold()` = per-shop `carryForwardThreshold` override else global default (`finance.ts:569`).
- `ageDays` — `oldestUnpaidFeeAgeDays()` = age of oldest unpaid settlement fee (`finance.ts:634`).

### Flow
- Default filter = **Owing shops** (`runningBalance < -0.005`), sorted most-negative first (`ShopBalances.tsx:46-48`).
- Action per row: if `issuable > 0` → **"Issue invoice now"** calling `issueShopInvoice(shopId,'manual')` (`store.ts:877`). Else if `carried > 0` → text "Invoiced — awaiting payment". Else nothing.
- **Auto-bill at threshold**: `reconcileBilling()` (`store.ts`) issues a `threshold` invoice automatically when `carried >= threshold` and no open threshold invoice exists. `issueShopInvoice` nets held earnings into the amount due (`earningsApplied = min(held, grossFees)`, `finance.ts`).

### OUR side
- **No running-balance ledger.** No `charges` ledger table, no `shopRunningBalance`/`carried`/`threshold`/`issuable` concept. Grep for `running balance | carry.forward | credit.limit | threshold | owing | carried` across `backend/`, `common/models/`, `frontend/`, `api/` returns only Yii Gii boilerplate comments — no real implementation.
- We have `common/models/ShopEarning.php` + `Earnings.php` (per-booking earnings rows) and `Withdrawal` aggregates, but there is **no admin screen that shows "which shops owe Navagoo right now"** nor any auto-billing-at-threshold engine. Status: **MISSING**.

---

## 2. Transfer Requests (demo) ≈ OUR Withdrawal

### Demo logic
- List: `state.transfers` sorted newest-first (`TransferRequests.tsx:23`).
- Each `TransferRequest` (types.ts:446) carries the **stamped** totals: `totalEarned, totalTips, totalMarketingFees, totalPaymentProcessingFees, totalFeeVat, netPayout, amountPaid`, plus `status (requested|settled)`, `shopConfirmationUser`, `confirmationAt`, `settledAt`, `invoicePdf`, `receipt`, `bookingIds`, `settledInvoiceIds`.
- **Settle action** `settleTransfer(id, amountPaid, {invoiceUrl, bankConfirmationUrl})` (`store.ts:745`):
  1. Flips every charge with this `transferRequestId` to `paid`.
  2. Builds a NEW `transfer_settlement` invoice for the *uninvoiced* fee rows only (avoids double-invoicing fees already on a threshold invoice), `paidMethod='net_from_payout'`, status `paid`.
  3. Marks every invoice linked to the transfer `paid`.
  4. Records `amountPaid`, `settledAt`, `invoicePdf`, `receipt`, `settledInvoiceIds` on the transfer; logs activity.
- `netPayout` = `totalEarned + totalTips − totalMarketingFees − totalPaymentProcessingFees − totalFeeVat` (the FormulaStrip in `TransferRequests.tsx:188-205`).

### OUR logic — `WithdrawalController` + `common/models/base/Withdrawal.php`
- List: `WithdrawalController::actionIndex` (Tailwind layout), `WithdrawalSearch`, sorted `id DESC` (`WithdrawalController.php:58-78`).
- `Withdrawal` columns: `total_collected`, `total_navagoo_fees`, `total_tips`, `net_transferable_amount`, `amount_transferred`, `settlement_status` (`withdrawal/index.php:84-92`).
- Net amount is **pre-computed at creation** in `frontend/controllers/AgentsWalletController.php:386-459`: sums `ShopEarning` rows — `net_transferable_amount += shopEarning->net_collectible_amount`, `total_navagoo_fees += navagoo_marketing_fees`, `total_tips`, etc. (NOT recomputed at settlement). `Withdrawal::beforeSave` also stamps `totalNavagooVat` from `Earnings.sum(vat_navagoo)` (`base/Withdrawal.php:240-262`).
- **Settle action** = `WithdrawalController::actionUpdate` with hidden `settlement_form=1` (`WithdrawalController.php:154-276`):
  1. Sets `settlement_status = SETTLED` (from blank/IN_PROGRESS) and `settlement_date = now`.
  2. Uploads `navagoo_invoice` + `transfer_receipt` (+ `other_documents`) via `FileValidationConfig::validateDocument`.
  3. On save → `synchronizeRelatedSettlementStatus()` + `pushSpecialistSettlementTransactions()` (flips agent/earning settlement statuses and pushes agent `Transaction` rows).
- Status transitions: `actionUpdateSettlementStatus` moves REQUESTED → IN_PROGRESS only (`WithdrawalController.php:294+`); settlement-form moves → SETTLED.

### Logic gaps (Transfer Requests)
| Demo logic | Ours |
|---|---|
| Fee split: marketing / payment-processing / fee-VAT as separate stamped totals | Single `total_navagoo_fees` + separate `total_tips`; payment-processing fee not modeled as its own column; VAT stamped (`totalNavagooVat`) but not shown in the list |
| `netPayout` recomputed from stamped components via FormulaStrip | `net_transferable_amount` summed once at request creation from `ShopEarning.net_collectible_amount`; no on-screen formula breakdown of marketing/processing/VAT |
| Settlement auto-generates a `transfer_settlement` invoice + links/clears associated invoices | We upload a Navagoo invoice file manually; **no invoice entity is generated or linked**, no charge-ledger flip-to-paid |
| Amount-paid mismatch warning vs net payout (`|Δ|>0.01`) | No mismatch check; `amount_transferred` is free entry |
| `settleTransfer` is one atomic store action | Multi-step controller with file IO; settlement gated behind separate IN_PROGRESS transition first |
| Status model: `requested | settled` | Richer: NEW(-1), REQUESTED(0), IN_PROGRESS(1), SETTLED(2) — IN_PROGRESS is extra granularity not in demo |

### Refs
- Demo: `portals/admin/finance/TransferRequests.tsx`, `lib/finance.ts:560-640`, `store/store.ts:745-915`, `store/selectors.ts:168-200`, `types.ts:393-466`.
- Ours: `backend/controllers/WithdrawalController.php`, `backend/controllers/FinanceController.php`, `common/models/base/Withdrawal.php`, `frontend/controllers/AgentsWalletController.php:386-459`, `backend/views/withdrawal/{index,_settlement_form,_detail}.php`.
