backend phase 13 & frontend phase 6

This commit is contained in:
hamid
2026-07-09 04:09:35 +03:30
parent dc64472631
commit de53f9d8a6
97 changed files with 11969 additions and 77 deletions
@@ -0,0 +1,76 @@
# Backend Phase 13 report — Weekly nurse payouts (mocked bank transfer)
**Date:** 2026-07-09 · **Track:** backend · **Status:** complete, gate green.
## What was built
- **`payouts` schema, 3 tables** (one migration `NursePayoutEngine`):
- `NursePayoutBatches` — weekly aggregation; `period_end`/`processing_date` holiday-shifted; `total_amount` /
`payout_count`; `status` (`draft|processing|partially_failed|completed|failed`); `initiated_by_admin_id` FK.
- `NursePayouts` — one per nurse per batch; DB CHECK `net = gross clawback` + all ≥ 0; **encrypted
`iban_snapshot`** (EF converter) frozen from the verified primary account; `status` (`pending|submitted|paid|failed`,
forward-only); `transfer_reference`, `paid_at`, `failure_reason`.
- `NursePayoutBookingLinks`**unconditional `UNIQUE(booking_id)`** (the one-payout-per-booking-ever guard);
nullable `session_id` for a future per-session model.
- **Domain:** `PayoutBatchStatus`/`PayoutStatus` + `*Transitions`; `LedgerPosting.NursePayout` (DEBIT nurse_payable /
CREDIT escrow_held) + `LedgerPosting.ClawbackRecovery` (DEBIT nurse_payable / CREDIT nurse_clawback_receivable);
`NurseClawback.Recover(payoutId, now)`.
- **Application:** `Features/Payouts/{Commands|Queries}``ComputeEligibleEarnings`, `GeneratePayoutBatch`
(build+link inline), `ExecutePayoutBatch`, `RetryFailedPayout`, `MarkPayoutFailed`, `GetBatchDetail`,
`ListPayoutBatches`, `GetNursePayoutHistory`; shared `PayoutSettlement` (ledger post + clawback netting).
`IPayoutRepository` on `IUnitOfWork`.
- **Infrastructure:** `PayoutRepository`, `PayoutsConfig/*`, `MockBankTransferProvider` (+ `BankTransferOptions`),
the authoritative `NursePayoutLinkStatusService` (swapped for the deleted interim `NursePayoutStatusService`),
`iban_snapshot` encryption wired in `ApplicationDbContext`, 2 new `platform_configs` seeds.
- **API:** `AdminPayoutsController` (admin, rate-limited) + `NursePayoutsController` (nurse, tenancy-scoped).
## What is now testable and exactly how (per §7 of the phase)
Seed a few **completed** bookings via the admin flow: some with `dispute_window_ends_at` in the past (eligible), some
future (not yet), one disputed, one with a pending clawback; one nurse with a verified primary IBAN, one without.
1. **Eligibility preview**`GET admin_payouts/eligible?periodStart=&periodEnd=` → only completed + dispute-window-
closed, unpaid bookings appear, grouped by nurse; future + disputed excluded; the no-IBAN nurse flagged
(`hasVerifiedPrimaryIban=false`). *(unit: `Preview_includes_only_closed_window_and_flags_missing_iban`)*
2. **Generate a batch**`POST admin_payouts/batches` → a `draft` batch, one payout per eligible nurse; the nurse
with a pending clawback shows `clawbackAppliedIrr>0` and `net = gross clawback`; `total_amount = Σ net`;
`iban_snapshot` populated (encrypted, served masked). *(unit: `Generate_materializes_one_payout_per_nurse_and_nets_clawback`,
`Generate_skips_nurse_without_verified_primary_iban_with_reason`)*
3. **Double-pay guard** — a second generate over the same window doesn't re-select the linked bookings.
*(unit: `Double_pay_guard_second_generate_does_not_reselect_linked_bookings`)*
4. **Holiday shift**`period_end`/`processing_date` shift off a seeded bank-closed day.
*(unit: `Holiday_shifts_period_end_and_processing_date`)*
5. **Execute**`POST admin_payouts/batches/{id}/process` → payouts go `paid` with a `transfer_reference`; the ledger
shows balanced `DEBIT nurse_payable / CREDIT escrow_held` per payout (the payable balance drops by the paid amount);
a netted clawback is marked `recovered` with `recovered_in_payout_id`. *(unit:
`Execute_posts_balanced_payout_ledger_and_drains_payable`, `Execute_recovers_clawback_and_posts_recovery_leg`)*
6. **Idempotency** — re-process → no second transfer / ledger group. *(unit: `Reprocess_is_idempotent_no_second_ledger_group`)*
7. **Failure / retry** — force a rail failure → `partially_failed`; `retry` (rail back to success) → `paid`, batch
`completed`. *(unit: `Partial_failure_then_retry_completes_the_batch`)*
8. **Nurse history**`GET nurse_payouts/history` as the nurse → their payouts (masked IBAN, net, reference);
another nurse's are invisible. *(api: `NursePayoutsApiTests`)*
API happy-path/401/400: `AdminPayoutsApiTests` (generate→process pays the nurse; 401 unauth; 400 bad period; list)
and `NursePayoutsApiTests` (401 unauth; own paid payout with masked IBAN).
## What is mocked + how to make it real
- **`IBankTransferProvider`** (🟡) — PAYA/SATNA rail. Make real = a Jibit/Vandar/Sadad payout adapter with a
registered source settlement account, per-nurse verified Sheba, PAYA-vs-SATNA selection, batch caps/minimums, and
the async reconciliation callback that flips `submitted → paid/failed`. Config keys `Seams:BankTransfer:*`.
- **`INursePayoutStatus`** (🟢) — now the real link-based lookup (`NursePayoutLinkStatusService`).
- Reused mocks: `IHolidayCalendar`, `IFieldEncryptor`, `IDistributedLock`, `ICacheService`.
## Contracts produced
- `dev/contracts/domains/payouts.md` (new) · `dev/contracts/openapi/swagger.v1.json` refreshed (7 payout paths).
## Confirmed rules recorded (product/business/10-payouts.md §d1)
- Clawback netting recovers **whole** clawbacks up to a batch's earnings (never negative net, never a partial single
clawback); a clawback larger than a batch's earnings waits for a later batch. Recovery is a real ledger movement.
- A booking with an active refund is held out of payouts (the operational reading of "no open dispute").
- `payout_satna_threshold_irr` picks PAYA vs SATNA; `require_bnpl_settlement_for_payout` (default off) gates BNPL.
## Follow-ups (deferred)
- The weekly **cron scheduler** (PAYA-aligned) — entry point is `GeneratePayoutBatchCommand`; cadence in
`nurse_payout_interval_days`. On-demand/instant withdrawal; per-nurse payout frequency; automated clawback recovery
beyond next-batch netting; the BNPL `settled_at` timing guard (flag shipped, off).
## Gate
`dotnet build Baya.sln` — 0 errors, 0 new code warnings (only pre-existing NU1510/NETSDK1057/NU1903).
`dotnet test Baya.sln` — 329 pass (223 foundation + 102 api + 4 identity), 0 fail.