FA-60681 / Loyalty points ledgers / Open access
Cancelled reward reinstatement: cancellation age limit · case 01
A cancellation exactly at the age limit is refused.
ROOT CAUSE
The age test uses >= instead of >.
VERIFIED REPAIR
Refuse only when more than max_age days have passed.
Unsuccessful approach: Measuring age from the earliest source expiry is unrelated to when the reward was redeemed.
Case contract
Input {redemption:{day, sources:[[lot_expiry, pts]]}, cancel_day, grace, max_age}. Cancelling more than max_age days after the redemption returns {"error":"too late"}. Each source lot is reinstated with its original expiry if that expiry is on or after the cancel day, otherwise with expiry cancel_day + grace. Lots with the same resulting expiry are merged. Return {lots:[[expiry,pts]] by expiry, total}.
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):
r = x['redemption']
if x['cancel_day'] - r['day'] >= x['max_age']:
return {'error': 'too late'}
lots = {}
for exp, p in r['sources']:
e = exp if exp >= x['cancel_day'] else x['cancel_day'] + x['grace']
lots[e] = lots.get(e, 0) + p
return {'lots': [[e, lots[e]] for e in sorted(lots)], 'total': sum(lots.values())}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mixed sources', {'redemption': {'day': 100, 'sources': [[91, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 51]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 51]], 'total': 51}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 102, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 11, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 101, 'grace': 14, 'max_age': 90}, {'lots': [[115, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 21], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 31], [300, 7]], 'total': 38}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 6]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 81, 'grace': 10, 'max_age': 90}, {'lots': [[91, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[92, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 52]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 52]], 'total': 52}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 103, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 12, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 102, 'grace': 14, 'max_age': 90}, {'lots': [[116, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 22], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 32], [300, 7]], 'total': 39}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 7]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 7], [150, 10], [200, 40]], 'total': 57}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 82, 'grace': 10, 'max_age': 90}, {'lots': [[92, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[93, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 53]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 53]], 'total': 53}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 104, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 13, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 103, 'grace': 14, 'max_age': 90}, {'lots': [[117, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 23], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 33], [300, 7]], 'total': 40}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 8]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 8], [150, 10], [200, 40]], 'total': 58}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 83, 'grace': 10, 'max_age': 90}, {'lots': [[93, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[94, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 54]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 54]], 'total': 54}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 105, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 14, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 104, 'grace': 14, 'max_age': 90}, {'lots': [[118, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 24], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 34], [300, 7]], 'total': 41}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 9]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 9], [150, 10], [200, 40]], 'total': 59}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 84, 'grace': 10, 'max_age': 90}, {'lots': [[94, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[95, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 55]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 55]], 'total': 55}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 106, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 15, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 105, 'grace': 14, 'max_age': 90}, {'lots': [[119, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 25], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 35], [300, 7]], 'total': 42}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 10]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 10], [150, 10], [200, 40]], 'total': 60}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 85, 'grace': 10, 'max_age': 90}, {'lots': [[95, 70]], 'total': 70}]]]
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 |
|---|---|---|---|
| mixed sources | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | Passed |
| expires on cancel day | {'lots': [[120, 51]], 'total': 51} | {'lots': [[120, 51]], 'total': 51} | Passed |
| too late | {'error': 'too late'} | {'error': 'too late'} | Passed |
| age edge | {'error': 'too late'} | {'lots': [[115, 5], [500, 100]], 'total': 105} | Failed |
| merge grace lots | {'lots': [[105, 31], [300, 7]], 'total': 38} | {'lots': [[105, 31], [300, 7]], 'total': 38} | Passed |
| merge with original | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | Passed |
| late cancel grace base | {'lots': [[91, 70]], 'total': 70} | {'lots': [[91, 70]], 'total': 70} | Passed |
SHA-256 / 5cb279a3294882b0a0cdfb30e3a2581f943f7d398b6c56d3929b9c28382e4f71
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
r = x['redemption']
if x['cancel_day'] - min(s[0] for s in r['sources']) > x['max_age']:
return {'error': 'too late'}
lots = {}
for exp, p in r['sources']:
e = exp if exp >= x['cancel_day'] else x['cancel_day'] + x['grace']
lots[e] = lots.get(e, 0) + p
return {'lots': [[e, lots[e]] for e in sorted(lots)], 'total': sum(lots.values())}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mixed sources', {'redemption': {'day': 100, 'sources': [[91, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 51]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 51]], 'total': 51}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 102, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 11, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 101, 'grace': 14, 'max_age': 90}, {'lots': [[115, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 21], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 31], [300, 7]], 'total': 38}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 6]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 81, 'grace': 10, 'max_age': 90}, {'lots': [[91, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[92, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 52]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 52]], 'total': 52}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 103, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 12, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 102, 'grace': 14, 'max_age': 90}, {'lots': [[116, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 22], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 32], [300, 7]], 'total': 39}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 7]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 7], [150, 10], [200, 40]], 'total': 57}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 82, 'grace': 10, 'max_age': 90}, {'lots': [[92, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[93, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 53]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 53]], 'total': 53}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 104, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 13, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 103, 'grace': 14, 'max_age': 90}, {'lots': [[117, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 23], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 33], [300, 7]], 'total': 40}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 8]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 8], [150, 10], [200, 40]], 'total': 58}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 83, 'grace': 10, 'max_age': 90}, {'lots': [[93, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[94, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 54]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 54]], 'total': 54}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 105, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 14, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 104, 'grace': 14, 'max_age': 90}, {'lots': [[118, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 24], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 34], [300, 7]], 'total': 41}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 9]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 9], [150, 10], [200, 40]], 'total': 59}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 84, 'grace': 10, 'max_age': 90}, {'lots': [[94, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[95, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 55]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 55]], 'total': 55}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 106, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 15, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 105, 'grace': 14, 'max_age': 90}, {'lots': [[119, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 25], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 35], [300, 7]], 'total': 42}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 10]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 10], [150, 10], [200, 40]], 'total': 60}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 85, 'grace': 10, 'max_age': 90}, {'lots': [[95, 70]], 'total': 70}]]]
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 |
|---|---|---|---|
| mixed sources | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | Passed |
| expires on cancel day | {'lots': [[120, 51]], 'total': 51} | {'lots': [[120, 51]], 'total': 51} | Passed |
| too late | {'lots': [[500, 100]], 'total': 100} | {'error': 'too late'} | Failed |
| age edge | {'lots': [[115, 5], [500, 100]], 'total': 105} | {'lots': [[115, 5], [500, 100]], 'total': 105} | Passed |
| merge grace lots | {'lots': [[105, 31], [300, 7]], 'total': 38} | {'lots': [[105, 31], [300, 7]], 'total': 38} | Passed |
| merge with original | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | Passed |
| late cancel grace base | {'lots': [[91, 70]], 'total': 70} | {'lots': [[91, 70]], 'total': 70} | Passed |
SHA-256 / ee070dd506eb5778e605ce55600d3b9aba1a3c55411b54e760d4ad057befb1e7
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
r = x['redemption']
if x['cancel_day'] - r['day'] > x['max_age']:
return {'error': 'too late'}
lots = {}
for exp, p in r['sources']:
e = exp if exp >= x['cancel_day'] else x['cancel_day'] + x['grace']
lots[e] = lots.get(e, 0) + p
return {'lots': [[e, lots[e]] for e in sorted(lots)], 'total': sum(lots.values())}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mixed sources', {'redemption': {'day': 100, 'sources': [[91, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 51]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 51]], 'total': 51}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 102, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 11, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 101, 'grace': 14, 'max_age': 90}, {'lots': [[115, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 21], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 31], [300, 7]], 'total': 38}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 6]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 81, 'grace': 10, 'max_age': 90}, {'lots': [[91, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[92, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 52]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 52]], 'total': 52}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 103, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 12, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 102, 'grace': 14, 'max_age': 90}, {'lots': [[116, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 22], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 32], [300, 7]], 'total': 39}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 7]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 7], [150, 10], [200, 40]], 'total': 57}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 82, 'grace': 10, 'max_age': 90}, {'lots': [[92, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[93, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 53]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 53]], 'total': 53}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 104, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 13, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 103, 'grace': 14, 'max_age': 90}, {'lots': [[117, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 23], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 33], [300, 7]], 'total': 40}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 8]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 8], [150, 10], [200, 40]], 'total': 58}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 83, 'grace': 10, 'max_age': 90}, {'lots': [[93, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[94, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 54]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 54]], 'total': 54}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 105, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 14, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 104, 'grace': 14, 'max_age': 90}, {'lots': [[118, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 24], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 34], [300, 7]], 'total': 41}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 9]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 9], [150, 10], [200, 40]], 'total': 59}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 84, 'grace': 10, 'max_age': 90}, {'lots': [[94, 70]], 'total': 70}]], [['mixed sources', {'redemption': {'day': 100, 'sources': [[95, 300], [150, 200], [400, 100]]}, 'cancel_day': 110, 'grace': 30, 'max_age': 90}, {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600}], ['expires on cancel day', {'redemption': {'day': 100, 'sources': [[120, 55]]}, 'cancel_day': 120, 'grace': 30, 'max_age': 90}, {'lots': [[120, 55]], 'total': 55}], ['too late', {'redemption': {'day': 10, 'sources': [[500, 100]]}, 'cancel_day': 106, 'grace': 30, 'max_age': 90}, {'error': 'too late'}], ['age edge', {'redemption': {'day': 15, 'sources': [[500, 100], [20, 5]]}, 'cancel_day': 105, 'grace': 14, 'max_age': 90}, {'lots': [[119, 5], [500, 100]], 'total': 105}], ['merge grace lots', {'redemption': {'day': 50, 'sources': [[55, 10], [58, 25], [300, 7]]}, 'cancel_day': 60, 'grace': 45, 'max_age': 90}, {'lots': [[105, 35], [300, 7]], 'total': 42}], ['merge with original', {'redemption': {'day': 50, 'sources': [[200, 40], [150, 10], [90, 10]]}, 'cancel_day': 60, 'grace': 140, 'max_age': 90}, {'lots': [[90, 10], [150, 10], [200, 40]], 'total': 60}], ['late cancel grace base', {'redemption': {'day': 20, 'sources': [[25, 70]]}, 'cancel_day': 85, 'grace': 10, 'max_age': 90}, {'lots': [[95, 70]], 'total': 70}]]]
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 |
|---|---|---|---|
| mixed sources | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | {'lots': [[140, 300], [150, 200], [400, 100]], 'total': 600} | Passed |
| expires on cancel day | {'lots': [[120, 51]], 'total': 51} | {'lots': [[120, 51]], 'total': 51} | Passed |
| too late | {'error': 'too late'} | {'error': 'too late'} | Passed |
| age edge | {'lots': [[115, 5], [500, 100]], 'total': 105} | {'lots': [[115, 5], [500, 100]], 'total': 105} | Passed |
| merge grace lots | {'lots': [[105, 31], [300, 7]], 'total': 38} | {'lots': [[105, 31], [300, 7]], 'total': 38} | Passed |
| merge with original | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | {'lots': [[90, 6], [150, 10], [200, 40]], 'total': 56} | Passed |
| late cancel grace base | {'lots': [[91, 70]], 'total': 70} | {'lots': [[91, 70]], 'total': 70} | Passed |
SHA-256 / 9f863b43e2301cbfca7f11ac1277af3637e48129db4959a92017a9e5826e5f6b
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:47.939856+00:00.
Case digest / 2a87f148b9e50db28415f610351e25b1d3fb3210fd64368eb0ced96956a63ea1