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) β
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 β
/** 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.
| Step | Condition | Method | Source |
|---|---|---|---|
| 0 | List price | list_price β output of margin engine. Base adult, base-accommodation, default-season, no-early-bird price. | PriceMatrix.list_price |
| 1 | DEMOGRAPHIC_DISCOUNT | Percentage 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 |
| 2 | ROOM_SURCHARGE | Additive. 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 |
| 3 | SEASON_SURCHARGE | Additive. 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[] |
| 4 | EARLY_BIRD_DISCOUNT | Percentage 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[] |
| 5 | Tax resolution | Standard 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'sprocurement_itemsandvariable_costs, matchingCostComponent.demographic_keyto the variant'sdemographic(shared costs wheredemographic_key = nullthe system distributes them equally). Proportioned byroom_typefor accommodation costs. Fixed/block costs (bus, driver) are strictly excluded β those are departure-level for DB2.
Matrix dimensions:
| Dimension | Values | Gate logic | Default |
|---|---|---|---|
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'] |
demographic | From pricing_rules[].demographic (operator-defined keys) | At least one rule required. | ['ADULT'] |
season | From pricing_config.season_config[].key | Empty config β 'DEFAULT'. | ['DEFAULT'] |
early_bird_tier | From 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 viaPricingConfig.[Phase 2+]: Multiple room types with individual surcharges viaroom_types: Array<{ key, label, surcharge }>replacingroom_surcharge: number.Validation: If
!includes_accommodation, thenroom_surchargemust benullor0.
Edge States β
| Edge Case | Behavior |
|---|---|
| No PricingRules | All variants use demographic: ADULT. Minimum 1 variant. |
| No seasonal/early-bird config | Defaults 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 surcharge | includes_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 windows | Validated at input. Overlap β ValidationError at generation time. |
| Overlapping early-bird tiers | Contiguous by definition. Gap/overlap β ValidationError. |
| Seasonal boundary spanning | Multi-day tour crossing seasons resolved by departure date only. |
| Seasonal surcharge per room type | Not supported in Phase 1. Surcharge is flat per person across all room types. |
| Early-bird + seasonal | Both stack. Step 3 then Step 4. |
| Currency mismatch | PriceMatrix.currency must equal CostingSheet.currency. Mismatch β CalculationError. |
| Negative price | Clamped to gross_price = 0.00. Non-blocking PriceMatrixWarning log. |
| Stale input rules | TourTemplate changes don't affect existing PriceMatrix snapshots. |
| TOMS tax display | tax_amount = null. UI shows gross with "inkl. MwSt." label. |
| Expired early-bird tier | Variant stays in matrix. Commerce valid_until filter makes it unselectable. |
| Empty pricing_rules_snapshot | Stored as [], not null. Column is NOT NULL. |
| CostComponent without demographic_key | Cost 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 Object | Embedded In | Purpose |
|---|---|---|
CostComponent | CostingSheet.procurement_items[] | A single net cost line item. Carries service_type (EIGEN or FREMD), geography (EU or THIRD_COUNTRY), and optional `demographic_key: string |
TaxRule | per CostComponent | Determines 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. |
CurrencyProfile | CostingSheet.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. |
MarginTarget | PriceMatrix.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. |
ContributionMargin | CostingSheet (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. |
AppliedCondition | PriceMatrix.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. |
PricingConfig | TourTemplate.pricing_config, TourDeparture.pricing_config (override), snapshotted on PriceMatrix.pricing_config_snapshot | Input VO β container grouping room_surcharge, includes_accommodation, season_config[], and early_bird_config[]. Null on TourDeparture = inherit from TourTemplate. |
SeasonConfig | within PricingConfig.season_config[] | Seasonal tier definition. Carries key, label, periods[] (calendar windows matched against departure date), and surcharge_amount. |
EarlyBirdTier | within 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:
- Before departure (Planning): The published
PriceMatrixdefines the Soll β the selling prices the operator expects to collect from customers. - During & after departure (Execution): The
FinancialLedgerin the Commerce schema aggregates Ist (actuals) β real booking revenues, actualExpenseReceiptcosts (fuel, tolls via OCR), and realizedOnboardSaleincome. - Management comparison: The system surfaces a direct Soll/Ist delta per departure, comparing the
PriceMatrix(expected revenue) againstCostingSheet(expected costs) andFinancialLedger(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'sTourOfferingPricetable 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.