FAILURE MAP
← Case archive

FA-60486 / Loyalty points ledgers / Open access

Pending points hold period: posting day boundary · case 01

Points stay pending one day longer than the published hold period.

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

ROOT CAUSE

Posting requires the day to be strictly after earn day + hold.

VERIFIED REPAIR

Points post on day >= earn day + hold.

Unsuccessful approach: Posting when day - earn >= hold - 1 releases points a day early.

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) - 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]]]], [['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]]]], [['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]]]], [['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]]]], [['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]]]]]
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
posts after hold['pending', [100, 0], [0, 0], [0, 100]]['pending', [100, 0], [0, 100], [0, 100]]Failed
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, 0]]['pending', [12, 0], [0, 12]]Failed
refund near posting['pending', 'cancelled', [0, 0]]['pending', 'cancelled', [0, 0]]Passed

SHA-256 / f755cc4ad81f8451f994957005b41bcf0ba5ebf55f3cb1abf135b78099f03c71

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'] - 1)
    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]]]], [['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]]]], [['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]]]], [['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]]]], [['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]]]]]
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
posts after hold['pending', [100, 100], [0, 100], [0, 100]]['pending', [100, 0], [0, 100], [0, 100]]Failed
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

SHA-256 / 0403b14542a1d3910eae9aae0ef60e47e73d8d31c5ea8387cb63845662dbfb51

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]]]], [['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]]]], [['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]]]], [['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]]]], [['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]]]]]
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
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

SHA-256 / 2868ebaecdf43582bde2d467978660153522d1cd3c03b37fc6f50a0cfbab5da9

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

Case digest / 041e79f0ed2d16a0e67c637f6a09be772553a1e7c08abeff995e2c91fcac9ee7