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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 amount | ERR:amount-below-fee | ERR:amount-below-fee | Passed |
| control: zero bps | [0, 842] | [0, 842] | Passed |
| control: invalid amount | ERR:input | ERR:input | Passed |
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 amount | ERR:amount-below-fee | ERR:amount-below-fee | Passed |
| control: zero bps | [0, 842] | [0, 842] | Passed |
| control: invalid amount | ERR:input | ERR:input | Passed |
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 amount | ERR:amount-below-fee | ERR:amount-below-fee | Passed |
| control: zero bps | [0, 842] | [0, 842] | Passed |
| control: invalid amount | ERR:input | ERR:input | Passed |
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