Busflow Docs

Internal documentation portal

Skip to content

PriceMatrix β€” Variant Schema, Composition Algorithm & Edge States ​

Parent document: See kalkulations-engine.md for the two-phase CostingSheet β†’ PriceMatrix pipeline, the domain service contract, and the Soll/Ist feedback loop. Schema: See schema-backoffice.md Β§price_matrices for the physical table definition.

Variant JSONB Schema (PriceMatrix.variants) ​

typescript
type Variants = PriceVariant[];

interface PriceVariant {
  /** Deterministic slug: "{room_type}:{demographic}:{season}:{early_bird_tier}". */
  variant_key: string;

  // ── Dimension Identifiers ──
  /** "BASE" is default accommodation. "SURCHARGE" carries the surcharge (e.g., EZ). "NONE" for day trips. */
  room_type: 'BASE' | 'SURCHARGE' | 'NONE';
  /**
   * Operator-defined demographic key from PricingRule.
   * Common values: "ADULT", "CHILD", "SENIOR", "STUDENT", "INFANT".
   * Not a fixed enum β€” operators define their own segments via pricing_rules[].
   * "ADULT" is the conventional base (no discount).
   */
  demographic: string;
  /** Age bracket boundaries from PricingRule. Null for base segment. */
  age_min: number | null;
  age_max: number | null;
  /** Seasonal tier key. "DEFAULT" if no seasonal pricing configured. */
  season: string;
  /** Early-bird tier key. "NONE" if no early-bird pricing configured. */
  early_bird_tier: string;

  // ── Cost Baseline (DB1 Scope) ──
  /**
   * Aggregated variable procurement cost for this variant.
   * Immutable snapshot from CostingSheet. Fixed costs strictly excluded.
   * Varies by room_type (EZ procurement β‰  DZ) and demographic
   * (CostComponents with demographic_key are matched; shared costs are equal).
   * Enables per-ticket DB1: gross_price - variable_cost_snapshot.
   * Computed in parallel with the price algorithm β€” see note below.
   */
  variable_cost_snapshot: number;

  // ── Final Computed Prices ──
  /** Customer-facing price including tax (Standard VAT) or full collection price (TOMS). */
  gross_price: number;
  /** Standard VAT: gross - tax. TOMS: equals gross_price (operator collects full amount). */
  net_price: number;
  /** Standard VAT: computed (gross Γ— 19/119). TOMS (Β§ 25 UStG): strictly null β€” deferred to Phase 3. */
  tax_amount: number | null;

  // ── Audit Trail ──
  /**
   * Ordered computation chain. Preserves full derivation for dispatcher transparency.
   * Always contains at least one entry β€” even the base ADULT variant gets an
   * explicit DEMOGRAPHIC_DISCOUNT with configured_value=0, applied_amount=0.
   */
  applied_conditions: AppliedCondition[];
}

interface AppliedCondition {
  /** Applied in fixed priority order (see Composition Algorithm below). */
  type: 'DEMOGRAPHIC_DISCOUNT' | 'ROOM_SURCHARGE' | 'SEASON_SURCHARGE' | 'EARLY_BIRD_DISCOUNT';
  /** Dispatcher-facing label. E.g., "Erwachsener", "Kind 6–11", "EZ-Zuschlag". */
  label: string;
  /** Positive = surcharge, negative = discount. */
  adjustment_type: 'ABSOLUTE' | 'PERCENTAGE';
  /** Raw configured value. E.g., 25.00 for €25 surcharge, or 50 for 50%. 0 for base segment. */
  configured_value: number;
  /** Actual € amount added/subtracted at this step. */
  applied_amount: number;
  /** Running gross total after this condition. */
  running_gross: number;
  /** Commerce uses these to expire early-bird tiers without matrix regeneration. */
  valid_from: string | null;  // ISO 8601
  valid_until: string | null; // ISO 8601
}

Glossary: DB1 = Deckungsbeitrag 1 (revenue βˆ’ variable costs per ticket). DB2 = Deckungsbeitrag 2 (DB1 βˆ’ allocated fixed costs per departure). The PriceMatrix provides DB1 inputs; DB2 is a departure-level concern.

Pricing Input VOs ​

typescript
/** Stored in TourTemplate.pricing_config, snapshotted on PriceMatrix.pricing_config_snapshot. */
interface PricingConfig {
  /** Absolute EZ surcharge in operator currency. Null/0 = no room differentiation. */
  room_surcharge: number | null;
  /** True if tour includes hotel accommodation. False = day trip, room dimension skipped. */
  includes_accommodation: boolean;
  /** Seasonal tier definitions. Empty array = no seasonal pricing. */
  season_config: SeasonConfig[];
  /** Early-bird tier definitions. Empty array = no early-bird pricing. */
  early_bird_config: EarlyBirdTier[];
}

interface SeasonConfig {
  key: string;          // e.g., "PEAK", "SHOULDER", "OFF_SEASON"
  label: string;        // e.g., "Hauptsaison"
  periods: Array<{ start: string; end: string }>; // ISO 8601 dates, matched against departure date
  surcharge_amount: number; // absolute, in operator currency
}

/** Stepped tiers, not continuous decay. Measured in days before departure start_date. */
interface EarlyBirdTier {
  key: string;                          // e.g., "TIER_1", "TIER_2", "STANDARD"
  label: string;                        // e.g., "FrΓΌhbucher 90+ Tage"
  min_days_before_departure: number | null; // null for the default (no-discount) tier
  max_days_before_departure: number | null; // null for the earliest tier (no upper limit)
  discount_percentage: number;
}

Composition Algorithm (Phase 2, Step 6) ​

Input: list_price is the output of the margin application step (Β§4 in Phase 2). The engine computes it by applying margin_config rules to CostingSheet.total_net_cost and resolving the base adult gross selling price. The composition algorithm below takes this anchor as its starting point.

The engine pre-generates all early-bird variants in the Cartesian product. Commerce performs runtime filtering by matching the customer's booking date against AppliedCondition.valid_from/valid_until. Expired early-bird tiers become unselectable without matrix regeneration.

The engine applies conditions in fixed priority order. Each step modifies the running gross price. Tax resolution happens once at the end.

IMPORTANT

Step order rationale: The system applies the demographic discount before the room surcharge. Room surcharges are procurement costs (hotel EZ rate) that the system must not discount β€” a child's room costs the same as an adult's. The discount applies only to the tour package base price.

StepConditionMethodSource
0List pricelist_price β€” output of margin engine. Base adult, base-accommodation, default-season, no-early-bird price.PriceMatrix.list_price
1DEMOGRAPHIC_DISCOUNTPercentage or absolute on base anchor only. One PricingRule per variant β€” mutually exclusive by demographic key. For the base segment (conventionally 'ADULT'): emits an explicit AppliedCondition with configured_value: 0, applied_amount: 0 for audit completeness.pricing_rules[] per demographic
2ROOM_SURCHARGEAdditive. Applied after demographic discount so the surcharge is not discounted. Base accommodation = no change, surcharge accommodation (e.g., EZ) = + surcharge, NONE = no change (day trip).pricing_config.room_surcharge
3SEASON_SURCHARGEAdditive. Flat per-person surcharge, independent of room type. Tier by TourDeparture.start_date matched against season_config.periods[]. Multi-day tours crossing seasonal boundaries resolved by departure date only. This is a commercial pricing decision, not a pass-through of supplier seasonal rates.pricing_config.season_config[]
4EARLY_BIRD_DISCOUNTPercentage on running total. One variant generated per configured tier β€” all tiers pre-generated unconditionally. Commerce selects the active tier at booking time using valid_from/valid_until.pricing_config.early_bird_config[]
5Tax resolutionStandard VAT: tax = gross Γ— 19/119, net = gross - tax. TOMS (Β§ 25 UStG): tax = null, net = gross. Margin tax deferred to Phase 3 (FinancialLedger) at departure level where actual costs and passenger counts are known.CostingSheet.tax_strategy

variable_cost_snapshot: Computed in parallel with Steps 1–4, not as a numbered step. The engine queries the CostingSheet's procurement_items and variable_costs, matching CostComponent.demographic_key to the variant's demographic (shared costs where demographic_key = null the system distributes them equally). Proportioned by room_type for accommodation costs. Fixed/block costs (bus, driver) are strictly excluded β€” those are departure-level for DB2.

Matrix dimensions:

DimensionValuesGate logicDefault
room_type['NONE'] if !includes_accommodation. ['BASE', 'SURCHARGE'] if includes_accommodation && room_surcharge > 0. ['BASE'] if includes_accommodation && room_surcharge == 0.includes_accommodation is sole gate.['BASE']
demographicFrom pricing_rules[].demographic (operator-defined keys)At least one rule required.['ADULT']
seasonFrom pricing_config.season_config[].keyEmpty config β†’ 'DEFAULT'.['DEFAULT']
early_bird_tierFrom pricing_config.early_bird_config[].key + 'NONE'Empty config β†’ 'NONE' only.['NONE']

Each combination β†’ one PriceVariant. variant_key = {room_type}:{demographic}:{season}:{early_bird_tier}.

Room type naming: 'BASE' is the standard accommodation (typically DZ, but could be a hostel, shared cabin, or any operator-defined default). 'SURCHARGE' is the room with the surcharge (typically EZ). The operator defines the labels via PricingConfig. [Phase 2+]: Multiple room types with individual surcharges via room_types: Array<{ key, label, surcharge }> replacing room_surcharge: number.

Validation: If !includes_accommodation, then room_surcharge must be null or 0.

Edge States ​

Edge CaseBehavior
No PricingRulesAll variants use demographic: ADULT. Minimum 1 variant.
No seasonal/early-bird configDefaults to DEFAULT / NONE. Matrix collapses to room Γ— adult.
Day trip (no hotel)includes_accommodation = false β†’ room_type = ['NONE']. No surcharge. Validation: room_surcharge must be null/0.
Hotel, no EZ surchargeincludes_accommodation = true, room_surcharge = 0 β†’ room_type = ['BASE'] only. No room differentiation.
Zero variants (impossible)Engine always produces β‰₯ 1 variant. Empty variants[] β†’ CalculationError.
Overlapping seasonal windowsValidated at input. Overlap β†’ ValidationError at generation time.
Overlapping early-bird tiersContiguous by definition. Gap/overlap β†’ ValidationError.
Seasonal boundary spanningMulti-day tour crossing seasons resolved by departure date only.
Seasonal surcharge per room typeNot supported in Phase 1. Surcharge is flat per person across all room types.
Early-bird + seasonalBoth stack. Step 3 then Step 4.
Currency mismatchPriceMatrix.currency must equal CostingSheet.currency. Mismatch β†’ CalculationError.
Negative priceClamped to gross_price = 0.00. Non-blocking PriceMatrixWarning log.
Stale input rulesTourTemplate changes don't affect existing PriceMatrix snapshots.
TOMS tax displaytax_amount = null. UI shows gross with "inkl. MwSt." label.
Expired early-bird tierVariant stays in matrix. Commerce valid_until filter makes it unselectable.
Empty pricing_rules_snapshotStored as [], not null. Column is NOT NULL.
CostComponent without demographic_keyCost distributed equally across all demographics. Only components with matching demographic_key create per-demographic cost variance.

Embedded Value Objects (Kalkulations-Bausteine) ​

The following domain types are not standalone database entities. They exist as JSONB structures within CostingSheet or PriceMatrix and the runtime validates them via schema validation (Valibot). This architectural choice deliberately keeps them flexible for multi-tenant and multi-market variance.

Value ObjectEmbedded InPurpose
CostComponentCostingSheet.procurement_items[]A single net cost line item. Carries service_type (EIGEN or FREMD), geography (EU or THIRD_COUNTRY), and optional `demographic_key: string
TaxRuleper CostComponentDetermines the applicable taxation regime for a cost line item: STANDARD_VAT (Regelbesteuerung, currently 19%) for Eigenleistungen, or MARGIN_SCHEME_25 (Β§ 25 UStG Margensteuer) for touristic Fremdleistungen. Includes rate and legal_basis for audit compliance.
CurrencyProfileCostingSheet.fx_config.fx_rates[]Captures the exchange rate (exchange_rate) and a configurable risk buffer (buffer_percentage) for procurement items denominated in foreign currencies. The buffer hedges against FX fluctuations between calculation time and trip execution.
MarginTargetPriceMatrix.margin_config[]The operator's configured markup per cost category β€” either percentage-based (e.g., +18% on hotels) or absolute per passenger (e.g., +€15/pax on transport). Inherited from TourTemplate, overridable per channel.
ContributionMarginCostingSheet (top-level fields)The hard financial floor the engine must never breach. Composed of planned_contribution_margin (absolute €), break_even_pax (minimum viable passenger count), and an optional floor_price_per_pax. The engine flags any configuration where targets are mathematically unreachable.
AppliedConditionPriceMatrix.variants[].applied_conditions[]Output VO β€” resolved audit record of surcharges/discounts applied to derive each variant from the anchor price. Carries type (ROOM_SURCHARGE, DEMOGRAPHIC_DISCOUNT, SEASON_SURCHARGE, EARLY_BIRD_DISCOUNT), adjustment_type, configured_value, applied_amount, running_gross, and optional valid_from/valid_until. Replaces the former PricingCondition VO.
PricingConfigTourTemplate.pricing_config, TourDeparture.pricing_config (override), snapshotted on PriceMatrix.pricing_config_snapshotInput VO β€” container grouping room_surcharge, includes_accommodation, season_config[], and early_bird_config[]. Null on TourDeparture = inherit from TourTemplate.
SeasonConfigwithin PricingConfig.season_config[]Seasonal tier definition. Carries key, label, periods[] (calendar windows matched against departure date), and surcharge_amount.
EarlyBirdTierwithin PricingConfig.early_bird_config[]Early-bird stepped discount tier. Carries key, label, min_days_before_departure, max_days_before_departure, and discount_percentage. Stepped (not continuous decay).

Multi-Tenant Extensibility: Because these objects live as validated JSONB instead of rigid relational tables, the system can accommodate tenant-specific requirements (e.g., Swiss VAT tiers, Polish tax rules, or UK withholding tax) by extending the Valibot schemas in code without altering the database schema.


The Soll/Ist Feedback Loop: PriceMatrix ↔ FinancialLedger ​

The PriceMatrix (Vorkalkulation β€” selling prices) and the FinancialLedger (Nachkalkulation) form a closed-loop controlling system:

  1. Before departure (Planning): The published PriceMatrix defines the Soll β€” the selling prices the operator expects to collect from customers.
  2. During & after departure (Execution): The FinancialLedger in the Commerce schema aggregates Ist (actuals) β€” real booking revenues, actual ExpenseReceipt costs (fuel, tolls via OCR), and realized OnboardSale income.
  3. Management comparison: The system surfaces a direct Soll/Ist delta per departure, comparing the PriceMatrix (expected revenue) against CostingSheet (expected costs) and FinancialLedger (actual costs + revenue).

This feedback loop is the foundation of the margin-calculator feature in the workspace app and feeds into the services/accounting package for DATEV-compliant financial exports.


Technological Integration: Three Operating Modes ​

The Kalkulations-Engine exposes its capabilities through three distinct integration modes:

1. Commerce Cache Push (Catalog Business) When the operator publishes the PriceMatrix for a season (e.g., Summer 2026), the system pushes gross prices into read-optimized caches that the Commerce context consumes (booking widget, agency portals) via the TourOfferingPrice read-model. End customers receive pre-calculated prices instantly β€” no real-time recalculation required. This mode emits domain events:

  • PriceMatrixPublished β€” emitted when a PriceMatrix transitions DRAFT β†’ PUBLISHED, pushing structured gross prices to Commerce's TourOfferingPrice table for zero-latency querying. The operator must have published the matrix for prices to propagate.
  • SeasonPricingFinalized β€” emitted once after the operator publishes all PriceMatrices in a season, signaling downstream systems that the full catalog is ready for distribution.
  • CostingSheetRecalculated β€” emitted when the operator updates costs on a CostingSheet, flagging linked PriceMatrices as potentially stale.

For formal event contracts (payload schemas, delivery mechanisms, idempotency), see event-contracts-pricing.md.

2. Yield Management Interface The operator edits the PriceMatrix directly to adjust selling prices. Manual overrides (Level 4) take absolute priority, and the pricing screen tracks them with visual indicators. When a CostingSheet recalculates (e.g., fuel spike), the system flags linked PriceMatrices with stale-override alerts if manual overrides drop below configured margin thresholds β€” the operator decides whether to update prices. [planned β€” Phase 2]: Automated algorithmic yield rules (Level 3) for time/occupancy-based price adjustments.

3. Real-Time Calculation (Charter & B2B) In the B2B & Charter module (Anmietverkehr), the caller invokes the engine synchronously in real-time. When a client enters trip parameters into the web form, the engine:

  • Calculates the route including Leerkilometer (empty positioning runs) via services/routing
  • Pulls toll costs, driver per diems, and vehicle operating costs β†’ CostingSheet
  • Applies the configured B2B margin β†’ PriceMatrix
  • Generates a complete PDF quote (CharterQuote) in seconds

This real-time path reuses the exact same two-phase pipeline as the batch Vorabkalkulation β€” only the trigger and response time differ.

Internal documentation β€” Busflow