ADR 011 Booking Service Term Side Effect Assembly
Revision Notice
Section titled “Revision Notice”Accepted. Revised 2026-07-10.
This revision supersedes the original client-supplied charge-link contract and JSON-only revision persistence design in place; it does not supersede another ADR. Historical implementation commits belong in migration evidence.
Context
Section titled “Context”Legacy booking creation mixed service dates, add-option quantity, prices, report invalidation, and settlement input across PHP controllers and browser JavaScript. A customer or branch operator could submit a value that looked like a finalized charge link even though only the backend had enough evidence to decide:
- whether the selection belongs to the accepted service term;
- which accepted price quote line applies;
- whether that line is a customer charge;
- which finalized charge line freezes the amount;
- how many service occurrences are chargeable;
- which occurrence rows prove that quantity; and
- which report, calendar, payout, and settlement projections must refresh.
Historical revisions also contain incomplete evidence. Inferring canonical facts from those JSON snapshots would rewrite business history.
Decision
Section titled “Decision”Reservation orchestration owns one BookingServiceTermSideEffectAssembly
command boundary.
Inbound intent
Section titled “Inbound intent”Frontend sends addOptionChargeIntents. Each intent contains only:
addOptionSelectionKey;chargeComponentCode; andsourceEvidenceKeys.
Frontend never sends quantity, quantity basis, price quote lineage, finalized charge lineage, fee, subsidy, discount, payout, or settlement amounts.
Backend resolution
Section titled “Backend resolution”Backend resolves every intent against frozen facts:
- accepted service-term plan;
- accepted price-catalog quote;
- immutable service-occurrence ledger;
- finalized reservation charge snapshot; and
- required consumer impact facts.
Backend publishes BookingServiceTermAddOptionChargeLinkPayload only after it
derives:
- semantic add-option policy key;
- positive quantity;
- typed quantity basis;
- exact quantity-basis evidence keys;
- accepted price quote line key; and
- customer-charge component code.
Physical persistence additionally binds each link to the actual
price_catalog_entry_id and finalized_charge_line_id. Those database
identities are internal lineage, not client-selected fields.
Atomic command write
Section titled “Atomic command write”One service-role RPC,
sanmopia_record_booking_service_term_side_effect_assembly, atomically writes:
- assembly snapshot;
- optional add-option selection revision;
- canonical normalization state;
- normalized charge links;
- typed quantity-basis evidence rows; and
- projection invalidation outbox rows.
Direct service-role inserts into assembly, revision, normalized-link, evidence, and initial outbox records are revoked. Replay must be byte-equivalent for business fields. Conflicting replay fails.
Canonical and historical states
Section titled “Canonical and historical states”Every revision has one normalization state:
canonical: physical links and evidence are complete and validated;legacy_unresolved: historical JSON remains unchanged and cannot have inferred normalized rows.
Correcting legacy_unresolved history requires a new revision key with current
evidence. Migration never silently changes historical quantity or evidence.
Immutable evidence
Section titled “Immutable evidence”Canonical service-occurrence evidence cannot be updated or deleted. Once a canonical link references a ledger, that ledger cannot accept new occurrence rows. A changed schedule produces a new ledger/revision instead of mutating settled history.
CQRS read rule
Section titled “CQRS read rule”Command side writes through the aggregate RPC only. Query side reads revision identity, normalization state, physical links, and child evidence rows.
Query side reconstructs finalized link payloads only for canonical revisions.
Reading legacy_unresolved returns a corrected-revision-required error instead
of guessed data.
Component View
Section titled “Component View”flowchart LR ui[Static Astro booking cards] command[Reservation booking command] term[Accepted service term] quote[Accepted price quote] ledger[Occurrence ledger] charge[Finalized charge snapshot] assembler[Service term side effect assembler] rpc[Atomic Supabase RPC] revision[Add-option revision] links[Normalized charge links] evidence[Typed evidence rows] outbox[Projection invalidation outbox] consumers[Reports, payout, branch and HQ settlement] ui -->|intent only| command command --> assembler term --> assembler quote --> assembler ledger --> assembler charge --> assembler assembler -->|finalized aggregate| rpc rpc --> revision rpc --> links rpc --> evidence rpc --> outbox outbox --> consumers
Booking Sequence
Section titled “Booking Sequence”sequenceDiagram actor Customer participant UI as Astro static UI participant Booking as Booking application participant Policy as Term and pricing policies participant DB as Supabase Postgres participant Worker as Projection worker Customer->>UI: Select additional service day UI->>Booking: addOptionChargeIntent Booking->>Policy: Validate term, quote, occurrence, charge Policy-->>Booking: Quantity and immutable evidence Booking->>DB: record_booking_service_term_side_effect_assembly DB->>DB: Persist aggregate and outbox atomically DB-->>Booking: Exact count receipt Booking-->>UI: Finalized booking result Worker->>DB: Claim invalidations Worker->>DB: Refresh report and settlement projections
Invariants
Section titled “Invariants”| Invariant | Enforcement |
|---|---|
| Selected option belongs to accepted term | application validation and RPC set equality |
| Charge component exists exactly once | accepted quote and finalized-line checks |
| Charge direction is customer charge | application validation and composite database lineage |
| Quantity matches evidence | backend derivation and RPC evidence recomputation |
| Finalized price cannot drift | immutable snapshot and physical charge-line FK |
| Historical JSON stays unchanged | legacy_unresolved state; no inferred backfill |
| Assembly/revision/outbox cannot partially write | single database transaction |
| Replays cannot overwrite facts | insert-or-compare conflict checks |
| UI cannot calculate money | intent-only request contract |
Boundaries
Section titled “Boundaries”- Reservation orchestration assembles cross-context references.
- Service-calendar policy owns occurrence semantics.
- Pricing and settlement own quote and finalized charge facts.
- Supabase owns transactional persistence, relational integrity, RLS, and generated database contracts.
- Restate owns long-running workflow progress, not business amount calculation.
- SpiceDB owns actor authorization; database RLS is not the RBAC policy engine.
- Frontend remains static Atomic Design composition with Nano Stores for local interaction state only.
Tach must reject reservation application imports that bypass published pricing contracts or approved domain boundaries. CQRS does not justify duplicate models that carry the same mutable fact.
Consequences
Section titled “Consequences”Booking fails earlier when accepted context or evidence is incomplete. This is intentional. Payment, refund, payout, document, branch settlement, and HQ settlement must consume frozen facts rather than reconstructing PHP behavior.
Physical normalized rows increase schema surface but remove hidden JSON calculation and enable exact foreign keys, audits, and query projections.
Historical revisions remain readable as identity/audit records but are not presented as canonical charge evidence.
Rollout
Section titled “Rollout”Stage gate:
- focused application and migration tests pass;
- Supabase CLI dry-run lists only expected migration;
- migration succeeds inside a rollback transaction against current stage data;
- generated Supabase types refresh from migrated schema;
- real reservation reaches financial closeout, caregiver payout, branch settlement, and HQ settlement; and
- replay, mutation, and missing-evidence probes fail as designed.
Large production tables require separate expand, concurrent-index, validation, and contract phases. Stage success on a small dataset does not waive lock and load testing.
Implementation Evidence
Section titled “Implementation Evidence”- Application assembly boundary
- Supabase assembly adapter
- Normalized-link migration
- Public contract schema
Pinned links prove code, schema, and migration presence. Rollout remains partial until production-scale lock/load behavior and complete runtime path are proven.
Historical Verification Snapshot
Section titled “Historical Verification Snapshot”Following results were recorded 2026-07-10. This audit did not rerun stage or deployment workflows.
- Contract request/response gates pass with intent/finalized-link separation.
- Backend focused suite passes 116 tests after atomic adapter integration.
- Migration contract suite passes ten tests.
- Stage rollback migration compiles and classifies 17 historical revisions as
legacy_unresolvedwithout updating revision JSON. - Tach dependency, interface, exactness, and external-package gates pass.
- Stage migration and replay repair were applied through the official Supabase CLI; schema lint and generated database types pass.
- Stage reservation
d1b8165d-e0da-497f-a55a-e9d6ecb6b924reachedsettled: 150,000 KRW customer payable, 50,000 KRW caregiver payout, 100,000 KRW branch settlement, 50,000 KRW HQ settlement, and 25,000 KRW service balance. - Canonical occurrence, finalized charge, catalog, and outbox mutation probes were rejected. Byte-equivalent finalized-line replay passed during service delivery.
Revision History
Section titled “Revision History”- 2026-07-08: initial side-effect assembly decision.
- 2026-07-10: separated client intent from finalized link, added normalized immutable evidence, atomic aggregate RPC, explicit historical state, and CQRS read rules.