Busflow Docs

Internal documentation portal

Skip to content
Reviewed 17 May 2026

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 โ€‹

100%

Cross-context references (soft FKs, not shown above):

  • TOUR_OFFERING โ† created from Backoffice TOUR_DEPARTURE via TourDeparturePublished event
  • TOUR_OFFERING pricing โ† synced from Backoffice PRICE_MATRIX via PriceMatrixPublished event
  • SEAT_RESERVATION โ†’ references Operations SERVICE_LEG for capacity tracking
  • PASSENGER โ†’ references Backoffice PERSON_PROFILE via soft FK
  • BOOKING โ†’ references Backoffice PERSON_PROFILE via booker_profile_id soft FK [planned โ€” Phase 1]
  • PASSENGER โ†’ references Backoffice BOARDING_POINT via soft FK
  • RESELLER (Backoffice) โ†’ brokers BOOKING
  • TICKET โ†’ read by Operations BOARDING_EVENT for QR validation

Entity Dictionary โ€‹

  • TourOffering: The sellable, Commerce-owned projection of a published TourDeparture. Created/updated when Backoffice publishes a TourDeparture via the TourDeparturePublished event. References the source TourDeparture and TourTemplate via soft FKs. Carries denormalized display data. Pricing comes from TourOfferingPrice โ€” a read-model projection of the Backoffice PriceMatrix, synced asynchronously via PriceMatrixPublished events. Lifecycle status: SCHEDULED, SOLD_OUT, COMPLETED, CANCELLED (Note: DRAFT and READY states belong natively to TourDeparture in Backoffice and are not projected to Commerce until published).
  • TourOfferingPrice (Read-Model): A flattened Commerce-side projection of the Backoffice PriceMatrix. Stores price_matrix_version_id (soft FK to backoffice.price_matrices), channel, and variants (JSONB). Synced via PriceMatrixPublished events. Checkout validates the cart's price against the authoritative price_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 selected TourOffering, passenger count, seat holds, and chosen boarding point. Status lifecycle: ACTIVE โ†’ EXPIRED (TTL elapsed) or CONVERTED (successful payment). Converts to a Booking upon successful payment. Enables abandoned cart recovery: session expires after TTL (default 30 minutes) โ†’ Hasura Scheduled Trigger sets status = 'EXPIRED' โ†’ emits CheckoutAbandoned event โ†’ 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] References booker_profile_id (soft FK to Backoffice PersonProfile) โ€” the person with billing authority who initiates and pays. The booker may or may not also appear as a Passenger (traveler). Phase 1 B2C: the form submitter is assumed to also be the primary contact passenger; submitCheckout resolves booker_profile_id from that passenger's PersonProfile. 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_contact flag 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 parent Booking), not necessarily the primary contact passenger. References a boarding_point_id (soft FK to Backoffice) for the chosen pickup location and a person_profile_id (soft FK to Backoffice PersonProfile) for CRM linkage. Border document authority: document_number and nationality exist only on Passenger โ€” 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 recent Passenger row 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 linked person_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_birth exists on both PersonProfile (CRM, demographic pricing) and Passenger (border manifests). The Passenger row captures the value entered at booking time. On BookingConfirmed, the PersonProfile updater writes back the Passenger's DOB to PersonProfile.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_method used (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 type field defines the behavioral category (INSURANCE, SEAT_UPGRADE, LUGGAGE, EXCURSION, MEAL, BOARDING_SURCHARGE, OTHER) which governs system behavior (refund rules, reporting, fulfillment hints). BOARDING_SURCHARGE is auto-created by Commerce when a passenger selects a boarding point with a surcharge. The label field is operator-defined (the passenger-facing name). Each ancillary has an independent lifecycle (ACTIVE, CANCELLED, REFUNDED) to support partial cancellation. Carries an optional catalog_item_id (soft FK to backoffice.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. Carries seat_identifier (matching a key in the vehicle's seat_map_layout JSONB), hold_expires_at (aligned with checkout session TTL, default 30 minutes โ€” see ADR-013), and status enum (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_status via qr_hash validation in the driver's offline app.
  • FinancialLedger (Nachkalkulation): The per-departure profitability ledger. The system auto-creates one FinancialLedger per TourOffering when the first booking reaches DEPOSIT_PAID. It aggregates actual booking revenues (realized_revenue = sum of Payment.amount with status = COMPLETED) and actual operational expenses (realized_expense = sum from ExpenseSubmitted and OnboardSaleRecorded domain 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_cost from CostingSheet.total_net_cost and planned_revenue from the active PriceMatrix (ฮฃ variant prices ร— expected pax at CapacityRule.max_capacity; version tracked via planned_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). Carries costing_sheet_id (soft FK) for Soll/Ist baseline and currency (ISO 4217). Status lifecycle: OPEN (accepting mutations from booking/expense events) โ†’ CLOSED (period-locked, the system finalizes all actuals via the closed_at timestamp). See ADR-002.
  • TaxLedgerEntry: A generalized entity utilizing a TaxStrategyType to calculate the correct tax burden based on ledger actuals. Relationship is 1:N โ€” a single FinancialLedger can produce multiple TaxLedgerEntries when a departure mixes tax strategies (e.g., one MARGIN_SCHEME_25 entry for the tour margin and one STANDARD_VAT entry for onboard sales). TaxLedgerEntry generation must fold classified economic facts, not only FinancialLedger.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 TaxLedgerEntry and Booking data without a persistent invoice_line_items table.
  • 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) and MANUAL (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 โ€‹

RoleEntity
RootTourOffering
ChildTourOfferingPrice (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 โ€‹

RoleEntity
RootBooking
ChildPassenger
ChildPayment
ChildAncillary
ChildTicket

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_at check).

State vs. event decoupling: DEPOSIT_PAID is a booking state (a position in the lifecycle); BookingConfirmed is a domain event (an integration contract). Today they fire 1:1 โ€” the Hasura Event Trigger on commerce.bookings.status โ†’ DEPOSIT_PAID emits BookingConfirmed โ€” but they are conceptually independent and the downstream consequences fan out to separate concerns: CostingSheet cost-lock (Backoffice, ADR-009, defended by ยง 651a BGB), FinancialLedger auto-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 โ€‹

RoleEntity
RootFinancialLedger
ChildTaxLedgerEntry

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 โ€‹

RoleEntity
RootInvoice
ChildInvoiceCancellation

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.

Internal documentation โ€” Busflow