broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = 365 if conv != '30/360' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: actual basis', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['regression: actual basis, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 12, 28], 'first_due': [2025, 2, 15]}, {'days': 49, 'basis': 365, 'interest': 179}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 15], 'first_due': [2023, 1, 30]}, {'days': 15, 'basis': 360, 'interest': 250}], ['control 3', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 1], 'first_due': [2024, 9, 1]}, {'days': 30, 'basis': 360, 'interest': 152}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 8, 15], 'first_due': [2024, 9, 15]}, {'days': 30, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['regression: actual basis, partial-repair probe', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 3', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 15]}, {'days': 346, 'basis': 360, 'interest': 11533}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['regression: actual basis, partial-repair probe', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2025, 2, 15]}, {'days': 352, 'basis': 365, 'interest': 11573}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 3', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 29], 'first_due': [2024, 9, 1]}, {'days': 32, 'basis': 360, 'interest': 533}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 2, 28], 'first_due': [2024, 1, 1]}, {'days': 303, 'basis': 360, 'interest': 5050}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['regression: actual basis, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 11, 1], 'first_due': [2025, 11, 1]}, {'days': 365, 'basis': 365, 'interest': 4380}]], [['regression: actual basis', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['regression: actual basis, partial-repair probe', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 28], 'first_due': [2025, 6, 30]}, {'days': 332, 'basis': 360, 'interest': 569}], ['control 2', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 6, 28], 'first_due': [2025, 5, 15]}, {'days': 321, 'basis': 365, 'interest': 3210}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 6, 30], 'first_due': [2025, 6, 1]}, {'days': 336, 'basis': 365, 'interest': 110466}], ['control 5', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 9, 28], 'first_due': [2025, 9, 30]}, {'days': 367, 'basis': 365, 'interest': 3670}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = (366 if y1 % 4 == 0 else 365) if conv == 'act/365' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: actual basis', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['regression: actual basis, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 12, 28], 'first_due': [2025, 2, 15]}, {'days': 49, 'basis': 365, 'interest': 179}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 15], 'first_due': [2023, 1, 30]}, {'days': 15, 'basis': 360, 'interest': 250}], ['control 3', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 1], 'first_due': [2024, 9, 1]}, {'days': 30, 'basis': 360, 'interest': 152}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 8, 15], 'first_due': [2024, 9, 15]}, {'days': 30, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['regression: actual basis, partial-repair probe', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 3', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 15]}, {'days': 346, 'basis': 360, 'interest': 11533}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['regression: actual basis, partial-repair probe', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2025, 2, 15]}, {'days': 352, 'basis': 365, 'interest': 11573}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 3', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 29], 'first_due': [2024, 9, 1]}, {'days': 32, 'basis': 360, 'interest': 533}], ['control 5', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 2, 28], 'first_due': [2024, 1, 1]}, {'days': 303, 'basis': 360, 'interest': 5050}]], [['regression: actual basis', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['regression: actual basis, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 11, 1], 'first_due': [2025, 11, 1]}, {'days': 365, 'basis': 365, 'interest': 4380}]], [['regression: actual basis', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['regression: actual basis, partial-repair probe', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 28], 'first_due': [2025, 6, 30]}, {'days': 332, 'basis': 360, 'interest': 569}], ['control 2', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 6, 28], 'first_due': [2025, 5, 15]}, {'days': 321, 'basis': 365, 'interest': 3210}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 6, 30], 'first_due': [2025, 6, 1]}, {'days': 336, 'basis': 365, 'interest': 110466}], ['control 5', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 9, 28], 'first_due': [2025, 9, 30]}, {'days': 367, 'basis': 365, 'interest': 3670}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

prompt:
Actual/360 loans accrue interest on a 365-day year.

x = {'principal', 'rate_bp', 'convention': '30/360'|'act/365'|'act/360', 'start': [y,m,d], 'first_due': [y,m,d]}. 30/360: a start day of 31 becomes 30; then a due day of 31 becomes 30 when the (adjusted) start day is 30; days = 360*dy + 30*dm + dd. Actual conventions count calendar days, basis 365 or 360 (365 even in leap years). If days <= 0 return {'error': 'due_not_after_start'}. Interest = round_half_up(P*bp*days/(10000*basis)). Return {'days', 'basis', 'interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days + 1
        basis = 365 if conv == 'act/365' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 15], 'first_due': [2023, 1, 30]}, {'days': 15, 'basis': 360, 'interest': 250}], ['control 3', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 1], 'first_due': [2024, 9, 1]}, {'days': 30, 'basis': 360, 'interest': 152}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 8, 15], 'first_due': [2024, 9, 15]}, {'days': 30, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['control 6', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 1], 'first_due': [2025, 7, 1]}, {'days': 360, 'basis': 360, 'interest': 1825}]], [['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 15]}, {'days': 346, 'basis': 360, 'interest': 11533}], ['control 5', {'principal': 1000000, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 4, 15], 'first_due': [2025, 4, 30]}, {'days': 375, 'basis': 360, 'interest': 52083}], ['control 6', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}]], [['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 29], 'first_due': [2024, 9, 1]}, {'days': 32, 'basis': 360, 'interest': 533}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 2, 28], 'first_due': [2024, 1, 1]}, {'days': 303, 'basis': 360, 'interest': 5050}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 12, 30], 'first_due': [2024, 11, 30]}, {'days': 330, 'basis': 360, 'interest': 110000}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 29], 'first_due': [2023, 4, 15]}, {'error': 'due_not_after_start'}]], [['regression: actual day inclusivity', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 1]}, {'days': 332, 'basis': 360, 'interest': 1229}], ['control 3', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 31], 'first_due': [2024, 9, 1]}, {'days': 1, 'basis': 360, 'interest': 2}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 8, 31], 'first_due': [2023, 10, 28]}, {'days': 58, 'basis': 360, 'interest': 9667}], ['control 5', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 30, 'basis': 360, 'interest': 38}], ['control 6', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 11, 1], 'first_due': [2024, 11, 15]}, {'days': 14, 'basis': 360, 'interest': 85}]], [['regression: actual day inclusivity', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 28], 'first_due': [2025, 6, 30]}, {'days': 332, 'basis': 360, 'interest': 569}], ['control 2', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 100000, 'rate_bp': 365, 'convention': '30/360', 'start': [2023, 8, 1], 'first_due': [2023, 10, 15]}, {'days': 74, 'basis': 360, 'interest': 750}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 3, 31], 'first_due': [2023, 3, 15]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 1000000, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 5, 31], 'first_due': [2023, 6, 1]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 6', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 9, 29], 'first_due': [2024, 10, 1]}, {'days': 2, 'basis': 360, 'interest': 7}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

meaning: Fixture-count band, not measured model difficulty

deps: stdlib

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days - 1
        basis = 365 if conv == 'act/365' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 1', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 1, 15], 'first_due': [2023, 1, 30]}, {'days': 15, 'basis': 360, 'interest': 250}], ['control 3', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 1], 'first_due': [2024, 9, 1]}, {'days': 30, 'basis': 360, 'interest': 152}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 8, 15], 'first_due': [2024, 9, 15]}, {'days': 30, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['control 6', {'principal': 36500, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 1], 'first_due': [2025, 7, 1]}, {'days': 360, 'basis': 360, 'interest': 1825}]], [['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 15]}, {'days': 346, 'basis': 360, 'interest': 11533}], ['control 5', {'principal': 1000000, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 4, 15], 'first_due': [2025, 4, 30]}, {'days': 375, 'basis': 360, 'interest': 52083}], ['control 6', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}]], [['regression: actual day inclusivity', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 7, 29], 'first_due': [2024, 9, 1]}, {'days': 32, 'basis': 360, 'interest': 533}], ['control 4', {'principal': 100000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 2, 28], 'first_due': [2024, 1, 1]}, {'days': 303, 'basis': 360, 'interest': 5050}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 12, 30], 'first_due': [2024, 11, 30]}, {'days': 330, 'basis': 360, 'interest': 110000}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 29], 'first_due': [2023, 4, 15]}, {'error': 'due_not_after_start'}]], [['regression: actual day inclusivity', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 10, 29], 'first_due': [2025, 10, 1]}, {'days': 332, 'basis': 360, 'interest': 1229}], ['control 3', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 8, 31], 'first_due': [2024, 9, 1]}, {'days': 1, 'basis': 360, 'interest': 2}], ['control 4', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 8, 31], 'first_due': [2023, 10, 28]}, {'days': 58, 'basis': 360, 'interest': 9667}], ['control 5', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 30, 'basis': 360, 'interest': 38}], ['control 6', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 11, 1], 'first_due': [2024, 11, 15]}, {'days': 14, 'basis': 360, 'interest': 85}]], [['regression: actual day inclusivity', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 7, 28], 'first_due': [2025, 6, 30]}, {'days': 332, 'basis': 360, 'interest': 569}], ['control 2', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 100000, 'rate_bp': 365, 'convention': '30/360', 'start': [2023, 8, 1], 'first_due': [2023, 10, 15]}, {'days': 74, 'basis': 360, 'interest': 750}], ['control 4', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 3, 31], 'first_due': [2023, 3, 15]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 1000000, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 5, 31], 'first_due': [2023, 6, 1]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 6', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 9, 29], 'first_due': [2024, 10, 1]}, {'days': 2, 'basis': 360, 'interest': 7}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

prompt:
Actual-day interest charges one extra day.

x = {'principal', 'rate_bp', 'convention': '30/360'|'act/365'|'act/360', 'start': [y,m,d], 'first_due': [y,m,d]}. 30/360: a start day of 31 becomes 30; then a due day of 31 becomes 30 when the (adjusted) start day is 30; days = 360*dy + 30*dm + dd. Actual conventions count calendar days, basis 365 or 360 (365 even in leap years). If days <= 0 return {'error': 'due_not_after_start'}. Interest = round_half_up(P*bp*days/(10000*basis)). Return {'days', 'basis', 'interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = 365 if conv == 'act/365' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'], 10000 * basis) * days
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 1', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 1]}, {'days': 1, 'basis': 360, 'interest': 10}], ['control 2', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['control 3', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 100000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 12, 31], 'first_due': [2024, 12, 31]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 9, 30], 'first_due': [2024, 9, 30]}, {'error': 'due_not_after_start'}], ['control 6', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 30]}, {'days': 30, 'basis': 365, 'interest': 4110}]], [['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 10, 29], 'first_due': [2025, 9, 30]}, {'days': 336, 'basis': 365, 'interest': 4032}], ['control 3', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 5, 15], 'first_due': [2023, 6, 30]}, {'days': 46, 'basis': 365, 'interest': 276}], ['control 4', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 10, 31], 'first_due': [2023, 10, 30]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2024, 11, 30], 'first_due': [2024, 12, 15]}, {'days': 15, 'basis': 365, 'interest': 90}], ['control 6', {'principal': 100000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 11, 30], 'first_due': [2023, 11, 30]}, {'error': 'due_not_after_start'}]], [['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2023, 10, 30], 'first_due': [2023, 10, 31]}, {'days': 1, 'basis': 360, 'interest': 167}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 3, 29], 'first_due': [2025, 3, 31]}, {'days': 367, 'basis': 365, 'interest': 4404}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 29], 'first_due': [2023, 4, 15]}, {'error': 'due_not_after_start'}], ['control 6', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 6, 29], 'first_due': [2023, 7, 1]}, {'days': 2, 'basis': 360, 'interest': 24}]], [['regression: interest rounding stage', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['regression: interest rounding stage, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 1', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 2', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 11, 1], 'first_due': [2025, 11, 1]}, {'days': 365, 'basis': 365, 'interest': 4380}]], [['regression: interest rounding stage', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['regression: interest rounding stage, partial-repair probe', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 1', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}], ['control 2', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 6, 28], 'first_due': [2025, 5, 15]}, {'days': 321, 'basis': 365, 'interest': 3210}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 9, 28], 'first_due': [2025, 9, 30]}, {'days': 367, 'basis': 365, 'interest': 3670}], ['control 5', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = 365 if conv == 'act/365' else 360
    if days <= 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000) // basis
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 1', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 30], 'first_due': [2024, 12, 1]}, {'days': 1, 'basis': 360, 'interest': 10}], ['control 2', {'principal': 36500, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 3, 29], 'first_due': [2023, 3, 31]}, {'days': 2, 'basis': 360, 'interest': 24}], ['control 3', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 100000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 12, 31], 'first_due': [2024, 12, 31]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 1000000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 9, 30], 'first_due': [2024, 9, 30]}, {'error': 'due_not_after_start'}], ['control 6', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 30]}, {'days': 30, 'basis': 365, 'interest': 4110}]], [['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 10, 29], 'first_due': [2025, 9, 30]}, {'days': 336, 'basis': 365, 'interest': 4032}], ['control 3', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 5, 15], 'first_due': [2023, 6, 30]}, {'days': 46, 'basis': 365, 'interest': 276}], ['control 4', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 10, 31], 'first_due': [2023, 10, 30]}, {'error': 'due_not_after_start'}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2024, 11, 30], 'first_due': [2024, 12, 15]}, {'days': 15, 'basis': 365, 'interest': 90}], ['control 6', {'principal': 100000, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 11, 30], 'first_due': [2023, 11, 30]}, {'error': 'due_not_after_start'}]], [['regression: interest rounding stage', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 2', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2023, 10, 30], 'first_due': [2023, 10, 31]}, {'days': 1, 'basis': 360, 'interest': 167}], ['control 4', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 3, 29], 'first_due': [2025, 3, 31]}, {'days': 367, 'basis': 365, 'interest': 4404}], ['control 5', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 29], 'first_due': [2023, 4, 15]}, {'error': 'due_not_after_start'}], ['control 6', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 6, 29], 'first_due': [2023, 7, 1]}, {'days': 2, 'basis': 360, 'interest': 24}]], [['regression: interest rounding stage', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['regression: interest rounding stage, partial-repair probe', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 1', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 2', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}], ['control 5', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 11, 1], 'first_due': [2025, 11, 1]}, {'days': 365, 'basis': 365, 'interest': 4380}]], [['regression: interest rounding stage', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['regression: interest rounding stage, partial-repair probe', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 1', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}], ['control 2', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 6, 28], 'first_due': [2025, 5, 15]}, {'days': 321, 'basis': 365, 'interest': 3210}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['control 4', {'principal': 100000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2024, 9, 28], 'first_due': [2025, 9, 30]}, {'days': 367, 'basis': 365, 'interest': 3670}], ['control 5', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

prompt:
Stub interest drifts by several cents on long first periods.

x = {'principal', 'rate_bp', 'convention': '30/360'|'act/365'|'act/360', 'start': [y,m,d], 'first_due': [y,m,d]}. 30/360: a start day of 31 becomes 30; then a due day of 31 becomes 30 when the (adjusted) start day is 30; days = 360*dy + 30*dm + dd. Actual conventions count calendar days, basis 365 or 360 (365 even in leap years). If days <= 0 return {'error': 'due_not_after_start'}. Interest = round_half_up(P*bp*days/(10000*basis)). Return {'days', 'basis', 'interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = 365 if conv == 'act/365' else 360
    if days < 0:
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: non-positive period guard', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 4, 30], 'first_due': [2024, 4, 30]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 7, 28], 'first_due': [2024, 7, 28]}, {'days': 366, 'basis': 360, 'interest': 628}], ['control 3', {'principal': 12345, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 30], 'first_due': [2025, 10, 30]}, {'days': 365, 'basis': 360, 'interest': 751}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 31, 'basis': 360, 'interest': 1033}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 11, 30]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 3, 29], 'first_due': [2025, 2, 1]}, {'days': 309, 'basis': 360, 'interest': 3760}], ['control 4', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 5', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 6', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 1], 'first_due': [2024, 12, 1]}, {'days': 30, 'basis': 360, 'interest': 3042}]], [['regression: non-positive period guard', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 5, 31], 'first_due': [2024, 5, 15]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 4', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 1], 'first_due': [2024, 10, 31]}, {'days': 30, 'basis': 360, 'interest': 500}], ['control 3', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 4', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 2, 29], 'first_due': [2024, 4, 28]}, {'days': 59, 'basis': 360, 'interest': 74}], ['control 5', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json
import datetime
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    y1, m1, d1 = x['start']
    y2, m2, d2 = x['first_due']
    conv = x['convention']
    if conv == '30/360':
        if d1 == 31:
            d1 = 30
        if d2 == 31 and d1 == 30:
            d2 = 30
        days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)
        basis = 360
    else:
        days = (datetime.date(y2, m2, d2) - datetime.date(y1, m1, d1)).days
        basis = 365 if conv == 'act/365' else 360
    if days <= 0 and conv != '30/360':
        return {'error': 'due_not_after_start'}
    interest = rnd(x['principal'] * x['rate_bp'] * days, 10000 * basis)
    return {'days': days, 'basis': basis, 'interest': interest}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: non-positive period guard', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 8, 30], 'first_due': [2023, 8, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 4, 30], 'first_due': [2024, 4, 30]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 3, 1]}, {'days': 352, 'basis': 360, 'interest': 441}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 7, 28], 'first_due': [2024, 7, 28]}, {'days': 366, 'basis': 360, 'interest': 628}], ['control 3', {'principal': 12345, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 30], 'first_due': [2025, 10, 30]}, {'days': 365, 'basis': 360, 'interest': 751}], ['control 4', {'principal': 100000, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2023, 1, 1], 'first_due': [2023, 2, 1]}, {'days': 31, 'basis': 360, 'interest': 1033}], ['control 5', {'principal': 12345, 'rate_bp': 1200, 'convention': '30/360', 'start': [2024, 7, 15], 'first_due': [2024, 9, 30]}, {'days': 75, 'basis': 360, 'interest': 309}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2024, 8, 29], 'first_due': [2024, 8, 29]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2023, 8, 28]}, {'days': 30, 'basis': 360, 'interest': 62}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 3, 15], 'first_due': [2024, 2, 1]}, {'days': 323, 'basis': 360, 'interest': 554}], ['control 3', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2024, 4, 30]}, {'days': 366, 'basis': 365, 'interest': 60164}], ['control 4', {'principal': 36500, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 6, 30], 'first_due': [2023, 8, 30]}, {'days': 60, 'basis': 360, 'interest': 365}], ['control 5', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 7, 30], 'first_due': [2024, 8, 28]}, {'days': 29, 'basis': 365, 'interest': 49}], ['control 6', {'principal': 1000000, 'rate_bp': 600, 'convention': '30/360', 'start': [2023, 7, 28], 'first_due': [2024, 6, 15]}, {'days': 317, 'basis': 360, 'interest': 52833}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 11, 30], 'first_due': [2024, 11, 30]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 12345, 'rate_bp': 500, 'convention': 'act/360', 'start': [2023, 11, 28], 'first_due': [2024, 10, 31]}, {'days': 338, 'basis': 360, 'interest': 580}], ['control 2', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2024, 1, 31], 'first_due': [2024, 1, 28]}, {'error': 'due_not_after_start'}], ['control 3', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/360', 'start': [2024, 3, 29], 'first_due': [2025, 2, 1]}, {'days': 309, 'basis': 360, 'interest': 3760}], ['control 4', {'principal': 12345, 'rate_bp': 500, 'convention': '30/360', 'start': [2023, 4, 30], 'first_due': [2023, 5, 28]}, {'days': 28, 'basis': 360, 'interest': 48}], ['control 5', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/365', 'start': [2023, 12, 1], 'first_due': [2023, 12, 15]}, {'days': 14, 'basis': 365, 'interest': 1400}], ['control 6', {'principal': 1000000, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 11, 1], 'first_due': [2024, 12, 1]}, {'days': 30, 'basis': 360, 'interest': 3042}]], [['regression: non-positive period guard', {'principal': 36500, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2023, 4, 30], 'first_due': [2023, 4, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 5, 31], 'first_due': [2024, 5, 15]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'convention': '30/360', 'start': [2023, 4, 1], 'first_due': [2024, 3, 1]}, {'days': 330, 'basis': 360, 'interest': 11000}], ['control 2', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2023, 4, 28], 'first_due': [2023, 6, 28]}, {'days': 61, 'basis': 365, 'interest': 305}], ['control 3', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 4, 30], 'first_due': [2024, 5, 1]}, {'days': 1, 'basis': 360, 'interest': 1}], ['control 4', {'principal': 12345, 'rate_bp': 1200, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 4, 29]}, {'days': 60, 'basis': 365, 'interest': 244}], ['control 5', {'principal': 36500, 'rate_bp': 600, 'convention': 'act/365', 'start': [2023, 12, 31], 'first_due': [2024, 1, 28]}, {'days': 28, 'basis': 365, 'interest': 168}]], [['regression: non-positive period guard', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 7, 30], 'first_due': [2024, 7, 30]}, {'error': 'due_not_after_start'}], ['regression: non-positive period guard, partial-repair probe', {'principal': 36500, 'rate_bp': 365, 'convention': '30/360', 'start': [2024, 6, 30], 'first_due': [2024, 6, 15]}, {'error': 'due_not_after_start'}], ['control 1', {'principal': 1000000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 6, 28], 'first_due': [2024, 7, 30]}, {'days': 32, 'basis': 360, 'interest': 5333}], ['control 2', {'principal': 100000, 'rate_bp': 600, 'convention': 'act/360', 'start': [2024, 10, 1], 'first_due': [2024, 10, 31]}, {'days': 30, 'basis': 360, 'interest': 500}], ['control 3', {'principal': 1000000, 'rate_bp': 500, 'convention': 'act/360', 'start': [2024, 1, 29], 'first_due': [2024, 1, 30]}, {'days': 1, 'basis': 360, 'interest': 139}], ['control 4', {'principal': 12345, 'rate_bp': 365, 'convention': 'act/360', 'start': [2024, 2, 29], 'first_due': [2024, 4, 28]}, {'days': 59, 'basis': 360, 'interest': 74}], ['control 5', {'principal': 36500, 'rate_bp': 500, 'convention': 'act/365', 'start': [2024, 2, 29], 'first_due': [2024, 3, 29]}, {'days': 29, 'basis': 365, 'interest': 145}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

prompt:
A 30/360 first period that collapses to zero days returns zero interest instead of an error.

x = {'principal', 'rate_bp', 'convention': '30/360'|'act/365'|'act/360', 'start': [y,m,d], 'first_due': [y,m,d]}. 30/360: a start day of 31 becomes 30; then a due day of 31 becomes 30 when the (adjusted) start day is 30; days = 360*dy + 30*dm + dd. Actual conventions count calendar days, basis 365 or 360 (365 even in leap years). If days <= 0 return {'error': 'due_not_after_start'}. Interest = round_half_up(P*bp*days/(10000*basis)). Return {'days', 'basis', 'interest'}.

split: open-access


broken_source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    avail = x['payment']
    applied = {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}
    if avail < x['threshold']:
        return {'applied': applied, 'curtailment': 0, 'refund': 0, 'suspense': avail, 'new_balance': x['balance']}
    for k in ('interest', 'principal', 'late_fee', 'fees'):
        take = min(avail, x['due'][k])
        applied[k] = take
        avail -= take
    room = x['balance'] - applied['principal']
    curt = min(avail, room)
    refund = avail - curt
    return {'applied': applied, 'curtailment': curt, 'refund': refund, 'suspense': 0, 'new_balance': x['balance'] - applied['principal'] - curt}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: suspense release', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 200, 'threshold': 1250, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 1200, 'refund': 4000, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 1250, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 300, 'new_balance': 800}], ['control 2', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 0, 'suspense': 0, 'threshold': 1000, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 800}], ['control 3', {'due': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 1300, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 300, 'new_balance': 800}], ['control 4', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 1285, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 5', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 800, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 6', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 0, 'suspense': 0, 'threshold': 535, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 5000}]], [['regression: suspense release', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'payment': 300, 'suspense': 700, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 500, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1500}], ['regression: suspense release, partial-repair probe', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'payment': 2500, 'suspense': 200, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'curtailment': 165, 'refund': 0, 'suspense': 0, 'new_balance': 2835}], ['control 1', {'due': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 950, 'balance': 2000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 1200, 'refund': 350, 'suspense': 0, 'new_balance': 0}], ['control 2', {'due': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 649, 'suspense': 0, 'threshold': 650, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 649, 'new_balance': 100000}], ['control 3', {'due': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 1550, 'suspense': 0, 'new_balance': 0}], ['control 4', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 1000, 'suspense': 0, 'threshold': 535, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 465, 'refund': 0, 'suspense': 0, 'new_balance': 1535}], ['control 5', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 0, 'threshold': 2200, 'balance': 5000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 4200, 'refund': 800, 'suspense': 0, 'new_balance': 0}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 300, 'suspense': 700, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 1000, 'late_fee': 0, 'fees': 0}, 'curtailment': -200, 'refund': 200, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 0, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 1000, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4000}], ['control 2', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'payment': 835, 'suspense': 0, 'threshold': 835, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 3', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 5800, 'suspense': 0, 'threshold': 800, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 5000, 'refund': 0, 'suspense': 0, 'new_balance': 94200}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'payment': 2084, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 34}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1200}], ['control 5', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 5035, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 5000, 'refund': 0, 'suspense': 0, 'new_balance': 95000}], ['control 6', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 35}, 'payment': 2184, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 34}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 98000}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 36, 'suspense': 700, 'threshold': 35, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 701, 'refund': 0, 'suspense': 0, 'new_balance': 99}], ['control 1', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1000, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 2', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 2650, 'suspense': 0, 'threshold': 2650, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 0}], ['control 3', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 3000}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 0, 'threshold': 2200, 'balance': 800}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 5000, 'suspense': 0, 'new_balance': 0}], ['control 5', {'due': {'interest': 500, 'principal': 800, 'late_fee': 150, 'fees': 35}, 'payment': 1485, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 800, 'late_fee': 150, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 6', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 534, 'suspense': 0, 'threshold': 535, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 534, 'new_balance': 100000}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': -1, 'suspense': 200, 'threshold': 0, 'balance': 2000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 199, 'refund': 0, 'suspense': 0, 'new_balance': 1801}], ['regression: suspense release, partial-repair probe', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 300, 'suspense': 700, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 800, 'refund': 165, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 1300, 'suspense': 0, 'threshold': 1000, 'balance': 800}, {'applied': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 0}], ['control 2', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 300, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 100000}], ['control 3', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1000, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 2050, 'suspense': 0, 'threshold': 2050, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1200}], ['control 5', {'due': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'curtailment': 1850, 'refund': 0, 'suspense': 0, 'new_balance': 150}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

source:
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    avail = x['payment'] + (x['suspense'] if x['payment'] < x['threshold'] else 0)
    applied = {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}
    if avail < x['threshold']:
        return {'applied': applied, 'curtailment': 0, 'refund': 0, 'suspense': avail, 'new_balance': x['balance']}
    for k in ('interest', 'principal', 'late_fee', 'fees'):
        take = min(avail, x['due'][k])
        applied[k] = take
        avail -= take
    room = x['balance'] - applied['principal']
    curt = min(avail, room)
    refund = avail - curt
    return {'applied': applied, 'curtailment': curt, 'refund': refund, 'suspense': 0, 'new_balance': x['balance'] - applied['principal'] - curt}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: suspense release', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 200, 'threshold': 1250, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 1200, 'refund': 4000, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 1250, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 300, 'new_balance': 800}], ['control 2', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 0, 'suspense': 0, 'threshold': 1000, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 800}], ['control 3', {'due': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 1300, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 300, 'new_balance': 800}], ['control 4', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 1285, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 5', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 800, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 6', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 0, 'suspense': 0, 'threshold': 535, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 5000}]], [['regression: suspense release', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'payment': 300, 'suspense': 700, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 500, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1500}], ['regression: suspense release, partial-repair probe', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'payment': 2500, 'suspense': 200, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 35}, 'curtailment': 165, 'refund': 0, 'suspense': 0, 'new_balance': 2835}], ['control 1', {'due': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 950, 'balance': 2000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 1200, 'refund': 350, 'suspense': 0, 'new_balance': 0}], ['control 2', {'due': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 649, 'suspense': 0, 'threshold': 650, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 649, 'new_balance': 100000}], ['control 3', {'due': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 1550, 'suspense': 0, 'new_balance': 0}], ['control 4', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 1000, 'suspense': 0, 'threshold': 535, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 465, 'refund': 0, 'suspense': 0, 'new_balance': 1535}], ['control 5', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 0, 'threshold': 2200, 'balance': 5000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 4200, 'refund': 800, 'suspense': 0, 'new_balance': 0}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 300, 'suspense': 700, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 1000, 'late_fee': 0, 'fees': 0}, 'curtailment': -200, 'refund': 200, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 0, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 1000, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4000}], ['control 2', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'payment': 835, 'suspense': 0, 'threshold': 835, 'balance': 5000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 3', {'due': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 5800, 'suspense': 0, 'threshold': 800, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 5000, 'refund': 0, 'suspense': 0, 'new_balance': 94200}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 35}, 'payment': 2084, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 34}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1200}], ['control 5', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 5035, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 5000, 'refund': 0, 'suspense': 0, 'new_balance': 95000}], ['control 6', {'due': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 35}, 'payment': 2184, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 2000, 'late_fee': 150, 'fees': 34}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 98000}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 36, 'suspense': 700, 'threshold': 35, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 701, 'refund': 0, 'suspense': 0, 'new_balance': 99}], ['control 1', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1000, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 2', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 2650, 'suspense': 0, 'threshold': 2650, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 0}], ['control 3', {'due': {'interest': 500, 'principal': 2000, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 2000, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 3000}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'payment': 7200, 'suspense': 0, 'threshold': 2200, 'balance': 800}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 150, 'fees': 0}, 'curtailment': 0, 'refund': 5000, 'suspense': 0, 'new_balance': 0}], ['control 5', {'due': {'interest': 500, 'principal': 800, 'late_fee': 150, 'fees': 35}, 'payment': 1485, 'suspense': 0, 'threshold': 0, 'balance': 5000}, {'applied': {'interest': 500, 'principal': 800, 'late_fee': 150, 'fees': 35}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 4200}], ['control 6', {'due': {'interest': 500, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 534, 'suspense': 0, 'threshold': 535, 'balance': 100000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 534, 'new_balance': 100000}]], [['regression: suspense release', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': -1, 'suspense': 200, 'threshold': 0, 'balance': 2000}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 199, 'refund': 0, 'suspense': 0, 'new_balance': 1801}], ['regression: suspense release, partial-repair probe', {'due': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'payment': 300, 'suspense': 700, 'threshold': 0, 'balance': 800}, {'applied': {'interest': 0, 'principal': 0, 'late_fee': 0, 'fees': 35}, 'curtailment': 800, 'refund': 165, 'suspense': 0, 'new_balance': 0}], ['control 1', {'due': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 1300, 'suspense': 0, 'threshold': 1000, 'balance': 800}, {'applied': {'interest': 500, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 0}], ['control 2', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 300, 'suspense': 0, 'threshold': 0, 'balance': 100000}, {'applied': {'interest': 300, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 100000}], ['control 3', {'due': {'interest': 1250, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'payment': 1000, 'suspense': 0, 'threshold': 1000, 'balance': 2000}, {'applied': {'interest': 1000, 'principal': 0, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 2000}], ['control 4', {'due': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'payment': 2050, 'suspense': 0, 'threshold': 2050, 'balance': 2000}, {'applied': {'interest': 1250, 'principal': 800, 'late_fee': 0, 'fees': 0}, 'curtailment': 0, 'refund': 0, 'suspense': 0, 'new_balance': 1200}], ['control 5', {'due': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'payment': 2500, 'suspense': 0, 'threshold': 0, 'balance': 2000}, {'applied': {'interest': 500, 'principal': 0, 'late_fee': 150, 'fees': 0}, 'curtailment': 1850, 'refund': 0, 'suspense': 0, 'new_balance': 150}]]]
for label, args, expected in fixtures[N-1]:
    try:
        actual = solve(args)
    except Exception as exc:
        actual = 'raised ' + type(exc).__name__
    check(label, actual, 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)

prompt:
Funds held in suspense disappear once a qualifying payment arrives.

x = {'due': {'interest', 'principal', 'late_fee', 'fees'}, 'payment', 'suspense', 'threshold', 'balance'}. Available = payment + suspense. If available < threshold, everything goes to suspense and nothing is applied. Otherwise apply in order interest, principal, late_fee, fees, then curtail principal with the remainder up to balance - applied principal; the rest is refunded. Return {'applied', 'curtailment', 'refund', 'suspense', 'new_balance'}.

split: open-access