FA-62021 / Currency rounding and FX conversion / Open access
A cash tender label with different case or padding is not recognised · case 01
'CASH' and ' cash ' payments are charged the exact total.
ROOT CAUSE
The tender is compared verbatim to 'cash'.
VERIFIED REPAIR
Trim and lower-case the tender before comparing.
Unsuccessful approach: Lower-casing without trimming still misses padded labels from the POS terminal.
Case contract
solve(lines, tender): lines are CHF decimal strings with up to three decimals (weight- or volume-priced items; refund lines negative). The exact total is their sum. For tender 'cash' (compared after trimming and lower-casing) the total is rounded to the nearest 0.05 with ties away from zero; every other tender ('card', 'twint', 'voucher', ...) pays the total rounded half-up to 0.01. Return [payable as a 2-decimal string, adjustment as a 3-decimal string] where adjustment = payable - exact; zeros are unsigned.
Why this case matters
Currency amounts must be rounded at the right stage and in the right unit, or ledgers, quotes and settlements drift by minor units.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_DOWN, ROUND_UP, ROUND_HALF_DOWN
N = 1
observations = []
def solve(lines, tender):
def cash(v):
return (v / Decimal('0.05')).quantize(Decimal(1), rounding=ROUND_HALF_UP) * Decimal('0.05')
def z(d, e):
d = d.quantize(Decimal(1).scaleb(-e), rounding=ROUND_HALF_UP)
return format(abs(d) if d == 0 else d, 'f')
exact = sum((Decimal(l) for l in lines), Decimal('0.000'))
if tender == 'cash':
r = cash(exact)
else:
r = exact.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return [z(r, 2), z(r - exact, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression tender-normalization 1', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('regression tender-normalization 2', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000'])],
[('regression tender-normalization 1', (['12.71', '-2.68'], 'Cash'), ['10.05', '0.020']),
('regression tender-normalization 2', (['19.47', '26.34'], 'CASH'), ['45.80', '-0.010']),
('partial repair guard 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('partial repair guard 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025'])],
[('regression tender-normalization 1', (['17.98', '8.07', '0.335', '23.38'], 'Cash'), ['49.75', '-0.015']),
('regression tender-normalization 2', (['-1.435'], 'Cash'), ['-1.45', '-0.015']),
('partial repair guard 1', (['4.92'], ' cash '), ['4.90', '-0.020']),
('partial repair guard 2', (['12.08', '3.43', '4.44', '-4.37'], ' cash '), ['15.60', '0.020']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025']),
('control: negative tie', (['-2.075'], 'cash'), ['-2.10', '-0.025']),
('control: voucher exact', (['7.07'], 'voucher'), ['7.07', '0.000']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020'])],
[('regression tender-normalization 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('regression tender-normalization 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('partial repair guard 1', (['22.60', '28.56', '15.17'], ' cash '), ['66.35', '0.020']),
('partial repair guard 2', (['23.36'], ' cash '), ['23.35', '-0.010']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020'])],
[('regression tender-normalization 1', (['2.49', '-3.655', '28.09', '6.96'], 'CASH'), ['33.90', '0.015']),
('regression tender-normalization 2', (['2.975'], 'CASH'), ['3.00', '0.025']),
('partial repair guard 1', (['4.13', '13.22', '14.73', '-1.77'], ' cash '), ['30.30', '-0.010']),
('partial repair guard 2', (['26.28', '1.09', '10.19', '-3.755'], ' cash '), ['33.80', '-0.005']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000'])]]
for label, args, expected in cases[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 |
|---|---|---|---|
| regression tender-normalization 1 | ['10.27', '0.000'] | ['10.25', '-0.020'] | Failed |
| regression tender-normalization 2 | ['-0.02', '0.000'] | ['0.00', '0.020'] | Failed |
| control: round down | ['2.00', '-0.020'] | ['2.00', '-0.020'] | Passed |
| control: round to franc | ['1.00', '-0.020'] | ['1.00', '-0.020'] | Passed |
| control: refund | ['-2.00', '0.020'] | ['-2.00', '0.020'] | Passed |
| control: card exact | ['1.02', '0.000'] | ['1.02', '0.000'] | Passed |
SHA-256 / 97ae1fbf6b404f3280663499d6f10d70a056ec2466c70cdaa76da92c047d5de2
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_DOWN, ROUND_UP, ROUND_HALF_DOWN
N = 1
observations = []
def solve(lines, tender):
def cash(v):
return (v / Decimal('0.05')).quantize(Decimal(1), rounding=ROUND_HALF_UP) * Decimal('0.05')
def z(d, e):
d = d.quantize(Decimal(1).scaleb(-e), rounding=ROUND_HALF_UP)
return format(abs(d) if d == 0 else d, 'f')
exact = sum((Decimal(l) for l in lines), Decimal('0.000'))
if tender.lower() == 'cash':
r = cash(exact)
else:
r = exact.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return [z(r, 2), z(r - exact, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression tender-normalization 1', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('regression tender-normalization 2', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000'])],
[('regression tender-normalization 1', (['12.71', '-2.68'], 'Cash'), ['10.05', '0.020']),
('regression tender-normalization 2', (['19.47', '26.34'], 'CASH'), ['45.80', '-0.010']),
('partial repair guard 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('partial repair guard 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025'])],
[('regression tender-normalization 1', (['17.98', '8.07', '0.335', '23.38'], 'Cash'), ['49.75', '-0.015']),
('regression tender-normalization 2', (['-1.435'], 'Cash'), ['-1.45', '-0.015']),
('partial repair guard 1', (['4.92'], ' cash '), ['4.90', '-0.020']),
('partial repair guard 2', (['12.08', '3.43', '4.44', '-4.37'], ' cash '), ['15.60', '0.020']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025']),
('control: negative tie', (['-2.075'], 'cash'), ['-2.10', '-0.025']),
('control: voucher exact', (['7.07'], 'voucher'), ['7.07', '0.000']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020'])],
[('regression tender-normalization 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('regression tender-normalization 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('partial repair guard 1', (['22.60', '28.56', '15.17'], ' cash '), ['66.35', '0.020']),
('partial repair guard 2', (['23.36'], ' cash '), ['23.35', '-0.010']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020'])],
[('regression tender-normalization 1', (['2.49', '-3.655', '28.09', '6.96'], 'CASH'), ['33.90', '0.015']),
('regression tender-normalization 2', (['2.975'], 'CASH'), ['3.00', '0.025']),
('partial repair guard 1', (['4.13', '13.22', '14.73', '-1.77'], ' cash '), ['30.30', '-0.010']),
('partial repair guard 2', (['26.28', '1.09', '10.19', '-3.755'], ' cash '), ['33.80', '-0.005']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000'])]]
for label, args, expected in cases[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 |
|---|---|---|---|
| regression tender-normalization 1 | ['10.27', '0.000'] | ['10.25', '-0.020'] | Failed |
| regression tender-normalization 2 | ['-0.02', '0.000'] | ['0.00', '0.020'] | Failed |
| control: round down | ['2.00', '-0.020'] | ['2.00', '-0.020'] | Passed |
| control: round to franc | ['1.00', '-0.020'] | ['1.00', '-0.020'] | Passed |
| control: refund | ['-2.00', '0.020'] | ['-2.00', '0.020'] | Passed |
| control: card exact | ['1.02', '0.000'] | ['1.02', '0.000'] | Passed |
SHA-256 / 7819f38200f3c3da0fb0188d48bebc2130f8fbf90bba46f59039c77c6f936693
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP, ROUND_FLOOR, ROUND_CEILING, ROUND_DOWN, ROUND_UP, ROUND_HALF_DOWN
N = 1
observations = []
def solve(lines, tender):
def cash(v):
return (v / Decimal('0.05')).quantize(Decimal(1), rounding=ROUND_HALF_UP) * Decimal('0.05')
def z(d, e):
d = d.quantize(Decimal(1).scaleb(-e), rounding=ROUND_HALF_UP)
return format(abs(d) if d == 0 else d, 'f')
exact = sum((Decimal(l) for l in lines), Decimal('0.000'))
if tender.strip().lower() == 'cash':
r = cash(exact)
else:
r = exact.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return [z(r, 2), z(r - exact, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression tender-normalization 1', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('regression tender-normalization 2', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000'])],
[('regression tender-normalization 1', (['12.71', '-2.68'], 'Cash'), ['10.05', '0.020']),
('regression tender-normalization 2', (['19.47', '26.34'], 'CASH'), ['45.80', '-0.010']),
('partial repair guard 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('partial repair guard 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025'])],
[('regression tender-normalization 1', (['17.98', '8.07', '0.335', '23.38'], 'Cash'), ['49.75', '-0.015']),
('regression tender-normalization 2', (['-1.435'], 'Cash'), ['-1.45', '-0.015']),
('partial repair guard 1', (['4.92'], ' cash '), ['4.90', '-0.020']),
('partial repair guard 2', (['12.08', '3.43', '4.44', '-4.37'], ' cash '), ['15.60', '0.020']),
('control: positive tie', (['3.00', '0.025'], 'cash'), ['3.05', '0.025']),
('control: negative tie', (['-2.075'], 'cash'), ['-2.10', '-0.025']),
('control: voucher exact', (['7.07'], 'voucher'), ['7.07', '0.000']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020'])],
[('regression tender-normalization 1', (['3.93'], ' cash '), ['3.95', '0.020']),
('regression tender-normalization 2', (['2.955'], ' cash '), ['2.95', '-0.005']),
('partial repair guard 1', (['22.60', '28.56', '15.17'], ' cash '), ['66.35', '0.020']),
('partial repair guard 2', (['23.36'], ' cash '), ['23.35', '-0.010']),
('control: mixed basket', (['12.40', '-3.12', '0.99'], ' CASH'), ['10.25', '-0.020']),
('control: round down', (['1.02', '1.00'], 'cash'), ['2.00', '-0.020']),
('control: round to franc', (['0.99', '0.03'], 'cash'), ['1.00', '-0.020']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020'])],
[('regression tender-normalization 1', (['2.49', '-3.655', '28.09', '6.96'], 'CASH'), ['33.90', '0.015']),
('regression tender-normalization 2', (['2.975'], 'CASH'), ['3.00', '0.025']),
('partial repair guard 1', (['4.13', '13.22', '14.73', '-1.77'], ' cash '), ['30.30', '-0.010']),
('partial repair guard 2', (['26.28', '1.09', '10.19', '-3.755'], ' cash '), ['33.80', '-0.005']),
('control: refund', (['-1.03', '-0.99'], 'cash'), ['-2.00', '0.020']),
('control: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020']),
('control: card exact', (['1.02'], 'card'), ['1.02', '0.000']),
('control: twint exact', (['3.33', '4.44'], 'twint'), ['7.77', '0.000'])]]
for label, args, expected in cases[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 |
|---|---|---|---|
| regression tender-normalization 1 | ['10.25', '-0.020'] | ['10.25', '-0.020'] | Passed |
| regression tender-normalization 2 | ['0.00', '0.020'] | ['0.00', '0.020'] | Passed |
| control: round down | ['2.00', '-0.020'] | ['2.00', '-0.020'] | Passed |
| control: round to franc | ['1.00', '-0.020'] | ['1.00', '-0.020'] | Passed |
| control: refund | ['-2.00', '0.020'] | ['-2.00', '0.020'] | Passed |
| control: card exact | ['1.02', '0.000'] | ['1.02', '0.000'] | Passed |
SHA-256 / 13bcdced60ce69df715a101250dbff40df43a2443f9cf4382c2c78328af57939
Verification & scope
A deterministic, bounded teaching model with a stipulated toy contract; it makes no claim of conformance to any real regulation, standard, or institution's rules. 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:47:00.710654+00:00.
Case digest / eed6136a2c8ec2ae771bda97f5d333f761d7a6e6cd63ea4b54ec0ca227022085