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.
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.
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.
Related
- Computing inclusive vs exclusive tax rates in Python — the worked conversion between gross and net bases.
- Rate Plan Structuring & Mapping — the rate plan definitions a tax profile binds to.
- Dynamic Pricing Rule Engines & Optimization — the downstream pillar that optimizes on the net yield this component computes.