FAILURE MAP
← Case archive

FA-95066 / Warehouse slotting and bin packing / Open access

FEFO putaway: lot code matched across SKUs · case 01

A receipt is put into a bin of a different SKU that happens to share the lot code.

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

ROOT CAUSE

The same-lot rule compares lot codes without requiring the same SKU.

VERIFIED REPAIR

Require both the SKU and the lot code to match for the same-lot preference.

Unsuccessful approach: Matching SKU alone gives any lot of the SKU top priority, bypassing the expiry window.

Case contract

bins [[bin, sku|None, lot, expiry_day, qty, capacity, distance]]; receipt [sku, lot, expiry_day, qty]. A bin is empty when sku is None or qty is 0. Preference: (1) non-empty bin with same sku and lot and room (capacity-qty >= receipt qty), nearest then id; (2) same sku, different lot, expiry within 30 days either way, room; smallest expiry gap, nearest, id; (3) empty bin with capacity >= qty, nearest then id; else "HOLD".

Why this case matters

FEFO storage keeps lots with similar expiry together so the earliest expiring stock ships first.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[1] is None or b[4] == 0
    same_lot = [b for b in bins if not empty(b) and b[2] == lot and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 30', [[['B0', None, None, 0, 0, 40, 2], ['B1', 'S2', 'L2', 130, 10, 60, 9], ['B2', None, None, 0, 0, 60, 4], ['B3', None, None, 0, 0, 60, 7], ['B4', 'S1', 'L3', 145, 0, 40, 8], ['B5', 'S1', 'L1', 160, 40, 60, 5], ['B6', 'S1', 'L3', 130, 5, 40, 3]], ['S1', 'L2', 100, 10]], 'B6'], ['generated mix 66', [[['B0', 'S1', 'L1', 130, 40, 20, 8], ['B1', 'S2', 'L3', 120, 5, 20, 9], ['B2', 'S1', 'L1', 160, 5, 40, 2]], ['S1', 'L4', 100, 30]], 'HOLD'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1']], [['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 39', [[['B0', 'S1', 'L2', 120, 30, 20, 3], ['B1', 'S1', 'L3', 145, 0, 20, 4], ['B2', None, None, 0, 0, 40, 6], ['B3', 'S1', 'L3', 131, 30, 60, 9], ['B4', 'S1', 'L1', 100, 5, 60, 7], ['B5', None, None, 0, 0, 40, 4], ['B6', 'S2', 'L2', 100, 30, 60, 6]], ['S1', 'L4', 130, 10]], 'B3'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 44', [[['B0', 'S1', 'L2', 145, 10, 60, 3], ['B1', 'S2', 'L1', 145, 0, 60, 6], ['B2', 'S1', 'L1', 131, 0, 40, 6], ['B3', 'S2', 'L1', 145, 40, 20, 8]], ['S1', 'L4', 150, 20]], 'B0'], ['generated mix 52', [[['B0', None, None, 0, 0, 20, 9], ['B1', 'S1', 'L3', 120, 10, 60, 1], ['B2', 'S1', 'L3', 120, 40, 20, 9], ['B3', 'S1', 'L2', 100, 40, 60, 4]], ['S1', 'L4', 100, 10]], 'B3']]]
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
lot code on other skuXEFailed
generated mix 3B1B1Passed
same lot preferredAAPassed
expiry 30 days laterAAPassed
residual label emptyRRPassed
room exactAAPassed
generated mix 7B2B2Passed
generated mix 15B1B1Passed

SHA-256 / 550f2d7e1cf85de3afe37414ccf542013e7f6d24421df0806cf093e23c6dd501

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[1] is None or b[4] == 0
    same_lot = [b for b in bins if not empty(b) and b[1] == sku and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 30', [[['B0', None, None, 0, 0, 40, 2], ['B1', 'S2', 'L2', 130, 10, 60, 9], ['B2', None, None, 0, 0, 60, 4], ['B3', None, None, 0, 0, 60, 7], ['B4', 'S1', 'L3', 145, 0, 40, 8], ['B5', 'S1', 'L1', 160, 40, 60, 5], ['B6', 'S1', 'L3', 130, 5, 40, 3]], ['S1', 'L2', 100, 10]], 'B6'], ['generated mix 66', [[['B0', 'S1', 'L1', 130, 40, 20, 8], ['B1', 'S2', 'L3', 120, 5, 20, 9], ['B2', 'S1', 'L1', 160, 5, 40, 2]], ['S1', 'L4', 100, 30]], 'HOLD'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1']], [['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 39', [[['B0', 'S1', 'L2', 120, 30, 20, 3], ['B1', 'S1', 'L3', 145, 0, 20, 4], ['B2', None, None, 0, 0, 40, 6], ['B3', 'S1', 'L3', 131, 30, 60, 9], ['B4', 'S1', 'L1', 100, 5, 60, 7], ['B5', None, None, 0, 0, 40, 4], ['B6', 'S2', 'L2', 100, 30, 60, 6]], ['S1', 'L4', 130, 10]], 'B3'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 44', [[['B0', 'S1', 'L2', 145, 10, 60, 3], ['B1', 'S2', 'L1', 145, 0, 60, 6], ['B2', 'S1', 'L1', 131, 0, 40, 6], ['B3', 'S2', 'L1', 145, 40, 20, 8]], ['S1', 'L4', 150, 20]], 'B0'], ['generated mix 52', [[['B0', None, None, 0, 0, 20, 9], ['B1', 'S1', 'L3', 120, 10, 60, 1], ['B2', 'S1', 'L3', 120, 40, 20, 9], ['B3', 'S1', 'L2', 100, 40, 60, 4]], ['S1', 'L4', 100, 10]], 'B3']]]
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
lot code on other skuEEPassed
generated mix 3B2B1Failed
same lot preferredAAPassed
expiry 30 days laterAAPassed
residual label emptyRRPassed
room exactAAPassed
generated mix 7B2B2Passed
generated mix 15B1B1Passed

SHA-256 / 005ba8181ee8a186ffc90d9650705eab9c6d861b2127f803eb851ab6d6a299b7

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(bins, receipt):
    sku, lot, exp, q = receipt
    def room(b):
        return b[5] - b[4] >= q
    def empty(b):
        return b[1] is None or b[4] == 0
    same_lot = [b for b in bins if not empty(b) and b[1] == sku and b[2] == lot and room(b)]
    if same_lot:
        return min(same_lot, key=lambda b: (b[6], b[0]))[0]
    mixed = [b for b in bins if not empty(b) and b[1] == sku and b[2] != lot and abs(b[3] - exp) <= 30 and room(b)]
    if mixed:
        return min(mixed, key=lambda b: (abs(b[3] - exp), b[6], b[0]))[0]
    free = [b for b in bins if empty(b) and b[5] >= q]
    if free:
        return min(free, key=lambda b: (b[6], b[0]))[0]
    return 'HOLD'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 3', [[['B0', None, None, 0, 0, 60, 5], ['B1', None, None, 0, 0, 60, 4], ['B2', 'S1', 'L1', 145, 30, 60, 2], ['B3', 'S2', 'L2', 100, 40, 20, 1], ['B4', 'S2', 'L1', 131, 30, 40, 2], ['B5', 'S1', 'L1', 145, 30, 40, 7]], ['S1', 'L4', 100, 10]], 'B1'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 7', [[['B0', 'S1', 'L1', 130, 10, 40, 3], ['B1', 'S2', 'L3', 160, 30, 20, 2], ['B2', 'S1', 'L2', 145, 0, 20, 1]], ['S2', 'L4', 130, 20]], 'B2'], ['generated mix 15', [[['B0', 'S1', 'L2', 145, 40, 20, 2], ['B1', 'S2', 'L1', 145, 40, 60, 3], ['B2', 'S1', 'L1', 100, 30, 40, 3], ['B3', 'S1', 'L2', 145, 5, 60, 3], ['B4', 'S1', 'L1', 131, 0, 20, 6], ['B5', None, None, 0, 0, 40, 9]], ['S2', 'L4', 150, 20]], 'B1']], [['generated mix 50', [[['B0', 'S2', 'L3', 160, 0, 40, 6], ['B1', 'S1', 'L2', 130, 0, 40, 1], ['B2', 'S2', 'L1', 130, 10, 60, 1]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 31', [[['B0', 'S1', 'L1', 145, 5, 60, 4], ['B1', None, None, 0, 0, 60, 7], ['B2', 'S1', 'L3', 160, 10, 60, 7], ['B3', 'S1', 'L1', 160, 0, 60, 8]], ['S1', 'L2', 100, 10]], 'B1'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 9', [[['B0', 'S1', 'L2', 160, 40, 40, 1], ['B1', 'S1', 'L1', 131, 40, 40, 1], ['B2', 'S1', 'L3', 145, 0, 60, 1], ['B3', 'S1', 'L2', 100, 10, 20, 7], ['B4', 'S2', 'L1', 100, 30, 60, 5]], ['S1', 'L4', 150, 20]], 'B2'], ['generated mix 17', [[['B0', 'S2', 'L2', 120, 0, 20, 8], ['B1', 'S1', 'L2', 131, 5, 40, 3], ['B2', 'S2', 'L2', 100, 10, 20, 8], ['B3', 'S2', 'L2', 100, 10, 60, 6], ['B4', 'S1', 'L3', 100, 30, 40, 3], ['B5', 'S1', 'L1', 145, 10, 60, 5]], ['S1', 'L4', 150, 20]], 'B5']], [['generated mix 30', [[['B0', None, None, 0, 0, 40, 2], ['B1', 'S2', 'L2', 130, 10, 60, 9], ['B2', None, None, 0, 0, 60, 4], ['B3', None, None, 0, 0, 60, 7], ['B4', 'S1', 'L3', 145, 0, 40, 8], ['B5', 'S1', 'L1', 160, 40, 60, 5], ['B6', 'S1', 'L3', 130, 5, 40, 3]], ['S1', 'L2', 100, 10]], 'B6'], ['generated mix 66', [[['B0', 'S1', 'L1', 130, 40, 20, 8], ['B1', 'S2', 'L3', 120, 5, 20, 9], ['B2', 'S1', 'L1', 160, 5, 40, 2]], ['S1', 'L4', 100, 30]], 'HOLD'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 31 days', [[['A', 'S1', 'L2', 131, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'E'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['generated mix 20', [[['B0', None, None, 0, 0, 20, 8], ['B1', 'S1', 'L1', 131, 30, 40, 6], ['B2', None, None, 0, 0, 20, 6], ['B3', 'S1', 'L3', 160, 0, 40, 6], ['B4', 'S1', 'L3', 160, 10, 20, 4], ['B5', 'S1', 'L3', 131, 30, 40, 9], ['B6', None, None, 0, 0, 60, 5]], ['S1', 'L4', 130, 20]], 'B6'], ['generated mix 28', [[['B0', 'S1', 'L3', 131, 10, 60, 9], ['B1', 'S2', 'L2', 131, 0, 20, 2], ['B2', 'S2', 'L2', 160, 40, 40, 5]], ['S2', 'L1', 100, 10]], 'B1']], [['generated mix 24', [[['B0', 'S1', 'L1', 131, 10, 40, 3], ['B1', 'S1', 'L2', 131, 5, 40, 9], ['B2', None, None, 0, 0, 60, 9], ['B3', 'S2', 'L1', 120, 40, 40, 8], ['B4', None, None, 0, 0, 60, 8], ['B5', 'S1', 'L3', 160, 0, 40, 6]], ['S2', 'L1', 130, 30]], 'B5'], ['generated mix 5', [[['B0', 'S1', 'L2', 131, 5, 20, 3], ['B1', None, None, 0, 0, 20, 3], ['B2', None, None, 0, 0, 40, 3], ['B3', None, None, 0, 0, 40, 1], ['B4', None, None, 0, 0, 40, 8], ['B5', None, None, 0, 0, 20, 3], ['B6', 'S2', 'L3', 100, 10, 40, 7]], ['S2', 'L2', 150, 20]], 'B3'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['nothing', [[['X', 'S2', 'L1', 100, 5, 10, 1]], ['S1', 'L1', 100, 20]], 'HOLD'], ['room exact', [[['A', 'S1', 'L1', 100, 20, 40, 9], ['E', None, None, 0, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['generated mix 33', [[['B0', 'S1', 'L1', 130, 10, 40, 6], ['B1', 'S1', 'L3', 160, 40, 40, 3], ['B2', None, None, 0, 0, 20, 1], ['B3', 'S2', 'L2', 160, 5, 60, 8], ['B4', None, None, 0, 0, 40, 4], ['B5', 'S2', 'L2', 131, 5, 60, 8]], ['S1', 'L1', 130, 30]], 'B0'], ['generated mix 41', [[['B0', 'S1', 'L2', 160, 30, 60, 7], ['B1', 'S1', 'L2', 160, 40, 60, 3], ['B2', 'S1', 'L3', 100, 0, 60, 7], ['B3', 'S2', 'L2', 130, 10, 60, 9], ['B4', 'S1', 'L2', 160, 0, 20, 4], ['B5', 'S2', 'L2', 100, 30, 40, 4]], ['S2', 'L1', 130, 30]], 'B3']], [['generated mix 14', [[['B0', 'S2', 'L1', 130, 5, 60, 3], ['B1', 'S2', 'L1', 120, 0, 60, 8], ['B2', None, None, 0, 0, 60, 9]], ['S1', 'L1', 100, 30]], 'B1'], ['generated mix 39', [[['B0', 'S1', 'L2', 120, 30, 20, 3], ['B1', 'S1', 'L3', 145, 0, 20, 4], ['B2', None, None, 0, 0, 40, 6], ['B3', 'S1', 'L3', 131, 30, 60, 9], ['B4', 'S1', 'L1', 100, 5, 60, 7], ['B5', None, None, 0, 0, 40, 4], ['B6', 'S2', 'L2', 100, 30, 60, 6]], ['S1', 'L4', 130, 10]], 'B3'], ['lot code on other sku', [[['X', 'S2', 'L1', 100, 5, 60, 1], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'E'], ['same lot preferred', [[['A', 'S1', 'L1', 100, 10, 40, 9], ['B', 'S1', 'L2', 100, 0, 40, 1]], ['S1', 'L1', 100, 20]], 'A'], ['expiry 30 days later', [[['A', 'S1', 'L2', 130, 10, 60, 5], ['E', None, None, 0, 0, 60, 1]], ['S1', 'L1', 100, 20]], 'A'], ['residual label empty', [[['R', 'S9', 'L7', 50, 0, 60, 2], ['E', None, None, 0, 0, 60, 4]], ['S1', 'L1', 100, 20]], 'R'], ['generated mix 44', [[['B0', 'S1', 'L2', 145, 10, 60, 3], ['B1', 'S2', 'L1', 145, 0, 60, 6], ['B2', 'S1', 'L1', 131, 0, 40, 6], ['B3', 'S2', 'L1', 145, 40, 20, 8]], ['S1', 'L4', 150, 20]], 'B0'], ['generated mix 52', [[['B0', None, None, 0, 0, 20, 9], ['B1', 'S1', 'L3', 120, 10, 60, 1], ['B2', 'S1', 'L3', 120, 40, 20, 9], ['B3', 'S1', 'L2', 100, 40, 60, 4]], ['S1', 'L4', 100, 10]], 'B3']]]
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
lot code on other skuEEPassed
generated mix 3B1B1Passed
same lot preferredAAPassed
expiry 30 days laterAAPassed
residual label emptyRRPassed
room exactAAPassed
generated mix 7B2B2Passed
generated mix 15B1B1Passed

SHA-256 / be792fa9986f4aa5dd5bcf256e93df0dc832323e7b774de7e7f213a91ec3cf28

Verification & scope

Stipulated day-number expiries and a single receipt; no quarantine or multi-bin splits. 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:52:10.277805+00:00.

Case digest / 50cd1c861a70b0b7bcabf5dba7ca2ca76d1e76864249c23fa1a291bb15935647