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.

Deterministic regional override evaluation for one normalized payloadOne normalized CompetitorPricePayload enters the evaluator, which sorts override rules by priority and scans them top-down, first match wins. For an example German gross-price reading, the priority-5 NA_MAP_ENFORCEMENT rule is skipped because its region condition US or CA does not match DE; the priority-10 EU_DE_VAT_OVERRIDE rule matches and fires the apply_margin_floor action, computing final price as base times one plus the floor percentage, quantized to two decimals. Lower-priority rules are short-circuited and never evaluated. The alternate dashed branch shows that when no rule matches, the highest-priority rule's first fallback_chain entry is used with fallback_used set true and applied_rule_id null. Both paths emit a single explainable PricingDecision that is identical on every retry.CompetitorPricePayload — normalized & typedmaster_id · region_code=DE · currency_iso=EUR · source=retailer_de · price_type=grossevaluate_override() — ordered = sorted(rules, key=priority)scan ↓ · first match wins5NA_MAP_ENFORCEMENTregion ∈ {US, CA} · USD · electronics → cap_at_map✗ region=DE10EU_DE_VAT_OVERRIDEregion ∈ {DE, AT} · EUR · source=retailer_de✓ MATCHpriority 20, 30, … — short-circuited, never evaluatedaction: apply_margin_floorfinal = base × (1 + 12.5%) → quantize(0.01)no rule matched → fallback_chain[0]GLOBAL_EUR_DEFAULT · fallback_used=True · applied_rule_id=NonePricingDecisionfinal_price=90.00 · applied_rule_id='EU_DE_VAT_OVERRIDE'action_type=APPLY_MARGIN_FLOOR · fallback_used=Falsesame input → identical decision 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.

FieldDirectionTypeNotes
master_idinstringResolved canonical product identity
skuinstringRetailer-specific SKU, retained for audit
region_codeinstringISO 3166-1 alpha-2 (DE, US, CA)
currency_isoinstringISO 4217 alpha-3, base currency
raw_priceinDecimalCompetitor value, already normalized
price_typeinenumgross / net / promotional
sourceinstringProvenance: scraper or marketplace id
final_priceoutDecimalResult after the matched action
applied_rule_idoutstring | nullWhich rule fired, for the audit log
fallback_usedoutboolTrue 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.