# 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 (`<UserSecretsId>` 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.
