Tax & Fee Resolution

Two rates that look identical can mean very different things once taxes and fees are resolved: a 200 tax-inclusive rate and a 200 tax-exclusive rate are not the same offer, and a pricing engine that confuses them will misprice against competitors and misreport net yield. Tax and fee resolution is the component that computes the exact relationship between the rate a guest sees, the taxes and fees layered on it, and the net revenue the property keeps. It sits in the Core Architecture & Pricing Taxonomy pillar next to Rate Plan Structuring & Mapping, because a rate plan is incomplete until its tax treatment is defined.

Rate decomposition into net, tax, and fees A guest-facing gross rate decomposes into net room revenue, occupancy tax, and mandatory fees, with net feeding yield calculations. Gross rate decomposition Gross rate guest-facing total Net room revenue Occupancy tax Mandatory fees → net yield

Taxonomy and schema governance

The canonical object is a tax profile attached to a rate plan and jurisdiction: an ordered set of tax and fee components, each with a type (percentage or flat), a basis (applied to net, or to net-plus-prior-components), and an inclusion flag stating whether the guest-facing rate already contains it. Order matters because a tax on a fee behaves differently from a fee on a tax, so components carry an explicit application sequence. Every component references a jurisdiction code, since occupancy tax rates are set locally, and the profile is versioned with an effective-date range so a mid-year rate change is a new version rather than a mutation. This schema fits inside the rate hierarchy from Rate Plan Structuring & Mapping: a rate plan is only fully specified once its tax profile is bound.

Ingestion and normalization

Tax profiles are authored per jurisdiction and loaded from a version-controlled store, never hard-coded into pricing logic. On load they are normalized: percentages stored as exact fractions, flat fees in integer minor units, and components sorted into their application order. The critical normalization is basis resolution — deciding, once, whether the incoming rate is gross or net and converting to a single internal convention (net, tax-exclusive) so every downstream comparison is apples-to-apples. Rates scraped from competitors, which may be tax-inclusive, are converted to net on ingestion exactly as described in Normalizing scraped rates to a canonical schema, so a competitor’s inclusive headline never looks artificially cheap next to your net rate.

Core implementation

The resolver decomposes a net rate into its gross total and component breakdown, applying percentage and flat components in their declared order. It works in integer minor units and rounds once per component.

python
from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import Enum

logger = logging.getLogger("pricing.tax")


class Kind(Enum):
    PERCENT = "percent"
    FLAT = "flat"


@dataclass(frozen=True)
class Component:
    name: str
    kind: Kind
    value: float          # fraction for percent, cents for flat
    on_prior: bool         # percent applies to net + prior components


@dataclass(frozen=True)
class Resolved:
    net_cents: int
    gross_cents: int
    breakdown: dict[str, int]


def resolve_from_net(net_cents: int, components: list[Component]) -> Resolved:
    if net_cents <= 0:
        raise ValueError("net rate must be positive")
    base = net_cents
    running = net_cents
    breakdown: dict[str, int] = {}
    for comp in components:
        if comp.kind is Kind.PERCENT:
            if not 0.0 <= comp.value < 1.0:
                raise ValueError(f"bad percent in {comp.name}")
            basis = running if comp.on_prior else base
            amount = round(basis * comp.value)
        else:
            amount = int(comp.value)
        breakdown[comp.name] = amount
        running += amount
    resolved = Resolved(net_cents=net_cents, gross_cents=running, breakdown=breakdown)
    logger.info("resolved net=%d gross=%d (%s)", net_cents, running, breakdown)
    return resolved

Applying on_prior components against the running total, and flat components as fixed cents, is what makes a compounding local tax-on-fee resolve correctly instead of quietly under-collecting.

Cross-system dependencies

Upstream, tax profiles depend on the rate plan definitions from Rate Plan Structuring & Mapping and on jurisdiction data maintained by finance. Downstream, resolution feeds Dynamic Pricing Rule Engines & Optimization, which optimizes on net yield rather than gross rate, and it underpins the parity checks in Rate Parity Compliance Across Booking Channels, since parity must be judged on comparable tax bases across channels.

Operational governance

Because tax resolution touches money owed to authorities, it inherits stricter controls than most pricing config. Profiles are versioned with effective dates, changes are reviewed with a finance approval trail, and every resolved rate logs the profile version used, giving auditors an immutable link between a charged total and the tax rules in force — a SOX-friendly property. CI validates that no profile under-specifies a required jurisdiction tax and that percentages and bases are within range. Observability watches for resolutions where the effective tax rate deviates from the jurisdiction’s expected band, which usually signals a stale profile or a misclassified fee.

Troubleshooting and failure modes

Symptom Likely cause Remediation
Net yield lower than expected Tax-inclusive rate treated as net Resolve basis on ingestion; convert to net first
Under-collected tax on fees Component order or on_prior flag wrong Fix application sequence; test against a worked example
Parity flagged incorrectly Channels compared on different tax bases Normalize all channels to net before comparing
Rounding drift on invoices Rounding per multiplication instead of per component Round once per component in minor units

Conclusion

Tax and fee resolution is production-ready when every rate carries a versioned, jurisdiction-aware profile, all comparisons happen on a single net basis, and each resolved total is auditable back to the rules that produced it. That rigor is what lets the pricing engine optimize real net yield and lets finance trust the numbers.