FAILURE MAP
← Case archive

FA-62016 / Currency rounding and FX conversion / Open access

Every non-card tender receives cash rounding · case 01

TWINT and voucher payments are rounded to 0.05 as if paid in coins.

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

ROOT CAUSE

The tender test is tender != 'card' instead of tender == 'cash'.

VERIFIED REPAIR

Apply 5-rappen rounding only when the normalized tender is cash.

Unsuccessful approach: Whitelisting cash and voucher still rounds voucher payments.

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.strip().lower() != 'card':
        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-scope 1', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
  ('regression tender-scope 2', (['7.07'], 'voucher'), ['7.07', '0.000']),
  ('partial repair guard 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('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: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020'])],
 [('regression tender-scope 1', (['22.99', '2.02', '4.93', '0.185'], 'twint'), ['30.13', '0.005']),
  ('regression tender-scope 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('partial repair guard 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('partial repair guard 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('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-scope 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('regression tender-scope 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('partial repair guard 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('partial repair guard 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('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-scope 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('regression tender-scope 2', (['-3.385'], 'twint'), ['-3.39', '-0.005']),
  ('partial repair guard 1', (['5.31', '-1.325'], 'voucher'), ['3.99', '0.005']),
  ('partial repair guard 2', (['12.32', '6.28', '-0.89', '2.395'], 'voucher'), ['20.11', '0.005']),
  ('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-scope 1', (['22.96'], 'twint'), ['22.96', '0.000']),
  ('regression tender-scope 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('partial repair guard 1', (['25.48'], 'voucher'), ['25.48', '0.000']),
  ('partial repair guard 2', (['19.98', '1.54'], 'voucher'), ['21.52', '0.000']),
  ('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 fixtureActualExpectedOutcome
regression tender-scope 1['7.75', '-0.020']['7.77', '0.000']Failed
regression tender-scope 2['7.05', '-0.020']['7.07', '0.000']Failed
partial repair guard 2['24.00', '-0.015']['24.02', '0.005']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: refund to zero['0.00', '0.020']['0.00', '0.020']Passed

SHA-256 / 9a9d22e898e54e2393d2aa9b816b9943bacc92ffa316d0f9fe146a5074ff60fa

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.strip().lower() in ('cash', 'voucher'):
        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-scope 1', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
  ('regression tender-scope 2', (['7.07'], 'voucher'), ['7.07', '0.000']),
  ('partial repair guard 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('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: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020'])],
 [('regression tender-scope 1', (['22.99', '2.02', '4.93', '0.185'], 'twint'), ['30.13', '0.005']),
  ('regression tender-scope 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('partial repair guard 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('partial repair guard 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('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-scope 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('regression tender-scope 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('partial repair guard 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('partial repair guard 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('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-scope 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('regression tender-scope 2', (['-3.385'], 'twint'), ['-3.39', '-0.005']),
  ('partial repair guard 1', (['5.31', '-1.325'], 'voucher'), ['3.99', '0.005']),
  ('partial repair guard 2', (['12.32', '6.28', '-0.89', '2.395'], 'voucher'), ['20.11', '0.005']),
  ('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-scope 1', (['22.96'], 'twint'), ['22.96', '0.000']),
  ('regression tender-scope 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('partial repair guard 1', (['25.48'], 'voucher'), ['25.48', '0.000']),
  ('partial repair guard 2', (['19.98', '1.54'], 'voucher'), ['21.52', '0.000']),
  ('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 fixtureActualExpectedOutcome
regression tender-scope 1['7.77', '0.000']['7.77', '0.000']Passed
regression tender-scope 2['7.05', '-0.020']['7.07', '0.000']Failed
partial repair guard 2['24.00', '-0.015']['24.02', '0.005']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: refund to zero['0.00', '0.020']['0.00', '0.020']Passed

SHA-256 / 78db9aef73c662d0fdc2c4c6412f3a40614e66d719f89e3337fbd2bbafd739f1

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-scope 1', (['3.33', '4.44'], 'twint'), ['7.77', '0.000']),
  ('regression tender-scope 2', (['7.07'], 'voucher'), ['7.07', '0.000']),
  ('partial repair guard 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('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: refund to zero', (['-0.02'], 'Cash '), ['0.00', '0.020'])],
 [('regression tender-scope 1', (['22.99', '2.02', '4.93', '0.185'], 'twint'), ['30.13', '0.005']),
  ('regression tender-scope 2', (['2.215', '-2.59', '0.15', '24.24'], 'voucher'), ['24.02', '0.005']),
  ('partial repair guard 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('partial repair guard 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('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-scope 1', (['12.22', '18.81', '9.42', '4.81'], 'voucher'), ['45.26', '0.000']),
  ('regression tender-scope 2', (['24.12', '-3.20', '-4.67', '20.42'], 'voucher'), ['36.67', '0.000']),
  ('partial repair guard 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('partial repair guard 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('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-scope 1', (['8.11'], 'voucher'), ['8.11', '0.000']),
  ('regression tender-scope 2', (['-3.385'], 'twint'), ['-3.39', '-0.005']),
  ('partial repair guard 1', (['5.31', '-1.325'], 'voucher'), ['3.99', '0.005']),
  ('partial repair guard 2', (['12.32', '6.28', '-0.89', '2.395'], 'voucher'), ['20.11', '0.005']),
  ('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-scope 1', (['22.96'], 'twint'), ['22.96', '0.000']),
  ('regression tender-scope 2', (['15.28'], 'voucher'), ['15.28', '0.000']),
  ('partial repair guard 1', (['25.48'], 'voucher'), ['25.48', '0.000']),
  ('partial repair guard 2', (['19.98', '1.54'], 'voucher'), ['21.52', '0.000']),
  ('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 fixtureActualExpectedOutcome
regression tender-scope 1['7.77', '0.000']['7.77', '0.000']Passed
regression tender-scope 2['7.07', '0.000']['7.07', '0.000']Passed
partial repair guard 2['24.02', '0.005']['24.02', '0.005']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: refund to zero['0.00', '0.020']['0.00', '0.020']Passed

SHA-256 / 3c5dbfe28d129bbb18c8d678644d5f29ac98cb8d1373ef8fb90316f501c2d512

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

Case digest / d910601556b1af684b2aa228e44ea1d17415500d53c52c13c8296130c9c9ddaf