Skip to content

ADR 011 Booking Service Term Side Effect Assembly

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.

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.

Reservation orchestration owns one BookingServiceTermSideEffectAssembly command boundary.

Frontend sends addOptionChargeIntents. Each intent contains only:

  • addOptionSelectionKey;
  • chargeComponentCode; and
  • sourceEvidenceKeys.

Frontend never sends quantity, quantity basis, price quote lineage, finalized charge lineage, fee, subsidy, discount, payout, or settlement amounts.

Backend resolves every intent against frozen facts:

  1. accepted service-term plan;
  2. accepted price-catalog quote;
  3. immutable service-occurrence ledger;
  4. finalized reservation charge snapshot; and
  5. 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.

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.

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.

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.

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.

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
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
InvariantEnforcement
Selected option belongs to accepted termapplication validation and RPC set equality
Charge component exists exactly onceaccepted quote and finalized-line checks
Charge direction is customer chargeapplication validation and composite database lineage
Quantity matches evidencebackend derivation and RPC evidence recomputation
Finalized price cannot driftimmutable snapshot and physical charge-line FK
Historical JSON stays unchangedlegacy_unresolved state; no inferred backfill
Assembly/revision/outbox cannot partially writesingle database transaction
Replays cannot overwrite factsinsert-or-compare conflict checks
UI cannot calculate moneyintent-only request contract
  • 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.

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.

Stage gate:

  1. focused application and migration tests pass;
  2. Supabase CLI dry-run lists only expected migration;
  3. migration succeeds inside a rollback transaction against current stage data;
  4. generated Supabase types refresh from migrated schema;
  5. real reservation reaches financial closeout, caregiver payout, branch settlement, and HQ settlement; and
  6. 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.

Pinned links prove code, schema, and migration presence. Rollout remains partial until production-scale lock/load behavior and complete runtime path are proven.

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_unresolved without 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-e9d6ecb6b924 reached settled: 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.
  • 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.