Files
baya-monorepo/archive/build-chain/working-context/reports/backend-phase-13-report.md
T
2026-08-02 18:48:32 +03:30

6.2 KiB
Raw Blame History

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.
    • NursePayoutBookingLinksunconditional 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 previewGET 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 batchPOST 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 shiftperiod_end/processing_date shift off a seeded bank-closed day. (unit: Holiday_shifts_period_end_and_processing_date)
  5. ExecutePOST 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 historyGET 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.