FA-92891 / EV charging session scheduling / Open access
Post-charge idle occupancy fee: zero cap waiver · case 01
A site configured to waive idle fees (cap 0) still bills them.
ROOT CAUSE
The cap test is truthiness, so a zero cap is mistaken for no cap.
VERIFIED REPAIR
Apply the cap whenever it is not None, including zero.
Unsuccessful approach: Excluding cap 0 explicitly repeats the same misreading of the waiver.
Case contract
Times are minutes 0..1440 of one day. done is when charging finished (None: never finished, no idle fee). Idle time starts at done+grace and ends at unplug (exclusive). Only minutes in [360, 1320) are billable (night waiver). Fee = ceil(billable/block)*rate cents, then capped at cap when cap is not None (cap 0 waives the fee). Return [billable minutes, fee].
Why this case matters
Depot, workplace and public EV chargers schedule sessions against prices, circuit limits and departure deadlines; a wrong decision silently strands a driver or overloads a feeder.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(plug, done, unplug, grace, rate, cap, block):
if done is None or unplug <= done:
return [0, 0]
start = done + grace
if unplug <= start:
return [0, 0]
billable = 0
for m in range(start, unplug):
if 360 <= m < 1320:
billable += 1
blocks = -(-billable // block)
fee = blocks * rate
if cap:
fee = min(fee, cap)
return [billable, fee]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [694, 1094, 1440, 30, 5, 0, 5], [196, 0]],
['regression: zero cap waiver (partial repair)', [576, 671, 855, 15, 40, 0, 5], [169, 0]],
['control 1', [708, 712, 1012, 15, 25, 1000, 5], [285, 1000]],
['control 2', [424, 721, 1119, 10, 5, None, 15], [388, 130]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [935, 1103, 1359, 15, 25, 0, 15], [202, 0]],
['regression: zero cap waiver (partial repair)', [370, 402, 585, 15, 5, 0, 5], [168, 0]],
['control 1', [1149, 1440, 1440, 0, 25, None, 1], [0, 0]],
['control 2', [421, 583, 815, 15, 5, 300, 15], [217, 75]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1142, 1227, 1440, 10, 25, 0, 1], [83, 0]],
['regression: zero cap waiver (partial repair)', [335, 562, 733, 10, 40, 0, 15], [161, 0]],
['control 1', [1122, 1422, 1434, 10, 25, 0, 10], [0, 0]],
['control 2', [325, 596, 781, 30, 25, None, 15], [155, 275]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [786, 1147, 1171, 15, 40, 0, 5], [9, 0]],
['regression: zero cap waiver (partial repair)', [278, 314, 487, 30, 5, 0, 15], [127, 0]],
['control 1', [887, 906, 899, 15, 5, 1000, 10], [0, 0]],
['control 2', [684, 849, 1335, 30, 25, 1000, 10], [441, 1000]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1260, 1286, 1440, 0, 40, 0, 10], [34, 0]],
['regression: zero cap waiver (partial repair)', [548, 763, 1064, 10, 5, 0, 15], [291, 0]],
['control 1', [942, 1011, 983, 15, 25, 300, 10], [0, 0]],
['control 2', [306, 373, 739, 10, 25, None, 10], [356, 900]]]]
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 |
|---|---|---|---|
| boundary: unplugged inside grace | [0, 0] | [0, 0] | Passed |
| boundary: never finished | [0, 0] | [0, 0] | Passed |
| boundary: idle crosses night waiver | [20, 200] | [20, 200] | Passed |
| regression: zero cap waiver | [196, 200] | [196, 0] | Failed |
| regression: zero cap waiver (partial repair) | [169, 1360] | [169, 0] | Failed |
| control 1 | [285, 1000] | [285, 1000] | Passed |
| control 2 | [388, 130] | [388, 130] | Passed |
SHA-256 / ad8a0a6152433f261580b525ae73b6f4309974c8ab18d9f6198b58530879921b
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(plug, done, unplug, grace, rate, cap, block):
if done is None or unplug <= done:
return [0, 0]
start = done + grace
if unplug <= start:
return [0, 0]
billable = 0
for m in range(start, unplug):
if 360 <= m < 1320:
billable += 1
blocks = -(-billable // block)
fee = blocks * rate
if cap is not None and cap > 0:
fee = min(fee, cap)
return [billable, fee]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [694, 1094, 1440, 30, 5, 0, 5], [196, 0]],
['regression: zero cap waiver (partial repair)', [576, 671, 855, 15, 40, 0, 5], [169, 0]],
['control 1', [708, 712, 1012, 15, 25, 1000, 5], [285, 1000]],
['control 2', [424, 721, 1119, 10, 5, None, 15], [388, 130]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [935, 1103, 1359, 15, 25, 0, 15], [202, 0]],
['regression: zero cap waiver (partial repair)', [370, 402, 585, 15, 5, 0, 5], [168, 0]],
['control 1', [1149, 1440, 1440, 0, 25, None, 1], [0, 0]],
['control 2', [421, 583, 815, 15, 5, 300, 15], [217, 75]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1142, 1227, 1440, 10, 25, 0, 1], [83, 0]],
['regression: zero cap waiver (partial repair)', [335, 562, 733, 10, 40, 0, 15], [161, 0]],
['control 1', [1122, 1422, 1434, 10, 25, 0, 10], [0, 0]],
['control 2', [325, 596, 781, 30, 25, None, 15], [155, 275]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [786, 1147, 1171, 15, 40, 0, 5], [9, 0]],
['regression: zero cap waiver (partial repair)', [278, 314, 487, 30, 5, 0, 15], [127, 0]],
['control 1', [887, 906, 899, 15, 5, 1000, 10], [0, 0]],
['control 2', [684, 849, 1335, 30, 25, 1000, 10], [441, 1000]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1260, 1286, 1440, 0, 40, 0, 10], [34, 0]],
['regression: zero cap waiver (partial repair)', [548, 763, 1064, 10, 5, 0, 15], [291, 0]],
['control 1', [942, 1011, 983, 15, 25, 300, 10], [0, 0]],
['control 2', [306, 373, 739, 10, 25, None, 10], [356, 900]]]]
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 |
|---|---|---|---|
| boundary: unplugged inside grace | [0, 0] | [0, 0] | Passed |
| boundary: never finished | [0, 0] | [0, 0] | Passed |
| boundary: idle crosses night waiver | [20, 200] | [20, 200] | Passed |
| regression: zero cap waiver | [196, 200] | [196, 0] | Failed |
| regression: zero cap waiver (partial repair) | [169, 1360] | [169, 0] | Failed |
| control 1 | [285, 1000] | [285, 1000] | Passed |
| control 2 | [388, 130] | [388, 130] | Passed |
SHA-256 / b95b4ad351c71c12b60ffade2ca417d85d9e522bbeaa619085a234dc2cc4325b
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(plug, done, unplug, grace, rate, cap, block):
if done is None or unplug <= done:
return [0, 0]
start = done + grace
if unplug <= start:
return [0, 0]
billable = 0
for m in range(start, unplug):
if 360 <= m < 1320:
billable += 1
blocks = -(-billable // block)
fee = blocks * rate
if cap is not None:
fee = min(fee, cap)
return [billable, fee]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [694, 1094, 1440, 30, 5, 0, 5], [196, 0]],
['regression: zero cap waiver (partial repair)', [576, 671, 855, 15, 40, 0, 5], [169, 0]],
['control 1', [708, 712, 1012, 15, 25, 1000, 5], [285, 1000]],
['control 2', [424, 721, 1119, 10, 5, None, 15], [388, 130]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [935, 1103, 1359, 15, 25, 0, 15], [202, 0]],
['regression: zero cap waiver (partial repair)', [370, 402, 585, 15, 5, 0, 5], [168, 0]],
['control 1', [1149, 1440, 1440, 0, 25, None, 1], [0, 0]],
['control 2', [421, 583, 815, 15, 5, 300, 15], [217, 75]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1142, 1227, 1440, 10, 25, 0, 1], [83, 0]],
['regression: zero cap waiver (partial repair)', [335, 562, 733, 10, 40, 0, 15], [161, 0]],
['control 1', [1122, 1422, 1434, 10, 25, 0, 10], [0, 0]],
['control 2', [325, 596, 781, 30, 25, None, 15], [155, 275]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [786, 1147, 1171, 15, 40, 0, 5], [9, 0]],
['regression: zero cap waiver (partial repair)', [278, 314, 487, 30, 5, 0, 15], [127, 0]],
['control 1', [887, 906, 899, 15, 5, 1000, 10], [0, 0]],
['control 2', [684, 849, 1335, 30, 25, 1000, 10], [441, 1000]]],
[['boundary: unplugged inside grace', [600, 700, 710, 15, 25, None, 5], [0, 0]],
['boundary: never finished', [600, None, 900, 0, 25, None, 5], [0, 0]],
['boundary: idle crosses night waiver', [1200, 1300, 1400, 0, 10, None, 1], [20, 200]],
['regression: zero cap waiver', [1260, 1286, 1440, 0, 40, 0, 10], [34, 0]],
['regression: zero cap waiver (partial repair)', [548, 763, 1064, 10, 5, 0, 15], [291, 0]],
['control 1', [942, 1011, 983, 15, 25, 300, 10], [0, 0]],
['control 2', [306, 373, 739, 10, 25, None, 10], [356, 900]]]]
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 |
|---|---|---|---|
| boundary: unplugged inside grace | [0, 0] | [0, 0] | Passed |
| boundary: never finished | [0, 0] | [0, 0] | Passed |
| boundary: idle crosses night waiver | [20, 200] | [20, 200] | Passed |
| regression: zero cap waiver | [196, 0] | [196, 0] | Passed |
| regression: zero cap waiver (partial repair) | [169, 0] | [169, 0] | Passed |
| control 1 | [285, 1000] | [285, 1000] | Passed |
| control 2 | [388, 130] | [388, 130] | Passed |
SHA-256 / e1dc8ef4127fb20d6aa5f143d81c3bd1d1cff3d06d32a0fcc941dd66744a3424
Verification & scope
Deterministic stipulated toy contract for teaching; no claim of conformance with any standard, vendor protocol or production controller. 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:51:50.097661+00:00.
Case digest / a23c2d09f00895395d6532496048f7b7f04e05e8eaad5d2f75b7e9e94e18a4d8