إنتقل إلى المحتوى الرئيسي

Company-controlled booking confirmation

Checkout confirmation and boarding fulfillment are separate decisions. A successful checkout always creates one durable Journey reservation and consumes the selected inventory atomically. A Company configured for manual review may delay fulfillment, but it cannot expose a usable Ticket, QR credential, or Booking-confirmation PDF before approval.

Tenant policy

Every existing and newly created Company owns one company_booking_confirmation_policies row. The default is AUTO_CONFIRM. Only an active, verified OWNER or ADMIN can change:

  • mode: AUTO_CONFIRM or MANUAL_CONFIRM;
  • review_window_minutes: 15 through 1,440 minutes; and
  • traveler channels: CUSTOMER_WEB, CUSTOMER_MOBILE, and/or RESELLER.

The policy version is copied into every decision request. A later settings change never rewrites an existing reservation's decision history.

State model

Aggregate confirmation stateReservationSigned boarding TicketTraveler presentation
REQUESTEDheldnoawaiting operator decision
CONFIRMEDconfirmedyes, after complete issuanceboarding-ready
REJECTEDcancellednooperator declined with reason
EXPIREDcancellednodecision deadline elapsed
CANCELLEDcancellednoclosed by another lifecycle

journey_booking_boarding_authorized() is the only boarding predicate. It is true only when the aggregate is CONFIRMED, the Ticket count exactly equals party_size × Journey Legs, and every Ticket is in a boardable state with an issuance timestamp, non-empty credential payload, and valid 64-character HMAC signature. Zero, missing, duplicate, cancelled, unsigned, or partially issued Ticket sets fail closed.

For a Journey Order, every child Journey must satisfy that predicate. One pending outbound or return operator therefore keeps the whole commercial order non-boardable.

Decision command

The dashboard does not update a Booking row directly. It calls decide_company_booking_confirmation with the Company, Journey Booking, authenticated actor, decision, optional rejection reason, and stable idempotency key. PostgreSQL verifies the actor's active Company role, locks the request and aggregate, and atomically performs one of these outcomes:

  • approval after the final required operator decision confirms all segments, extras, assignments, Journey/Order projections, issues and signs all Tickets, records the actor, and queues confirmation delivery;
  • rejection closes the whole affected Journey/Order scope, releases inventory, invalidates credentials, records the reason, and queues rejection delivery;
  • expiry performs the same safe release from a bounded SKIP LOCKED worker.

The decision is replay-safe. A conflicting key or a second different decision does not mutate the already closed request.

Cross-application projection

  • Customer web and mobile show REQUESTED, REJECTED, and EXPIRED separately from a confirmed Journey. They never render an active QR without aggregate boarding authority.
  • Company Booking workspace shows the deadline and approve/reject actions to authorized roles. Missing confirmation data becomes UNAVAILABLE, disables fulfillment, and asks the operator to reload rather than guessing.
  • Driver manifest keeps pending travelers outside the boardable manifest and requires explicit server-projected eligibility plus complete canonical Ticket identity.
  • Booking-confirmation PDF insertion and connected legacy QR/HMAC creation are guarded in PostgreSQL. Historical immutable snapshots remain renderable but do not gain new boarding authority.

Legacy compatibility projection

Some trusted office, reseller, and administrative paths still create a direct bookings compatibility row before the canonical Journey aggregate is visible to the caller. Migration 00306 projects those rows into the same Company confirmation lifecycle immediately after canonical artifacts are created. A manual-Company reservation therefore becomes PENDING_CONFIRMATION, loses any premature credential, and receives the same immutable decision request as a native Journey checkout.

The trigger is deliberately ordered after canonical artifact synchronization and before session/notification projection. It suppresses the obsolete per-segment BOOKING_CONFIRMED notification whenever a Company decision request exists. The trigger cannot be called directly, and the internal no-policy checkout/history helpers remain executable only by service_role; clients use the policy-aware public seams.

Migration 00307 closes the terminal-state edge case explicitly. Rejecting or expiring a Ticket that never passed approval keeps its credential payload, signature, and boarding-authorization timestamp empty. A Ticket that was legitimately issued before a later cancellation retains a signed terminal-state audit record, but remains non-boardable. A validated table constraint requires every boardable state to carry both a complete signed credential and an authorization timestamp.

Notifications

The lifecycle queues idempotent events for the traveler and each eligible Company operator: reservation received, operator review required, confirmed and Ticket issued, rejected with reason, and review deadline expired.

Guest reservations use the segment's canonical Passenger when no authenticated aggregate owner exists, so in-app history and external delivery stay attached to the same traveler record. Email remains suppressed unless verified; channel workers retain their existing delivery, retry, and audit contracts.

Verification

company-booking-confirmation-lifecycle.sql proves approval, decision replay, rejection, expiry, mixed-operator round trips, inventory release, credential gating, document/legacy-QR protection, traveler history, notification fan-out, and a direct legacy write for a manual Company in a rollback-only transaction. document-share-issuer-authorization.sql proves the automatic compatibility path remains confirmed and boardable without emitting a stale per-segment notification. The complete local database suite passes all 49 contracts, and the runtime schema audit passes through migration 00307 with no RLS or privilege-posture violation. The independent Supabase function lint still reports 11 pre-existing legacy procedure defects; none of migrations 00305 through 00307 appears in those findings.

The browser contract is also exercised at the product boundary. An authenticated Chromium flow changes the Company policy, reloads the settings page to prove persistence, and restores the original value. A Customer Chromium flow renders the same Journey first as pending—with neither QR nor tracking—and then as confirmed with both surfaces enabled. Production-configured, self-contained Customer and Driver Flutter Web release artifacts both render a real first frame at a 412 × 915 phone viewport with no page, console, or HTTP failure; public discovery responses are deterministic fixtures in that local artifact check.

Production release evidence is intentionally separate from this local contract.