FA-60416 / Loyalty points ledgers / Open access
Points redemption at checkout: request block rounding · case 01
Members are charged more points than they asked for.
ROOT CAUSE
The request is rounded up to the next block instead of down.
THE FAILURE
The request is rounded up to the next block instead of down.
Unsuccessful approach: Rounding the request to the nearest block still overshoots when the request is 50 or more past a block.
Case contract
Input {balance, order_cents, request_pts, max_pct, cpp (cents per point)}. Balance below 500 returns {"error":"below minimum"}. Points are used in blocks of 100: used = min(request floored to a block, balance floored to a block, cap) where cap = floor(order*max_pct/100 / cpp) floored to a block. Cash due = order - used*cpp. Return {points_used, cash_due}.
Why this case matters
Member point balances, statements and the program liability depend on this ledger rule.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
bal = x['balance']
order = x['order_cents']
if bal < 500:
return {'error': 'below minimum'}
blk = 100
cap_cents = order * x['max_pct'] // 100
cap = cap_cents // x['cpp'] // blk * blk
use = min(-(-x['request_pts'] // blk) * blk, bal // blk * blk, cap)
return {'points_used': use, 'cash_due': order - use * x['cpp']}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal', {'balance': 2750, 'order_cents': 4001, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2201}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5001, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1291, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 51, 'cpp': 2}, {'points_used': 1200, 'cash_due': 2600}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1285, 'max_pct': 90, 'cpp': 1}, {'points_used': 1200, 'cash_due': 8800}]], [['normal', {'balance': 2750, 'order_cents': 4002, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2202}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5002, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 2}, {'points_used': 500, 'cash_due': 8999}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1292, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 52, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1295, 'max_pct': 90, 'cpp': 1}, {'points_used': 1200, 'cash_due': 8800}]], [['normal', {'balance': 2750, 'order_cents': 4003, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2203}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5003, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1293, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 53, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1305, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]], [['normal', {'balance': 2750, 'order_cents': 4004, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2204}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5004, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 2}, {'points_used': 500, 'cash_due': 8999}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1294, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 54, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1315, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]], [['normal', {'balance': 2750, 'order_cents': 4005, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2205}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5005, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1295, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 55, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1325, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]]]
for label, args, expected in fixtures[N-1]:
check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| normal | {'cash_due': 2101, 'points_used': 1900} | {'cash_due': 2201, 'points_used': 1800} | Failed |
| cap binds | {'cash_due': 1800, 'points_used': 1200} | {'cash_due': 1800, 'points_used': 1200} | Passed |
| minimum balance edge | {'cash_due': 8499, 'points_used': 500} | {'cash_due': 8499, 'points_used': 500} | Passed |
| below minimum | {'error': 'below minimum'} | {'error': 'below minimum'} | Passed |
| balance binds | {'cash_due': 48800, 'points_used': 1200} | {'cash_due': 48800, 'points_used': 1200} | Passed |
| two cents per point | {'cash_due': 2600, 'points_used': 1200} | {'cash_due': 2600, 'points_used': 1200} | Passed |
| odd request | {'cash_due': 8700, 'points_used': 1300} | {'cash_due': 8800, 'points_used': 1200} | Failed |
SHA-256 / c01b571a49d0d04fbb80e1cd8ff58d2bed814b293d6cb0f19a661a29b86d7d36
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
bal = x['balance']
order = x['order_cents']
if bal < 500:
return {'error': 'below minimum'}
blk = 100
cap_cents = order * x['max_pct'] // 100
cap = cap_cents // x['cpp'] // blk * blk
use = min((x['request_pts'] + blk // 2) // blk * blk, bal // blk * blk, cap)
return {'points_used': use, 'cash_due': order - use * x['cpp']}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal', {'balance': 2750, 'order_cents': 4001, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2201}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5001, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1291, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 51, 'cpp': 2}, {'points_used': 1200, 'cash_due': 2600}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1285, 'max_pct': 90, 'cpp': 1}, {'points_used': 1200, 'cash_due': 8800}]], [['normal', {'balance': 2750, 'order_cents': 4002, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2202}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5002, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 2}, {'points_used': 500, 'cash_due': 8999}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1292, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 52, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1295, 'max_pct': 90, 'cpp': 1}, {'points_used': 1200, 'cash_due': 8800}]], [['normal', {'balance': 2750, 'order_cents': 4003, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2203}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5003, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1293, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 53, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1305, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]], [['normal', {'balance': 2750, 'order_cents': 4004, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2204}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5004, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 2}, {'points_used': 500, 'cash_due': 8999}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1294, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 54, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1315, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]], [['normal', {'balance': 2750, 'order_cents': 4005, 'request_pts': 1850, 'max_pct': 50, 'cpp': 1}, {'points_used': 1800, 'cash_due': 2205}], ['cap binds', {'balance': 9000, 'order_cents': 3000, 'request_pts': 5005, 'max_pct': 40, 'cpp': 1}, {'points_used': 1200, 'cash_due': 1800}], ['minimum balance edge', {'balance': 500, 'order_cents': 9999, 'request_pts': 500, 'max_pct': 100, 'cpp': 3}, {'points_used': 500, 'cash_due': 8499}], ['below minimum', {'balance': 499, 'order_cents': 9999, 'request_pts': 400, 'max_pct': 100, 'cpp': 1}, {'error': 'below minimum'}], ['balance binds', {'balance': 1295, 'order_cents': 50000, 'request_pts': 3000, 'max_pct': 60, 'cpp': 1}, {'points_used': 1200, 'cash_due': 48800}], ['two cents per point', {'balance': 5000, 'order_cents': 5000, 'request_pts': 4000, 'max_pct': 55, 'cpp': 2}, {'points_used': 1300, 'cash_due': 2400}], ['odd request', {'balance': 3000, 'order_cents': 10000, 'request_pts': 1325, 'max_pct': 90, 'cpp': 1}, {'points_used': 1300, 'cash_due': 8700}]]]
for label, args, expected in fixtures[N-1]:
check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| normal | {'cash_due': 2101, 'points_used': 1900} | {'cash_due': 2201, 'points_used': 1800} | Failed |
| cap binds | {'cash_due': 1800, 'points_used': 1200} | {'cash_due': 1800, 'points_used': 1200} | Passed |
| minimum balance edge | {'cash_due': 8499, 'points_used': 500} | {'cash_due': 8499, 'points_used': 500} | Passed |
| below minimum | {'error': 'below minimum'} | {'error': 'below minimum'} | Passed |
| balance binds | {'cash_due': 48800, 'points_used': 1200} | {'cash_due': 48800, 'points_used': 1200} | Passed |
| two cents per point | {'cash_due': 2600, 'points_used': 1200} | {'cash_due': 2600, 'points_used': 1200} | Passed |
| odd request | {'cash_due': 8700, 'points_used': 1300} | {'cash_due': 8800, 'points_used': 1200} | Failed |
SHA-256 / 28b24b939931fdecc1ec753f49730f032d15fc8ce7bdd0d90476533ccbd1e439
HELD IN THE MEMBER ARCHIVE
The verified repair and its recorded checks are member-only.
This mechanism has 7 recorded checks per implementation. The open-access tier publishes the failure and the unsuccessful fix; the repaired source that passes every check, and the observations that prove it, are available to members.
Every case sharing this mechanism uses the same contract and the same repair, so this one record is held back for all of them.
Member access is invitation-based. Sign in with your invited account to inspect the repair.
Sign in to the archive ↗Verification & scope
Stipulated bounded loyalty-program model with explicit toy rules; not any real program terms. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:46:45.527171+00:00.
Case digest / bf4d0cae762a08557fa0f1f432dde690ea228462bc44cb6130f1884aa1abb9d6