FA-60501 / Loyalty points ledgers / Open access
Pending points hold period: redeemable balance · case 01
Members can redeem points that are still in the return window.
ROOT CAUSE
The redemption check counts pending points as spendable.
VERIFIED REPAIR
Redeem only against posted points less points already spent.
Unsuccessful approach: Ignoring earlier redemptions lets the same posted points be spent twice.
Case contract
Events: ["earn",day,order,pts] starts pending; points post (become available) on day >= earn day + hold. ["refund",day,order] cancels a live earn if day < earn day + hold ("cancelled"), otherwise claws it back ("clawed"; available may go negative); unknown or already refunded orders are "ignored". ["redeem",day,pts] spends available (posted - spent) points or returns "insufficient". ["query",day] returns [pending, available]. Return results list.
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):
earns = {}
spent = 0
out = []
posted = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day >= e[0] + x['hold'])
pending = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day < e[0] + x['hold'])
for ev in x['events']:
k, day = ev[0], ev[1]
if k == 'earn':
earns[ev[2]] = [day, ev[3], 'live']
out.append('pending')
elif k == 'refund':
e = earns.get(ev[2])
if e is None or e[2] != 'live':
out.append('ignored')
continue
if day < e[0] + x['hold']:
e[2] = 'cancelled'
else:
e[2] = 'clawed'
out.append(e[2])
elif k == 'redeem':
if ev[2] > posted(day) + pending(day) - spent:
out.append('insufficient')
continue
spent += ev[2]
out.append('ok')
else:
out.append([pending(day), posted(day) - spent])
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 32]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 41], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 41]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 23]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 31], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 151], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 12], ['query', 5], ['query', 12]]}, ['pending', [12, 0], [0, 12]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 26, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 101], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 33]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 42], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 42]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 24]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 32], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 152], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 13], ['query', 5], ['query', 12]]}, ['pending', [13, 0], [0, 13]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 27, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 102], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 34]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 43], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 43]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 25]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 33], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 153], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 14], ['query', 5], ['query', 12]]}, ['pending', [14, 0], [0, 14]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 28, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 103], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 35]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 44], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 44]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 26]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 34], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 154], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 15], ['query', 5], ['query', 12]]}, ['pending', [15, 0], [0, 15]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 29, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 104], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 36]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 45], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 45]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 27]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 35], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 155], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 16], ['query', 5], ['query', 12]]}, ['pending', [16, 0], [0, 16]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 25, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 105], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]]]
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 |
|---|---|---|---|
| posts after hold | ['pending', [100, 0], [0, 100], [0, 100]] | ['pending', [100, 0], [0, 100], [0, 100]] | Passed |
| refund in hold | ['pending', 'pending', 'cancelled', [0, 41]] | ['pending', 'pending', 'cancelled', [0, 41]] | Passed |
| refund after post | ['pending', 'ok', 'clawed', [0, -50]] | ['pending', 'ok', 'clawed', [0, -50]] | Passed |
| double refund | ['pending', 'cancelled', 'ignored', 'ignored'] | ['pending', 'cancelled', 'ignored', 'ignored'] | Passed |
| redeem pending | ['pending', 'pending', 'ok', 'insufficient'] | ['pending', 'pending', 'insufficient', 'ok'] | Failed |
| same day earn query | ['pending', [12, 0], [0, 12]] | ['pending', [12, 0], [0, 12]] | Passed |
| refund near posting | ['pending', 'cancelled', [0, 0]] | ['pending', 'cancelled', [0, 0]] | Passed |
| second redemption | ['pending', 'ok', 'insufficient'] | ['pending', 'ok', 'insufficient'] | Passed |
SHA-256 / 81174bea5f324c75d61b48dd08dacb0a54778c1f9c7cbcedfcc7db37f0ad7ce3
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
earns = {}
spent = 0
out = []
posted = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day >= e[0] + x['hold'])
pending = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day < e[0] + x['hold'])
for ev in x['events']:
k, day = ev[0], ev[1]
if k == 'earn':
earns[ev[2]] = [day, ev[3], 'live']
out.append('pending')
elif k == 'refund':
e = earns.get(ev[2])
if e is None or e[2] != 'live':
out.append('ignored')
continue
if day < e[0] + x['hold']:
e[2] = 'cancelled'
else:
e[2] = 'clawed'
out.append(e[2])
elif k == 'redeem':
if ev[2] > posted(day):
out.append('insufficient')
continue
spent += ev[2]
out.append('ok')
else:
out.append([pending(day), posted(day) - spent])
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 32]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 41], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 41]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 23]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 31], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 151], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 12], ['query', 5], ['query', 12]]}, ['pending', [12, 0], [0, 12]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 26, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 101], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 33]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 42], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 42]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 24]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 32], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 152], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 13], ['query', 5], ['query', 12]]}, ['pending', [13, 0], [0, 13]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 27, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 102], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 34]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 43], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 43]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 25]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 33], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 153], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 14], ['query', 5], ['query', 12]]}, ['pending', [14, 0], [0, 14]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 28, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 103], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 35]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 44], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 44]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 26]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 34], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 154], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 15], ['query', 5], ['query', 12]]}, ['pending', [15, 0], [0, 15]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 29, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 104], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 36]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 45], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 45]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 27]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 35], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 155], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 16], ['query', 5], ['query', 12]]}, ['pending', [16, 0], [0, 16]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 25, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 105], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]]]
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 |
|---|---|---|---|
| posts after hold | ['pending', [100, 0], [0, 100], [0, 100]] | ['pending', [100, 0], [0, 100], [0, 100]] | Passed |
| refund in hold | ['pending', 'pending', 'cancelled', [0, 41]] | ['pending', 'pending', 'cancelled', [0, 41]] | Passed |
| refund after post | ['pending', 'ok', 'clawed', [0, -50]] | ['pending', 'ok', 'clawed', [0, -50]] | Passed |
| double refund | ['pending', 'cancelled', 'ignored', 'ignored'] | ['pending', 'cancelled', 'ignored', 'ignored'] | Passed |
| redeem pending | ['pending', 'pending', 'insufficient', 'ok'] | ['pending', 'pending', 'insufficient', 'ok'] | Passed |
| same day earn query | ['pending', [12, 0], [0, 12]] | ['pending', [12, 0], [0, 12]] | Passed |
| refund near posting | ['pending', 'cancelled', [0, 0]] | ['pending', 'cancelled', [0, 0]] | Passed |
| second redemption | ['pending', 'ok', 'ok'] | ['pending', 'ok', 'insufficient'] | Failed |
SHA-256 / 0be8237e32f6987ea3662e95da174f104b9be261c8b9399570fbf6afb0906ad1
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(x):
earns = {}
spent = 0
out = []
posted = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day >= e[0] + x['hold'])
pending = lambda day: sum(e[1] for e in earns.values() if e[2] == 'live' and day < e[0] + x['hold'])
for ev in x['events']:
k, day = ev[0], ev[1]
if k == 'earn':
earns[ev[2]] = [day, ev[3], 'live']
out.append('pending')
elif k == 'refund':
e = earns.get(ev[2])
if e is None or e[2] != 'live':
out.append('ignored')
continue
if day < e[0] + x['hold']:
e[2] = 'cancelled'
else:
e[2] = 'clawed'
out.append(e[2])
elif k == 'redeem':
if ev[2] > posted(day) - spent:
out.append('insufficient')
continue
spent += ev[2]
out.append('ok')
else:
out.append([pending(day), posted(day) - spent])
return out
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 32]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 41], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 41]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 23]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 31], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 151], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 12], ['query', 5], ['query', 12]]}, ['pending', [12, 0], [0, 12]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 26, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 101], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 33]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 42], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 42]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 24]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 32], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 152], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 13], ['query', 5], ['query', 12]]}, ['pending', [13, 0], [0, 13]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 27, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 102], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 34]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 43], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 43]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 25]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 33], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 153], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 14], ['query', 5], ['query', 12]]}, ['pending', [14, 0], [0, 14]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 28, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 103], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 35]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 44], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 44]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 26]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 34], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 154], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 15], ['query', 5], ['query', 12]]}, ['pending', [15, 0], [0, 15]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 29, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 104], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]], [['posts after hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['query', 29], ['query', 30], ['query', 36]]}, ['pending', [100, 0], [0, 100], [0, 100]]], ['refund in hold', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 2, 'o2', 45], ['refund', 10, 'o1'], ['query', 40]]}, ['pending', 'pending', 'cancelled', [0, 45]]], ['refund after post', {'hold': 14, 'events': [['earn', 0, 'o1', 70], ['redeem', 20, 50], ['refund', 21, 'o1'], ['query', 27]]}, ['pending', 'ok', 'clawed', [0, -50]]], ['double refund', {'hold': 10, 'events': [['earn', 0, 'o1', 35], ['refund', 3, 'o1'], ['refund', 4, 'o1'], ['refund', 5, 'zz']]}, ['pending', 'cancelled', 'ignored', 'ignored']], ['redeem pending', {'hold': 30, 'events': [['earn', 0, 'o1', 100], ['earn', 25, 'o2', 100], ['redeem', 40, 155], ['redeem', 40, 100]]}, ['pending', 'pending', 'insufficient', 'ok']], ['same day earn query', {'hold': 7, 'events': [['earn', 5, 'o1', 16], ['query', 5], ['query', 12]]}, ['pending', [16, 0], [0, 16]]], ['refund near posting', {'hold': 30, 'events': [['earn', 0, 'o1', 60], ['refund', 25, 'o1'], ['query', 60]]}, ['pending', 'cancelled', [0, 0]]], ['second redemption', {'hold': 10, 'events': [['earn', 0, 'o1', 105], ['redeem', 40, 60], ['redeem', 41, 60]]}, ['pending', 'ok', 'insufficient']]]]
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 |
|---|---|---|---|
| posts after hold | ['pending', [100, 0], [0, 100], [0, 100]] | ['pending', [100, 0], [0, 100], [0, 100]] | Passed |
| refund in hold | ['pending', 'pending', 'cancelled', [0, 41]] | ['pending', 'pending', 'cancelled', [0, 41]] | Passed |
| refund after post | ['pending', 'ok', 'clawed', [0, -50]] | ['pending', 'ok', 'clawed', [0, -50]] | Passed |
| double refund | ['pending', 'cancelled', 'ignored', 'ignored'] | ['pending', 'cancelled', 'ignored', 'ignored'] | Passed |
| redeem pending | ['pending', 'pending', 'insufficient', 'ok'] | ['pending', 'pending', 'insufficient', 'ok'] | Passed |
| same day earn query | ['pending', [12, 0], [0, 12]] | ['pending', [12, 0], [0, 12]] | Passed |
| refund near posting | ['pending', 'cancelled', [0, 0]] | ['pending', 'cancelled', [0, 0]] | Passed |
| second redemption | ['pending', 'ok', 'insufficient'] | ['pending', 'ok', 'insufficient'] | Passed |
SHA-256 / 649081505b354c8870e19ae99a11bdc6098353da21b8a298a81de3fa32216445
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:46.162884+00:00.
Case digest / 4fd8f79b37a0ae235d6be71a5a240e15a7e38fd42761112819d45c7f71d284f0