Setting Up Price Override Rules for Regional Variants
Regional pricing needs deterministic override logic that respects localized market rules — VAT handling, Minimum Advertised Price (MAP) floors, geo-specific competitor sources — while preserving global margin guardrails. This guide implements that logic as a single, testable stage inside the Price Hierarchy & Rule-Based Fallback Routing router. The sequence is strict: a competitor reading is first resolved to a master entity through Core Architecture & Catalog Matching Fundamentals, then evaluated against an ordered rule set, then routed to a fallback chain if nothing matches. Get the ordering wrong and overrides land on the wrong variant — ABC-123-DE priced against a US listing — eroding margin silently or breaching a regional MAP agreement.
The single problem this page solves: given one normalized competitor price for a regional variant, produce one explainable PricingDecision that is identical on every retry.
Prerequisites & Input Contract
Override evaluation is the last hop in the router, so three upstream stages must already have run. Each variant must be mapped to its canonical node — voltage, packaging, language, and SKU-suffix differences resolved — using Building a Unified Product Catalog Schema and the confidence-scored joins from Fuzzy Matching Algorithms for SKU Alignment. The monetary value must be on one base currency from Currency Conversion & Exchange Rate Sync, and tax/shipping must already be reconciled per Tax & Shipping Cost Normalization Rules. The evaluator never parses raw HTML — only a strictly typed payload.
Environment assumptions: Python 3.11+, pydantic>=2.5 for rule-set validation, and PyYAML>=6.0 to load versioned rule files. No other runtime dependencies — the evaluator itself is standard library only, which keeps it trivially portable into a worker process or a serverless function.
| Field | Direction | Type | Notes |
|---|---|---|---|
master_id | in | string | Resolved canonical product identity |
sku | in | string | Retailer-specific SKU, retained for audit |
region_code | in | string | ISO 3166-1 alpha-2 (DE, US, CA) |
currency_iso | in | string | ISO 4217 alpha-3, base currency |
raw_price | in | Decimal | Competitor value, already normalized |
price_type | in | enum | gross / net / promotional |
source | in | string | Provenance: scraper or marketplace id |
final_price | out | Decimal | Result after the matched action |
applied_rule_id | out | string | null | Which rule fired, for the audit log |
fallback_used | out | bool | True when no rule matched |
Use decimal.Decimal, never float, for money — binary floating point silently corrupts a 12.5% margin floor over millions of rows.
Step-by-Step Implementation
Step 1 — Author the rule set as versioned config
Rules live in YAML under version control, never inline in code. Each rule declares a priority (lower integer = higher precedence), a conditions block (a missing key is a wildcard), an action, and a fallback_chain. This is the same precedence contract the Price Hierarchy & Rule-Based Fallback Routing router enforces site-wide.
# rules/regional_overrides.yaml — version: 2026-06-27
override_rules:
- id: "NA_MAP_ENFORCEMENT"
priority: 5 # evaluated before the EU VAT rule
conditions:
region: ["US", "CA"]
currency: "USD"
brand_category: ["electronics", "premium_appliances"]
action:
type: "cap_at_map"
map_threshold_pct: 0.0 # cap exactly at the master MAP price
fallback_chain: ["GLOBAL_USD_DEFAULT"]
- id: "EU_DE_VAT_OVERRIDE"
priority: 10
conditions:
region: ["DE", "AT"]
currency: "EUR"
competitor_price_source: ["retailer_de", "marketplace_eu"]
price_type: "gross"
action:
type: "apply_margin_floor"
floor_margin_pct: 12.5
fallback_chain: ["GLOBAL_EUR_DEFAULT", "USD_CONVERSION_FALLBACK"]
Step 2 — Validate the rule set at load time
A malformed rule must fail loudly at deploy, not silently at scrape time. Validate the file against a pydantic model before the evaluator ever sees it.
from decimal import Decimal
from enum import Enum
from typing import Optional
import yaml
from pydantic import BaseModel, Field, field_validator
class PriceActionType(str, Enum):
APPLY_MARGIN_FLOOR = "apply_margin_floor"
CAP_AT_MAP = "cap_at_map"
FALLBACK_TO_BASE = "fallback_to_base"
class RuleAction(BaseModel):
type: PriceActionType
floor_margin_pct: Optional[Decimal] = None
map_threshold_pct: Optional[Decimal] = None
class OverrideRule(BaseModel):
id: str
priority: int = Field(ge=0)
conditions: dict
action: RuleAction
fallback_chain: list[str] = []
def load_rules(path: str) -> list[OverrideRule]:
"""Parse and validate the YAML rule set; raises on any schema violation."""
raw = yaml.safe_load(open(path, encoding="utf-8"))
rules = [OverrideRule(**r) for r in raw["override_rules"]]
ids = [r.id for r in rules]
if len(ids) != len(set(ids)): # duplicate ids break the audit trail
raise ValueError(f"Duplicate rule ids: {ids}")
return rules
Step 3 — Implement the stateless evaluator
The evaluator sorts by priority once, returns on the first matching rule, and otherwise routes to the highest-priority rule’s fallback chain. It holds no state between calls, so a retry yields a byte-identical result.
from dataclasses import dataclass, field
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class CompetitorPricePayload:
master_id: str
sku: str
region_code: str
currency_iso: str
raw_price: Decimal
price_type: str # "gross" | "net" | "promotional"
source: str
@dataclass
class PricingDecision:
final_price: Decimal
applied_rule_id: Optional[str]
action_type: PriceActionType
fallback_used: bool = False
metadata: dict = field(default_factory=dict)
def _matches(value, allowed) -> bool:
"""A missing condition is a wildcard; lists test membership, scalars compare."""
if allowed is None:
return True
if isinstance(allowed, (list, tuple, set)):
return value in allowed
return value == allowed
def evaluate_override(
payload: CompetitorPricePayload,
rules: list[OverrideRule],
base_price: Decimal,
) -> PricingDecision:
"""Return one deterministic PricingDecision for a single regional variant."""
ordered = sorted(rules, key=lambda r: r.priority) # lower int = higher precedence
for rule in ordered:
c = rule.conditions
if (
_matches(payload.region_code, c.get("region"))
and _matches(payload.currency_iso, c.get("currency"))
and _matches(payload.source, c.get("competitor_price_source"))
and _matches(payload.price_type, c.get("price_type"))
):
act = rule.action
if act.type is PriceActionType.APPLY_MARGIN_FLOOR:
floor = (base_price * (1 + act.floor_margin_pct / 100)).quantize(Decimal("0.01"))
return PricingDecision(floor, rule.id, act.type,
metadata={"floor_margin_pct": str(act.floor_margin_pct)})
if act.type is PriceActionType.CAP_AT_MAP:
cap = (base_price * (1 + act.map_threshold_pct / 100)).quantize(Decimal("0.01"))
return PricingDecision(min(payload.raw_price, cap), rule.id, act.type,
metadata={"map_cap": str(cap)})
chain = ordered[0].fallback_chain if ordered else []
logger.info("No override matched sku=%s; fallback chain=%s", payload.sku, chain)
return PricingDecision(base_price, None, PriceActionType.FALLBACK_TO_BASE,
fallback_used=True, metadata={"fallback_chain": chain})
Calling it on a German gross-price reading returns a margin-floored decision:
rules = load_rules("rules/regional_overrides.yaml")
payload = CompetitorPricePayload(
master_id="M-9001", sku="ABC-123-DE", region_code="DE",
currency_iso="EUR", raw_price=Decimal("79.00"),
price_type="gross", source="retailer_de",
)
print(evaluate_override(payload, rules, base_price=Decimal("80.00")))
# PricingDecision(final_price=Decimal('90.00'), applied_rule_id='EU_DE_VAT_OVERRIDE',
# action_type=<PriceActionType.APPLY_MARGIN_FLOOR>, fallback_used=False, ...)
Verification & Testing
Correctness here means precedence, determinism, and explainability. Pin all three with assertions; precision bugs and rule-ordering regressions are otherwise invisible until margin reports drift.
from decimal import Decimal
def test_priority_wins_when_two_rules_match():
# A US electronics reading matches MAP enforcement (priority 5) before anything else.
p = CompetitorPricePayload("M-1", "S-1", "US", "USD",
Decimal("120.00"), "gross", "retailer_us")
rules = load_rules("rules/regional_overrides.yaml")
d = evaluate_override(p, rules, base_price=Decimal("100.00"))
assert d.applied_rule_id == "NA_MAP_ENFORCEMENT"
assert d.final_price == Decimal("100.00") # capped at MAP, not 120.00
def test_unmatched_payload_routes_to_fallback():
p = CompetitorPricePayload("M-2", "S-2", "JP", "JPY",
Decimal("5000"), "net", "marketplace_jp")
d = evaluate_override(p, load_rules("rules/regional_overrides.yaml"), Decimal("4800"))
assert d.fallback_used is True
assert d.applied_rule_id is None
def test_evaluation_is_idempotent():
p = CompetitorPricePayload("M-3", "S-3", "DE", "EUR",
Decimal("79.00"), "gross", "retailer_de")
rules = load_rules("rules/regional_overrides.yaml")
first = evaluate_override(p, rules, Decimal("80.00"))
second = evaluate_override(p, rules, Decimal("80.00"))
assert first == second # retry-safe by construction
Run with pytest -q. For a fuller guard, snapshot a few hundred real payloads with analyst-approved outcomes and diff the evaluator’s output against that golden file in CI — any precedence change then shows up as a failing assertion before it reaches production.
Edge Cases & Gotchas
Overlapping rules with equal priority. Two rules at priority: 10 make the winner depend on file order — non-deterministic in spirit even if sorted is stable. Remediation: reject duplicate priorities at load time, just as load_rules rejects duplicate ids:
prios = [r.priority for r in rules]
if len(prios) != len(set(prios)):
raise ValueError(f"Ambiguous precedence — duplicate priorities: {sorted(prios)}")
float margin drift. A floor_margin_pct of 12.5 applied with float yields 89.99999…; over a catalog this rounds inconsistently. Remediation: keep money and percentages as Decimal and quantize to two places, as the evaluator does.
Stale base price. The override caps against base_price, but if the master MAP changed and the cache is stale, the cap is wrong. Remediation: tie an idempotency key to master_id + region_code + rule_version and invalidate on any version bump rather than on a TTL.
Wildcard over-matching. Omitting a condition makes it a wildcard, so a broad rule can swallow readings meant for a narrower one. Remediation: lint rule sets for rules with empty conditions and require at least region to be present.
Performance Notes
Evaluation is O(n) in the number of rules per payload, and n is small (tens, not thousands), so the cost is dominated by the one-time O(n log n) sort. Hoist that sort out of the hot path: sort once at load and pass the ordered list in, turning per-payload work into a linear scan that runs in microseconds. Batch competitor feeds asynchronously — mirroring the broker patterns in Async Data Pipelines with Python & Scrapy — but keep rule evaluation synchronous per SKU so outcomes stay deterministic.
This linear approach holds until rule sets reach the high hundreds or conditions span many dimensions. At that point compile the rules into a decision tree or hand them to a dedicated engine such as JSONLogic, cache the compiled graph, and invalidate it only on a version bump — the same trade-off the parent router documents for large precedence chains.
Frequently Asked Questions
Should regional overrides run before or after global price normalization? Before. A regional rule must intercept the base price first; global normalization is the fallback that runs only when no regional rule matches.
How do I stop an override from landing on the wrong variant? Resolve every reading to a master_id through Fuzzy Matching Algorithms for SKU Alignment first, and gate the evaluator behind a confidence threshold so low-confidence matches never trigger a price action.
Can machine learning replace these rules? Predictive matching can flag anomalies and suggest new rules, but explicit override logic stays authoritative in production — MAP and tax obligations are legal constraints, not probabilistic ones.
Related
- Price Hierarchy & Rule-Based Fallback Routing — the parent router whose precedence and fallback contract this stage implements.
- Fuzzy Matching Algorithms for SKU Alignment — resolves regional SKU suffixes to a master id so overrides target the right variant.
- Cross-Platform Category Taxonomy Mapping — supplies the
brand_categoryvalues that MAP-enforcement conditions key on. - Tax & Shipping Cost Normalization Rules — reconciles gross/net prices so a VAT override compares like with like.
- Currency Conversion & Exchange Rate Sync — guarantees every payload reaches the evaluator on one base currency.