backend phase 11

This commit is contained in:
hamid
2026-07-09 02:13:30 +03:30
parent 23605591eb
commit 465f75c29e
78 changed files with 9555 additions and 27 deletions
@@ -44,6 +44,24 @@
</ul>
<h2 id="d-supporting-database-entities">(d) Supporting database entities <a class="anchor" href="#d-supporting-database-entities" aria-hidden="true">#</a></h2>
<p><strong><code>cancellation_policies</code></strong>, <code>bookings</code> (policy snapshot, <code>dispute_window_ends_at</code>), <code>refunds</code> (admin-only, <code>ticket_id</code>, fee-leg decomposition, <code>refund_channel</code>), <code>tickets</code>, <code>nurse_clawbacks</code> (post-payout case), <code>ledger_entries</code>.</p>
<h2 id="e-as-built-backend-phase-11">(e) As built (backend-phase-11) <a class="anchor" href="#e-as-built-backend-phase-11" aria-hidden="true">#</a></h2>
<p>Decisions fixed while building the refund/clawback/invoice engine (config-driven where possible):</p>
<ul>
<li><strong>Refund channel canonical code:</strong> the out-of-band bank refund is stored and served as <strong><code>manual</code></strong> (the wire</li>
</ul>
<p> code), not <code>manual_bank</code> — they are the same channel. Full set: <code>psp_card</code> | <code>bnpl_revert</code> | <code>manual</code>.</p>
<ul>
<li><strong>Ticket link is config-gated until b15.</strong> <code>refunds.ticket_id</code> is a nullable column with <strong>no FK yet</strong></li>
</ul>
<p> (<code>tickets</code> arrives in b15). "A refund must link a ticket" is enforced by the <code>refund_ticket_required</code> config flag, <strong>default <code>false</code></strong> so admin refunds are testable today; b15 wires the FK and flips it on.</p>
<ul>
<li><strong>BNPL refund ETA</strong> is <code>now + config(bnpl_refund_eta_business_days, default 10)</code> business days (Fridays skipped),</li>
</ul>
<p> surfaced as <code>expected_customer_refund_eta</code>; the refund sits in <code>processing</code> until reconciled. Card refunds are immediate (<code>succeeded</code>, no ETA). Card and BNPL post the <strong>same</strong> reversal ledger legs.</p>
<ul>
<li><strong>Pre-payout vs post-payout fork</strong> is decided by whether the nurse was already paid — derived from the booking's</li>
</ul>
<p> <code>dispute_window_ends_at</code> close until b13 ships <code>nurse_payouts</code> (a <code>refund_assume_nurse_paid</code> override exists). Clawback <strong>recovery/netting is b13</strong>; b11 only opens the <code>pending</code> receivable + supports admin write-off.</p>
<blockquote><p><strong>Related:</strong> Data model — <a href="../data-model/06-payments-ledger-and-refunds.html">Payments Ledger &amp; Refunds</a>.</p>
</blockquote>
<a class="back-to-top" href="#">↑ Back to top</a>
@@ -25,4 +25,18 @@
## (d) Supporting database entities
**`cancellation_policies`**, `bookings` (policy snapshot, `dispute_window_ends_at`), `refunds` (admin-only, `ticket_id`, fee-leg decomposition, `refund_channel`), `tickets`, `nurse_clawbacks` (post-payout case), `ledger_entries`.
## (e) As built (backend-phase-11)
Decisions fixed while building the refund/clawback/invoice engine (config-driven where possible):
- **Refund channel canonical code:** the out-of-band bank refund is stored and served as **`manual`** (the wire
code), not `manual_bank` — they are the same channel. Full set: `psp_card` | `bnpl_revert` | `manual`.
- **Ticket link is config-gated until b15.** `refunds.ticket_id` is a nullable column with **no FK yet**
(`tickets` arrives in b15). "A refund must link a ticket" is enforced by the `refund_ticket_required` config
flag, **default `false`** so admin refunds are testable today; b15 wires the FK and flips it on.
- **BNPL refund ETA** is `now + config(bnpl_refund_eta_business_days, default 10)` business days (Fridays skipped),
surfaced as `expected_customer_refund_eta`; the refund sits in `processing` until reconciled. Card refunds are
immediate (`succeeded`, no ETA). Card and BNPL post the **same** reversal ledger legs.
- **Pre-payout vs post-payout fork** is decided by whether the nurse was already paid — derived from the booking's
`dispute_window_ends_at` close until b13 ships `nurse_payouts` (a `refund_assume_nurse_paid` override exists).
Clawback **recovery/netting is b13**; b11 only opens the `pending` receivable + supports admin write-off.
> **Related:** Data model — [Payments Ledger & Refunds](../data-model/06-payments-ledger-and-refunds.md).
@@ -111,6 +111,27 @@
<tr><td><code>issued_at</code></td><td>DATETIME2</td><td></td></tr>
</tbody></table></div>
<p><strong>Relations:</strong> 1:1 → <code>bookings</code>; N:1 → <code>partner_centers</code> (when issuer).</p>
<h3 id="as-built-backend-phase-11">As built (backend-phase-11) <a class="anchor" href="#as-built-backend-phase-11" aria-hidden="true">#</a></h3>
<ul>
<li><strong><code>refunds.refund_channel</code> canonical code is <code>manual</code></strong> (not <code>manual_bank</code>) for the out-of-band bank refund —</li>
</ul>
<p> the two are the same channel; <code>manual</code> is the value stored and served. Set: <code>psp_card</code> | <code>bnpl_revert</code> | <code>manual</code>.</p>
<ul>
<li><strong><code>refunds.ticket_id</code></strong> ships as a <strong>nullable column with no FK</strong> (the <code>tickets</code> table arrives in b15); the</li>
</ul>
<p> "ticket required" rule is gated by the <code>refund_ticket_required</code> config flag (default off). <strong><code>nurse_clawbacks.original_payout_id</code> / <code>recovered_in_payout_id</code></strong> and <strong><code>invoices.partner_center_id</code></strong> are likewise nullable, FK-less join points that b13 / b15 fill.</p>
<ul>
<li><strong><code>invoices.vat_irr = round(platform_commission_irr × vat_rate)</code></strong>, integer-only, on the commission line only; a</li>
</ul>
<p> <code>vat_rate = 0</code> exemption yields <code>vat_irr = 0</code>.</p>
<ul>
<li><strong><code>invoices.invoice_number</code></strong> is drawn from a single-row <strong>counter table</strong> (<code>invoice_number_sequences</code>), locked</li>
</ul>
<p> and committed with the invoice insert — gap-free, unique, portable across SQL Server / SQLite (no DB sequence), never random/timestamp-derived. One issued invoice per booking (idempotent).</p>
<ul>
<li>The refund posts the balanced reversal via the b10 ledger helper; the <strong><code>refund_payable ↔ escrow_held</code> clearing</strong></li>
</ul>
<p> posts immediately for a succeeded card refund and is deferred to reconciliation for a <code>processing</code> BNPL/manual refund. Card and BNPL post the <strong>same</strong> reversal legs.</p>
<a class="back-to-top" href="#">↑ Back to top</a>
</div></main>
</div>
@@ -118,3 +118,19 @@ This is the most-changed domain. The previous model **inferred** money state fro
| `issued_at` | DATETIME2 | |
**Relations:** 1:1 → `bookings`; N:1 → `partner_centers` (when issuer).
### As built (backend-phase-11)
- **`refunds.refund_channel` canonical code is `manual`** (not `manual_bank`) for the out-of-band bank refund —
the two are the same channel; `manual` is the value stored and served. Set: `psp_card` | `bnpl_revert` | `manual`.
- **`refunds.ticket_id`** ships as a **nullable column with no FK** (the `tickets` table arrives in b15); the
"ticket required" rule is gated by the `refund_ticket_required` config flag (default off).
**`nurse_clawbacks.original_payout_id` / `recovered_in_payout_id`** and **`invoices.partner_center_id`** are
likewise nullable, FK-less join points that b13 / b15 fill.
- **`invoices.vat_irr = round(platform_commission_irr × vat_rate)`**, integer-only, on the commission line only; a
`vat_rate = 0` exemption yields `vat_irr = 0`.
- **`invoices.invoice_number`** is drawn from a single-row **counter table** (`invoice_number_sequences`), locked
and committed with the invoice insert — gap-free, unique, portable across SQL Server / SQLite (no DB sequence),
never random/timestamp-derived. One issued invoice per booking (idempotent).
- The refund posts the balanced reversal via the b10 ledger helper; the **`refund_payable ↔ escrow_held` clearing**
posts immediately for a succeeded card refund and is deferred to reconciliation for a `processing` BNPL/manual
refund. Card and BNPL post the **same** reversal legs.