FAILURE MAP
← Case archive

FA-60666 / Loyalty points ledgers / Open access

Cancelled reward reinstatement: still-valid test · case 01

Points that are still valid on the cancellation day receive a new grace expiry instead of their original one.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Original expiry is kept only when it is strictly after the cancel day.

VERIFIED REPAIR

Keep the original expiry when it is on or after the cancel day.

Unsuccessful approach: Keeping original expiries that lapsed the day before the cancel reinstates expired points without grace.

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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[31, 9]]}, 'cancel_day': 32, 'grace': 7, 'max_age': 90}, {'lots': [[39, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[32, 9]]}, 'cancel_day': 33, 'grace': 7, 'max_age': 90}, {'lots': [[40, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[33, 9]]}, 'cancel_day': 34, 'grace': 7, 'max_age': 90}, {'lots': [[41, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[34, 9]]}, 'cancel_day': 35, 'grace': 7, 'max_age': 90}, {'lots': [[42, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[35, 9]]}, 'cancel_day': 36, 'grace': 7, 'max_age': 90}, {'lots': [[43, 9]], 'total': 9}]]]
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 fixtureActualExpectedOutcome
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': [[150, 51]], 'total': 51}{'lots': [[120, 51]], 'total': 51}Failed
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
expired day before{'lots': [[39, 9]], 'total': 9}{'lots': [[39, 9]], 'total': 9}Passed

SHA-256 / 25c959062f82fd61e493db022287a4843c4c8766c7fc5a4c210a338f460b28f3

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'] - r['day'] > x['max_age']:
        return {'error': 'too late'}
    lots = {}
    for exp, p in r['sources']:
        e = exp if exp >= x['cancel_day'] - 1 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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[31, 9]]}, 'cancel_day': 32, 'grace': 7, 'max_age': 90}, {'lots': [[39, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[32, 9]]}, 'cancel_day': 33, 'grace': 7, 'max_age': 90}, {'lots': [[40, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[33, 9]]}, 'cancel_day': 34, 'grace': 7, 'max_age': 90}, {'lots': [[41, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[34, 9]]}, 'cancel_day': 35, 'grace': 7, 'max_age': 90}, {'lots': [[42, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[35, 9]]}, 'cancel_day': 36, 'grace': 7, 'max_age': 90}, {'lots': [[43, 9]], 'total': 9}]]]
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 fixtureActualExpectedOutcome
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
expired day before{'lots': [[31, 9]], 'total': 9}{'lots': [[39, 9]], 'total': 9}Failed

SHA-256 / eed475be4d61e301448f4def73bd2769fda6eff9b89e5b5287331ca8a3f904e0

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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[31, 9]]}, 'cancel_day': 32, 'grace': 7, 'max_age': 90}, {'lots': [[39, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[32, 9]]}, 'cancel_day': 33, 'grace': 7, 'max_age': 90}, {'lots': [[40, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[33, 9]]}, 'cancel_day': 34, 'grace': 7, 'max_age': 90}, {'lots': [[41, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[34, 9]]}, 'cancel_day': 35, 'grace': 7, 'max_age': 90}, {'lots': [[42, 9]], 'total': 9}]], [['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}], ['expired day before', {'redemption': {'day': 5, 'sources': [[35, 9]]}, 'cancel_day': 36, 'grace': 7, 'max_age': 90}, {'lots': [[43, 9]], 'total': 9}]]]
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 fixtureActualExpectedOutcome
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
expired day before{'lots': [[39, 9]], 'total': 9}{'lots': [[39, 9]], 'total': 9}Passed

SHA-256 / 58b05f831a7c5e78097a2c0f3f5407b75cc6a42f632e375c9fe03d6cc8ddf455

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.745500+00:00.

Case digest / af816619915b009960ea388c123acf5594314997f0ed922680180083a949e521