Kalkulations-Engine β Costing & Pricing Specification β
Architectural decision: We separate Costing and Pricing into two distinct entities per the costing-pricing-separation ADR. The
CostingSheetcalculates costs; thePriceMatrixmanages selling prices.Domain context: Backoffice Bounded Context. See PRODUCT_domain-model.md for entities, schema-backoffice.md for schema, tax-engine.md for service placement, and pricing-screen.md for UI.
Domain Service: CalculationEngine β
The CalculationEngine is a central Domain Service residing in the Bounded Context "Backoffice" (Tour Planning & Inventory). It operates in two sequential phases:
- Cost Calculation β populates the
CostingSheetwith aggregated costs - Price Generation β creates a
PriceMatrixentity by applying margin rules and tax strategy to the cost base
- Orchestration: Gathers procurement data from the
Inventorycontext (Supplier,Allotment), fleet operating costs from theVehicle/Fleetcontext (km rates, tolls, driver per diems). - Input: Accepts a
CostingSheet(eitherTEMPLATE_BASELINE,DEPARTURE_CLONE, orCHARTER_CUSTOM). - Output Phase 1: Produces a fully costed
CostingSheetwithtotal_net_costand status βCALCULATED. - Output Phase 2: Creates a
PriceMatrix(DRAFT) by applyingmargin_config+PricingRule+ tax strategy to the cost base. EmitsPriceMatrixGenerateddomain event. - Execution Modes:
- Asynchronous (Catalog): For the classic KataloggeschΓ€ft, the engine runs as a batch process β often months before season start β pre-calculating all selling prices for downstream systems.
- Synchronous (B2B & Charter): For Anmietverkehr, the same engine operates in real-time, computing prices on-the-fly.
Phase 1: Cost Calculation (CostingSheet) β
Every bookable product passes through the same deterministic pipeline to calculate costs. This phase produces the CostingSheet β the operator's factual cost base.
1. Cost Aggregation (Selbstkosten-Ermittlung) The engine collects all cost components that the operator incurs to produce the trip:
- Own operating costs (Eigenleistungen): Diesel per projected km, driver daily rates, vehicle depreciation, insurance, parking fees, tolls (the routing service supplies these).
- Third-party procurement costs (Fremdleistungen): Net purchase prices for hotels, ferries, local guides, entrance tickets β each linking to a specific
Allotmentfrom a contractedSupplier. The system stores these as individualprocurement_itemson theCostingSheet.
The sum of all Eigen- and Fremdleistungen yields the Netto-Selbstkosten (total net cost of production) for the trip.
1b. Currency & FX Risk Calculation (WΓ€hrungs- und Risikopuffer) For destinations involving foreign currencies (e.g., CZK for Czech Republic, GBP for Great Britain), the engine automatically applies stored exchange rates. It also uses a configurable risk buffer to hedge against currency fluctuations between calculation time and trip execution.
base_currency: The operator's home currency (default:EUR)fx_rates[]: Array of{ target_currency, rate, buffer_percentage }entries- The engine applies the buffer multiplicatively on top of the spot rate (e.g., a 3% buffer on GBP protects the operator against moderate GBP appreciation).
2. Contribution Margin Validation (Deckungsbeitrags-PrΓΌfung) Before the engine derives prices, it validates the cost base against profitability targets:
- Break-even point (
break_even_pax): The minimum number of paying passengers required to cover all costs. - Planned contribution margin (
planned_contribution_margin): The absolute β¬ amount the operator expects to earn above cost. The engine flags any configuration where the target margin is mathematically unreachable at the given capacity.
This turns the CostingSheet from a simple calculator into an active planning instrument β the operator sees immediately whether a departure is viable.
3. Tax Strategy Resolution (Steuer-Engine) The tax_strategy field on the CostingSheet determines which taxation regime applies:
STANDARD_VAT: Regular German VAT (currently 19%) applied to selling prices. Applicable when the operator provides purely own services (Eigenleistungen).MARGIN_SCHEME_25(Β§ 25 UStG β Margenbesteuerung): Tax calculated only on the margin (difference between gross customer price and gross procurement costs). Mandatory for most DACH package tours bundling third-party services (Reisevorleistungen).
The tax engine dynamically selects the correct strategy based on the composition of the CostingSheet. If all line items qualify as Eigenleistungen β STANDARD_VAT. If any Fremdleistung is present β MARGIN_SCHEME_25.
Output: After Phase 1, the
CostingSheetholdstotal_net_costand status =CALCULATED. No selling prices exist yet.
Phase 2: Price Generation (PriceMatrix) β
The Pricing Engine takes the CostingSheet cost base and produces a PriceMatrix β the operator's market-driven selling price strategy.
4. Margin Application (Margenaufschlag) The operator configures margin rules per cost category. The system supports both percentage-based and absolute markups, applied granularly:
- A flat percentage on all third-party hotel costs (e.g., +18%)
- A different margin on ferry bookings (e.g., +12%)
- A fixed markup per seat on own transport costs (e.g., +β¬15/pax)
These rules reside in margin_config on the PriceMatrix (margin is a pricing rule, not a cost). The system inherits them from the TourTemplate baseline, but the user can override them per channel.
5. List Price Generation (Brutto-VK-Ermittlung) The engine computes the customer-facing base gross price and stores it as list_price on the PriceMatrix. This represents the List Price β the default adult, base-accommodation rate before any demographic, seasonal, or promotional adjustments. It serves as the operator's headline catalog price.
6. Price Matrix Generation (Preismatrix-Erzeugung) Starting from the anchor price, the engine generates a complete price matrix by applying conditions in fixed priority order (Demographic β Room β Season β Early-bird β Tax). See Β§Composition Algorithm below for the full specification. The engine stores the resulting matrix as variants[] (JSONB) on the PriceMatrix entity. Each PriceMatrix starts as DRAFT and requires explicit publication by the operator.
Yield Management: After initial generation, the operator can directly edit the
PriceMatrixto adjust selling prices β no CostingSheet modification needed. Manual overrides (Level 4: "Dispatcher's Veto") take absolute priority.[planned β Phase 2]: Channel Rules (Level 2) and Algorithmic Yield Rules (Level 3) for automated time/occupancy-based price adjustments.
Calculation Flow Diagram β
flowchart TD
A["**Eigenleistungen** (Diesel, Fahrer, Maut, Abschreibung)"] --> C
B["**Fremdleistungen** (Hotel, FΓ€hre, Guide β via Allotment)"] --> C
C["Ξ£ Netto-Selbstkosten"]
C --> D["+ Marge (konfigurierbar pro Kategorie)"]
D --> E["= Netto-Verkaufspreis"]
E --> F{"Steuer-Strategie?"}
F -- "Nur Eigenleistungen" --> G["Regel-USt (19% auf Netto-VK)"]
F -- "Fremdleistungen enthalten" --> H["Β§ 25 UStG Margensteuer\n(USt nur auf Marge)"]
G --> I["**Brutto-Verkaufspreis**"]
H --> I
I --> J["Γ· PricingRules\n(Erwachsene / Kinder / Senioren)"]
J --> K["Endkundenpreis pro Kategorie"]
C -.-> L{"DB-PrΓΌfung"}
L -- "Break-even Pax erreicht?" --> M["β
Abfahrt wirtschaftlich tragfΓ€hig"]
L -- "Unter Break-even" --> N["β οΈ Warnung an Disponenten"]
style A fill:#1e3a5f,color:#fff
style B fill:#1e3a5f,color:#fff
style C fill:#2d5a3d,color:#fff
style I fill:#5a1e3a,color:#fff
style K fill:#5a1e3a,color:#fffCostingSheet β Internal Data Structure (Pure Costs) β
The CostingSheet entity lives in schema: backoffice and uses PostgreSQL JSONB columns for maximum flexibility across diverse tour configurations. This entity contains only costs β selling prices live on the PriceMatrix entity.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
operator_id | UUID (FK) | Owning tenant |
source_type | Enum | TEMPLATE_BASELINE, DEPARTURE_CLONE, or CHARTER_CUSTOM β indicates whether this sheet is a reusable template budget, a date-specific clone, or a one-off charter calculation |
status | Enum | DRAFT, CALCULATED, or LOCKED β lifecycle gate preventing cost edits after bookings exist. LOCKED = costs frozen, not prices. |
version | Integer | Optimistic locking counter for concurrent access |
parent_sheet_id | UUID (FK) | For DEPARTURE_CLONE: references the TEMPLATE_BASELINE from which the system cloned this sheet. Tracks revision lineage. |
calculated_at | Timestamp | Timestamp of last successful cost calculation. Null if status = DRAFT. |
fixed_costs | JSONB | Projected fixed costs: driver daily rates, vehicle depreciation, insurance premiums, parking |
variable_costs | JSONB | Distance-dependent costs: diesel per km, projected toll fees (sourced from services/routing) |
procurement_items[] | JSONB[] | Array of typed CostComponent value objects (see Embedded Value Objects below). Each entry: { supplier_id, allotment_id, description, net_unit_cost, quantity, currency, category, service_type, geography, tax_rule } |
planned_contribution_margin | Decimal | Part of the ContributionMargin value object β target contribution margin in β¬ for the entire departure |
break_even_pax | Integer | Part of the ContributionMargin value object β calculated minimum passenger count to cover all costs |
tax_strategy | Enum | STANDARD_VAT or MARGIN_SCHEME_25 β auto-determined by presence of Fremdleistungen (via CostComponent.service_type), manually overridable |
fx_config | JSONB | Typed CurrencyProfile value objects: { base_currency, fx_rates: [{ target_currency, rate, buffer_percentage }] } β applies when the system uses procurement items in non-EUR currencies |
total_net_cost | Decimal | Computed: Ξ£ fixed + variable + procurement (incl. FX-adjusted amounts) |
currency | String (ISO 4217) | Calculation currency (default: EUR) |
created_at / updated_at | Timestamp | Audit trail |
PriceMatrix β Internal Data Structure (Selling Prices) β
The PriceMatrix entity lives in schema: backoffice as a first-class entity separate from CostingSheet. It manages selling prices and the margin rules that derive them. See costing-pricing-separation ADR.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
tenant_id | UUID (FK) | Owning tenant |
costing_sheet_id | UUID (FK) | Source cost calculation this price derives from |
tour_departure_id | UUID (FK) | Nullable β null for template-level previews |
channel | String | Sales channel this matrix serves (default: DEFAULT). Supports 1:N per CostingSheet. |
status | Enum | DRAFT, PUBLISHED, or ARCHIVED |
version | Integer | Monotonic version counter (immutable append β each edit creates a new version) |
margin_config | JSONB | Array of typed MarginTarget value objects β margin rules applied to derive selling prices from costs |
pricing_rules_snapshot | JSONB | Deep copy of PricingRule[] active at generation time. Immutable β enables historical audit of discount rules even after TourTemplate changes. |
pricing_config_snapshot | JSONB | Deep copy of PricingConfig (room surcharge, accommodation flag, season tiers, early-bird tiers) at generation time. Immutable. |
variants | JSONB | Typed PriceVariant[] β resolved price variants with variable_cost_snapshot, applied_conditions[], and tax fields. See full schema below. |
list_price | Decimal | The List Price β base adult, base-accommodation, default-season, no-early-bird gross selling price. All variants derive from this anchor. |
currency | String (ISO 4217) | Selling price currency (default: EUR) |
generated_at | Timestamp | When initially derived from CostingSheet |
published_at | Timestamp | When made available to Commerce (null if still DRAFT) |
superseded_by | UUID (FK) | Points to the next version in the immutable chain (null = current) |
created_at | Timestamp | Audit trail |
PriceMatrix β Variant Schema, Composition Algorithm & Edge States β
Full specification: See price-matrix-variant-spec.md for the complete
PriceVariantTypeScript interfaces,PricingConfiginput VOs, the 5-step composition algorithm, edge state table, and embedded value object catalog (Kalkulations-Bausteine).