Fail-Closed Quote Preparation: Detecting Conflicting Pricing Rules Before Anything Is Sent
Abstract
A recurring failure mode in quote-preparation work is not arithmetic but ambiguity: the same SKU is governed by several price-rule documents that disagree on validity window, customer scope, currency, unit, or tax basis. A human then picks one by memory or habit, and the conflict reaches the customer. This article reframes that problem as a concrete engineering task: build a validator that refuses to produce a quote when a conflict or a missing field exists, and emits a structured conflict report instead. I describe an input/output contract, implementation choices, a complete runnable Python example, synthetic test fixtures, explicit error cases, acceptance steps, and honest limitations. All SKUs, rules, and results here are synthetic and illustrate the design; no deployed system, measured metric, or external source is claimed. The article is written from the perspective of someone who customizes AI software against enterprise rules, but the technique is ordinary deterministic validation and is useful regardless of vendor.
The Implementation Problem
Consider a synthetic product, SKU A-100. Two price-rule records exist:
- an older record valid until 30 June, scoped to East-China distributors, currency CNY, unit
box, tax-inclusive; - a newer record effective 1 July, scoped to nationwide direct sales, currency USD, unit
piece, tax-exclusive.
If a quote is requested for A-100 on 15 June for a nationwide direct-sales customer, neither record cleanly applies, yet a hurried operator may still send one. The engineering goal is therefore not to guess the "right" price but to make the ambiguity visible and block transmission. This is a fail-closed design: absence of a clean, unambiguous, approved rule is a stop condition, not a default.
The task decomposes into three layers:
- Rule structuring — normalize each rule into explicit fields; a rule missing any required field is marked incomplete and cannot pass.
- Conflict detection — for the same SKU in the same time window, detect more than one applicable rule, and report differences in currency, unit, or tax basis without choosing one.
- Gate — any outbound draft must carry a rule-version identifier and an approver record; without approval it stays in a pending state.
Input/Output Contract
The validator is a pure function over structured data. It does not call an LLM and does not infer missing values.
Input
rules: list of rule objects, each withsku,version,valid_from,valid_to,customer_scope,currency,unit,tax_basis,min_qty,quote_valid_days.request:sku,as_ofdate,customer_scope,quantity.approval: optional record withversionandapprover.
Output (always a JSON-serializable object)
status: one ofOK,INCOMPLETE,CONFLICT,NO_RULE,UNAPPROVED.selected_version: present only whenstatus == OK.issues: list of objects withcode,message, and the involvedversions.quote_allowed: boolean, true only forOK.
Invariants: quote_allowed is true if and only if status == OK; every non-OK status carries at least one issue; the function never mutates its inputs.
Implementation Choices
Deterministic validation, not generation. Conflicts are decidable from fields, so a rule engine is more auditable than a model. An LLM could help extract rules from documents, but extraction is a separate, reviewable step; the validator trusts only confirmed structured input.
Explicit required-field list. A rule with any missing required field is INCOMPLETE, reported with the missing field names. This matches the operational rule that a missing item must not silently pass.
Half-open date windows. valid_from <= as_of < valid_to avoids double-counting boundary days and makes overlaps unambiguous.
Scope matching is exact string equality in this example. Real deployments need a scope taxonomy (region vs. customer tier); that is a configuration concern, not a change to the algorithm.
Approval as a gate, not a check inside selection. Selection and approval are separate stages so that a correct rule can still be held pending.
Runnable Python Example
"""Fail-closed quote-rule validator. All data below is synthetic."""
from dataclasses import dataclass, field
from datetime import date
from typing import Optional
REQUIRED_FIELDS = [
"sku", "version", "valid_from", "valid_to", "customer_scope",
"currency", "unit", "tax_basis", "min_qty", "quote_valid_days",
]
@dataclass
class Rule:
sku: str
version: str
valid_from: date
valid_to: date
customer_scope: str
currency: str
unit: str
tax_basis: str
min_qty: int
quote_valid_days: int
@dataclass
class Request:
sku: str
as_of: date
customer_scope: str
quantity: int
@dataclass
class Approval:
version: str
approver: str
@dataclass
class Result:
status: str
selected_version: Optional[str] = None
issues: list = field(default_factory=list)
quote_allowed: bool = False
def _missing_fields(raw: dict) -> list:
return [f for f in REQUIRED_FIELDS if raw.get(f) in (None, "")]
def _in_window(rule: Rule, as_of: date) -> bool:
# Half-open interval: start inclusive, end exclusive.
return rule.valid_from <= as_of < rule.valid_to
def validate(raw_rules: list, request: Request,
approval: Optional[Approval] = None) -> Result:
issues = []
# Layer 1: structuring — reject rules with missing fields.
parsed = []
for raw in raw_rules:
missing = _missing_fields(raw)
if missing:
issues.append({
"code": "INCOMPLETE_RULE",
"message": f"rule {raw.get('version', '?')} missing: {missing}",
"versions": [raw.get("version", "?")],
})
else:
parsed.append(Rule(**raw))
if issues:
return Result("INCOMPLETE", issues=issues, quote_allowed=False)
# Layer 2: conflict detection within SKU + time window + scope.
candidates = [
r for r in parsed
if r.sku == request.sku
and _in_window(r, request.as_of)
and r.customer_scope == request.customer_scope
and request.quantity >= r.min_qty
]
if not candidates:
return Result("NO_RULE", issues=[{
"code": "NO_APPLICABLE_RULE",
"message": f"no rule for {request.sku} on {request.as_of}",
"versions": [],
}], quote_allowed=False)
if len(candidates) > 1:
return Result("CONFLICT", issues=[{
"code": "MULTIPLE_RULES",
"message": "more than one rule matches",
"versions": [r.version for r in candidates],
}], quote_allowed=False)
chosen = candidates[0]
# Only rules applicable to this exact request can conflict.
# Prices for another customer scope are not competing candidates.
# Layer 3: approval gate.
if approval is None or approval.version != chosen.version or not approval.approver.strip():
return Result("UNAPPROVED", issues=[{
"code": "APPROVAL_MISSING",
"message": f"version {chosen.version} not approved",
"versions": [chosen.version],
}], quote_allowed=False)
return Result("OK", selected_version=chosen.version, quote_allowed=True)
Test Fixtures (Synthetic)
RULES = [
{"sku": "A-100", "version": "v1", "valid_from": date(2024, 1, 1),
"valid_to": date(2024, 7, 1), "customer_scope": "east_distributor",
"currency": "CNY", "unit": "box", "tax_basis": "inclusive",
"min_qty": 10, "quote_valid_days": 14},
{"sku": "A-100", "version": "v2", "valid_from": date(2024, 7, 1),
"valid_to": date(2025, 1, 1), "customer_scope": "national_direct",
"currency": "USD", "unit": "piece", "tax_basis": "exclusive",
"min_qty": 1, "quote_valid_days": 7},
]
# 1. Clean match, approved -> OK
req = Request("A-100", date(2024, 3, 1), "east_distributor", 20)
assert validate(RULES, req, Approval("v1", "lead")) == Result("OK", "v1", [], True)
# 2. No matching scope -> NO_RULE
req = Request("A-100", date(2024, 3, 1), "national_direct", 20)
assert validate(RULES, req).status == "NO_RULE"
# 3. Correct rule but no approval -> UNAPPROVED
req = Request("A-100", date(2024, 3, 1), "east_distributor", 20)
assert validate(RULES, req).status == "UNAPPROVED"
# 4. Missing field -> INCOMPLETE
broken = [dict(RULES[0], unit="")]
assert validate(broken, req).status == "INCOMPLETE"
Error Cases to Handle Explicitly
- Overlapping versions. Two rules for the same SKU, scope, and window produce
MULTIPLE_RULES; do not break ties by version number. - Basis disagreement. Compare currency, unit and tax basis only after matching SKU, date, customer scope and minimum quantity. A different customer scope is not a conflicting candidate. Two applicable rules are rejected as
MULTIPLE_RULESrather than choosing a basis. - Missing fields.
INCOMPLETE_RULElists the missing names so the quote-preparation operator can supply them. - Date boundary. Because the window is half-open, a rule ending 1 July and one starting 1 July do not both match on 1 July.
- Quantity below minimum. Treated as no applicable rule in this example; in production this may be a distinct code.
- Stale approval. An approval tied to
v1does not authorizev2.
Acceptance Steps
- Run the fixture assertions above; all must pass.
- Prepare a batch of synthetic SKUs and rules that deliberately include overlaps, missing fields, and scope mismatches; confirm each returns the expected non-OK status and that
quote_allowedis false. - Confirm that no code path returns
OKwithout an approval whose version equals the selected rule. - Have a person who did not write the validator re-run the same batch using only the written instructions, and record how many conflicts they could trace to specific versions and fields. The relevant signals are rework count and missed items, not generation speed.
- Verify that the conflict report names both versions and the differing field, so a human can resolve it without re-reading all documents.
Honest Limitations
This validator does not extract rules from PDFs or spreadsheets; that extraction is a separate, human-reviewed step and is where most real ambiguity hides. Scope matching here is exact string equality, which will not survive real customer taxonomies without a configuration layer. The example ignores multi-currency conversion, proration, and quantity-tier pricing. Approval is modeled as a single record, not a workflow engine. And crucially, none of this has been deployed as a quote system; the code and fixtures are synthetic illustrations of a fail-closed design. The contribution is the discipline of stopping at a conflict and printing the reason, not a claim of operational results.
Test Run
The code above plus these additional synthetic acceptance assertions ran locally on 28 October 2026. All 12 assertions passed; this is a local example test, not a production deployment result.
import copy
base = RULES[0]
request = Request("A-100", date(2024,3,1), "east_distributor",20)
assert validate([base],request,Approval("v1"," ")).status == "UNAPPROVED"
assert validate([base],request,Approval("v2","lead")).status == "UNAPPROVED"
conflict = dict(base,version="v1b",currency="USD")
assert validate([base,conflict],request,Approval("v1","lead")).status == "CONFLICT"
other = dict(base,version="other-scope",customer_scope="national_direct",currency="USD")
assert validate([base,other],request,Approval("v1","lead")).status == "OK"
before=copy.deepcopy(RULES)
validate(RULES,request,Approval("v1","lead"))
assert RULES == before
assert validate(RULES,Request("A-100",date(2024,7,1),"east_distributor",20)).status == "NO_RULE"
assert validate(RULES,Request("A-100",date(2024,7,1),"national_direct",20),Approval("v2","lead")).status == "OK"
assert validate(RULES,Request("A-100",date(2024,3,1),"east_distributor",1)).status == "NO_RULE"
print("12 synthetic assertions passed: approval, conflicts, scope, dates, quantity, missing fields, non-mutation")
Summary
Quote-rule conflicts are a data and process problem that can be made explicit. Structure each rule into required fields, reject incomplete rules, detect overlapping or basis-disagreeing rules within a half-open time window, and gate outbound drafts on an approval bound to a specific rule version. The validator never guesses: it returns OK only when exactly one applicable, fully specified, approved rule exists, and otherwise emits a conflict report naming the versions and fields involved. The same fail-closed posture applies beyond pricing — wherever multiple document versions can reach a customer, the workflow should stop and show its reasoning rather than let a person's memory decide.
Author: Li Zhou (FDE李宙), Yiwu Beiyao Technology Co., Ltd. (义乌市贝爻科技有限公司).