Fail-safe decision table — gaps force review, never silent approval
Fail-safe decision table — gaps force review, never silent approval
Credit/risk decisions evaluate an ordered rule table (decision machine) against normalised facts. Outcome precedence is fixed (DECLINE > REFER > APPROVE), and any rule that cannot be evaluated because a fact is missing forces at least REFER — incomplete data can never silently approve.
Problem it solves
- Rules engines that treat missing data as "condition false" approve applicants with incomplete reports. Regulators and lenders both require the opposite default: uncertainty routes to a human.
Shape
- Decision table: ordered rules, each with
whenconditions over named facts and an outcome (APPROVE / REFER / DECLINE). - Facts are normalised upstream (
facts.py) from heterogeneous sources (bureau reports, bank data, application forms) into a flat{fact: value}map. - Evaluation returns the outcome plus per-rule detail (which rules fired, which facts were missing) — the detail is the audit trail.
- Versions are explicit (
versions.py) — a decision outcome must be reproducible against the rule version that produced it.
Rules
- Precedence is fixed and total: DECLINE > REFER > APPROVE — no rule ordering tricks change it.
- Missing fact ⇒ condition evaluates to unknown ⇒ outcome floors at REFER, never APPROVE.
- Every evaluation persists its inputs, rule version, and outcome detail (7-year record-keeping friendly).
- Rules are data (table/DSL), not code — new policies ship without deploys.
Anti-patterns
- Missing-data-as-false logic (silent approvals).
- Outcome precedence left to rule order — fragile under edits.
- Unversioned rule changes — historical decisions become unreproducible.
- Facts smuggled in as raw vendor payloads instead of normalised values.
Sources
- repayments/backend/app/decision/engine.py (eval_condition/eval_rule, RANK precedence)
- repayments/backend/app/decision/facts.py, versions.py, service.py
- repayments/backend/app/decision/fixtures (test fixtures)