ADR-006: Costing vs. Pricing Separation โ
Status: โ Approved โ 2026-04-10 Supersedes: ADR-007 (CostingSheet yield override via
price_overridesJSONB overlay) Impacts:PRODUCT_domain-model.md,schema-backoffice.md,schema-commerce.md
Context โ
The CostingSheet entity bundled two distinct business concerns:
- Costing โ "What does this trip cost me?" (procurement, fuel, tolls, driver wages)
- Pricing โ "What do I charge the customer?" (margins, discounts, yield adjustments)
These are always separate business decisions. An operator may update costs (diesel spike) without changing published prices (contractual obligation). Conversely, yield management adjusts prices (last-minute discount) without touching the cost base. The combined entity forced a price_overrides JSONB hack to allow price changes on LOCKED sheets โ a violation of the immutability contract.
Decision โ
1. Core Domain Separation โ
Costing and pricing are separated into two distinct entities within the Backoffice Domain.
CostingSheet(The Facts): Manages deterministic inputs (vendor rates, fuel). Calculates the base cost model. Separates Eigenleistungen (in-house) from Fremdleistungen (third-party). Lifecycle:DRAFTโCALCULATEDโLOCKED. LOCKED means costs are frozen โ nothing more.PriceMatrix(The Strategy): Manages market-driven selling prices. Derives its baseline from theCostingSheetbut evolves independently based on yield and channel strategies. Lifecycle:DRAFTโPUBLISHEDโARCHIVED. Containsmargin_config(margin is a pricing rule, not a cost).- Commerce Read-Model: The Commerce domain stores a flattened
TourOfferingPricetable, updated asynchronously viaPriceMatrixPublishedevents, ensuring high-performance API reads for storefronts.
2. Pricing Hierarchy & Yield Engine โ
The system supports a 1:N relationship โ one CostingSheet to multiple PriceMatrices per sales channel. Prices are resolved through a strict hierarchical engine:
- Level 1: Base Price โ default gross price calculated from the
CostingSheetcosts + margin rules. - Level 4: Manual Overrides โ the "Dispatcher's Veto". Absolute highest priority. Halts automated yield adjustments for the specific modified cell.
NOTE
[planned โ Phase 2] Two additional levels will be added in Phase 2:
- Level 2: Channel Rules โ broad modifiers applied to specific catalogs (e.g., "OTA gets -15%").
- Level 3: Algorithmic Yield Rules โ automated, time/occupancy-based triggers (e.g., "<30% occupancy at T-21 days = -10%").
3. Versioning & Audit Trail โ
The PriceMatrix uses an Immutable Append strategy. Every mutation generates a new version (new id, incremented version counter). The previous version's superseded_by field points to the new version.
- Booking tickets store the exact
price_matrix_idactive at checkout, ensuring perfect Soll/Ist reconciliation regardless of downstream yield changes. - A scheduled garbage collection worker prunes/archives stale versions not referenced by an active cart, offering, or finalized ticket.
4. Tax Compliance & Financial Ledger Integration โ
The system isolates predictive estimates from legally binding actuals to satisfy ยง 25 UStG (Margenbesteuerung) and audit requirements.
- Soll-Steuer (Vorkalkulation): The
PriceMatrixcomputes estimated margin tax for UI projections using predicted Fremdleistungen and the current target selling prices. Values are visually framed as estimates (prefixed with~). - Ist-Steuer (Nachkalkulation): The
FinancialLedgerhandles final, legally binding tax calculations post-trip by matching actual vendor invoices against realized checkout receipts.
5. Checkout Validation Boundary โ
Before payment capture, the Commerce API validates the cart's price against the authoritative Backoffice price_matrix_id. Mismatches trigger a hard reject and cart refresh. This prevents stale-price purchases when yield adjustments happen between cart creation and payment.
6. CostingSheet Recalculation โ PriceMatrix Staleness โ
When a CostingSheet is recalculated (e.g., fuel price update), the system:
- Flags all linked
PriceMatrixentries as potentially stale - If a manual override's margin drops below the configured threshold, alerts the dispatcher for mandatory review
- Does not auto-regenerate prices โ the operator decides when to re-derive
Consequences โ
Positive:
- Clean separation of concerns โ costs and prices have independent lifecycles
- Yield management is native (edit PriceMatrix directly) โ no overlay hacks
- 1:N channel support enables B2C/B2B price differentiation
- Immutable versioning provides a perfect audit trail for Soll/Ist reconciliation
Negative:
- Two entities to manage instead of one (mitigated by UI: PriceMatrix auto-generates from CostingSheet)
- Garbage collection worker required for stale versions
- Commerce needs a read-model (
TourOfferingPrice) synced via events
Neutral:
margin_configmoves from CostingSheet to PriceMatrix โ changes the mental model for operators who think of margins as part of cost planning
Superseded:
- ADR-007 (CostingSheet Override on LOCKED Sheets) โ the
price_overridesJSONB overlay is no longer needed
pricing-screen.md documents the UI/UX safeguards for the pricing screen (Margin Inspector, Blended Tour Summary, stale override alerts).