Backoffice & Catalog Domain (schema: backoffice) โ
The root authority for business configuration, operational staff, abstract product definitions, third-party inventory, CRM, and all financial planning/quoting.
โ 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):
PRICE_MATRIXโ publishes to CommerceTOUR_OFFERINGviaPriceMatrixPublishedeventTOUR_DEPARTUREโ publishes to CommerceTOUR_OFFERINGviaTourDeparturePublishedeventCREW_MEMBERโ referenced by OperationsLEG_ASSIGNMENTVEHICLEโ referenced by OperationsLEG_ASSIGNMENT
Entity Dictionary โ
- Operator: The root tenant entity representing a bus tour company. All domain entities reference the operator via
tenant_id. Core fields:name,legal_name,address,country,default_locale(e.g.,de-DE),default_currency(ISO 4217, e.g.,EUR),vat_id(USt-IdNr.),tax_id, andlegal_form(e.g.,GMBH,UG,EINZELUNTERNEHMEN). Status lifecycle:ONBOARDING(post-signup, pre-first-login) โACTIVEโSUSPENDED(non-payment or manager action) โCHURNED(voluntary cancellation or GDPR deletion request).subscription_tiergates feature limits (enforced at application layer viaX-Hasura-Plan).[planned]extensions:brand_config(JSONB:{ logo_url, primary_color, accent_color }),bank_details(JSONB:{ iban, bic, account_holder }โ for SEPA payouts and DATEV export). See tenant-provisioning ADR for the signup-to-first-login flow. - TenantSubscription: Billing plan state per operator. Holds the
plan_id(e.g.,CORE,PRO,ENTERPRISE) which the Nhost custom claims webhook uses to inject theX-Hasura-Planinto JWT session variables. Operates on a two-layer billing model (see PRODUCT_payments-and-billing.md ยง3): Lago handles the metered usage and invoicing, while Stripe handles the actual payment collection. Therefore, this entity must track identifiers for both the metering engine (Lago) and the payment collector (Stripe). - SubscriptionTier: The abstract level of service (e.g.,
CORE,PRO,ENTERPRISE) defining the feature access gates for an operator. Enforced at the application layer viaX-Hasura-Plan. - User (Nhost
auth.users): Authentication identity for all actors who log into the Busflow platform (admins, dispatchers, drivers). Managed entirely by Nhost Auth โ no separateuserstable exists in the Backoffice schema. Platform-level role (auth.users.default_role):userorbusflow_staff. Theuser_tenant_assignments.default_role(MANAGER, DISPATCHER, DRIVER) determines tenant-level access, with additional capabilities grantable viauser_access_grantsfor small operators where one person fills multiple roles. A single user can belong to multiple Operators. Deactivation:auth.users.disabled = true(Nhost Admin SDK). See multi-tenant-jwt-session ADR. - CrewMember: Operational staff record for drivers and guides. Carries
role(DRIVER,GUIDE,DRIVER_GUIDE), contact fields (phone,email), and basic license data. Linked to Nhostauth.usersfor authentication viauser_idFK (nullable โ you can register crew members before the system provisions their login account). Status lifecycle:ACTIVEโINACTIVEโTERMINATED. Granular qualifications tracked viaCrewQualification. EmitsCrewMemberStatusChangedโ Operations. - CrewQualification: Typed, expiry-tracked professional certifications held by a
CrewMember(e.g., Module 95, ADR, border visa, digital tachograph card). Status lifecycle:VALIDโEXPIRING_SOON(Hasura Scheduled Trigger) โEXPIRED/REVOKED. EmitsQualificationExpiringโ Communications. The system tracks theDIGITAL_TACHOGRAPH_CARDtype but does not dispatch-block by default โ it becomes a hard block only when the operator enables theTACHOGRAPHintegration (Phase 2). - CrewAbsence: Leave tracking for
CrewMember. Types: vacation, sick, rest day, training. Status lifecycle:REQUESTEDโAPPROVED/REJECTED. OnlyAPPROVEDabsences block dispatch. EmitsCrewAbsenceApprovedโ Operations. - Vehicle: Physical fleet assets with
vehicle_class(coach, minibus, van, double-decker),transmission_type,current_mileage_km, and aseat_map_layoutJSONB (the schema documents the structure). Status lifecycle:ACTIVEโIN_MAINTENANCEโDECOMMISSIONED. EmitsVehicleStatusChangedโ Operations. - VehicleInspection: Scheduled regulatory inspection tracking (HU, SP, tachograph calibration, UVV). Safety-critical overdue inspections auto-set
blocks_dispatch = true. Status lifecycle:PENDINGโCOMPLETED/OVERDUE. EmitsVehicleInspectionOverdueโ Communications. - Supplier & Allotment: Manages third-party relationships and reserved inventory blocks (e.g., 20 hotel rooms, ferry slots) essential for executing multi-day tours. Each
Allotmentcarries acurrencyfield (ISO 4217) to support multi-currency procurement as required by the FX engine, and astatuslifecycle:ACTIVE(negotiated and available) โCONSUMED(fully allocated to departures) โEXPIRED(pastend_date) /CANCELLED. Within the Kalkulations-Engine, theSupplierentity acts as the decisive discriminator between Fremdleistung (third-party procurement โ subject to ยง 25 UStG margin taxation) and Eigenleistung (own services โ standard VAT). Every cost line item on theCostingSheetreferences its originatingSupplierto enable automatic tax strategy resolution. - Reseller: Represents business clients (travel agencies, institutional clients, tourism boards) whose functional role is to resell or procure the operator's inventory โ whether brokering seats on scheduled
TourOfferingsor requesting customCharterQuotesfor private groups. Distinguished byreseller_typeenum:AGENCY(commission-based sales channel) orINSTITUTIONAL(schools, clubs, public bodies, companies, or other organized groups buying a custom service). - PersonProfile: The central CRM record for any person the operator has a relationship with โ booker, traveler, or both. Unifies interaction histories, dietary needs, and preferred seating across multiple bookings. The system creates a PersonProfile when a person first appears as either a booker or a passenger on any booking. De-duplication matches by email or phone number. Profiles without contact identifiers (e.g., children) cannot de-duplicate and may accumulate duplicate records โ manual merge is the operator's responsibility via the CRM interface. Traveler-specific fields (
dietary_needs,date_of_birth) are nullable and only populated when relevant โ even for travelers, dietary needs are optional (not every tour type cares). DOB sync direction: OnBookingConfirmed, the PersonProfile updater writes back the Passenger'sdate_of_birthtoPersonProfile.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 document fields (document_number,nationality) are deliberately absent โ these are point-in-time travel credentials that belong on the booking-scoped CommercePassengerentity. Passport numbers expire and renew between bookings; the Passenger row captures the document the traveler carries on a specific departure. For returning travelers, the UI pre-fills border fields from the most recentPassengerrow linked viaperson_profile_idas a read-only suggestion. See ADR-037. - TourDeparture: A concrete, scheduled departure of a
TourTemplateโ e.g., "Nordsee 7-Tage, 15 June 2026". Holds all date-specific operational context:start_date,end_date, a clonedCostingSheet, and linkedBoardingPoints. TheTourDepartureis the Backoffice staging entity that, when published, creates/updates the CommerceTourOfferingvia aTourDeparturePublishedevent. Status lifecycle:DRAFTโREADYโPUBLISHEDโCOMPLETED/CANCELLED. - BoardingPoint (Library Item): An operator-level pickup location (Zustiegsstelle) maintained in a reusable library. Carries
name,address,geo_coordinates,zone_label(implicit grouping),surcharge, and optional door pickup configuration (door_pickup_available,door_pickup_surcharge,door_pickup_radius_km). Templates reference library items viatemplate_boarding_point_assignments, which controls per-template origin designation (is_origin), surcharge overrides, and door pickup toggles. Commerce maintains a denormalized read projection (available_boarding_pointsJSONB onTourOffering) synced viaTourDeparturePublishedevents. See boarding-points.md and adr-001-boarding-point-strategy.md. - AncillaryCatalogItem: An operator-level library of bookable extras (insurance, seat upgrades, luggage, excursions, meals). Each item carries a behavioral
type(INSURANCE,SEAT_UPGRADE,LUGGAGE,EXCURSION,MEAL,OTHER) and an operator-definedlabel. These types are deliberately behavior-driven: they govern refund assumptions, reporting buckets, fulfillment hints, and invoice line grouping. Room categories and single-room supplements remain inPriceMatrix/PricingConfig, not ancillaries. Templates reference catalog items viatemplate_ancillary_assignments, which supports per-template price overrides and anincluded_by_defaulttoggle. Follows the same centralized-overridable cascade as boarding points and deposit config. Commerce receives the resolved catalog viaTourDeparturePublishedevents asavailable_ancillariesJSONB onTourOffering. See ancillary-catalog.md. - NotificationTemplate: Operator-configurable message templates for automated communications (booking confirmations, pre-departure reminders, delay broadcasts). Carries
channel(EMAIL, WHATSAPP, PUSH),trigger_event,subject,body_templatewith variable interpolation, andlocale. Validtrigger_eventvalues correspond to registered Hasura Event Trigger and Scheduled Trigger names โ see workflow-orchestration.md for the canonical event catalog. - CostingSheet (Vorkalkulation): The universal cost planning engine. Contains projected fixed costs, variable costs (leveraging
Allotmentdata), FX risk buffers, and break-even metrics. Does not contain selling prices โ those live onPriceMatrix. Carries astatuslifecycle:DRAFT(editable, under construction) โCALCULATED(engine has produced valid cost aggregates and triggered PriceMatrix generation) โLOCKED(immutable โ triggered by theBookingConfirmedevent on first commercial commitment per ADR-009, defended by ยง 651a BGB; today this event fires onDEPOSIT_PAID, but cost-lock is modeled as an independent consumer ofBookingConfirmedand is unaffected if other consequences are re-bound to a different trigger โ see domain-commerce.md ยง Booking State vs. event decoupling). LOCKED means the system freezes costs, not prices. Edits to aLOCKEDsheet require cloning into a newDRAFTrevision. See costing-pricing-separation ADR. - PriceMatrix (Preisgestaltung): The market-driven selling price entity. Derives its baseline from a
CostingSheetbut evolves independently based on yield and channel strategies. Containsmargin_config(margin is a pricing rule, not a cost), resolved pricevariants[], andlist_price. Supports a 1:N relationship โ one CostingSheet can generate multiple PriceMatrices per sales channel. Uses immutable append versioning (every mutation creates a new version). Lifecycle:DRAFT(generated, under review) โPUBLISHED(pushed to Commerce viaPriceMatrixPublishedevent) โARCHIVED(superseded by a new version). Pricing hierarchy: Level 1 (Base Price from costs + margins), Level 4 (Manual Override โ "Dispatcher's Veto").[planned โ Phase 2]: Level 2 (Channel Rules), Level 3 (Algorithmic Yield Rules). See costing-pricing-separation ADR and pricing-screen.md. Each variant carries a completeapplied_conditions[]audit trail. - TourTemplate: The abstract commercial product โ the reusable blueprint for a tour type. Uses
pgvectorforai_embeddingto enable AI-assisted parsing of unstructured PDF itineraries into structured data. References a baselineCostingSheet. Scheduling a concrete departure creates aTourDeparture. Carries astatuslifecycle:DRAFT(incomplete, not yet publishable) โACTIVE(the user can schedule departures from this template) โARCHIVED(no new departures, existing ones unaffected). Storespricing_rules,pricing_config,capacity_rules, anddeposit_configas JSONB value objects (see below). - PricingRule, PricingConfig, CapacityRule & DepositConfig (Value Objects): Embedded as JSONB on
TourTemplate, validated at runtime via Valibot schemas (consistent with theCostingSheetvalue object pattern). PricingRule (input VO) structure:{ demographic: string, age_min, age_max, discount_type: 'PERCENTAGE' | 'ABSOLUTE', discount_value, label }โ defines operator-configurable demographic segments (e.g.,'ADULT','CHILD','SENIOR','STUDENT','INFANT') and their discount.'ADULT'is the conventional base segment (no discount). At PriceMatrix generation, PricingRules becomeAppliedConditionoutput VOs in each variant's audit trail. PricingConfig (input VO) structure:{ room_surcharge, includes_accommodation, season_config: SeasonConfig[], early_bird_config: EarlyBirdTier[] }โ configures room surcharges, seasonal tiers, and early-bird discount tiers. CapacityRule structure:{ strategy: 'FREE_SALE' | 'LIMITED' | 'REQUEST_ONLY', max_capacity, overbooking_buffer_pct }. DepositConfig structure:{ percentage: number, type: 'PERCENTAGE' | 'FIXED', min_amount: number | null }. Deposit rules require central defaults and immutable snapshots: operator/system defaults define the baseline; a concreteTourDepartureshould snapshot the resolved deposit rule so later default changes cannot silently alter a published tour; eachBookingmust snapshot the deposit terms applied at checkout for audit and legal stability. A template-leveldeposit_configoverride is a possible convenience, but should be validated before treating it as a core invariant. See PRODUCT_mollie-integration.md ยง3 for the cascading resolution logic. EachTourDepartureinherits all VOs at creation, and the user can override them per departure.PricingRuleandPricingConfigare deep-copied (snapshotted) onto thePriceMatrixat generation time for historical audit. - CharterQuote: A B2B commercial wrapper calculating dynamic Leerkilometer (empty runs) and toll costs before converting to a scheduled
TourDeparture. The associatedCostingSheet.variable_costsJSONB stores the calculated Leerkilometer distance and itemized toll cost segments under the keysleerkilometer_km,leerkilometer_cost, andtoll_segments[].
The Costing & Pricing Engine (Vorabkalkulation) โ
Full specification: See kalkulations-engine.md for the complete engine specification, including the two-phase pipeline (CostingSheet โ PriceMatrix), data structures, TypeScript variant/condition interfaces, composition algorithm, edge states, and embedded value objects.
Event contracts: See event-contracts-pricing.md for Backoffice โ Commerce payload schemas and delivery contracts.
Related docs: costing-pricing-separation ADR ยท pricing-screen.md ยท tax-engine-protocol.md
The CalculationEngine is a central Domain Service in the Backoffice context. It operates in two sequential phases:
Phase 1 โ Cost Calculation: Aggregates Eigenleistungen (own costs) and Fremdleistungen (third-party procurement via
Supplier/Allotment), applies FX risk buffers, validates break-even, and resolves tax strategy (STANDARD_VATvs.MARGIN_SCHEME_25). Output: aCostingSheetwithtotal_net_costand status =CALCULATED.Phase 2 โ Price Generation: Applies
margin_configrules to the cost base, generates thelist_price, then expands into a fullPriceMatrixwithvariants[]covering all combinations of room type ร demographic ร season ร early-bird tier. Each variant carries a completeapplied_conditions[]audit trail.
Three operating modes: (1) Commerce Cache Push for catalog business, (2) Yield Management Interface for manual price overrides, (3) Real-Time Calculation for B2B charter quotes.
Aggregates โ
Each aggregate defines a transactional consistency boundary. The aggregate root controls access to all children and enforces invariants. Cross-aggregate references follow ADR-036.
Operator โ
| Role | Entity |
|---|---|
| Root | Operator |
| Child | OperatorSettings (1:1) |
| Child | OperatorIntegrations |
| Child | TenantSubscription (1:1) |
Invariants: Exactly one settings/subscription row. Status: ONBOARDING โ ACTIVE โ SUSPENDED โ CHURNED. At most one integration per type. Events: TenantProvisioned, TenantActivated, TenantSuspended, TenantChurned.
TourTemplate โ
| Role | Entity |
|---|---|
| Root | TourTemplate |
| Child | TemplateBoardingPointAssignment |
| Child | TemplateAncillaryAssignment |
| VO | PricingRule[], PricingConfig, CapacityRule[], DepositConfig (JSONB) |
Invariants: At most one is_origin per template. Status: DRAFT โ ACTIVE โ ARCHIVED. Cross-aggregate refs: boarding_point_id and ancillary_catalog_item_id on assignments are soft FKs (library items have independent archival lifecycle).
TourDeparture โ
Single-entity aggregate. Concrete scheduled instance of a TourTemplate.
Invariants: Status: DRAFT โ READY โ PUBLISHED โ COMPLETED / CANCELLED. Publishing emits TourDeparturePublished. Cross-aggregate refs: tour_template_id (hard FK โ stable reference), costing_sheet_id (hard FK โ stable reference).
CostingSheet โ
Single-entity aggregate with embedded JSONB VOs (procurement_items[], variable_costs, fx_risk_buffer).
Invariants: Status: DRAFT โ CALCULATED โ LOCKED. LOCKED triggers automatically on first DEPOSIT_PAID. Edits to LOCKED require cloning.
PriceMatrix โ
Single-entity aggregate with embedded JSONB VOs (variants[], margin_config, pricing_rules_snapshot, pricing_config_snapshot).
Invariants: Status: DRAFT โ PUBLISHED โ ARCHIVED. Immutable append versioning. 1:N per CostingSheet. Events: PriceMatrixPublished, SeasonPricingFinalized. Cross-aggregate refs: costing_sheet_id and tour_departure_id are soft FKs (ADR-006 โ independent lifecycle). superseded_by is a hard FK (self-referential version chain).
CrewMember โ
| Role | Entity |
|---|---|
| Root | CrewMember |
| Child | CrewQualification |
| Child | CrewAbsence |
Invariants: Status: ACTIVE โ INACTIVE โ TERMINATED. Only ACTIVE members in dispatch. Only APPROVED absences block dispatch. VALID qualifications required for dispatch eligibility. Events: CrewMemberStatusChanged, CrewAbsenceApproved, QualificationExpiring.
Vehicle โ
| Role | Entity |
|---|---|
| Root | Vehicle |
| Child | VehicleInspection |
| VO | SeatMapLayout (JSONB) |
Invariants: Status: ACTIVE โ IN_MAINTENANCE โ DECOMMISSIONED. Only ACTIVE in dispatch. Overdue inspections block dispatch. Events: VehicleStatusChanged, VehicleInspectionOverdue, VehicleInspectionScheduled, VehicleInspectionCompleted.
Supplier โ
Single-entity aggregate. Discriminates Fremdleistung vs. Eigenleistung for tax strategy resolution.
Allotment โ
Single-entity aggregate. Reserved inventory blocks from a supplier.
Invariants: Status: ACTIVE โ CONSUMED / EXPIRED / CANCELLED. Cross-aggregate refs: supplier_id is a soft FK (independent lifecycle โ supplier archival must not cascade).
BoardingPointLibrary โ
Single-entity aggregate. Operator-level reusable pickup location.
AncillaryCatalogItem โ
Single-entity aggregate. Operator-level reusable bookable extra.
Reseller โ
Single-entity aggregate. B2B partner. reseller_type: AGENCY or INSTITUTIONAL.
PersonProfile โ
Single-entity aggregate. CRM record for any person the operator manages โ booker, traveler, or both. De-duplicated by contact identifier (email or phone).
NotificationTemplate โ
Single-entity aggregate. Operator-configurable message template.
Clock authorities for scheduled reminders: The
PRE_DEPARTURE_REMINDERcron fires from BackofficeTourDeparture.start_date(planning clock). The Freshness Gate validates against OperationsServiceLeg.statusbefore dispatch (execution truth). Template variables resolveboarding_timefrom OperationsServiceLeg.scheduled_departurefor thePICKUP-typed leg, falling back to BackofficeBoardingPointtime if no ServiceLeg exists yet.
CharterQuote โ
Single-entity aggregate. B2B quote wrapper.
Invariants: Status: DRAFT โ SENT โ ACCEPTED / REJECTED. Can convert to TourDeparture. Cross-aggregate refs: reseller_id (hard FK โ stable reference), costing_sheet_id (hard FK โ stable reference).
Infrastructure Entities (not aggregates) โ
change_events, import_jobs + import_error_rows, tenant_scrub_logs, fx_reference_rates, user_tenant_assignments, user_access_grants.