Event Contracts: Commerce โ
Formal event contracts for all Commerce domain events. For the full Hasura Action and Scheduled Trigger specs, see booking-lifecycle-protocol.md and cancellation-protocol.md. For the broader domain events catalog, see event-catalog.md.
Event Catalog โ
| Event | Emitter | Consumer | Purpose |
|---|---|---|---|
BookingConfirmed | Commerce (Booking โ DEPOSIT_PAID) | Backoffice (CostingSheet auto-lock โ ADR-009, PersonProfile DOB write-back โ ADR-037 ยง7), Communications (confirmation) | Signals first payment received; triggers cost lock, CRM sync, and passenger notification |
BookingFullyPaid | Commerce (Booking โ FULLY_PAID) | Communications (full payment confirmation) | Signals all payments complete |
BookingCancelled | Commerce (Booking โ CANCELLED) | Communications (cancellation notification), Operations (manifest update) | Signals booking cancellation with optional refund initiation |
BookingRefunded | Commerce (Booking โ REFUNDED) | Communications (refund confirmation) | Signals Mollie refund settlement; optionally triggers Invoice Storno |
BookingCompleted | Commerce (Booking โ COMPLETED) | Communications (post-trip review request) | Signals successful trip completion (Scheduled Trigger: end_date + 24h) |
BookingNoShow | Commerce (Booking โ NO_SHOW) | Communications (operator-specific triggers) | Signals no boarding event recorded (Scheduled Trigger: end_date + 48h) |
PaymentReceived | Commerce (Payment โ COMPLETED) | Commerce (FinancialLedger revenue tracking), Backoffice | Fires on every Mollie payment capture (deposit, final payment, ancillary). Distinct from BookingConfirmed โ that event signals the state transition, this one signals the financial event. |
FinancialLedgerClosed | Commerce (FinancialLedger โ CLOSED) | DATEV Export Service, Communications | Signals ledger finalization; triggers TaxLedgerEntry creation and DATEV export readiness |
CheckoutAbandoned | Commerce (CheckoutSession โ EXPIRED, session_type = BOOKING) | Communications (re-engagement: email at +1h, WhatsApp at +24h) | Signals checkout session expiry without conversion |
OnboardPaymentExpired | Commerce (CheckoutSession โ EXPIRED, session_type = ONBOARD_SALE) | Operations (sets onboard_sales.payment_status โ FAILED, notifies driver via push) | Signals onboard payment link expiry without payment. No re-engagement โ driver decides next step. |
FinalPaymentDue | Commerce (Scheduled Trigger) | Communications (payment reminder with Mollie payment link) | Fires when departure_date - reminder_days_before_start for bookings in DEPOSIT_PAID |
FinalPaymentOverdue | Commerce (Scheduled Trigger) | Communications (urgent notification), Backoffice (dispatcher flagging) | Fires when departure_date - flag_days_before_start โค now() for DEPOSIT_PAID bookings with no FINAL_PAYMENT |
SeatHoldExpired | Commerce (Scheduled Trigger) | Commerce (self โ availability count update) | Fires when hold_expires_at < now() for HELD reservations |
PassengerCancelled | Commerce (Hasura Action) | Commerce (seat release, ticket void, refund calc), Communications (notification) | Fires when removing a single passenger from an active booking (partial cancellation) |
AncillaryCancelled | Commerce (Hasura Action) | Communications (notification) | Fires when removing an ancillary from an active booking |
PassengerAdded | Commerce (Hasura Action) | Commerce (self โ supplementary payment link), Communications (notification), Operations (manifest) | Fires when adding a new passenger to an active booking |
PassengerUpdated | Commerce (Hasura Action) | Operations (manifest update) | Fires when passenger details change on an active booking |
Payload Schemas โ
BookingConfirmed โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Confirmed booking |
tour_offering_id | UUID | Target offering |
price_matrix_id | UUID | PriceMatrix version booked against |
passenger_count | INT | Total passengers on booking |
deposit_amount | Decimal | Deposit payment amount |
reference_number | String | Human-readable booking reference |
booker_profile_id | UUID | [planned โ Phase 1] Soft FK to Backoffice PersonProfile. Billing authority. Automatically included via Hasura row trigger payload. See ADR-037. |
confirmed_at | TIMESTAMPTZ | Confirmation timestamp |
Note: Also consumed by Backoffice for CostingSheet auto-lock. See event-contracts-pricing.md ยง
BookingConfirmedfor the Backoffice consumption contract. The PersonProfile updater also consumes this event โ it fetches passengers viabooking_id, resolves eachperson_profile_id, and writes backdate_of_birthtoPersonProfile.date_of_birth(most recent booking input wins; NULL Passenger DOB does not overwrite existing PersonProfile DOB). See ADR-037 ยง7.
State vs. event decoupling: The trigger column above (
Booking โ DEPOSIT_PAID) describes the current firing condition, not a domain invariant.BookingConfirmedis conceptually independent of theDEPOSIT_PAIDstate, and each downstream consumer (CostingSheet cost-lock, PersonProfile DOB sync, confirmation messaging, ticket issuance, FinancialLedger creation) is modeled as a separate concern. Only the cost-lock consumer is legally defended by ยง 651a BGB at this trigger point (ADR-009 ยง Scope & Legal Contingency). Other consumers may be re-bound to a later trigger if legal review of Widerrufsrecht (ยง 312g BGB) or ยง 651t (Reisepreisabsicherung) requires a "confirmed but withdrawable" sub-state โ see PRODUCT_payments-and-billing.md ยง4 and domain-commerce.md ยง Booking.
BookingFullyPaid โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Fully paid booking |
total_amount | Decimal | Total amount paid |
payment_method | String | Mollie payment method used for final payment |
paid_at | TIMESTAMPTZ | Timestamp of final payment completion |
BookingCancelled โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Cancelled booking |
reason | String | Cancellation reason (user-provided or system-generated) |
refund_initiated | Boolean | Whether the system initiated a Mollie refund |
cancelled_by | String | Actor: DISPATCHER, PASSENGER, SYSTEM (timeout). Legacy value: PASSENGER denotes the self-service booker (billing authority per ADR-037), not necessarily a traveler. Rename to BOOKER deferred to avoid breaking existing event consumers. |
cancelled_at | TIMESTAMPTZ | Cancellation timestamp |
BookingRefunded โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Refunded booking |
refund_amount | Decimal | Amount refunded |
refund_payment_id | UUID | FK to payments.id (the refund Payment row) |
refunded_at | TIMESTAMPTZ | Mollie refund settlement timestamp |
BookingCompleted โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Completed booking |
tour_offering_id | UUID | Target offering |
passenger_count | INT | Total passengers |
completed_at | TIMESTAMPTZ | Completion timestamp |
BookingNoShow โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | No-show booking |
passenger_ids | UUID[] | All passengers who did not board |
detected_at | TIMESTAMPTZ | Detection timestamp |
NOTE
Trigger query (cross-schema): The NoShow evaluation window opens when ServiceLegCompleted fires for the final leg of a TourDeparture (payload: is_final_leg = true). The Commerce consumer queries:
SELECT p.id AS passenger_id, b.id AS booking_id
FROM commerce.bookings b
JOIN commerce.passengers p ON p.booking_id = b.id
JOIN commerce.tickets t ON t.passenger_id = p.id
WHERE b.tour_offering_id = :offering_id
AND b.status = 'CONFIRMED'
AND t.status = 'ACTIVE'
AND NOT EXISTS (
SELECT 1 FROM operations.boarding_events be
WHERE be.ticket_id = t.id
AND be.check_in_status IN ('SUCCESS', 'MANUAL_OVERRIDE')
);For each booking_id with โฅ1 unboarded passenger, emit one BookingNoShow event. Per-passenger granularity (not per-booking) ensures that group bookings with partial no-shows are handled correctly โ e.g., a booking of 4 where 3 boarded and 1 did not emits a BookingNoShow with passenger_ids = [the_missing_one].
PaymentReceived โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Associated booking |
payment_id | UUID | The payments.id row |
payment_type | String | DEPOSIT, FINAL_PAYMENT, or PARTIAL_REFUND |
amount | Decimal | Payment amount |
payment_method | String | Mollie payment method |
provider_transaction_id | String | Mollie transaction ID |
captured_at | TIMESTAMPTZ | Mollie capture timestamp |
CheckoutAbandoned โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
session_id | UUID | Expired checkout session |
tour_offering_id | UUID | Offering the session was for |
booker_email | String | Booker email for re-engagement workflow |
expired_at | TIMESTAMPTZ | Session expiry timestamp |
OnboardPaymentExpired โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
session_id | UUID | Expired onboard checkout session |
onboard_sale_id | UUID | FK to operations.onboard_sales โ the driver-initiated sale that triggered the payment link |
tour_offering_id | UUID | Offering context |
amount | Decimal | Payment amount that expired |
expired_at | TIMESTAMPTZ | Session expiry timestamp |
FinalPaymentDue โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Booking awaiting final payment |
booker_email | String | Booker email for payment reminder delivery |
amount_remaining | Decimal | Outstanding amount |
due_date | DATE | Derived from departure_date - operator.final_payment_config.reminder_days_before_start |
payment_link | String | Pre-generated Mollie payment URL |
severity | String | Escalation tier: REMINDER (first notice) or URGENT (second notice). See booking-lifecycle-protocol.md ยงfinal_payment_escalation_sweep. |
channel | String | Preferred delivery channel: EMAIL (REMINDER tier) or WHATSAPP (URGENT tier) |
FinalPaymentOverdue โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Overdue booking |
severity | String | Always CRITICAL |
flagged_at | TIMESTAMPTZ | Timestamp when the system flagged the booking |
tickets_voided | Boolean | Whether the system voided tickets (true if the system issued ADR-014 tickets at DEPOSIT_PAID) |
SeatHoldExpired โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
seat_reservation_id | UUID | The released reservation |
service_leg_id | UUID | Affected service leg |
seat_identifier | String | Seat position key |
expired_at | TIMESTAMPTZ | Hold expiry timestamp |
PassengerCancelled โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Parent booking |
passenger_id | UUID | Cancelled passenger |
refund_amount | Decimal | Amount refunded for this passenger (after cancellation fee) |
cancelled_at | TIMESTAMPTZ | Cancellation timestamp |
AncillaryCancelled โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Parent booking |
ancillary_id | UUID | Cancelled ancillary |
refund_amount | Decimal | Amount refunded (after cancellation fee) |
cancelled_at | TIMESTAMPTZ | Cancellation timestamp |
PassengerAdded โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Parent booking |
passenger_id | UUID | Newly added passenger |
additional_amount | Decimal | Price added to booking total |
supplementary_payment_required | Boolean | Whether a the system generated a new Mollie payment link |
added_at | TIMESTAMPTZ | Addition timestamp |
PassengerUpdated โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
booking_id | UUID | Parent booking |
passenger_id | UUID | Updated passenger |
changes | String[] | List of changed field names (e.g., ["first_name", "last_name"]) |
ticket_reissued | Boolean | Whether the system voided and reissued the ticket due to a name change |
updated_at | TIMESTAMPTZ | Update timestamp |
FinancialLedgerClosed โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
financial_ledger_id | UUID | Closed ledger |
tour_offering_id | UUID | Parent offering |
realized_revenue | Decimal | Final revenue at closure |
realized_expense | Decimal | Final expense at closure |
margin_delta | Decimal | Final margin deviation from plan |
tax_entry_count | INT | Number of TaxLedgerEntries created at closure |
closed_at | TIMESTAMPTZ | Closure timestamp |
InvoiceIssued โ
| Field | Type | Description |
|---|---|---|
event_id | UUID | Idempotency key |
tenant_id | UUID | Owning operator |
invoice_id | UUID | The issued invoice |
booking_id | UUID | Parent booking |
invoice_number | String | Gap-free human-readable number |
total_gross | Decimal | Invoice grand total |
issued_at | TIMESTAMPTZ | Issuance timestamp |
Delivery Contracts โ
| Event | Mechanism | Trigger | Idempotency |
|---|---|---|---|
BookingConfirmed | Hasura Event Trigger (async, post-commit) | commerce.bookings.status โ DEPOSIT_PAID | event_id |
BookingFullyPaid | Hasura Event Trigger (async, post-commit) | commerce.bookings.status โ FULLY_PAID | event_id |
BookingCancelled | NestJS @nestjs/event-emitter โ Hasura Event Trigger | cancelBooking Action handler commits | event_id |
BookingRefunded | Hasura Event Trigger (async, post-commit) | commerce.bookings.status โ REFUNDED | event_id |
BookingCompleted | NestJS Cron Trigger handler | booking_completion_sweep (end_date + 24h) | event_id |
BookingNoShow | NestJS Cron Trigger handler | no_show_detection_sweep (end_date + 48h) | event_id |
PaymentReceived | Hasura Event Trigger (async, post-commit) | commerce.payments.status โ COMPLETED | event_id + provider_transaction_id uniqueness |
CheckoutAbandoned | NestJS Cron Trigger handler | checkout_abandoned_sweep (every 5 min), session_type = BOOKING | Session status = ACTIVE guard in WHERE clause |
OnboardPaymentExpired | NestJS Cron Trigger handler | checkout_abandoned_sweep (every 5 min), session_type = ONBOARD_SALE | Session status = ACTIVE guard in WHERE clause |
FinalPaymentDue | NestJS Cron Trigger handler | final_payment_escalation_sweep (daily 08:00) | NOT EXISTS check on pending/completed FINAL_PAYMENT + tier derivation from days_until_departure |
FinalPaymentOverdue | NestJS Cron Trigger handler | final_payment_escalation_sweep (daily 08:00) | Same sweep as FinalPaymentDue, emitted at CRITICAL tier (flag_days_before_start). Sets booking.flagged = true, voids tickets if applicable. |
SeatHoldExpired | NestJS Cron Trigger handler | seat_hold_cleanup (every 60s) | status = HELD guard in WHERE clause |
PassengerCancelled | NestJS @nestjs/event-emitter โ Hasura Event Trigger | cancelPassenger Action handler commits | event_id |
AncillaryCancelled | NestJS @nestjs/event-emitter โ Hasura Event Trigger | cancelAncillary Action handler commits | event_id |
PassengerAdded | NestJS @nestjs/event-emitter โ Hasura Event Trigger | addPassengerToBooking Action handler commits | event_id |
PassengerUpdated | NestJS @nestjs/event-emitter โ Hasura Event Trigger | updatePassengerDetails Action handler commits | event_id |
FinancialLedgerClosed | Hasura Event Trigger (async, post-commit) | commerce.financial_ledgers.status โ CLOSED | event_id |
InvoiceIssued | NestJS @nestjs/event-emitter โ Hasura Event Trigger | issueInvoice Action handler commits | event_id |
All consumers implement at-least-once processing with event_id-based deduplication. See domain-driven-design.md ยง4 for the standard delivery pattern.