# Decisions — the distilled engineering decision log > Last verified: 2026-08-02 against commit `51e86a1`. Non-obvious decisions with a reason, extracted from `dev/`'s ~3MB of build history so they survive `dev/`'s move to `archive/` in phase 6. **`product/` wins for business rules** — this file is for *engineering* decisions, and for business decisions made **during** the build that never made it back into `product/` (each of those also gets a note filed in [product/notes/](../../product/notes/open-questions.md)). Each entry: what was decided, when, why, where it binds. --- ## Business rules made during the build (candidates for `product/` too) **Commission 0.15 / VAT 0.10, VAT on commission only.** The canonical fee model as of refinement-phase-3: platform commission is 15% of the gross booking amount; VAT is 10% of the *commission*, not of gross. This became the single source of truth after the pre-refinement code computed VAT two different ways on two surfaces (carved out of commission at checkout vs. added to it on the invoice, a 341 IRR disagreement on one seeded booking — see [backlog.md](backlog.md) BL-060, still open on the invoice screen specifically). Binds: `server/src/Core/Baya.Application/Features/Payments/`, every invoice/checkout DTO. **`district_id = NULL` means whole-city, in both directions.** A nurse service area with no district covers every district in the city; a search with no district filter matches both district-scoped and whole-city rows. Binds: `nurse_service_areas`, `SqlNurseSearch`, the coverage-picker UI. See [backlog.md](backlog.md) BL-077 for the one place this invariant currently double-counts a nurse. **Verification `status` is the source of truth; `is_verified` is a guarded flip.** The nurse-facing status enum drives all business logic; `nurse_profiles.is_verified` only flips inside the same transaction that moves status to `approved`, never independently. Decided backend-phase-6. Binds: `NurseVerificationService`, `nurse_search_index.is_searchable`. **Booking status is forward-only; the three-amount split has a DB CHECK constraint.** No booking status transition may move backward, enforced in `BookingTransitions`/`BookingSessionTransitions` (CONVENTIONS §6 pattern). The `gross`/`platform_commission`/`nurse_payout` three-way split on a booking is CHECK-constrained to balance at the DB layer, not just in application code. Decided backend-phase-9. **The two-stage clinical-disclosure gate; EVV is advisory, never a block.** Care details are disclosed in two stages — coarse notes pre-acceptance, full encrypted `booking_care_instructions` only to the assigned nurse post-confirmation. A geofence mismatch on check-in/out (EVV) raises a support alert but never blocks the visit from proceeding — decided explicitly to avoid a GPS false-positive stranding a nurse mid-shift. Binds: `booking_requests` vs `booking_care_instructions`, `CheckInVisitCommand`/`CheckOutVisitCommand`. **Webhook idempotency is upsert-first; ledger postings must balance.** Every external webhook handler (payment PSP, BNPL, payout transfer) upserts on the provider's idempotency key *before* any side effect, so a replayed webhook is a no-op rather than a double-post. Every `LedgerPosting` is validated to balance (debits = credits) before commit — decided backend-phase-10, the same phase that found the payment-webhook confirm path needs two DB commits (booking creation, then transaction+ledger) because ledger legs need the DB-generated `booking_id`; flagged as future hardening once `IUnitOfWork` grows a transaction scope (see [backlog.md](backlog.md) BL-253). **Reviews recompute from source; nurse care records are append-only.** A nurse's `average_rating`/ `total_reviews` are always recomputed from the live review rows, never incrementally maintained, to avoid drift. Visit-note/care-record writes are append-only — no record is ever edited or deleted, only superseded by a newer entry. Decided backend-phase-14. **`is_internal` ticket messages never appear in user-facing types.** The admin-only internal-note boundary on a support ticket is enforced by keeping `isInternal` out of every client-facing TypeScript type entirely — not by a runtime filter that could be bypassed. Decided frontend-phase-14, re-confirmed frontend-phase-15 when the admin console was built on top of the same domain. **One payout per booking (UNIQUE); whole-clawback greedy netting.** A `nurse_payouts` row is UNIQUE-constrained to one per booking — a booking can never be paid out twice. Clawback recovery uses greedy whole-amount netting against the next batch rather than partial installments. Decided backend-phase-13; automated recovery beyond simple netting is deferred (see [backlog.md](backlog.md) BL-242). **Escrow releases after a confirmed check-out; weekly payout generation is automatic, processing stays manual.** Funds move from `escrow_held` to `nurse_payable` only after a checked-out session passes its dispute window. Payout *batch generation* runs on a weekly cron (refinement-phase-7); actually *transferring* money in a batch stays a deliberate, explicit admin action by design — decided refinement-phase-7, re-affirmed in the backend-phase-13 handoff. Not a bug; see [backlog.md](backlog.md) BL-240. **Config lives in files, not a secret store — deliberate pre-launch trade.** `dotnet user-secrets` was removed (`` dropped from the `.csproj`); all configuration, including live credentials, lives in `appsettings.*.json` / `.env.*` / `docker-compose.yml`. This is explicitly temporary — root [CLAUDE.md](../../CLAUDE.md) §6 requires rotating every credential and moving the secret half out of git before real users (see [backlog.md](backlog.md) BL-003). The one value that must **never** change once real data exists: `Seams:FieldEncryption:Key`/`:HashKey`. **Error state is never an empty state (the client convention).** A failed query must never silently render the same UI as "no data" — decided in the frontend-phase-1 primitives pass. Still occasionally violated; see [backlog.md](backlog.md) BL-078 for the one live regression found this phase (`nurse-service-areas`'s coverage screen drops `isError`). **Root `/` forks by auth via a middleware rewrite, never a redirect.** An anonymous visitor to `/` gets the public landing page's content rewritten in at the same URL; the canonical URL and address bar never change. Decided ui-phase-13, chosen specifically so `/` stays a stable, shareable, indexable URL for both audiences. --- ## Engineering decisions **Phase 2 → Phase 4 handoff, "the rename is filed."** The placeholder secret sentinel `SET_VIA_USER_SECRETS_OR_ENV` keeps its name — renaming it needs a server-code + test change that is out of a documentation phase's scope, and the string is load-bearing across several live files. Filed as [backlog.md](backlog.md) BL-219, deferred (cleanup-of-convenience, no urgency). **Phase 7 dropped the pre-commit secret-scan hook — MVP stage, no need for it yet.** `.githooks/pre-commit` (the `qw123321`/private-key/AWS-key/SQL-host/connection-string scan) and `.githooks/README.md` were deleted outright, along with every doc reference to them (root `CLAUDE.md`'s repo-layout table and quick start, [git-and-gates.md](../rules/shared/git-and-gates.md), [documentation.md](../rules/documentation.md), [identity.md](../rules/server/identity.md), [config-matrix.md](../integration/config-matrix.md), [DEPLOY.md](../../DEPLOY.md)). The underlying trade this repo already made — committed live credentials, config in files not a secret store (root [CLAUDE.md](../../CLAUDE.md) §6) — is unchanged; this only removes the local mechanical backstop against a *new* leak. Revisit before onboarding real users, alongside the credential rotation already required by that trade. **Hardening ledger re-verification (C-10) confirms the ledger was right to distrust its own checkboxes.** All 18 hardening items were re-traced against `b876490` rather than trusted as-filed: 3 were already fixed (client-only, landed in the "manual improvement" commits well before this doc chain started), 4 are partially fixed (real progress with concrete residue), and 11 are unchanged since 2026-07-16 despite 14 UI phases, 2 manual-testing iterations, and a deploy running on top of them. See [backlog-closed.md](backlog-closed.md) and [backlog.md](backlog.md) for the full disposition of each. **REQ-061 (admin user directory) is filed for real, despite never getting a ledger header (C-15).** `ui-phase-11-report.md` claimed "REQ-061…064 appended," but the append-only ledger's numbering jumps 060→062 — REQ-061's body survived as an orphaned, headerless block. Ten live client files depend on the endpoints it describes. Ruling: it is a genuine, currently-undelivered backend gap and is carried into [backlog.md](backlog.md) as BL-029, not treated as a documentation artifact to discard. **Five REQs were filed narrower or wrong relative to the live server (C-16).** REQ-050, REQ-063, REQ-066, REQ-067 and the REQ-029/030 admin-mock justification were all checked against the live swagger rather than against another document, during phase 2. `variantLabel` already shipped before REQ-050 was filed; `tickets/close`+`/reopen` already ship (only `assign` is missing); `search/nurses` and `nurses/{id}/profile` are already anonymous (only rate-limiting and a privacy sign-off are missing, respectively). Ruling: REQ status is derived from code, never copied from the ledger's own prose — this phase's REQ classification (see [backlog.md](backlog.md) and [backlog-closed.md](backlog-closed.md)) followed the same rule throughout, and found one further instance on its own: REQ-050's "neither field exists" claim was stale even for the field it explicitly named (`variantLabel`), and REQ-057's residue was found duplicated across two unrelated flows independently citing the same missing DTO field. **A `true` mock flag does not mean the domain is fully fake, and a `false` flag does not mean it's fully real.** `verification` has 10 of 14 operations live and probed while mocked; `payment` is flag-real with only 2 of 6 operations actually working. Decided as the framing for this whole reconciliation (phase 3's finding) — every BL item in this backlog that touches a `USE_*_MOCK` flag states the *specific* operations affected, never just cites the flag. **Deferred items keep their pull-trigger, not a target date.** Following the phase-chain convention set in `dev/shared-working-context/backend/handoff/`, every item in [backlog.md](backlog.md)'s Deferred section carries the condition that should cause it to be picked up (a specific product decision, a scale threshold, a second integration) rather than a scheduled date. Phase 5 owns turning these into a sequenced roadmap. **BL-245's own trigger ("phase 5 verification pass") was executed during phase 5.** `ResolveSupportAlert`, `AssignSupportAlert`, and nurse suspension (`AdminSuspendVerificationCommand`) were all confirmed real and code-traced; only `FlagConcern` is genuinely unbuilt. Recorded here rather than left as an open question in [roadmap/deferred.md](../roadmap/deferred.md) — a small, direct illustration of "verify, don't copy" applied one phase later than the item that requested it.