Busflow Docs

Internal documentation portal

Skip to content

ADR-006: Costing vs. Pricing Separation โ€‹

Status: โœ… Approved โ€” 2026-04-10 Supersedes: ADR-007 (CostingSheet yield override via price_overrides JSONB overlay) Impacts: PRODUCT_domain-model.md, schema-backoffice.md, schema-commerce.md


Context โ€‹

The CostingSheet entity bundled two distinct business concerns:

  1. Costing โ€” "What does this trip cost me?" (procurement, fuel, tolls, driver wages)
  2. 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 the CostingSheet but evolves independently based on yield and channel strategies. Lifecycle: DRAFT โ†’ PUBLISHED โ†’ ARCHIVED. Contains margin_config (margin is a pricing rule, not a cost).
  • Commerce Read-Model: The Commerce domain stores a flattened TourOfferingPrice table, updated asynchronously via PriceMatrixPublished events, 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 CostingSheet costs + 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_id active 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 PriceMatrix computes 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 FinancialLedger handles 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:

  1. Flags all linked PriceMatrix entries as potentially stale
  2. If a manual override's margin drops below the configured threshold, alerts the dispatcher for mandatory review
  3. 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_config moves 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_overrides JSONB 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).

Internal documentation โ€” Busflow