Files
baya-monorepo/docs/status/decisions.md
T
2026-08-02 18:33:43 +03:30

10 KiB

Decisions — the distilled engineering decision log

Last verified: 2026-08-02 against commit b876490.

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/). 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 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 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 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 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 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 §6 requires rotating every credential and moving the secret half out of git before real users (see 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 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 + git-hook + test change that is out of a documentation phase's scope, and the string is load-bearing across 7 live files. Filed as backlog.md BL-219, deferred (cleanup-of-convenience, no urgency).

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 and 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 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 and 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'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 — a small, direct illustration of "verify, don't copy" applied one phase later than the item that requested it.