backend phase 10

This commit is contained in:
hamid
2026-07-06 21:17:00 +03:30
parent 12c7e51c32
commit aae056b4e5
70 changed files with 8124 additions and 73 deletions
+17
View File
@@ -58,6 +58,23 @@ DEBIT nurse_clawback_receivable amount_irr (nurse_id set; nurse now owes th
```
Recovered by **netting against the nurse's next `nurse_payable`** at batch time, or marked `written_off` if uncollectable. A `nurse_clawbacks` row carries the lifecycle (`pending` / `recovered` / `written_off`). This is unavoidable because **Iranian payouts are real bank transfers — hard/impossible to reverse** — so the right defense is *gating payout on the dispute window*, with clawback as the fallback.
## 3.2a Implementation rules confirmed in build (backend b10)
These are the exact orderings the card-capture rail was built to (see `dev/contracts/domains/payments.md`):
- **Upsert the webhook event first, then re-verify server-side, then confirm.** Every PSP callback lands in
`payment_webhook_events` keyed on `UNIQUE(provider_code, external_event_id)` **before** any money state
changes; a duplicate replay is a **no-op**. Only a new, signature-valid *success* event proceeds — and it is
**re-verified against the gateway** (amount + reference) before the capture posts. A callback with an invalid
signature is stored `ignored` and mutates nothing (**never trust a callback alone**).
- **A unique-violation on confirm is an idempotent no-op success, not an error.** The filtered
`UNIQUE(booking_id) WHERE status='succeeded'` is the authoritative anti-double-capture backstop: if a
concurrent confirm races past the Redis lock, the second one fails the constraint and is treated as
"already captured → success".
- **Payment is initiated against the accepted *request*, not a booking.** Because a `bookings` row exists only
on capture, `payment_transactions.booking_id` is **nullable** and bound at confirm (when the booking is
created/loaded). The captured gross is the request's frozen amount (variant price × session count).
## 3.3 Why the ledger, not more columns
A marketplace that holds escrow, pays out weekly minus commission, and handles refunds + clawbacks has exactly the shape double-entry was invented for. The MVP cost is **one table + posting discipline**. The alternative (more money columns on bookings/payouts) cannot answer "how much is held but unreleased" without fragile joins and makes bank/Shaparak reconciliation nearly impossible. Keep the per-booking fee snapshot as the *pricing* record; the ledger is the *financial-truth / reconciliation* layer posted alongside.