FA-60396 / Loyalty points ledgers / Open access
Expiring point lots: availability pool · case 01
A redemption is accepted against expired points and then only partly filled.
ROOT CAUSE
The sufficiency check sums every lot, including expired ones.
VERIFIED REPAIR
Check sufficiency against live lots only.
Unsuccessful approach: Checking only lots expiring strictly after today rejects redemptions that live lots can fill.
Case contract
Events ["earn",day,pts,ttl] create a lot expiring at day+ttl (usable through that day); ["redeem",day,pts] draws only unexpired lots, soonest expiry first (ties by earn day), rejecting with "insufficient" when unexpired points are short; ["sweep",day] removes lots whose expiry is before day and reports the points removed. Emptied lots are dropped. Return {results, lots:[[expiry, earn_day, pts]]}.
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):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] < day)
lots = [l for l in lots if l[0] >= day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in lots):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Passed |
| expired unusable | {'lots': [[30, 0, 200]], 'results': [30, 95, [[95, 50]]]} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Failed |
| sweep reports points | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Passed |
| sweep boundary | {'lots': [], 'results': [11, 0, 70]} | {'lots': [], 'results': [11, 0, 70]} | Passed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / 6da79937c384adce34392d8b542e7aa6115259f62d4b14949e25682873fe7bbc
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] < day)
lots = [l for l in lots if l[0] >= day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in live if l[0] > day):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 200]], 'results': [30, 'insufficient']} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Failed |
| expired unusable | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Passed |
| sweep reports points | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Passed |
| sweep boundary | {'lots': [], 'results': [11, 0, 70]} | {'lots': [], 'results': [11, 0, 70]} | Passed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / d90e3c0c5c7b9cf1e97a8b85feafb9a87e66e860119b325f24e9ba304e85723e
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
lots = []
out = []
for ev in x['events']:
kind, day = ev[0], ev[1]
if kind == 'earn':
exp = day + ev[3]
lots.append([exp, day, ev[2]])
out.append(exp)
elif kind == 'sweep':
gone = sum(l[2] for l in lots if l[0] < day)
lots = [l for l in lots if l[0] >= day]
out.append(gone)
else:
need = ev[2]
live = sorted([l for l in lots if l[0] >= day], key=lambda l: (l[0], l[1]))
if need > sum(l[2] for l in live):
out.append('insufficient')
continue
used = []
for l in live:
t = min(need, l[2])
if t > 0:
l[2] -= t
need -= t
used.append([l[0], t])
lots = [l for l in lots if l[2] > 0]
out.append(used)
return {'results': out, 'lots': lots}
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 31], ['redeem', 20, 400]]}, {'results': [366, 41, [[41, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 151]]}, {'results': [30, [[30, 151]]], 'lots': [[30, 0, 49]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 101]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 81, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 201], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 11], ['sweep', 11], ['sweep', 12]]}, {'results': [11, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 31]]}, {'results': [50, 61, [[50, 100]], [[61, 31]]], 'lots': [[61, 1, 69]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 151], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 51]]}, {'results': [25, 25, [[25, 40], [25, 11]]], 'lots': [[25, 5, 29]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 32], ['redeem', 20, 400]]}, {'results': [366, 42, [[42, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 152]]}, {'results': [30, [[30, 152]]], 'lots': [[30, 0, 48]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 102]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 82, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 202], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 12], ['sweep', 12], ['sweep', 13]]}, {'results': [12, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 32]]}, {'results': [50, 61, [[50, 100]], [[61, 32]]], 'lots': [[61, 1, 68]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 152], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 52]]}, {'results': [25, 25, [[25, 40], [25, 12]]], 'lots': [[25, 5, 28]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 33], ['redeem', 20, 400]]}, {'results': [366, 43, [[43, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 153]]}, {'results': [30, [[30, 153]]], 'lots': [[30, 0, 47]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 103]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 83, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 203], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 13], ['sweep', 13], ['sweep', 14]]}, {'results': [13, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 33]]}, {'results': [50, 61, [[50, 100]], [[61, 33]]], 'lots': [[61, 1, 67]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 153], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 53]]}, {'results': [25, 25, [[25, 40], [25, 13]]], 'lots': [[25, 5, 27]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 34], ['redeem', 20, 400]]}, {'results': [366, 44, [[44, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 154]]}, {'results': [30, [[30, 154]]], 'lots': [[30, 0, 46]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 104]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 84, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 204], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 14], ['sweep', 14], ['sweep', 15]]}, {'results': [14, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 34]]}, {'results': [50, 61, [[50, 100]], [[61, 34]]], 'lots': [[61, 1, 66]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 154], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 54]]}, {'results': [25, 25, [[25, 40], [25, 14]]], 'lots': [[25, 5, 26]]}]], [['soonest expiry first', {'events': [['earn', 1, 500, 365], ['earn', 10, 300, 35], ['redeem', 20, 400]]}, {'results': [366, 45, [[45, 300], [366, 100]]], 'lots': [[366, 1, 400]]}], ['use on expiry day', {'events': [['earn', 0, 200, 30], ['redeem', 30, 155]]}, {'results': [30, [[30, 155]]], 'lots': [[30, 0, 45]]}], ['expired unusable', {'events': [['earn', 0, 200, 30], ['earn', 5, 50, 90], ['redeem', 31, 105]]}, {'results': [30, 95, 'insufficient'], 'lots': [[30, 0, 200], [95, 5, 50]]}], ['sweep reports points', {'events': [['earn', 0, 120, 10], ['earn', 0, 85, 10], ['earn', 3, 60, 100], ['sweep', 11]]}, {'results': [10, 10, 103, 205], 'lots': [[103, 3, 60]]}], ['sweep boundary', {'events': [['earn', 0, 70, 15], ['sweep', 15], ['sweep', 16]]}, {'results': [15, 0, 70], 'lots': []}], ['partial lot cleanup', {'events': [['earn', 0, 100, 50], ['earn', 1, 100, 60], ['redeem', 2, 100], ['redeem', 3, 35]]}, {'results': [50, 61, [[50, 100]], [[61, 35]]], 'lots': [[61, 1, 65]]}], ['insufficient', {'events': [['earn', 0, 100, 50], ['redeem', 1, 155], ['redeem', 2, 100]]}, {'results': [50, 'insufficient', [[50, 100]]], 'lots': []}], ['ties by earn day', {'events': [['earn', 5, 40, 20], ['earn', 1, 40, 24], ['redeem', 6, 55]]}, {'results': [25, 25, [[25, 40], [25, 15]]], 'lots': [[25, 5, 25]]}]]]
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 |
|---|---|---|---|
| soonest expiry first | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | {'lots': [[366, 1, 400]], 'results': [366, 41, [[41, 300], [366, 100]]]} | Passed |
| use on expiry day | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | {'lots': [[30, 0, 49]], 'results': [30, [[30, 151]]]} | Passed |
| expired unusable | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | {'lots': [[30, 0, 200], [95, 5, 50]], 'results': [30, 95, 'insufficient']} | Passed |
| sweep reports points | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Passed |
| sweep boundary | {'lots': [], 'results': [11, 0, 70]} | {'lots': [], 'results': [11, 0, 70]} | Passed |
| partial lot cleanup | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | {'lots': [[61, 1, 69]], 'results': [50, 61, [[50, 100]], [[61, 31]]]} | Passed |
| insufficient | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | {'lots': [], 'results': [50, 'insufficient', [[50, 100]]]} | Passed |
| ties by earn day | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | {'lots': [[25, 5, 29]], 'results': [25, 25, [[25, 40], [25, 11]]]} | Passed |
SHA-256 / 5bc68b951670e50c9a94821866bb25d5150737754da5f6fdee108df25fde5473
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.256093+00:00.
Case digest / afd90667f28dedc09d8e31c03f9b6defae81fa0e32fbb76e066790aa77a7ddcb