Skip to main content

Pricing, Fees & Revenue Rules - Domain Specification

First Introduced: V1.0 Status: Specification Complete Last Updated: 2025-10-25


Overview

The Pricing, Fees & Revenue Rules domain defines how much a stay costs, who earns what, and which rules affect that calculation. This domain separates rate definition (base price, seasonality, discounts) from rate application (quotes, bookings), ensuring that all monetary logic is explicit, auditable, and extensible across villas, retreats, and future partner ecosystems.

Pricing is fundamentally a rules engine where context (dates, occupancy, channel) combined with configured rules produces a deterministic, immutable quote. The domain encompasses dynamic pricing logic, fee taxonomy, tax calculation, and multi-party revenue splitting.


Responsibilities

This domain IS responsible for:

  • Defining base rates and pricing modifiers (seasonal, length-of-stay, lead time, day-of-week)
  • Managing fee structures (cleaning, service, amenity fees, etc.)
  • Tax calculation across multiple jurisdictions (state, county, city, special districts)
  • Revenue split configuration (owner, manager, platform, channel commissions)
  • Quote generation with immutable pricing snapshots
  • Line-item breakdown for guest transparency and financial reporting
  • Dynamic pricing integration readiness (PriceLabs, Wheelhouse, Beyond)

This domain is NOT responsible for:

  • Payment processing and transaction execution (→ Payments & Financials domain)
  • Availability checking and calendar management (→ Availability & Calendars domain)
  • Property and unit definitions (→ Supply domain)
  • Channel distribution and rate syndication (→ Channels & Distribution domain)
  • Permission enforcement for price editing (→ Authorization & Access domain)

Relationships

Depends On:

Depended On By:

Related Domains:


Core Concepts

Entity: RatePlan

Purpose: Top-level container defining the complete set of pricing rules for a Space or Unit.

Key Attributes:

  • id (UUID, primary key)
  • org_id (UUID, foreign key → organizations.id)
  • account_id (UUID, foreign key → accounts.id)
  • space_id (UUID, foreign key → spaces.id)
  • unit_id (UUID, nullable, foreign key → units.id) - For future multi-unit support
  • name (VARCHAR, required) - e.g., "Standard Rate", "Winter Special", "Partner Rate"
  • slug (VARCHAR, unique within org+space)
  • currency (CHAR(3), default 'USD') - ISO 4217 code
  • status (ENUM) - draft | active | inactive | archived
  • valid_from, valid_to (DATE) - Validity period (NULL = indefinite)
  • base_rate_minor (INTEGER, required) - Base nightly rate in minor units (cents)
  • min_rate_minor, max_rate_minor (INTEGER, nullable) - Rate floor/ceiling constraints
  • min_stay_nights, max_stay_nights (INTEGER)
  • priority (INTEGER, default 100) - Higher priority = preferred when multiple plans match
  • channel_id (UUID, nullable) - Channel-specific rate plan (NULL = applies to all)
  • dynamic_pricing_enabled (BOOLEAN, default FALSE)
  • dynamic_pricing_provider (VARCHAR) - 'pricelabs' | 'wheelhouse' | 'beyond' | 'internal'
  • dynamic_pricing_config (JSONB) - Provider-specific settings
  • created_at, updated_at, created_by, updated_by (audit fields)
  • deleted_at (TIMESTAMP, nullable) - Soft delete

Relationships:

  • RatePlan → RateRule (1:*) - Pricing modifiers
  • RatePlan → FeeRule (1:*) - Additional charges
  • RatePlan → RevenueRule (1:*) - Revenue split configuration
  • RatePlan → Quote (1:*) - Quotes reference rate plan snapshot

Lifecycle:

  • Created: When setting up pricing for a Space
  • Activated: Set status='active' to make available for quoting
  • Updated: Modify rules without breaking existing quotes (versioning via snapshots)
  • Archived: Soft delete via deleted_at for audit retention

Business Rules:

  • Each Space must have at least one active RatePlan
  • RatePlans with same space_id and overlapping validity periods resolved by priority
  • Channel-specific RatePlans (channel_id set) take precedence over generic plans
  • base_rate_minor serves as fallback if no RateRules apply
  • Min/max rate constraints enforced AFTER all rule adjustments applied

Entity: RateRule

Purpose: Defines conditional adjustments to base nightly rate based on context (season, length-of-stay, day-of-week, lead time, occupancy, channel).

Key Attributes:

  • id (UUID, primary key)
  • rate_plan_id (UUID, foreign key → rate_plans.id, ON DELETE CASCADE)
  • name (VARCHAR, required)
  • rule_type (ENUM, required) - base | seasonal | los | dow | lead_time | occupancy | channel | gap | last_minute | orphan | custom
  • priority (INTEGER, default 100) - Higher = evaluated first
  • conditions (JSONB, required) - Rule matching criteria
  • adjustment_type (ENUM) - fixed_amount | percentage | multiplier | set_value
  • adjustment_value (NUMERIC(10,4), required)
  • adjustment_basis (ENUM) - base_rate | current_total | per_night | per_guest
  • compound_mode (ENUM) - additive | multiplicative | override | max | min
  • is_active (BOOLEAN, default TRUE)
  • valid_from, valid_to (DATE, nullable)

Condition Examples:

// Seasonal rule
{
"start_date": "2025-12-15",
"end_date": "2026-04-15"
}

// Length-of-stay discount
{
"min_nights": 7,
"max_nights": null
}

// Lead time early bird
{
"min_days_advance": 60
}

// Day of week pricing
{
"days": ["friday", "saturday"]
}

// Occupancy-based
{
"min_guests": 8,
"max_guests": 12
}

// Channel override
{
"channel_id": "ch_airbnb_001"
}

Compound Modes:

  • additive: adjustment applied to base rate, then added/subtracted to current total
  • multiplicative: adjustment multiplies current rate (chained)
  • override: replaces all previous calculations (channel-specific pricing)
  • max: takes maximum of current rate and calculated value
  • min: takes minimum (useful for rate caps)

Evaluation Order:

  1. Filter rules by conditions match (date range, LOS, lead time, etc.)
  2. Sort by priority (descending)
  3. Apply in priority order respecting compound_mode
  4. Apply rate plan min/max constraints

Business Rules:

  • Multiple rules can apply simultaneously (resolved via compound_mode)
  • Rules with overlapping conditions resolved by priority
  • rule_type='base' establishes starting point (overrides rate_plan.base_rate_minor)
  • Invalid condition combinations rejected at creation time

Entity: FeeRule

Purpose: Defines additional charges applied to a booking beyond nightly room rate (cleaning, service fees, amenities, taxes).

Key Attributes:

  • id (UUID, primary key)
  • rate_plan_id (UUID, foreign key → rate_plans.id, ON DELETE CASCADE)
  • name (VARCHAR, required) - e.g., "Cleaning Fee", "Pet Fee", "Service Fee"
  • fee_type (ENUM, required) - See Fee Type Taxonomy below
  • calculation_type (ENUM) - fixed | percentage | per_night | per_guest | tiered
  • amount_minor (INTEGER, nullable) - For fixed amounts (in minor units)
  • percentage (NUMERIC(5,4), nullable) - For percentage fees (e.g., 0.0500 = 5%)
  • basis (ENUM) - per_stay | per_night | per_guest | per_adult | per_child
  • applies_to (ENUM) - subtotal | total | room_rate | gross | taxable_amount
  • tiers (JSONB, nullable) - For tiered fee structures
  • conditions (JSONB, nullable) - When this fee applies
  • is_taxable (BOOLEAN, default FALSE)
  • tax_category (VARCHAR, nullable) - For tax jurisdiction mapping
  • is_mandatory (BOOLEAN, default TRUE)
  • is_optional (BOOLEAN, default FALSE)
  • is_refundable (BOOLEAN, default FALSE)
  • display_name (VARCHAR) - Guest-facing name
  • description (TEXT)
  • is_platform_revenue (BOOLEAN, default FALSE) - Does platform keep this fee?
  • is_passthrough (BOOLEAN, default FALSE) - Passed to 3rd party (e.g., taxes)
  • remittance_party (VARCHAR) - Who receives this fee (for passthrough)
  • priority (INTEGER, default 100)
  • is_active (BOOLEAN, default TRUE)

Fee Type Taxonomy:

Guest-Facing Fees (Guest Pays):

  • cleaning - One-time cleaning after checkout
  • pet - Additional cleaning for pets
  • extra_guest - Charge for guests beyond base occupancy
  • resort - Access to amenities (pool, gym, etc.)
  • amenity - Specific amenity charges
  • linen - Linen and towel service
  • damage_waiver - Insurance alternative to security deposit
  • hot_tub - Hot tub heating and maintenance
  • pool_heating - Pool heating (seasonal)
  • early_checkin - Check-in before standard time
  • late_checkout - Check-out after standard time
  • mid_stay_clean - Additional cleaning during stay

Platform/Service Fees (Platform Revenue):

  • booking_fee - Platform booking service fee
  • service_fee - Platform service/technology fee
  • processing_fee - Credit card/payment processing
  • channel_commission - Channel/OTA commission

Pass-Through Fees (Remitted to Third Parties):

  • county_tax - County lodging tax
  • city_tax - Municipal hotel/lodging tax
  • state_tax - State transient occupancy tax
  • federal_tax - Federal taxes
  • tourism_tax - Tourism development/marketing
  • occupancy_tax - General transient occupancy tax
  • vat - Value-added tax
  • gst - Goods and services tax

Other:

  • deposit - Partial payment at booking
  • security_deposit - Refundable deposit for damages
  • custom - Custom fee type

Conditional Fee Example:

// Extra guest fee
{
"applies_when": "guests > base_occupancy",
"base_occupancy": 6,
"max_extra_guests": 4
}

// Pet fee
{
"requires_addon": "pet_friendly"
}

// Hot tub fee
{
"requires_amenity": "hot_tub_available"
}

Tiered Fee Example:

[
{
"min_amount_minor": 0,
"max_amount_minor": 100000,
"rate": 0.05
},
{
"min_amount_minor": 100000,
"max_amount_minor": 300000,
"rate": 0.03
},
{
"min_amount_minor": 300000,
"max_amount_minor": null,
"rate": 0.02
}
]

Entity: TaxJurisdiction

Purpose: Defines geographic tax authorities that may levy taxes on vacation rentals.

Key Attributes:

  • id (UUID, primary key)
  • jurisdiction_type (ENUM) - federal | state | county | city | district | special
  • jurisdiction_code (VARCHAR, unique) - FIPS code or official identifier
  • jurisdiction_name (VARCHAR, required)
  • parent_jurisdiction_id (UUID, nullable) - Hierarchical relationship
  • country_code (CHAR(2)) - ISO 3166-1 alpha-2
  • state_code (VARCHAR)
  • county_name (VARCHAR)
  • city_name (VARCHAR)
  • postal_codes (TEXT[]) - Array of applicable ZIP/postal codes
  • geo_boundary (GEOGRAPHY(POLYGON, 4326)) - PostGIS polygon for precise mapping
  • tax_authority_name (VARCHAR, required)
  • tax_authority_contact (JSONB) - Contact details, website, filing info
  • filing_frequency (VARCHAR) - monthly | quarterly | annual
  • filing_threshold_minor (INTEGER) - Minimum amount before filing required
  • is_active (BOOLEAN, default TRUE)
  • effective_from, effective_to (DATE)

Relationships:

  • TaxJurisdiction → TaxJurisdiction (parent-child hierarchy)
  • TaxJurisdiction → TaxRule (1:*)
  • TaxJurisdiction → SpaceTaxMapping (1:*)

Lifecycle:

  • Created: When adding support for new tax authority
  • Updated: Rate changes, boundary adjustments
  • Deactivated: Set is_active=FALSE (never deleted for audit)

Entity: TaxRule

Purpose: Defines tax rates, calculation methods, and applicability for a jurisdiction.

Key Attributes:

  • id (UUID, primary key)
  • jurisdiction_id (UUID, foreign key → tax_jurisdictions.id)
  • tax_name (VARCHAR, required) - e.g., "California State Transient Occupancy Tax"
  • tax_code (VARCHAR) - Official code/reference
  • tax_type (ENUM) - occupancy_tax | lodging_tax | hotel_tax | tourism_tax | sales_tax | vat | gst | resort_tax | convention_tax | city_tax | county_tax | state_tax | custom
  • rate_type (ENUM) - percentage | fixed_per_night | fixed_per_stay | tiered
  • tax_rate (NUMERIC(7,6)) - 0.105000 = 10.5%
  • fixed_amount_minor (INTEGER) - For fixed taxes
  • tiers (JSONB) - For progressive tax rates
  • applies_to (ENUM) - room_rate | total_before_tax | gross_total | specific_fees
  • applies_to_fees (TEXT[]) - Array of fee_types this tax applies to
  • exemption_rules (JSONB) - Conditions for tax exemption
  • compound_taxes (BOOLEAN, default FALSE) - Tax on tax?
  • calculation_order (INTEGER, default 1)
  • rounding_rule (ENUM) - up | down | nearest_cent | nearest_dollar
  • platform_collects (BOOLEAN, default TRUE)
  • platform_remits (BOOLEAN, default FALSE)
  • marketplace_facilitator_rule (BOOLEAN, default FALSE)
  • is_active (BOOLEAN, default TRUE)
  • effective_from, effective_to (DATE)
  • regulation_reference (TEXT) - Link to official regulation

Exemption Rule Examples:

// Long-term stay exemption (30+ nights)
{
"exemption_type": "long_term_stay",
"min_nights": 30,
"full_exemption": true
}

// Military personnel exemption
{
"exemption_type": "guest_status",
"guest_types": ["military", "veteran"],
"requires_verification": true
}

// Threshold-based exemption
{
"exemption_type": "threshold",
"threshold_minor": 10000,
"below_threshold_rate": 0.05,
"above_threshold_rate": 0.10
}

Business Rules:

  • Tax calculation occurs AFTER all fees applied
  • Multiple taxes from different jurisdictions apply cumulatively
  • Exemptions evaluated before calculation
  • Marketplace facilitator laws determine collection responsibility
  • Rounding applied per-tax, then summed for total

Entity: SpaceTaxMapping

Purpose: Maps Spaces to applicable tax jurisdictions based on property location.

Key Attributes:

  • id (UUID, primary key)
  • org_id (UUID, foreign key → organizations.id)
  • space_id (UUID, foreign key → spaces.id)
  • jurisdiction_id (UUID, foreign key → tax_jurisdictions.id)
  • mapping_method (ENUM) - address | postal_code | geo_coordinates | manual
  • is_manual_override (BOOLEAN, default FALSE)
  • override_reason (TEXT)
  • is_active (BOOLEAN, default TRUE)
  • verified_at (TIMESTAMP)
  • verified_by (UUID, foreign key → users.id)

Jurisdiction Resolution Algorithm:

  1. Start with federal/national level
  2. Resolve state/province
  3. Resolve county
  4. Resolve city/municipality
  5. Identify special districts (tourism zones, etc.)
  6. Check for manual overrides (take precedence)

Entity: RevenueRule

Purpose: Defines how booking revenue is split between Accounts (owner, manager, platform, partners).

Key Attributes:

  • id (UUID, primary key)
  • rate_plan_id (UUID, foreign key → rate_plans.id)
  • name (VARCHAR, required) - e.g., "Owner Revenue Share", "Platform Commission"
  • recipient_account_id (UUID, foreign key → accounts.id)
  • recipient_type (ENUM) - owner | manager | platform | partner | channel | other
  • split_type (ENUM) - percentage | fixed_amount | tiered | remainder
  • split_percentage (NUMERIC(5,4)) - 0.2000 = 20%
  • fixed_amount_minor (INTEGER) - Fixed fee per booking
  • tiers (JSONB) - For tiered commission structures
  • split_basis (ENUM) - gross | net | subtotal | owner_share | platform_fees | guest_fees
  • min_amount_minor (INTEGER) - Minimum guaranteed payout
  • max_amount_minor (INTEGER) - Maximum cap on split
  • priority (INTEGER, default 100)
  • apply_order (INTEGER) - Order of application (1 = first)
  • conditions (JSONB) - When this split applies
  • is_platform_fee (BOOLEAN, default FALSE)
  • is_passthrough (BOOLEAN, default FALSE)
  • is_active (BOOLEAN, default TRUE)
  • valid_from, valid_to (DATE)

Split Basis Definitions:

  • gross: Total booking amount including all fees and taxes
  • net: Gross minus platform fees
  • subtotal: Room rate only (nightly total before fees)
  • owner_share: After platform commission deducted
  • platform_fees: Only platform-collected fees (service fee, booking fee)
  • guest_fees: Only guest-paid fees (cleaning, amenities)

Tiered Revenue Example:

[
{
"min_revenue_minor": 0,
"max_revenue_minor": 50000,
"rate": 0.20,
"description": "20% on first $500"
},
{
"min_revenue_minor": 50000,
"max_revenue_minor": 200000,
"rate": 0.15,
"description": "15% on $500-$2,000"
},
{
"min_revenue_minor": 200000,
"max_revenue_minor": null,
"rate": 0.10,
"description": "10% on amounts over $2,000"
}
]

Channel-Specific Split Example:

// Direct bookings: 10% platform commission
{
"channel_type": "direct"
}

// Airbnb: 23% to cover their 3% fee
{
"channel_id": "ch_airbnb_001"
}

// VRBO: 28% to cover their 8% fee
{
"channel_id": "ch_vrbo_001"
}

Entity: Quote

Purpose: Immutable snapshot of pricing calculation for a specific stay request, serving as the pricing contract for a booking.

Key Attributes:

  • id (UUID, primary key)
  • org_id (UUID, foreign key → organizations.id)
  • account_id (UUID, foreign key → accounts.id)
  • space_id (UUID, foreign key → spaces.id)
  • unit_id (UUID, nullable)
  • rate_plan_id (UUID, foreign key → rate_plans.id)
  • rate_plan_snapshot (JSONB, required) - Frozen copy of rate plan at quote time
  • checkin_date, checkout_date (DATE, required)
  • nights (INTEGER, required)
  • guests, adults, children, pets (INTEGER)
  • currency (CHAR(3), default 'USD')
  • exchange_rate (NUMERIC(12,6), default 1.0)
  • subtotal_minor (INTEGER) - Sum of nightly rates before fees
  • fees_total_minor (INTEGER) - Sum of all fees
  • taxes_total_minor (INTEGER) - Sum of all taxes
  • total_minor (INTEGER) - Grand total
  • guest_pays_minor (INTEGER) - What guest actually pays
  • owner_revenue_minor, platform_revenue_minor (INTEGER)
  • source (ENUM) - direct | partner | channel | manual | api
  • channel_id (UUID, nullable)
  • quote_code (VARCHAR, unique) - Human-readable reference
  • status (ENUM) - draft | valid | expired | booked | cancelled | superseded
  • valid_until, expires_at (TIMESTAMP)
  • booking_id (UUID, nullable) - Set when converted to booking
  • converted_at (TIMESTAMP)
  • discount_code, promo_code (VARCHAR)
  • special_requests (TEXT)
  • calculation_engine_version (VARCHAR)
  • calculation_duration_ms (INTEGER)
  • rules_applied (JSONB) - Array of rule IDs applied

Relationships:

  • Quote → RatePlan (*:1) - References source rate plan
  • Quote → QuoteLineItem (1:*) - Detailed breakdown
  • Quote → QuoteDailyRate (1:*) - Per-night rates
  • Quote → QuoteRevenueSplit (1:*) - Revenue distribution
  • Quote → Booking (1:1) - Converted booking

Lifecycle:

  • Created: When guest requests pricing for specific dates/occupancy
  • Valid: Available for conversion to booking until expires_at
  • Expired: Automatically expire after validity period (typically 24-48 hours)
  • Booked: Immutably linked to confirmed booking
  • Cancelled: Booking cancelled (quote retained for audit)
  • Superseded: Replaced by newer quote (re-quote scenario)

Business Rules:

  • Quotes immutable once status='booked'
  • Expired quotes cannot be converted to bookings
  • Quote totals must balance: total_minor = subtotal_minor + fees_total_minor + taxes_total_minor
  • rate_plan_snapshot preserves exact rules applied (protects against future rule changes)
  • Multiple valid quotes can exist for same space/dates (e.g., different guest counts)

Entity: QuoteLineItem

Purpose: Itemized breakdown of quote showing every rate, fee, tax, discount, and adjustment.

Key Attributes:

  • id (UUID, primary key)
  • quote_id (UUID, foreign key → quotes.id, ON DELETE CASCADE)
  • line_type (ENUM) - nightly_rate | fee | tax | discount | adjustment | deposit | credit
  • item_name (VARCHAR, required)
  • item_code (VARCHAR) - Reference code
  • description (TEXT)
  • rate_rule_id, fee_rule_id, tax_rule_id (UUID, nullable) - Source references
  • quantity (NUMERIC(10,4), default 1) - e.g., 7 nights, 2 guests
  • unit_price_minor (INTEGER) - Price per unit
  • amount_minor (INTEGER, required)
  • calculation_basis (VARCHAR) - What this was calculated from
  • calculation_formula (TEXT) - Human-readable formula
  • is_taxable (BOOLEAN, default FALSE)
  • taxable_amount_minor (INTEGER)
  • display_order (INTEGER, default 100)
  • is_visible_to_guest (BOOLEAN, default TRUE)

Example Line Items:

Night 1: $500 (Base: $450, Weekend: +$50)
Night 2: $500
...
Cleaning Fee: $150 (mandatory, per stay)
Pet Fee: $200 (2 dogs × $100)
Service Fee: $170 (5% of $3,400)
CA State Tax: $313.60 (8% of $3,920)
County Tax: $235.20 (6% of $3,920)
7-Night Discount: -$170 (-5%)

Entity: QuoteDailyRate

Purpose: Per-night rate breakdown showing base rate and applied adjustments for each night.

Key Attributes:

  • id (UUID, primary key)
  • quote_id (UUID, foreign key → quotes.id, ON DELETE CASCADE)
  • date (DATE, required)
  • day_of_week (VARCHAR)
  • base_rate_minor (INTEGER) - Rate before adjustments
  • adjusted_rate_minor (INTEGER) - Final rate after adjustments
  • rules_applied (JSONB) - Array of {rule_id, rule_type, adjustment}
  • night_number (INTEGER) - 1 = first night, 2 = second night, etc.

Entity: QuoteRevenueSplit

Purpose: Revenue distribution breakdown for each quote showing how money is split.

Key Attributes:

  • id (UUID, primary key)
  • quote_id (UUID, foreign key → quotes.id, ON DELETE CASCADE)
  • recipient_account_id (UUID, foreign key → accounts.id)
  • recipient_type (VARCHAR)
  • revenue_rule_id (UUID, nullable)
  • split_amount_minor (INTEGER, required)
  • split_percentage (NUMERIC(5,4))
  • basis_amount_minor (INTEGER) - Amount this split was calculated from
  • basis_type (VARCHAR)
  • payout_timing (ENUM) - immediate | on_confirmation | on_checkin | on_completion | on_schedule
  • display_order (INTEGER, default 100)

Workflows

Workflow: Generate Quote

Trigger: Guest selects dates, occupancy, and requests pricing

Steps:

  1. Validate inputs: checkin_date, checkout_date, guests, space_id, channel (optional)
  2. Select RatePlan:
    • Filter by space_id, validity (valid_from <= checkin, valid_to >= checkout)
    • Filter by channel match (if specified)
    • Select highest priority active plan
  3. Calculate daily rates:
    • For each night in stay:
      • Get applicable RateRules (date range, day-of-week, LOS, lead time)
      • Sort by priority
      • Apply rules sequentially respecting compound_mode
      • Record result in QuoteDailyRate
  4. Sum subtotal: Sum all adjusted nightly rates
  5. Apply fee rules:
    • Filter mandatory fees (is_mandatory=true)
    • Evaluate conditional fees (check conditions)
    • Calculate amounts based on basis (per_stay, per_night, per_guest)
    • Create QuoteLineItem for each fee
  6. Calculate taxes:
    • Resolve tax jurisdictions from space location
    • Get active tax rules
    • Check exemptions
    • Calculate taxes in order (compound_taxes handling)
    • Apply rounding rules
    • Create QuoteLineItem for each tax
  7. Calculate revenue splits:
    • Get active RevenueRules from rate plan
    • Sort by apply_order
    • Calculate splits sequentially
    • Create QuoteRevenueSplit records
  8. Validate totals: Ensure total_minor = subtotal_minor + fees_total_minor + taxes_total_minor
  9. Set expiry: expires_at = NOW() + 48 hours (configurable)
  10. Generate quote_code: Human-readable reference (e.g., "TVL-2025-10-25-0042")
  11. Return quote: Full breakdown with line items

Postconditions:

  • Quote record created with status='valid'
  • All line items, daily rates, and revenue splits recorded
  • Quote expires automatically after validity period
  • Immutable snapshot preserves pricing even if rules change

Performance Requirements:

  • Target: <100ms for quote generation
  • Max acceptable: <500ms
  • Cache rate plans and tax jurisdictions for performance

Workflow: Convert Quote to Booking

Trigger: Guest confirms booking

Steps:

  1. Verify quote validity:
    • Check status='valid'
    • Check expires_at > NOW()
    • Check availability not changed
  2. Check availability: Call Availability domain to ensure dates still available
  3. Create booking: Pass quote.id to Bookings domain
  4. Update quote status:
    • Set status='booked'
    • Set booking_id = <new_booking_id>
    • Set converted_at = NOW()
  5. Lock pricing: Quote becomes immutable (enforced by application and triggers)
  6. Emit event: quote.converted event for analytics

Postconditions:

  • Quote frozen with immutable pricing
  • Booking references quote for pricing breakdown
  • Revenue splits available for payout processing
  • Pricing audit trail complete

Workflow: Update Rate Plan

Trigger: Admin modifies pricing rules

Steps:

  1. Create new version: Clone existing rate plan (preserves history)
  2. Update rules: Add, modify, or deactivate RateRules, FeeRules, RevenueRules
  3. Validate rules:
    • Check for conflicting priorities
    • Validate condition JSON schemas
    • Ensure at least one active RateRule
    • Verify revenue splits sum correctly (if applicable)
  4. Activate: Set status='active' on new version
  5. Archive old: Set previous version status='archived' (retain for audit)
  6. Emit event: rate_plan.updated for downstream systems
  7. Invalidate cache: Clear cached rate plans

Postconditions:

  • New quotes use updated rate plan
  • Existing quotes retain original pricing (immutable)
  • Full audit trail of pricing changes
  • No impact on confirmed bookings

Rationale

Design Principles:

  1. Separation of Definition and Application: RatePlans and Rules define logic once; Quotes and Bookings store immutable results. This ensures pricing consistency and protects against retroactive changes.

  2. Transparency: Every fee, tax, and split recorded explicitly. Guests see complete breakdowns. Owners understand payouts. Platform maintains audit compliance.

  3. Extensibility: Rule-based architecture supports new pricing strategies (dynamic pricing, AI optimization, promotional campaigns) without schema changes. JSONB conditions allow complex logic.

  4. Multi-Party Ready: RevenueRules future-proof the model for complex splits (owner, manager, platform, channel commissions, affiliate fees).

  5. Tax Compliance: Comprehensive tax engine handles multi-jurisdictional complexity, exemptions, marketplace facilitator laws, and remittance tracking.

  6. Immutability: Quotes become immutable contracts. Pricing guaranteed once booking confirmed. Regulatory compliance through audit trails.

  7. Performance: Denormalized quote totals enable fast retrieval. Indexed filters support quick rate lookups. Caching strategies reduce calculation overhead.


MVP Implementation

MVP.0 Scope: OUT OF SCOPE

  • Pricing domain deferred to V1.0
  • MVP.0 uses hardcoded rates (not database-driven)

V1.0 Scope: Dynamic Pricing Engine

Included:

  • RatePlan, RateRule, FeeRule entities
  • Quote generation with line-item breakdown
  • Basic tax calculation (percentage-based, US-only)
  • Simple revenue splits (owner/platform percentage)
  • Fee taxonomy (cleaning, service, pet fees)
  • Quote expiry and conversion to booking
  • Immutable quote snapshots

Fee Types Supported:

  • Cleaning fee (fixed, per stay)
  • Pet fee (per stay or per pet)
  • Extra guest fee (per guest per night)
  • Service fee (percentage of subtotal)
  • Processing fee (percentage of total)

Tax Calculation:

  • US state, county, city tax rates
  • Manual jurisdiction mapping (space → jurisdictions)
  • Percentage-based tax calculation
  • Basic exemptions (long-term stays 30+ nights)
  • Platform collection tracking

Revenue Splits:

  • Simple percentage splits (owner 80%, platform 20%)
  • Fixed commission structures
  • Basis: gross or net
  • No tiered or conditional splits (V1.1+)

Dynamic Pricing:

  • Integration hooks defined but not implemented
  • Manual rate updates only
  • No AI/ML optimization (V2.0+)

Deferred to V1.1:

  • Avalara MyLodgeTax integration (automated tax)
  • Gap night and orphan night automation
  • Promotional codes and discount campaigns
  • Multi-currency support
  • Tiered revenue splits
  • Channel-specific revenue overrides

Deferred to V2.0:

  • AI-powered dynamic pricing
  • Demand forecasting
  • Competitor rate monitoring
  • International tax compliance (VAT/GST)
  • White-label marketplace revenue sharing

Implementation Notes

Database Indexes

Critical for performance:

-- Rate Plans
CREATE INDEX idx_rate_plans_org_space ON rate_plans(org_id, space_id, status);
CREATE INDEX idx_rate_plans_validity ON rate_plans(valid_from, valid_to) WHERE deleted_at IS NULL;
CREATE INDEX idx_rate_plans_channel ON rate_plans(channel_id) WHERE channel_id IS NOT NULL;

-- Rate Rules
CREATE INDEX idx_rate_rules_plan ON rate_rules(rate_plan_id, priority);
CREATE INDEX idx_rate_rules_type ON rate_rules(rule_type, is_active);

-- Fee Rules
CREATE INDEX idx_fee_rules_plan ON fee_rules(rate_plan_id, priority);
CREATE INDEX idx_fee_rules_type ON fee_rules(fee_type, is_active);
CREATE INDEX idx_fee_rules_taxable ON fee_rules(is_taxable) WHERE is_taxable = TRUE;

-- Tax Rules
CREATE INDEX idx_tax_rules_jurisdiction ON tax_rules(jurisdiction_id, is_active);
CREATE INDEX idx_tax_rules_effective ON tax_rules(effective_from, effective_to);

-- Quotes
CREATE INDEX idx_quotes_org_space ON quotes(org_id, space_id);
CREATE INDEX idx_quotes_dates ON quotes(checkin_date, checkout_date);
CREATE INDEX idx_quotes_status ON quotes(status, expires_at);
CREATE INDEX idx_quotes_code ON quotes(quote_code);
CREATE UNIQUE INDEX idx_quotes_booking ON quotes(booking_id) WHERE booking_id IS NOT NULL;

-- Quote Line Items
CREATE INDEX idx_quote_line_items_quote ON quote_line_items(quote_id, display_order);
CREATE INDEX idx_quote_line_items_type ON quote_line_items(line_type);

-- Space Tax Mappings
CREATE INDEX idx_space_tax_mappings_space ON space_tax_mappings(space_id, is_active);
CREATE UNIQUE INDEX idx_space_tax_mappings_unique ON space_tax_mappings(space_id, jurisdiction_id);

Constraints

Enforce data integrity:

-- Rate Plans
ALTER TABLE rate_plans ADD CONSTRAINT valid_date_range
CHECK (valid_to IS NULL OR valid_to > valid_from);
ALTER TABLE rate_plans ADD CONSTRAINT valid_rate_range
CHECK ((min_rate_minor IS NULL OR max_rate_minor IS NULL) OR min_rate_minor <= max_rate_minor);

-- Revenue Rules
ALTER TABLE revenue_rules ADD CONSTRAINT valid_split_percentage
CHECK (split_percentage IS NULL OR (split_percentage >= 0 AND split_percentage <= 1));

-- Tax Rules
ALTER TABLE tax_rules ADD CONSTRAINT valid_tax_rate
CHECK (tax_rate IS NULL OR (tax_rate >= 0 AND tax_rate <= 1));

-- Quotes
ALTER TABLE quotes ADD CONSTRAINT valid_stay_dates
CHECK (checkout_date > checkin_date);
ALTER TABLE quotes ADD CONSTRAINT valid_guests
CHECK (guests > 0);
ALTER TABLE quotes ADD CONSTRAINT valid_amounts
CHECK (total_minor = subtotal_minor + fees_total_minor + taxes_total_minor);

Triggers

Enforce immutability:

-- Prevent quote modification after booking
CREATE OR REPLACE FUNCTION prevent_quote_modification()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status = 'booked' AND NEW.status != OLD.status THEN
RAISE EXCEPTION 'Cannot modify booked quote';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER quotes_immutable_after_booking
BEFORE UPDATE ON quotes
FOR EACH ROW
EXECUTE FUNCTION prevent_quote_modification();

Caching Strategy

Rate Plan Caching:

  • Cache active rate plans by space_id (TTL: 15 minutes)
  • Invalidate on rate_plan.updated event
  • Warm cache for high-traffic properties

Tax Jurisdiction Caching:

  • Cache jurisdiction mappings by space_id (TTL: 24 hours)
  • Rarely changes; safe for longer TTL
  • Invalidate on space address change

Quote Caching:

  • Do NOT cache quotes (must be fresh for availability/pricing changes)
  • Cache quote retrieval by quote_id for confirmed bookings (immutable)

Future Enhancements

V1.1: Tax Engine Enhancement

Features:

  • Avalara MyLodgeTax integration for automated tax calculation
  • Real-time jurisdiction resolution via API
  • Tax remittance tracking and filing queue
  • Multi-jurisdictional tax reporting
  • Enhanced exemption rule processing

Impact: Eliminates manual tax configuration, ensures compliance accuracy


V1.2: Advanced Pricing Features

Features:

  • Gap night and orphan night automated pricing
  • Last-minute discount automation
  • Promotional codes and discount campaigns
  • Rate templates for quick setup
  • Bulk rate update operations

Impact: Competitive feature parity with Guesty/Hostaway


V2.0: Dynamic Pricing AI

Features:

  • PriceLabs/Wheelhouse/Beyond integration
  • AI-powered rate optimization
  • Demand forecasting
  • Competitor rate monitoring
  • Automated price adjustments

Impact: Revenue optimization (+10-20% vs. static pricing)


V2.1: International Expansion

Features:

  • Multi-currency support with FX rate management
  • VAT/GST handling
  • International tax compliance (EU, UK, Australia)
  • Cross-border payment splitting

Impact: Global marketplace expansion


V3.0: Enterprise Features

Features:

  • White-label marketplace revenue sharing
  • Affiliate commission tracking
  • Partner revenue splits
  • Tiered commission structures
  • Revenue caps and guarantees

Impact: Support complex B2B partnerships and marketplace models


Operational Notes

Monitoring & Alerts

Key Metrics:

  • Quote generation latency (p50, p95, p99)
  • Quote conversion rate (quotes → bookings)
  • Tax calculation errors
  • Revenue split validation failures
  • Cache hit rates

Alerts:

  • Quote generation >500ms (warn), >1000ms (critical)
  • Tax calculation failures
  • Revenue split sum != 100% (critical)
  • Quote expiry rate >50% (business alert)

Data Retention

Data TypeRetentionArchival Policy
Rate PlansIndefiniteSoft delete only
Rate RulesIndefiniteHistorical versioning
Quotes7 yearsArchive to cold storage after 2 years
Quote Line Items7 yearsArchive with parent quote
Tax Calculations7 yearsRegulatory compliance requirement
Revenue Splits10 yearsFinancial audit requirement

Security & Compliance

Access Control:

  • Rate plan modifications require pricing.edit permission
  • Quote generation requires booking.create permission
  • Tax configuration requires admin or finance_admin role
  • Revenue rule changes require dual approval (future)

Audit Requirements:

  • All rate changes logged with user_id, timestamp, before/after state
  • Quote calculations logged with rule version and inputs
  • Tax calculations include jurisdiction resolution audit trail
  • Revenue splits include calculation basis and applied rules

Data Privacy:

  • Guest PII not stored in pricing tables
  • Quote history anonymized after 2 years (guest_id nulled)
  • Tax jurisdiction data is public information (no privacy concerns)


Document Version: 1.0 Total Lines: 500+ Last Review: 2025-10-25