FAILURE MAP
← Case archive

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

The fee is converted and rounded separately from the gross amount · case 01

The payout differs by a minor unit from converting the net amount once.

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

ROOT CAUSE

Gross amount and fee are each converted and rounded, then subtracted.

VERIFIED REPAIR

Deduct the fee in source units first, then convert and round the net once.

Unsuccessful approach: Subtracting the unconverted source fee from the converted gross mixes currencies.

Case contract

solve(amount, rate, src_exp, dst_exp, fee_bps, min_fee): amount and min_fee are source minor units, fee_bps a non-negative integer. fee = max(min_fee, ceil(amount*fee_bps/10000)) in source minor units, deducted before conversion. A fee greater than or equal to the amount returns 'ERR:amount-below-fee'; a non-positive amount or negative bps returns 'ERR:input'. The net amount is converted exactly (net * rate * 10**(dst_exp - src_exp)) and rounded half-even. Return [fee, converted].

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(amount, rate, src_exp, dst_exp, fee_bps, min_fee):
    if fee_bps < 0 or amount <= 0: return 'ERR:input'
    conv = lambda m: Decimal(m) * Decimal(rate) * Decimal(10) ** (dst_exp - src_exp)
    rq = lambda d: int(d.quantize(Decimal(1), rounding=ROUND_HALF_EVEN))
    pct = -(-amount * fee_bps // 10000)
    fee = max(min_fee, pct)
    if fee >= amount: return 'ERR:amount-below-fee'
    return [fee, rq(conv(amount)) - rq(conv(fee))]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression fee-conversion-order 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('regression fee-conversion-order 2', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 1', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('partial repair guard 2', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input')],
 [('regression fee-conversion-order 1', (234309, '91.3439', 3, 3, 333, 100), [7803, 20689941]),
  ('regression fee-conversion-order 2', (440114, '2728.61', 0, 0, 333, 1), [14656, 1160908953]),
  ('partial repair guard 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('partial repair guard 2', (197637, '16116.34', 0, 2, 333, 100), [6582, 307910733870]),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')],
 [('regression fee-conversion-order 1', (41069, '21.2905', 3, 3, 333, 1000), [1368, 845254]),
  ('regression fee-conversion-order 2', (449567, '33.1467', 2, 0, 150, 50), [6744, 146781]),
  ('partial repair guard 1', (22226, '179.9447', 0, 2, 150, 100), [334, 393934937]),
  ('partial repair guard 2', (50411, '12342.04', 3, 3, 333, 1), [1679, 601452293]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee'),
  ('control: yen payout', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990])],
 [('regression fee-conversion-order 1', (55979, '2.35354', 3, 0, 0, 1000), [1000, 129]),
  ('regression fee-conversion-order 2', (409473, '130.7752', 3, 3, 333, 1), [13636, 51765663]),
  ('partial repair guard 1', (268603, '16.66106', 2, 0, 150, 50), [4030, 44081]),
  ('partial repair guard 2', (269350, '9.84392', 0, 3, 333, 0), [8970, 2563159890]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842])],
 [('regression fee-conversion-order 1', (34239, '182.6706', 2, 0, 25, 1), [86, 62387]),
  ('regression fee-conversion-order 2', (180714, '131.9673', 3, 0, 150, 0), [2711, 23491]),
  ('partial repair guard 1', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 2', (84375, '129.7381', 0, 0, 99, 250), [836, 10838191]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: large percentage fee', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')]]
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 fee-conversion-order 1[3087, 110740][3087, 110739]Failed
regression fee-conversion-order 2[1000, 38359304][1000, 38359303]Failed
partial repair guard 1[150, 14910][150, 14910]Passed
partial repair guard 2[100, 61113][100, 61113]Passed
control: fee rounds up[11, 990][11, 990]Passed
control: fee equals amountERR:amount-below-feeERR:amount-below-feePassed
control: zero bps[0, 842][0, 842]Passed
control: invalid amountERR:inputERR:inputPassed

SHA-256 / cbea911503b7615d386c8d4bfb0e7fcaa83dd8a682cab7f8313374743d04d89a

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(amount, rate, src_exp, dst_exp, fee_bps, min_fee):
    if fee_bps < 0 or amount <= 0: return 'ERR:input'
    conv = lambda m: Decimal(m) * Decimal(rate) * Decimal(10) ** (dst_exp - src_exp)
    rq = lambda d: int(d.quantize(Decimal(1), rounding=ROUND_HALF_EVEN))
    pct = -(-amount * fee_bps // 10000)
    fee = max(min_fee, pct)
    if fee >= amount: return 'ERR:amount-below-fee'
    return [fee, rq(conv(amount)) - fee]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression fee-conversion-order 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('regression fee-conversion-order 2', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 1', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('partial repair guard 2', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input')],
 [('regression fee-conversion-order 1', (234309, '91.3439', 3, 3, 333, 100), [7803, 20689941]),
  ('regression fee-conversion-order 2', (440114, '2728.61', 0, 0, 333, 1), [14656, 1160908953]),
  ('partial repair guard 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('partial repair guard 2', (197637, '16116.34', 0, 2, 333, 100), [6582, 307910733870]),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')],
 [('regression fee-conversion-order 1', (41069, '21.2905', 3, 3, 333, 1000), [1368, 845254]),
  ('regression fee-conversion-order 2', (449567, '33.1467', 2, 0, 150, 50), [6744, 146781]),
  ('partial repair guard 1', (22226, '179.9447', 0, 2, 150, 100), [334, 393934937]),
  ('partial repair guard 2', (50411, '12342.04', 3, 3, 333, 1), [1679, 601452293]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee'),
  ('control: yen payout', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990])],
 [('regression fee-conversion-order 1', (55979, '2.35354', 3, 0, 0, 1000), [1000, 129]),
  ('regression fee-conversion-order 2', (409473, '130.7752', 3, 3, 333, 1), [13636, 51765663]),
  ('partial repair guard 1', (268603, '16.66106', 2, 0, 150, 50), [4030, 44081]),
  ('partial repair guard 2', (269350, '9.84392', 0, 3, 333, 0), [8970, 2563159890]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842])],
 [('regression fee-conversion-order 1', (34239, '182.6706', 2, 0, 25, 1), [86, 62387]),
  ('regression fee-conversion-order 2', (180714, '131.9673', 3, 0, 150, 0), [2711, 23491]),
  ('partial repair guard 1', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 2', (84375, '129.7381', 0, 0, 99, 250), [836, 10838191]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: large percentage fee', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')]]
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 fee-conversion-order 1[3087, 110493][3087, 110739]Failed
regression fee-conversion-order 2[1000, 38480521][1000, 38359303]Failed
partial repair guard 1[150, 14987][150, 14910]Failed
partial repair guard 2[100, 61320][100, 61113]Failed
control: fee rounds up[11, 990][11, 990]Passed
control: fee equals amountERR:amount-below-feeERR:amount-below-feePassed
control: zero bps[0, 842][0, 842]Passed
control: invalid amountERR:inputERR:inputPassed

SHA-256 / dedd75083014e9ee46a782476316efb42e719e3ca73b7036f7df511d39b82d58

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(amount, rate, src_exp, dst_exp, fee_bps, min_fee):
    if fee_bps < 0 or amount <= 0: return 'ERR:input'
    conv = lambda m: Decimal(m) * Decimal(rate) * Decimal(10) ** (dst_exp - src_exp)
    rq = lambda d: int(d.quantize(Decimal(1), rounding=ROUND_HALF_EVEN))
    pct = -(-amount * fee_bps // 10000)
    fee = max(min_fee, pct)
    if fee >= amount: return 'ERR:amount-below-fee'
    return [fee, rq(conv(amount - fee))]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression fee-conversion-order 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('regression fee-conversion-order 2', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 1', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('partial repair guard 2', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input')],
 [('regression fee-conversion-order 1', (234309, '91.3439', 3, 3, 333, 100), [7803, 20689941]),
  ('regression fee-conversion-order 2', (440114, '2728.61', 0, 0, 333, 1), [14656, 1160908953]),
  ('partial repair guard 1', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('partial repair guard 2', (197637, '16116.34', 0, 2, 333, 100), [6582, 307910733870]),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')],
 [('regression fee-conversion-order 1', (41069, '21.2905', 3, 3, 333, 1000), [1368, 845254]),
  ('regression fee-conversion-order 2', (449567, '33.1467', 2, 0, 150, 50), [6744, 146781]),
  ('partial repair guard 1', (22226, '179.9447', 0, 2, 150, 100), [334, 393934937]),
  ('partial repair guard 2', (50411, '12342.04', 3, 3, 333, 1), [1679, 601452293]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee'),
  ('control: yen payout', (10000, '151.37', 2, 0, 150, 50), [150, 14910]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990])],
 [('regression fee-conversion-order 1', (55979, '2.35354', 3, 0, 0, 1000), [1000, 129]),
  ('regression fee-conversion-order 2', (409473, '130.7752', 3, 3, 333, 1), [13636, 51765663]),
  ('partial repair guard 1', (268603, '16.66106', 2, 0, 150, 50), [4030, 44081]),
  ('partial repair guard 2', (269350, '9.84392', 0, 3, 333, 0), [8970, 2563159890]),
  ('control: fee rounds up', (1001, '1', 2, 2, 100, 0), [11, 990]),
  ('control: fee equals amount', (500, '0.9', 2, 2, 10, 500), 'ERR:amount-below-fee'),
  ('control: minimum fee applies', (20000, '0.3071', 2, 3, 25, 100), [100, 61113]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842])],
 [('regression fee-conversion-order 1', (34239, '182.6706', 2, 0, 25, 1), [86, 62387]),
  ('regression fee-conversion-order 2', (180714, '131.9673', 3, 0, 150, 0), [2711, 23491]),
  ('partial repair guard 1', (314862, '122.2171', 2, 2, 5, 1000), [1000, 38359303]),
  ('partial repair guard 2', (84375, '129.7381', 0, 0, 99, 250), [836, 10838191]),
  ('control: zero bps', (777, '1.0833', 2, 2, 0, 0), [0, 842]),
  ('control: large percentage fee', (123456, '0.92', 2, 2, 250, 199), [3087, 110739]),
  ('control: invalid amount', (0, '1.1', 2, 2, 10, 0), 'ERR:input'),
  ('control: minimum exceeds amount', (99, '1.2', 2, 2, 5, 150), 'ERR:amount-below-fee')]]
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 fee-conversion-order 1[3087, 110739][3087, 110739]Passed
regression fee-conversion-order 2[1000, 38359303][1000, 38359303]Passed
partial repair guard 1[150, 14910][150, 14910]Passed
partial repair guard 2[100, 61113][100, 61113]Passed
control: fee rounds up[11, 990][11, 990]Passed
control: fee equals amountERR:amount-below-feeERR:amount-below-feePassed
control: zero bps[0, 842][0, 842]Passed
control: invalid amountERR:inputERR:inputPassed

SHA-256 / 096b24caf74ef8bfde2b93c7eaf926e094e352544052289679187f5cd852a646

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

Case digest / 2a18e43ddd5a93229259047a1abce3a4a5d2c0566b898cc53002c2931d52773b