Skip to content

Reservation Control Collaboration Gap Mining

Reservation Control Collaboration Gap Mining

Section titled “Reservation Control Collaboration Gap Mining”

Scope: branch/HQ reservation control UX that currently loses operator context because writes are broad, non-versioned, non-realtime, and browser-history driven. UI polish cannot fix this without a backend collaboration contract.

2026-07-08 Reservation Scenario Smoke Loop

Section titled “2026-07-08 Reservation Scenario Smoke Loop”

The reservation scenario is now runnable before the Supabase/ReState production stack is fully composed. The backend owns the workflow, state transition, and mother-visible charge summary; frontend smoke runs only render and submit the contract.

Backend command:

Terminal window
cd backend-repo
uv run uvicorn sanmopia_modernization.dev_smoke_app:app \
--app-dir src \
--host 127.0.0.1 \
--port 8001

Frontend command:

Terminal window
cd frontend-repo
PUBLIC_SANMOPIA_API_BASE_URL='http://127.0.0.1:8001' \
SANMOPIA_MOTHER_BOOKING_OPERATIONAL_CONTEXT_JSON='<qualified fixture json>' \
pnpm exec astro dev --background --host 127.0.0.1 --port 4326 --force

If port 4326 is occupied, use another local Astro port in the 4300-4399 range. The smoke backend accepts those localhost origins by regex. For remote or nonstandard preview origins, set:

Terminal window
SANMOPIA_RESERVATION_SMOKE_CORS_ORIGINS='https://reservation-smoke.example.test'

2026-07-08 proof artifact: frontend-repo/output/playwright/reservation-scenario-20260708-full-stack-qualified/. The run submitted /mother/booking, received 202 from POST /reservation-booking-workflow-starts, read a completed workflow through GET /reservation-booking-workflow-requests/{bookingRequestId}, and read the frozen mother-visible charge summary through GET /reservation-financial-lifecycles/{reservationBookingId}/mother-visible-charge-summary. The UI showed workflow status completed / 확정, reservation id 366cbba6-eb71-4aee-b311-c9abd607aeab, and backend-owned approved charge 989,881원.

2026-07-08 repeat proof artifact after frontend payment-method derivation and smoke CORS fix: frontend-repo/output/playwright/reservation-scenario-20260708-cors-repeat/. The run used Astro 4327, payment method stayed family_bank, the browser POST completed, and the page showed completed reservation state without the old context-mismatch message or CORS failure.

2026-07-08 backend follow-up proof after payment purpose preflight: RBQ-SCENARIO-20260708-1 submitted through the smoke backend with canonical card, returned 202, completed the workflow, produced reservation id e32cb903-89cd-45a1-babc-da6c1ea275b5, and returned mother-visible charge summary 200 with customer payable 150019. The older family_bank browser fixture value is now treated as an unsupported hidden legacy value by backend preflight; booking-start payment now also checks the reservation.customer_share.deposit product plan before workflow acceptance.

2026-07-09 backend smoke proof extends the same local app through settlement: POST /reservation-smoke-scenarios/{reservationBookingId}/settlement-completions advances the in-memory financial lifecycle from charge_finalized to settled after the booking workflow persists its service occurrence ledger. The response returns backend-owned caregiver payout, branch settlement, HQ settlement, ledger line codes, and policy version. The smoke test verifies the mother-visible charge summary before and after settlement is byte-for-byte identical, so a frontend card flow can demonstrate reservation-to-settlement without doing UI pricing, payout, or settlement arithmetic.

2026-07-10 backend 597b56d tightened that local smoke path: settlement completion now runs through AdvanceReservationFinancialLifecycleHandler with RecordReservationServiceDeliveryCommand, caregiver payout, branch settlement, HQ settlement, and final settled commands. The demo ledger split is carried by a versioned _SmokeReservationSettlementPolicy, so the preview route no longer mutates the aggregate directly or hides basis-point arithmetic in route logic. This is still smoke policy, not production settlement policy; production proof must continue to use catalog-backed payout, branch, HQ, and refund/repair facts.

Observed blockers converted into work items:

  • Frontend fixture defaults must derive hidden payment method values from the operational context. A stale hardcoded card value broke contexts that only allowed family_bank; the frontend lane fixed this by deriving the initial payment method from paymentMethodOptions[0].value.
  • Non-headquarters branch fixtures must carry a headquarters reference before reservation, branch settlement, or HQ settlement scenarios run.
  • Caregiver assignment fixtures must include normalized qualification facts, including approved license titles, before workflow execution.
  • Backend ReservationBookingContextQualityGate now runs before direct booking or durable workflow start. Branch topology and caregiver profile/qualification gaps fail with named preflight invariant codes instead of being accepted into a workflow that later fails during assignment.
  • Payment method preflight now rejects hidden browser-only or unsupported method values with payment_method_catalog_eligibility_invalid before workflow start. Booking-start purpose eligibility now rejects canonical but purpose-invalid methods with payment_purpose_method_eligibility_invalid; provider-route workflow verification now requires a frozen pre-registration provider-route fee snapshot before ReState starts from either the public API or Kill Bill paid webhook ingestion. Virtual-account lifecycle, card-replacement conversion, and provider-success/local-write-failure compensation remain tracked in SFC-20260708-CUR-086.
  • Production proof still needs the same path against the Supabase/ReState stack, including projection invalidation queue enqueue, failure, repair, requeue, and completion.

2026-07-13 Branch Reservation Worklist Delta

Section titled “2026-07-13 Branch Reservation Worklist Delta”

Legacy reservation history initializes a branch operator’s branch from the session, but the browser then posts branchIndex with every DataTables GET. The controller copies that query value into options['branch'], and the model uses it directly as the SQL branch predicate. Evidence:

  • source-refs/sanmopia-admin/application/controllers/Reservation.php:165-205 resolves the first branch value from the authenticated session;
  • source-refs/sanmopia-admin/static/js/reservation_status/reservation_history_new.js:224-260 sends browser-owned branch, date, quick-filter, status, and pagination values;
  • source-refs/sanmopia-admin/application/controllers/Reservation.php:208-315 accepts that branchIndex, serializes the complete query state into a cookie, and delegates it to the model; and
  • source-refs/sanmopia-admin/application/models/Reservation_model.php:4483-4685 joins the broad reservation/customer/address/payment/coupon projection and applies options['branch'] as the branch SQL filter.

The modern P0 replacement is implemented in Layer-first form:

application/reservation_operations/features/reservation_operation_worklist/queries/
adapters/reservation_operations/features/reservation_operation_worklist/
interfaces/reservation_operations/features/reservation_operation_worklist/

GET /branch-reservation-operation-worklist accepts search, semantic repeated status, opaque cursor, and limit only. It accepts no branch or actor id. Supabase Auth resolves the principal, the membership adapter requires exactly one branch_operator membership, SpiceDB checks branch:view, and the booking read itself is filtered by that server-owned branch profile. Public rows expose the business reservation id, revision, display-only names, schedule, canonical status, updated time, and policy decisions. They do not expose booking/context UUIDs or raw user ids.

Contract 18955f7, backend c9b06d7, and frontend 1f95287 add this boundary, SSR states, status/search controls, 15 semantic status labels, and an explicit executionReady=false gate. Stage-data proof observed branch operator 200, mother 403, visibleItemCount=501, first page 50, cursor present, and no operation-context decision rows. Desktop/mobile WebP evidence is registered at branch-reservation-worklist-stage-20260713T023958Z.

This is Partial actual, not reservation-operation completion. Remaining parity:

  • date-basis, expected/actual birth, unplanned schedule, unassigned caregiver, service-balance, export, and HQ cross-branch query projections;
  • server-resolved command actor and branch, expected revision, idempotency, conflict/replay, audit, and cleanup proof before mutation is enabled; and
  • a source-state catalog that preserves legacy extension request 10, pre-reservation waiting 16, waiting-deposit cancellation 17, and pre-reservation cancellation 18 instead of merging their meanings.

Backend f77db221 closes the command mutation gap without changing P05 from blocked. POST /branch-reservation-operation-commands/{reservationId} accepts expectedRevision, Idempotency-Key, command, reason, and note only. Supabase Auth resolves the user; booking, member profile, exact branch membership, and branch profile resolve branch/HQ scope on the server. SpiceDB still owns operation permission.

One PostgreSQL RPC locks booking/context rows and atomically updates booking status/revision, operation context, and immutable plan receipt. It rejects foreign branch scope, stale revision/status, and same-key changed facts. Exact replay returns the stored receipt and does not restart the follow-up workflow.

Disposable pinned Supabase CLI proof passed fresh migration, public-schema lint, happy-path smoke, same-key concurrent commit/replay, different-key concurrent commit/revision-conflict, forced insert rollback, grants, cleanup, and container teardown. Focused Python passed 33, HTTP passed 12, API/follow-up passed 5; Tach dependency/interface/external checks passed and Vulture confidence 100 reported 0 candidates. Platform follow-up d33be2b1 replaced the obsolete global SECURITY DEFINER count with per-function search-path, PUBLIC-revoke, and Data API grant checks, and restored the settlement acknowledgement table inventory. The focused access-control corpus passed 92; changed-impact pytest passed 6310 with 244 unaffected tests deselected. The clean backend policy gate is therefore GREEN.

Frontend cbc993f binds allowed worklist decisions to the strict public command route without client branch/actor authority. It sends revisioned commands with deterministic replay keys, preserves current URL filters through success reload, and maps 400/401/403/404/409/503 to actor-safe guidance. Focused Vitest passed 38, full Vitest passed 642, ESLint passed, Astro check reported 0 errors, and the production server build passed.

P05 remains blocked until P04 receives production legal approval. No P05 actual branch actor, browser, primary-state manifest, screenshot, animated WebP, or four-repository acceptance run exists yet.

2026-07-06 Reservation Lifecycle Backend Task Delta

Section titled “2026-07-06 Reservation Lifecycle Backend Task Delta”

These tasks are backend bounded-context work. They must not be implemented as frontend state checks or caregiver-matching changes.

PriorityBounded context / feature sliceSource evidenceConcrete backend task
P0reservation_operations / reservation_state_version_catalogsource-refs/sanmopia-admin/application/config/constants.php:115-160, source-refs/sanmopia_web/application/config/constants.php:47-92, source-refs/sanmopia-admin/application/views/reservation_status/reservation_detail.php:86-120, source-refs/sanmopia-admin/static/js/reservation_status/reservation_add.js:1350-1352Create ReservationStateVersionCatalog and LegacyReservationStateSnapshot. Seed every old booking with source status code, label, source system, effective catalog version, pre-reservation flags, virtual-account flags, offline marker, and closeout timestamp. Domain commands use stable state names only; raw codes -2/-1/0..19 stay adapter evidence. Add migration checks for old bookings whose status meaning changes by PRERESERVATION_FL, PRE_CONVERT_FL, or offline status 19.
P0service_calendar / service_calendar_policy_profilesource-refs/sanmopia-admin/application/models/Manage_system_model.php:9-24, source-refs/sanmopia-admin/application/models/Manage_system_model.php:51-181, source-refs/sanmopia-admin/application/models/Reservation_model.php:4183-4193, source-refs/sanmopia-admin/application/models/Reservation_model.php:4204-4216Split global holiday rows from service-calendar policy. Import source HOLIDAY_TB and free-text LIST_HOLIDAY_TYPE_TB as versioned holiday publications with source evidence, but add missing modern fields: national/substitute/company/branch-closure kind, substitute-of relation, announcement date, supersession, branch scope, and audit actor. BranchServiceClosurePolicy must model local closures explicitly because source only has global date rows and cannot represent branch-specific closed service days.
P0reservation_operations / reservation_service_calendar_plansource-refs/sanmopia-admin/application/models/Reservation_model.php:4009-4047, source-refs/sanmopia-admin/application/controllers/Reservation.php:902-917, source-refs/sanmopia-admin/application/models/Reservation_model.php:4089-4139, source-refs/sanmopia-admin/application/models/Reservation_model.php:4153-4256Build ReservationServiceCalendarPlanRebuildCommand. Inputs: start date, duration unit, policy profile, selected Saturday dates, selected holiday dates, Wanju/local benefit extension, source booking state version. Output immutable service occurrences and ServiceCalendarImpactPlan facts for report obligations, pricing, refund, coupon entitlement, settlement, and document eligibility. Preserve source warnings for selected-count mismatch, but reject or flag SAT/HOLI bucket collisions and substitute-holiday/local-closure impact through backend validation.
P0reservation_operations / service_suspension_lifecyclesource-refs/sanmopia_web/application/controllers/api/Reservation.php:86-239, source-refs/sanmopia_web/application/models/Reservation_model.php:1091-1111, source-refs/sanmopia-admin/application/models/Reservation_model.php:1768-1792, source-refs/sanmopia-admin/application/models/Reservation_model.php:1888-1892, source-refs/sanmopia_web/application/models/Report_model.php:267-391Implement ServiceSuspensionCommand set: request pause, approve pause, reject pause, resume service. Persist ServiceSuspension interval, effective stop date, resume boundary, reason, actor, expected revision, and downstream impact plan. Legacy has stop request 12, stopped 13, coupon reset on actual transition to 13, and report readers inferring resume from change logs; modern code must stop deriving pause/resume from raw status logs.
P0reservation_operations / reservation_extension_chainsource-refs/sanmopia_web/application/models/Reservation_model.php:2039-2145, source-refs/sanmopia_web/static/js/step3.js:563-576, source-refs/sanmopia_web/application/models/Reservation_model.php:2473-2501Implement ReservationExtensionChainCommand. Store immutable original/new booking link, sequence number, previous service-end snapshot, requested extension start, copied-fact audit, and voucher-continuation compatibility decision. Backend must enforce start-after-previous-end and ancestor-chain voucher rules; customer pages must not be the authority for extension start validation or voucher eligibility.
P0promotion_entitlements / product_coupon_service_day_policysource-refs/sanmopia-admin/application/config/constants.php:276-277, source-refs/sanmopia-admin/application/models/Coupon_model.php:33-155, source-refs/sanmopia-admin/application/models/Coupon_model.php:211-324, source-refs/sanmopia-admin/application/models/Coupon_model.php:500-552, source-refs/sanmopia-admin/application/models/Reservation_model.php:1299-1328, source-refs/sanmopia-admin/application/models/Reservation_model.php:3888-3916, source-refs/sanmopia-admin/application/models/Reservation_model.php:4537-4556Create ProductCouponEntitlementByServiceDayPolicy. Freeze eligibility from service type, extension-chain source service type, service term, working type, service-calendar occurrences, actual attendance count, suspension interval, cancellation/stop state, and source state catalog version. Resolve source conflicts: insert logic uses 5 days -> +3, >=10 days live-in -> +3, >=10 days commute -> +8; list/status logic also uses global PRODUCT_COUPON_PUBLISH_INTERVAL_DAY = 15 or service-start-only gates; reset logic downgrades stopped/cancelled normal reservations by actual attendance count 3..7 or >=8. Coupon issuance becomes event/outbox state, not view-time date math.
  • Full-form stale overwrite. Status: Contracted and backend-backed. Evidence: source-refs/sanmopia-admin/application/controllers/Reservation.php:331-358, source-refs/sanmopia-admin/application/views/reservation_status/reservation_detail.php:1406-1448, source-refs/sanmopia-admin/static/js/reservation_status/reservation_detail.js:270-333, source-refs/sanmopia-admin/application/controllers/Reservation.php:650-704, source-refs/sanmopia-admin/application/controllers/Reservation.php:834-1018, source-refs/sanmopia-admin/application/models/Reservation_model.php:1682, source-refs/sanmopia-admin/application/models/Reservation_model.php:1769, and source-refs/sanmopia-admin/application/models/Reservation_model.php:2166. Source behavior: detail opens one snapshot, posts broad FormData, and saves many field groups together. The endpoint logs before/after values around the save, but it does not reject another actor’s newer values before applying the form. Service-day, status, and caregiver updates write current rows without a row version predicate, so two branch/HQ users can silently last-write-win. Modern replacement: ReservationCollaborationCommand requires expected_revision, idempotency_key, actor, operation command, mutation kinds, and a lower-snake change_set. ReservationCollaborationPolicy returns a conflict diff when the current revision differs. Contract repo now exposes ReservationCollaborationCommitRequest, ReservationCollaborationCommitResponse, and ReservationCollaborationConflictResponse, and OpenAPI documents 409 conflict responses for stale saves. If the Supabase RPC detects a revision race after the pre-write snapshot read, the application re-reads the latest snapshot and returns the same 409 conflict contract instead of a false infrastructure 503.

  • Destructive child-list rewrite without version guard. Status: Backend-backed for service addresses. Evidence: source-refs/sanmopia-admin/application/models/Reservation_model.php:1349-1456. Source behavior: service addresses are deleted and recreated from the posted form. Modern replacement: address.service_addresses is now a command-specific field group under the same expectedRevision gate and emits one journal entry that lists the before and after address payloads. The Supabase collaboration RPC replaces sanmopia_reservation_service_addresses rows only after the revision bump, journal write, and realtime event succeed in the same transaction.

  • Browser-history and filter-state loss. Status: Backend-backed. Evidence: source-refs/sanmopia-admin/static/js/reservation_status/reservation_detail.js:331-342, source-refs/sanmopia-admin/static/js/reservation_status/pre_reservation_detail.js:241, source-refs/sanmopia-admin/static/js/reservation_status/pre_reservation_detail.js:322, source-refs/sanmopia-admin/static/js/reservation_status/pre_reservation_detail.js:332, source-refs/sanmopia-admin/static/js/reservation_status/pre_reservation_detail.js:276-332, source-refs/sanmopia-admin/static/js/reservation_status/reservation_history_new.js:5-13, source-refs/sanmopia-admin/static/js/reservation_status/reservation_history_new.js:46-63, source-refs/sanmopia-admin/static/js/reservation_status/reservation_history_new.js:130-143, and source-refs/sanmopia-admin/application/controllers/Reservation.php:299-314. Source behavior: successful saves use history.back() and list screens reconstruct search state from hashes/cookies. Modern replacement: commands carry navigation_intent and return_state_token; the backend response tells the portal whether to stay, return to a saved list state, open the next work item, or close a modal. POST /reservation-collaboration-workspaces/{reservationId} now returns the current revision, realtime topic, latest audit event id, and last return-state token before the operator starts editing. The same workspace response now returns coordinationPolicy.dirtyNavigationPolicy = require_save_discard_or_draft, so route changes and browser back must be driven by explicit save, discard, or draft persistence. Frontend state must store the returned draftSession in Nanostores and recover it after route change or browser history navigation instead of relying on history.back(). The backend now persists ReservationWorkspaceDraftSession rows in sanmopia_reservation_workspace_sessions, with baseRevision, latestKnownRevision, status, openedAt, lastSeenAt, and dirtySince so stale browser sessions can be resumed or discarded explicitly.

  • Branch transfer bypasses the main journal. Status: Implemented for branch handoff. Evidence: source-refs/sanmopia-admin/application/views/reservation_status/reservation_detail.php:213-232, source-refs/sanmopia-admin/application/views/reservation_status/reservation_detail.php:1463-1478, source-refs/sanmopia-admin/application/controllers/ajax/Reservation.php:22-28, and source-refs/sanmopia-admin/application/models/Branch_model.php:23-73. Source behavior: branch transfer posts only reservation id and branch id, then writes a narrow branch log. Modern replacement: branch handoff is ReservationMutationKind.BRANCH_HANDOFF with actor, reason, expected revision, idempotency key, reservation id, field diff, and realtime invalidation. transfer_reservation_branch is now a HQ-only reservation operation command requiring target branch profile, actor branch audit context, and before/after diff. The sanmopia_commit_reservation_collaboration RPC now applies branch_handoff.branch_profile_id to sanmopia_reservation_bookings inside the same expected-revision update that writes the journal and private Broadcast event, so branch ownership cannot be overwritten by a stale page.

  • Fragmented change logging. Status: Contracted and backend-backed. Evidence: source-refs/sanmopia-admin/application/controllers/Reservation.php:1023-1092, source-refs/sanmopia-admin/application/controllers/Reservation.php:1799-1814, source-refs/sanmopia-admin/application/models/Reservation_model.php:3730-3740, and source-refs/sanmopia-admin/application/models/Reservation_model.php:5252-5258. Source behavior: normal save, pre-reservation save, memo update, and branch transfer use inconsistent or narrow logs. Normal save creates a post-write diff log, but pre-reservation save has no matching RESERVATION_CHANGE_LOG coverage, and source logs are not part of the transactional concurrency decision. Modern replacement: ReservationChangeJournalEntry is append-only and covers every accepted mutation path. POST /reservation-collaboration-journal/{reservationId} now exposes the recent operator-facing timeline through the service-role adapter. Browser-level events that are not domain mutations, such as navigation_blocked, draft_marked_dirty, draft_recovered, and draft_discarded, now go through POST /reservation-collaboration-workspace-events/{reservationId} and are stored in sanmopia_reservation_workspace_events beside the durable session. pgAudit can remain a database compliance and troubleshooting layer, but operator-visible history must come from this domain journal.

  • Realtime projection missing. Status: Contracted and backend-backed. Evidence: source-refs/sanmopia-admin/static/js/reservation_status/reservation_history_new.js:224-230, and source-refs/sanmopia-admin/application/controllers/Reservation.php:1014-1020. Source behavior: list/detail screens reload Ajax data or navigate away after save. Modern replacement: every accepted command emits ReservationRealtimeProjectionEvent with a reservation-specific Supabase Broadcast topic, revision, changed fields, and audit event id. Stale open workspaces receive invalidation and can show a conflict/refresh state before the operator saves over newer data. The workspace API returns realtimeEventName = reservation_projection_invalidated and coordinationPolicy.realtimeChannelMode = private_broadcast_and_presence so the portal can subscribe and publish operator presence without hardcoding channel behavior in UI code. Supabase Realtime Presence is only for slow-changing editor state such as active workspace and actor profile; cursor-level or high-frequency UI details must use Broadcast or stay local.

  • Stop/resume collaboration command shape. Status: Partial. Evidence: source-refs/sanmopia_web/static/js/mypage/reservation.js:336-421, source-refs/sanmopia_web/application/controllers/api/Reservation.php:86-239, source-refs/sanmopia_web/application/models/Reservation_model.php:1091-1111, source-refs/sanmopia-admin/application/models/Reservation_model.php:1768-1792, source-refs/sanmopia-admin/application/models/Reservation_model.php:1888-1892, and source-refs/sanmopia_web/application/models/Report_model.php:267-391. Source behavior: customer stop is only a request row plus status 12; branch/admin stop is a raw status 13 transition, with product coupon reset only when the status actually changes to 13. No source pause/resume command was found. Report code infers a resume boundary from a later change log whose BEFORE_DATA contains status 13. Modern gap: stop confirmation and resume must be first-class reservation collaboration commands with expectedRevision, idempotency key, effective service date, reason, actor, impacted interval id, and stale-conflict response. The command result should include one impact plan for calendar dates, reports, caregiver assignment, coupons, refund/payment quotes, customer communication, and stopped-voucher settlement eligibility.

  • Realtime conflict event payload and support log. Status: Partial. Evidence: source-refs/sanmopia-admin/application/controllers/Reservation.php:650-704, source-refs/sanmopia-admin/application/controllers/Reservation.php:834-1020, source-refs/sanmopia-admin/application/models/Reservation_model.php:3730-3740, and source-refs/sanmopia-admin/static/js/reservation_status/reservation_detail.js:270-342. Source behavior: stale edits are not rejected before mutation, and post-write change logs cannot explain which open workspace overwrote which field. Modern gap: ReservationCollaborationConflictLog should be recorded whenever a Broadcast invalidation or commit-time revision check produces a stale-save conflict. Log fields: conflict event id, reservation id, workspace id, actor kind/profile, expected revision, current revision, conflicting field paths, latest audit event id, realtime topic/event name, refresh-required flag, attempted mutation kinds, idempotency key, user-facing resolution, and recorded time. commit_conflicted workspace events should reference this log so support can trace realtime delivery, stale page save, and operator resolution without reading raw DB audit.

  • Caregiver-assignment audit UX is too thin for branch-manager conflict review. Status: Partial. Priority: P1. Evidence: source-refs/sanmopia-admin/application/views/reservation_status/reservation_detail.php:1148-1178, source-refs/sanmopia-admin/static/js/reservation_status/reservation_detail.js:1105-1155, source-refs/sanmopia-admin/application/controllers/ajax/Reservation.php:180-208, source-refs/sanmopia-admin/application/models/Reservation_model.php:2165-2204, and source-refs/sanmopia-admin/application/models/Reservation_model.php:2270-2285. Source behavior: reservation detail exposes “change history” buttons for primary and assistant caregivers, then opens an Ajax modal that lists old caregiver, new caregiver, and created date from source manager-log tables. The save path inserts main or sub caregiver log rows only when ids change. It does not capture actor, branch/head-office role, reason, expected revision, selected candidate snapshot, realtime conflict id, or whether a branch manager overwrote another open workspace. Modern gap: ReservationCaregiverAssignmentAuditTrail should be an operator-facing timeline backed by ReservationChangeJournalEntry, not a separate popup over source log tables. Each CaregiverAssignmentChanged event should link assignment key, primary/assistant role, handoff order, before/after caregiver identity, selected candidate snapshot, actor branch profile, reason, expected/current revision, conflict log id, and idempotency key so branch manager conflict review can explain both assignment history and stale-save resolution.

The branch/HQ reservation screen must behave like a collaborative operations workspace, not a PHP form.

sequenceDiagram
autonumber
actor BranchOperator as Branch Operator A
actor BranchManager as Branch Operator B
participant PortalA as Operator Portal A
participant PortalB as Operator Portal B
participant API as Backend API
participant Realtime as Supabase Realtime
participant DB as Supabase Postgres

BranchOperator->>PortalA: Open reservation
PortalA->>API: POST /reservation-collaboration-workspaces/{reservationId}
API->>DB: Read revision, fields, latest journal
API->>DB: Upsert workspace session, append workspace_opened event
API-->>PortalA: currentRevision=7, realtimeTopic, draftSession
PortalA->>Realtime: Subscribe private Broadcast and Presence
PortalA->>API: POST /reservation-collaboration-workspace-events navigation_blocked
BranchManager->>PortalB: Open same reservation
PortalB->>API: POST /reservation-collaboration-workspaces/{reservationId}
PortalB->>Realtime: Subscribe private Broadcast and Presence
PortalB->>API: POST /reservation-collaboration-commits/{reservationId} expectedRevision=7
API->>DB: Bump revision to 8, append journal, store realtime event
API->>Realtime: Broadcast reservation_projection_invalidated
Realtime-->>PortalA: revision 8 invalidation
PortalA->>API: POST /reservation-collaboration-commits/{reservationId} expectedRevision=7
API-->>PortalA: 409 conflict with currentRevision=8 and conflictingFields
PortalA->>API: POST /reservation-collaboration-workspace-events commit_conflicted
PortalA->>API: POST /reservation-collaboration-journal/{reservationId}
API-->>PortalA: Operator audit timeline

UX requirements:

  • Save button submits only changed lower_snake field paths with expectedRevision; stale save shows conflict diff and refresh action.
  • If another operator commits between workspace read and RPC commit, API still returns 409 with current revision and conflict fields; UI must not blindly retry or overwrite.
  • Route change, modal close, browser back, and list navigation all consult coordinationPolicy.dirtyNavigationPolicy. Dirty workspace offers save, discard, or draft recovery, and records the chosen UX event through the workspace-event endpoint instead of hiding it in browser state.
  • Presence shows other active reservation editors but never replaces authorization or optimistic concurrency.
  • Journal timeline is append-only domain history. Database audit or OTel logs support compliance/operations, not operator UX.
  • Workspace event timeline is append-only operational UX history. It records page/session events that do not change reservation facts but matter for support, stale-save debugging, and branch manager accountability.
  • Conflict timeline entries include expected/current revision, conflicting field paths, idempotency key, workspace id, realtime event id, and selected resolution so stale-save support cases are searchable without replaying raw PHP logs.
  • Commit responses carry navigationIntent and returnStateToken; UI never infers destination from history.back().
  • Supabase Realtime Broadcast is the operator invalidation channel. The backend writes the domain journal and calls realtime.send(..., private := true), while realtime.messages RLS limits reservation topics to HQ or the reservation branch. Supabase documents realtime.send() as the flexible database broadcast function for custom payloads, while realtime.broadcast_changes() is better for trigger-shaped row changes. The reservation contract therefore keeps custom domain invalidation payloads on realtime.send() and avoids raw table streaming as the operator contract.
  • Supabase Realtime Presence is the operator co-editing signal. Presence is a warning layer showing who else has the reservation workspace open; the expected-revision commit remains the overwrite guard.
  • pgAudit is useful for database compliance/security/troubleshooting logs, not as the operator-facing change history.
  • OpenTelemetry logs are the operations export format for backend audit observability because the OTel log data model is stable and can carry correlation ids from the domain journal.
  • supa_audit can be evaluated for generic row-version auditing on self-hosted Supabase, but the migration should not depend on it for business replay until extension availability and retention cost are verified.
  • pgMemento is an OSS PostgreSQL row-history option for self-hosted forensic reconstruction and schema-versioned audit trails. It is not the default operator UX source because it requires extension/script installation and produces database-centric history, not domain command language.
  • Native PostgreSQL temporal-table support can become a future storage option only after the self-hosted Supabase Postgres version supports it. Today the business contract uses explicit revision columns and append-only journals so it is portable across PostgREST, generated Supabase clients, and Python service-role adapters.

Reference:

  • Domain: backend-repo/src/sanmopia_modernization/domain/reservation_operations/features/reservation_collaboration/reservation_collaboration.py
  • Application contract: backend-repo/src/sanmopia_modernization/application/reservation_operations/features/reservation_collaboration/reservation_collaboration_contract.py
  • Application port: backend-repo/src/sanmopia_modernization/application/reservation_operations/features/reservation_collaboration/commit_reservation_collaboration.py
  • Supabase adapter: backend-repo/src/sanmopia_modernization/adapters/reservation_operations/features/reservation_collaboration/supabase_reservation_collaboration.py
  • Supabase migration: backend-repo/supabase/migrations/20260702184000_reservation_collaboration_journal.sql plus replay fix backend-repo/supabase/migrations/20260702190000_fix_reservation_collaboration_idempotent_audit_return.sql and branch handoff application backend-repo/supabase/migrations/20260702193000_apply_branch_handoff_collaboration.sql and service schedule application backend-repo/supabase/migrations/20260702194000_apply_service_schedule_collaboration.sql and extra service-day application backend-repo/supabase/migrations/20260702195000_apply_extra_service_days_collaboration.sql and Presence RLS backend-repo/supabase/migrations/20260702200000_reservation_collaboration_presence_policy.sql and caregiver assignment roster application backend-repo/supabase/migrations/20260702201000_reservation_caregiver_assignment_collaboration.sql and service term, payment fact, address, and memo application backend-repo/supabase/migrations/20260703193000_reservation_collaboration_admin_fact_bindings.sql
  • Generated Supabase contract: backend-repo/supabase/generated/database.types.ts

Current boundary:

  • ReservationCollaborationCommand
  • ReservationCollaborationPolicy
  • ReservationCollaborationConflict
  • ReservationCollaborationConflictLog
  • ReservationChangeJournalEntry
  • ReservationRealtimeProjectionEvent
  • ReservationCollaborationCommitPlan
  • ReservationWorkspaceCoordinationPolicy
  • ReservationCollaborationSnapshot
  • CommitReservationCollaborationHandler
  • OpenReservationCollaborationWorkspaceHandler
  • ListReservationCollaborationJournalHandler
  • ReservationCollaborationCommitStore
  • ReservationCollaborationJournalReader
  • SupabaseReservationCollaborationSnapshotReader
  • SupabaseReservationCollaborationCommitStore
  • SupabaseReservationCollaborationJournalReader
  • POST /reservation-collaboration-workspaces/{reservationId}
  • POST /reservation-collaboration-journal/{reservationId}
  • POST /reservation-collaboration-commits/{reservationId}
  • sanmopia_commit_reservation_collaboration
  • transfer_reservation_branch
  • branch_handoff.branch_profile_id
  • service_schedule.start_on
  • service_schedule.end_on
  • service_schedule.extra_service_days
  • service_term.code
  • payment_fact.customer_share_deposit_paid_at
  • payment_fact.customer_share_balance_paid_at
  • payment_fact.service_balance_paid_at
  • address.service_addresses
  • memo.operator_note
  • caregiver_assignment.roster
  • coordinationPolicy.realtimeChannelMode
  • coordinationPolicy.dirtyNavigationPolicy

Remaining work:

  • Bind every branch/HQ reservation mutation path to CommitReservationCollaborationHandler before the owning persistence command writes a legacy save field. Branch handoff, booking service date fields, service term, payment date facts, service-address replacement, memo, extra service-day child rows, and caregiver assignment roster rows are now concrete mutation paths applied by the Supabase collaboration RPC. Service date changes update service_start_at and service_end_at with timestamptz parsing and range validation inside the expected-revision commit. Extra service days replace sanmopia_reservation_extra_service_days rows only after expected-revision commit succeeds, and the same transaction rolls back the journal if child-row validation fails. Caregiver assignment roster replaces sanmopia_reservation_caregiver_assignments rows by contract_assignment_key, preserves primary/assistant roles, grade, planned service days, term dates, and assigned Supabase user ids, then updates the booking primary assigned_caregiver_user_id for current access compatibility. Service term writes update service_term_code; payment date facts update or create purpose-keyed sanmopia_payment_records; address replacement rewrites sanmopia_reservation_service_addresses; memo writes update operator_memo. Commit-time RPC revision races now raise ReservationCollaborationPersistenceConflict; the application handler re-reads the latest snapshot and returns the normal stale-save 409 conflict response. Remaining conflict-log hardening: persist a durable ReservationCollaborationConflictLog row or equivalent workspace event metadata that links the returned 409, Broadcast invalidation, attempted mutation kinds, idempotency key, and operator resolution.
  • Stop confirmation/resume must become explicit mutation kinds, not free status edits. Legacy source infers stop intervals from status 13 entries in RESERVATION_CHANGE_LOG; modern collaboration commits should write ServiceSuspension request/approval/resume facts and let downstream reports, assignment, pricing, refund, and coupon workflows consume ServiceSuspensionImpactPlan.
  • Frontend Nanostores collaboration foundation is implemented in frontend-repo/src/features/reservation-collaboration-workspace/. It opens POST /reservation-collaboration-workspaces/{reservationId} before edit, commits through POST /reservation-collaboration-commits/{reservationId}, handles top-level 409 conflict payloads with expectedRevision, currentRevision, conflictingFields, and refreshRequired, subscribes to the returned private realtimeTopic, publishes Presence, and lists operator history through POST /reservation-collaboration-journal/{reservationId}. It now protects browser back, pagehide, beforeunload, link navigation, explicit reload/return, dirty discard, and stale save states, and records draft_marked_dirty, navigation_blocked, draft_discarded, and commit_conflicted through POST /reservation-collaboration-workspace-events/{reservationId}. Remaining frontend work is generated client import replacement for the temporary local contract types, branch-manager screen wiring, and a two-workspace stale-save smoke test against the running Supabase Realtime stack.
  • Contract repo now exposes OpenAPI paths, JSON Schema, and TypeScript client types for collaboration workspace, journal, commit, conflict, realtime event, coordination policy, and route/header helpers. Frontend must import those contracts instead of handwritten fetch shapes.
  • Backfill strategy from existing reservation rows into initial revision and journal seeds.
  • Stage smoke: open one reservation in two workspaces, commit one branch/HQ edit, prove the second workspace receives Broadcast invalidation, then reject the stale save with a conflict diff.
  • Assignment history panel and decision evidence. Status: Pending. Evidence: source-refs/sanmopia-admin/application/controllers/ajax/Reservation.php:180-208, source-refs/sanmopia-admin/application/models/Reservation_model.php:57-113, and source-refs/sanmopia-admin/application/models/Reservation_model.php:2165-2303. Source behavior: branch/HQ screens can fetch old/new manager names from manager-change logs, while assignment changes still mutate reservation rows and secondary caregiver slots. Modern gap: CaregiverAssignmentDecision should be exposed beside collaboration journal with decision source, candidate evidence id, old/new caregiver, role/slot, replacement reason, and assignment conflict result.

  • Narrow reservation corrections outside full-form save. Status: Pending. Evidence: source-refs/sanmopia-admin/application/controllers/ajax/Reservation.php:212-221, source-refs/sanmopia-admin/application/controllers/ajax/Reservation.php:68-178, and source-refs/sanmopia-admin/application/models/Reservation_model.php:606-643. Source behavior: baby due, payment-change status/list edits, and service-balance corrections are narrow ajax routes outside the main detail form. Modern gap: every narrow branch-manager correction needs its own ReservationCollaborationCommand mutation kind, expected revision, idempotency key, field diff, and return-state token.

  • Branch manager history/read models. Status: Pending. Evidence: source-refs/sanmopia-admin/application/models/Reservation_model.php:985-1043, source-refs/sanmopia-admin/application/controllers/Reservation.php:198-205, and source-refs/sanmopia-admin/application/controllers/Calculate.php:460-498. Source behavior: list/history screens derive branch filters, reservation status groupings, Danbee completion flags, and settlement filters from current query state. Modern gap: branch manager control needs stable read models for reservation work queue, assignment history, payment-change board, settlement status, and return-state tokens so operator screens do not reconstruct work context from GET filters and source status ids.