Commerce & Finance Domain (schema: commerce) โ
The conversion and accounting engine, focused on B2C/B2B sales, capacity holds, cross-border manifest data, and generalized taxation logic based on actuals.
โ Hub: See domain-model.md for the bounded context map and cross-boundary integration surface.
Entity Relationship Diagram โ
Cross-context references (soft FKs, not shown above):
TOUR_OFFERINGโ created from BackofficeTOUR_DEPARTUREviaTourDeparturePublishedeventTOUR_OFFERINGpricing โ synced from BackofficePRICE_MATRIXviaPriceMatrixPublishedeventSEAT_RESERVATIONโ references OperationsSERVICE_LEGfor capacity trackingPASSENGERโ references BackofficePERSON_PROFILEvia soft FKBOOKINGโ references BackofficePERSON_PROFILEviabooker_profile_idsoft FK[planned โ Phase 1]PASSENGERโ references BackofficeBOARDING_POINTvia soft FKRESELLER(Backoffice) โ brokersBOOKINGTICKETโ read by OperationsBOARDING_EVENTfor QR validation
Entity Dictionary โ
- TourOffering: The sellable, Commerce-owned projection of a published
TourDeparture. Created/updated when Backoffice publishes aTourDeparturevia theTourDeparturePublishedevent. References the sourceTourDepartureandTourTemplatevia soft FKs. Carries denormalized display data. Pricing comes fromTourOfferingPriceโ a read-model projection of the BackofficePriceMatrix, synced asynchronously viaPriceMatrixPublishedevents. Lifecycle status:SCHEDULED,SOLD_OUT,COMPLETED,CANCELLED(Note:DRAFTandREADYstates belong natively toTourDeparturein Backoffice and are not projected to Commerce until published). - TourOfferingPrice (Read-Model): A flattened Commerce-side projection of the Backoffice
PriceMatrix. Storesprice_matrix_version_id(soft FK to backoffice.price_matrices),channel, andvariants(JSONB). Synced viaPriceMatrixPublishedevents. Checkout validates the cart's price against the authoritativeprice_matrix_version_idโ mismatches trigger a hard reject and cart refresh. - CheckoutSession: A short-lived entity tracking a user's purchase intent before the system confirms a
Booking. Captures the selectedTourOffering, passenger count, seat holds, and chosen boarding point. Status lifecycle:ACTIVEโEXPIRED(TTL elapsed) orCONVERTED(successful payment). Converts to aBookingupon successful payment. Enables abandoned cart recovery: session expires after TTL (default 30 minutes) โ Hasura Scheduled Trigger setsstatus = 'EXPIRED'โ emitsCheckoutAbandonedevent โ Communications context executes re-engagement workflow (Email at +1h, WhatsApp at +24h). See PRODUCT_mollie-integration.md ยง4 for the full checkout pipeline. - Booking: The primary transactional container bundling requested products and managing checkout states. The system defines the booking lifecycle through a strict state machine:
DRAFTโPENDING_PAYMENTโDEPOSIT_PAIDโFULLY_PAIDโCOMPLETEDโCANCELLED/REFUNDED/NO_SHOW.[planned โ Phase 1]Referencesbooker_profile_id(soft FK to BackofficePersonProfile) โ the person with billing authority who initiates and pays. The booker may or may not also appear as aPassenger(traveler). Phase 1 B2C: the form submitter is assumed to also be the primary contact passenger;submitCheckoutresolvesbooker_profile_idfrom that passenger'sPersonProfile. Non-traveler booker capture (the grandmother case) deferred to Phase 1.x. In dispatcher-created bookings, the dispatcher sets the booker directly. See ADR-037. - Passenger: The individual traveler on a booking. Carries cross-border documentation fields (name, DOB, document number, nationality) required for cross-border tours; these fields are nullable for domestic tours where no border manifest is needed. The
is_primary_contactflag identifies the on-trip communication recipient and driver's on-site contact โ the person who receives operational messages (pre-departure reminders, boarding details). Note: the person who pays is the booker (on the parentBooking), not necessarily the primary contact passenger. References aboarding_point_id(soft FK to Backoffice) for the chosen pickup location and aperson_profile_id(soft FK to BackofficePersonProfile) for CRM linkage. Border document authority:document_numberandnationalityexist only onPassengerโ they are point-in-time travel credentials, not identity attributes. PersonProfile deliberately omits these fields because passport numbers expire and renew between bookings; the border manifest requires the document the traveler carries on this departure. For returning travelers, the checkout and dispatcher UIs pre-fill border document fields from the traveler's most recentPassengerrow as a read-only suggestion (passengers WHERE person_profile_id = :id ORDER BY created_at DESC LIMIT 1). The traveler confirms or updates before submission. Pre-fill requires a linkedperson_profile_idโ children and other passengers without contact identifiers may lack a stable profile link and require manual data entry. DOB sync direction:date_of_birthexists on bothPersonProfile(CRM, demographic pricing) andPassenger(border manifests). The Passenger row captures the value entered at booking time. OnBookingConfirmed, the PersonProfile updater writes back the Passenger's DOB toPersonProfile.date_of_birthโ the most recent booking input wins. If a Passenger row omits DOB (domestic tour), the system does not overwrite an existing PersonProfile DOB. Border manifest delivery: The Operations context generates the driver-facing manifest via a denormalized read model joining Passenger data with Departure, Seats, and BoardingPoints ([v0.2]โ see PRODUCT_demo-loop.md ยงS5). - Payment: Granular tracking via Mollie Marketplaces, where the system handles split transactions like deposit (Anzahlung) and final payment (Restzahlung). Uses Mollie's delayed capture and marketplace routing for multi-party payouts to operators. Supports SEPA, PayPal, Apple/Google Pay, Klarna (BNPL), iDEAL, and credit cards. Records the actual
payment_methodused (populated from Mollie webhook). See PRODUCT_mollie-integration.md for the complete integration spec including webhook-to-state mapping. - Ancillary: Upsells added to a booking. The
typefield defines the behavioral category (INSURANCE,SEAT_UPGRADE,LUGGAGE,EXCURSION,MEAL,BOARDING_SURCHARGE,OTHER) which governs system behavior (refund rules, reporting, fulfillment hints).BOARDING_SURCHARGEis auto-created by Commerce when a passenger selects a boarding point with a surcharge. Thelabelfield is operator-defined (the passenger-facing name). Each ancillary has an independent lifecycle (ACTIVE,CANCELLED,REFUNDED) to support partial cancellation. Carries an optionalcatalog_item_id(soft FK tobackoffice.ancillary_catalog_items) for traceability to the operator's catalog; null for system-generated items like boarding surcharges. - SeatReservation: Time-locked capacity holds mapped against the operational
ServiceLeg. Carriesseat_identifier(matching a key in the vehicle'sseat_map_layoutJSONB),hold_expires_at(aligned with checkout session TTL, default 30 minutes โ see ADR-013), andstatusenum (HELD,CONFIRMED,RELEASED). A Hasura Scheduled Trigger (seat_hold_cleanup) runs every 60s to release expired holds. When a vehicle swap occurs, reservations are re-mapped to equivalent seat positions on the new layout. - Ticket (Fulfillment Artifact): The digital fulfillment and boarding pass. Owned by Commerce (it is a commercial fulfillment artifact of a confirmed booking). Operations reads the ticket via a cross-schema soft reference or denormalized read model to track
check_in_statusviaqr_hashvalidation in the driver's offline app. - FinancialLedger (Nachkalkulation): The per-departure profitability ledger. The system auto-creates one
FinancialLedgerperTourOfferingwhen the first booking reachesDEPOSIT_PAID. It aggregates actual booking revenues (realized_revenue= sum ofPayment.amountwithstatus = COMPLETED) and actual operational expenses (realized_expense= sum fromExpenseSubmittedandOnboardSaleRecordeddomain events consumed from Operations). These scalar totals are management/profitability accumulators, not the semantic authority for tax classification: payments prove that money moved, while booking, cancellation, ancillary, invoice, and onboard-sale commands classify what the money means. Snapshots two Soll baselines at creation:planned_costfromCostingSheet.total_net_costandplanned_revenuefrom the activePriceMatrix(ฮฃ variant prices ร expected pax atCapacityRule.max_capacity; version tracked viaplanned_price_matrix_version_id). Computes three delta columns:cost_delta=realized_expense โ planned_cost,revenue_delta=realized_revenue โ planned_revenue,margin_delta=(realized_revenue โ realized_expense) โ (planned_revenue โ planned_cost). Carriescosting_sheet_id(soft FK) for Soll/Ist baseline andcurrency(ISO 4217). Status lifecycle:OPEN(accepting mutations from booking/expense events) โCLOSED(period-locked, the system finalizes all actuals via theclosed_attimestamp). See ADR-002. - TaxLedgerEntry: A generalized entity utilizing a
TaxStrategyTypeto calculate the correct tax burden based on ledger actuals. Relationship is 1:N โ a singleFinancialLedgercan produce multipleTaxLedgerEntrieswhen a departure mixes tax strategies (e.g., oneMARGIN_SCHEME_25entry for the tour margin and oneSTANDARD_VATentry for onboard sales). TaxLedgerEntry generation must fold classified economic facts, not onlyFinancialLedger.realized_revenue/realized_expense; otherwise cancellation-derived retained amounts can be folded as ordinary travel-service revenue while the final ledger rows still look arithmetically consistent. Persists the four mandatory ยง 25 Abs. 5 UStG recording fields as immutable columns. See ADR-012 and tax-engine-protocol.md. - Invoice: Consolidates billing information and integrates e-invoicing standards. The system generates invoices as PDFs directly from aggregated
TaxLedgerEntryandBookingdata without a persistentinvoice_line_itemstable. - LedgerPeriodLock: Prevents mutation of financial records (
FinancialLedger,Invoice,TaxLedgerEntry) after the user closes a reporting period or the system exports it to DATEV. Lock types:EXPORT(irreversible, auto-created on DATEV export) andMANUAL(operator-triggered month-end close, admin-unlockable with audit trail). See Invoice Service Protocol ยง Period Lock. - InvoiceCancellation (Storno): GoBD-compliant cancellation record linking a voided invoice to its counter-document (Stornorechnung) and optional replacement invoice. The system never modifies the original invoice โ it creates a new counter-invoice with negative amounts. See Invoice Service Protocol ยง Storno Workflow.
- Infrastructure Entities: The physical schema includes lower-level tables (
ReconciliationUpload,ReconciliationEntry) supporting DATEV BWA/SuSa import and Soll/Ist comparison at the account-code level. These are import/comparison utilities without domain lifecycle.
Aggregates โ
Each aggregate defines a transactional consistency boundary. Cross-aggregate references follow ADR-036.
TourOffering โ
| Role | Entity |
|---|---|
| Root | TourOffering |
| Child | TourOfferingPrice (read-model projection) |
Invariants: Status: SCHEDULED โ SOLD_OUT / COMPLETED / CANCELLED. Price version must match active Backoffice PriceMatrix version. Domain Services: Exposes a read-model endpoint to auto-generate the standard "Anlage 11" Formblatt PDF dynamically using the offering's template data and the operator's insolvency insurance details.
Booking โ
| Role | Entity |
|---|---|
| Root | Booking |
| Child | Passenger |
| Child | Payment |
| Child | Ancillary |
| Child | Ticket |
Invariants: Status state machine: DRAFT โ PENDING_PAYMENT โ DEPOSIT_PAID โ FULLY_PAID โ COMPLETED โ CANCELLED / REFUNDED / NO_SHOW. Must have a booker_profile_id before transitioning past DRAFT (exception: B2B bookings with reseller_id may omit โ see ADR-037). At least one Passenger (traveler). Deposit/final payment split. Each Passenger and Ancillary carries an independent lifecycle for partial cancellation. Events: BookingConfirmed, BookingFullyPaid, BookingCancelled, BookingRefunded, BookingCompleted, BookingNoShow, PassengerCancelled, AncillaryCancelled, PaymentReceived. Cross-aggregate refs: tour_offering_id is a hard FK (stable โ offering never deleted).
Contention note: Mollie webhooks can arrive concurrently for the same booking (deposit + final payment race). Use optimistic concurrency on the Booking root (version column /
updated_atcheck).
State vs. event decoupling:
DEPOSIT_PAIDis a booking state (a position in the lifecycle);BookingConfirmedis a domain event (an integration contract). Today they fire 1:1 โ the Hasura Event Trigger oncommerce.bookings.status โ DEPOSIT_PAIDemitsBookingConfirmedโ but they are conceptually independent and the downstream consequences fan out to separate concerns: CostingSheet cost-lock (Backoffice, ADR-009, defended by ยง 651a BGB),FinancialLedgerauto-creation (Commerce internal), Ticket issuance (ADR-014), confirmation PDF + Formblatt + Sicherungsschein delivery (Communications), and PersonProfile DOB write-back (Backoffice, ADR-037 ยง7). Each consequence is modeled as an independent consumer so the firing condition can be re-bound without touching the others โ relevant if legal review of Widerrufsrecht (ยง 312g BGB) or ยง 651t (Reisepreisabsicherung) forces a "confirmed but withdrawable" sub-state. See ADR-009 ยง Scope & Legal Contingency and PRODUCT_payments-and-billing.md ยง4.
CheckoutSession โ
Single-entity aggregate. Short-lived purchase intent tracker.
Invariants: Status: ACTIVE โ EXPIRED / CONVERTED. TTL-based lifecycle (default 30 minutes). A CheckoutSession cannot transition to CONVERTED unless legal_consent.agb_accepted and privacy_accepted are both true. If the associated TourOffering has is_pauschalreise = true, formblatt_acknowledged must also be true. Events: CheckoutAbandoned, OnboardPaymentExpired. Cross-aggregate refs: tour_offering_id (hard FK โ stable), booking_id (soft FK โ independent TTL lifecycle, set only on conversion).
SeatReservation โ
Single-entity aggregate. Time-locked capacity hold against an operational ServiceLeg.
Invariants: Partial unique: (service_leg_id, seat_identifier) WHERE status IN ('HELD', 'CONFIRMED') โ no double-booking. Status: HELD โ CONFIRMED / RELEASED. Events: SeatHoldExpired (emitted by the seat_hold_cleanup Scheduled Trigger when hold_expires_at elapses). Cross-aggregate refs: service_leg_id (soft FK to Operations ServiceLeg), passenger_id (hard FK to Booking.Passenger โ set when status transitions to CONFIRMED).
FinancialLedger โ
| Role | Entity |
|---|---|
| Root | FinancialLedger |
| Child | TaxLedgerEntry |
Invariants: One per TourOffering. Status: OPEN โ CLOSED. ยง 25 Abs. 5 UStG immutable tax recording fields. Events emitted: FinancialLedgerClosed. Events consumed: PaymentReceived (increments realized_revenue), classified booking/cancellation facts (preserve sale vs. cancellation-fee semantics for tax close), ExpenseSubmitted and OnboardSaleRecorded (increment realized_expense โ consumed from Operations context). Cross-aggregate refs: tour_offering_id (hard FK โ stable, UNIQUE).
Invoice โ
| Role | Entity |
|---|---|
| Root | Invoice |
| Child | InvoiceCancellation |
Invariants: Status: DRAFT โ ISSUED โ PAID โ VOIDED. Gap-free, tenant-scoped sequential numbering (GoBD). Original invoices never modified โ the system creates counter-invoices. JSONB snapshots freeze supplier, recipient, and line-item data at generation. Events: InvoiceIssued. Cross-aggregate refs: financial_ledger_id and booking_id are soft FKs (Invoice has own lifecycle + JSONB snapshot independence).
LedgerPeriodLock โ
Single-entity aggregate. Immutable lock record.
Invariants: Prevents mutation of financial data after period close or DATEV export. Types: EXPORT (irreversible) and MANUAL (admin-unlockable with audit trail).
Infrastructure Entities (not aggregates) โ
change_events, tenant_invoice_sequences, reconciliation_uploads + reconciliation_entries.