FA-60401 / Loyalty points ledgers / Open access
Expiring point lots: sweep report unit · case 01
The expiry notice says "2 points expired" when two lots holding hundreds of points expired.
ROOT CAUSE
The sweep reports the number of expired lots instead of expired points.
VERIFIED REPAIR
Report the sum of points in the swept lots.
Unsuccessful approach: Reporting the remaining balance after the sweep is a different figure.
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 = len([l 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, 2]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Failed |
| sweep boundary | {'lots': [], 'results': [11, 0, 1]} | {'lots': [], 'results': [11, 0, 70]} | Failed |
| 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 / dcc246b003b4d73f925b70f2832c07e15bf4a780f18732f5b52fc6ca009cabdf
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):
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, 60]} | {'lots': [[103, 3, 60]], 'results': [10, 10, 103, 201]} | Failed |
| sweep boundary | {'lots': [], 'results': [11, 70, 0]} | {'lots': [], 'results': [11, 0, 70]} | Failed |
| 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 / 35ce8fdda8286e6017a4a39c86ed2ce281e3323127bccbceb6b507f0dd458990
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.352045+00:00.
Case digest / 3749e38e27bc3b241533217454309e4bf17c178e10cd6b4ffe0b242fa8ca721b