# Threat Model — OMT / Recharge Cell Shop > **Purpose.** Document, before any code is written, every realistic way an > employee (or a colluding pair) can steal money or goods from the shop, and > the specific control the system must enforce to block each path. Every later > migration, RLS policy, trigger, and UI rule must trace back to a row in this > table. > > This file is **append-only** in spirit: when a new theft vector is > discovered, add a row; do not delete history. ## Actors | Actor | Description | | ---------- | -------------------------------------------------------------- | | `cashier` | Operates a till during a shift. Highest fraud-risk role. | | `manager` | Approves voids, refunds, overrides. Can collude with cashier. | | `owner` | Read-everything. Surprise inspections. Sets prices/fees. | | `auditor` | Read-only third party (accountant). | | `customer` | May be an accomplice (fake refund, fake cancellation). | ## Trust boundaries 1. **Browser/POS ↔ Supabase**: client is hostile. Never trust client-supplied prices, fees, FX rates, timestamps, user IDs, or shift IDs. 2. **App role ↔ Postgres**: even the service role must not be able to `DELETE` from the ledger or rewrite hashes. Enforce with table grants + triggers. 3. **Shop ↔ Provider (OMT / Alfa / touch / Bank)**: the provider's statement is the source of truth for reconciliation. Any local row not present on the provider statement is suspect. ## Theft vectors and required controls | # | Vector | Control (must exist before go-live) | Enforced in | | -- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------- | | 1 | Pocket cash, never enter the transaction | Mandatory printed receipt, sequential `reference_no` per shop, customer SMS with reference, gap-detection report | DB + server | | 2 | Enter transaction, then edit / delete it after customer leaves | Append-only ledger; `UPDATE`/`DELETE` revoked; void requires reason + manager PIN; row hash chain | DB triggers | | 3 | Recharge own/friend's number flagged as "test" | No "test" flag exists; every recharge debits stock or e-float | Schema | | 4 | Sell a scratch card, keep cash, claim "lost card" | Voucher serial scanned on intake **and** on sale; loss requires manager approval; variance assigned to cashier | DB + UI | | 5 | Use shop OMT terminal to send to themselves at zero / reduced fee | Fee schedule server-side and immutable per cashier; OMT statement reconciliation; alert on cashier as sender or beneficiary | DB + recon job | | 6 | Manipulate FX rate (e.g. "I gave 89,000 LBP/USD instead of 90,000") | `fx_rates` populated by scheduled job; cashier selects, never types; rate stamped on each txn | DB + cron | | 7 | Skim cash from drawer, blame "shortfall" | Blind closing count (declared before expected revealed); per-cashier variance trend; chronic shortage alert | UI + report | | 8 | Collect customer cash, "deposit later", never deposit | Forced shift close before leaving; aging-outstanding alert > N hours; bank-deposit reconciliation | DB + recon | | 9 | Share login with a colleague | Per-user PIN re-prompt, device binding, session timeout, no shared accounts | Auth | | 10 | Print fake receipts on a second printer | Receipt carries server-signed QR (JWT of `txn_id`); spot-scan by owner returns DB row or "FAKE" | Server | | 11 | Refund/void to themselves | Void/refund requires manager PIN on same device; cashier cannot self-approve; daily void report per cashier | DB + UI | | 12 | Sell recharge below price ("friend discount") | Price list server-controlled; cashier UI has no price field; override only by manager and logged | DB + UI | | 13 | Take goods (phones/accessories) without sale | Per-shift inventory count; CCTV ↔ txn time sync; variance report | UI + ops | | 14 | Tamper with the closing count | Declared count entered first and locked; expected revealed only after; both stored | DB | | 15 | Accomplice customer "cancels" after cash handed | Cancellations require manager + reason + photo of voided receipt; CCTV cross-check | UI + ops | | 16 | Structure large transfers under multiple fake walk-in identities | KYC threshold per (customer, day) and (beneficiary, week); customer record mandatory ≥ threshold | DB + AML report | | 17 | Off-hours transaction when nobody is watching | Shift hours per shop; after-hours flag + alert | DB + alert | | 18 | Manager–cashier collusion to mass-void real sales | Void rate per (cashier, manager) pair trended; owner-only weekly review; voids count against shift variance | Report | | 19 | Replay an old OMT receipt to a new customer | `external_ref` unique per provider; duplicate detection on insert | DB constraint | | 20 | Cashier opens a second, undeclared till on the same device | One open shift per `till_id`; device fingerprint pinned to till | DB + auth | | 21 | Cashier marks recharge "failed at provider" and keeps cash | Provider e-recharge response stored as `external_ref` and reconciled; "failed" requires provider failure id | DB + recon | | 22 | Cashier "exchanges currency" at a worse rate than recorded, pocketing the spread | FX swap is a typed `cash_movement` with both legs at the system rate; deviation requires manager override | DB | | 23 | Cashier deletes their browser data to "lose" pending offline transactions | Offline queue persisted with server-issued idempotency key; missing key sequences flagged on reconnect | Client + server | | 24 | Insider (developer/DBA) silently edits the database | Hash chain on ledger; daily hash anchor exported off-site; restricted DB roles; audit of all DDL and privileged SQL | DB + ops | | 25 | Backdated transaction to fit a doctored shift count | `occurred_at` server-side `now()`; cashier cannot set; backdate only by owner role with reason | DB | ## Non-negotiables (no go-live without these) 1. RLS on every table; no table is publicly readable or writable. 2. `INSERT`-only ledger with row hash chain; `DELETE` revoked from every role. 3. All money writes go through `SECURITY DEFINER` Postgres functions, not raw table writes. 4. Server-controlled prices, fees, and FX rates. No cashier-typed money rules. 5. Blind cash close per shift, with declared-vs-expected variance stored. 6. External reconciliation (OMT, Alfa, touch, bank) before any month-close. 7. Per-user account, device-bound, with PIN re-prompt for sensitive ops. 8. Receipt with signed QR linking back to the ledger row. ## Review cadence - Every new feature PR must reference at least one row above (or add one). - Quarterly walk-through: pick 5 random rows, demonstrate the control still works on a staging environment.