FAILURE MAP
← Case archive

FA-58406 / Loan amortization schedules / Open access

Principal curtailment with recast or term shortening: recast remaining term · case 01

After a recast the payment drops as if the loan were brand new.

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

ROOT CAUSE

The recast re-levels over the full original term.

VERIFIED REPAIR

Re-level over the n - k payments that remain.

Unsuccessful approach: Counting the current month again spreads the balance over one payment too many.

Case contract

x = {'principal', 'rate_bp', 'months', 'prepay': {'k': extra cents after payment k}, 'mode': 'shorten'|'recast'}. The level payment is the exact annuity rounded half-up. Each month: interest = round_half_up(balance*bp/120000); if balance + interest <= payment or it is month n, pay balance + interest and stop. Otherwise reduce the balance by payment - interest, then apply the extra (capped at the balance). In recast mode a nonzero extra that leaves a balance re-levels the payment over the n - k remaining months. An extra that clears the balance ends the loan with final payment = payment + extra. Return {'months_paid', 'total_interest', 'final_payment', 'payments': payment history}.

Why this case matters

Amortization engines drive borrower statements, payoff quotes and investor remittances; a misplaced rounding step, boundary or ordering rule compounds across hundreds of periods.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: recast remaining term', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'26': 20000, '10': 5000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 3, 'total_interest': 0, 'final_payment': 56666, 'payments': [1667]}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 3445, 'final_payment': 72421, 'payments': [4432]}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'33': 200000, '21': 200000}, 'mode': 'recast'}, {'months_paid': 21, 'total_interest': 4647, 'final_payment': 28147, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'11': 20000, '19': 20000, '2': 1000}, 'mode': 'shorten'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 8994, 'payments': [1667]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'5': 2000000, '1': 200000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 1, 'total_interest': 0, 'final_payment': 60000, 'payments': [10000]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 12, 'prepay': {'4': 200000}, 'mode': 'recast'}, {'months_paid': 4, 'total_interest': 2115, 'final_payment': 46122, 'payments': [5331]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'17': 20000, '29': 20000, '5': 200000}, 'mode': 'recast'}, {'months_paid': 5, 'total_interest': 1423, 'final_payment': 54123, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'20': 5000, '18': 2000000, '15': 1000}, 'mode': 'recast'}, {'months_paid': 18, 'total_interest': 11987, 'final_payment': 31200, 'payments': [4707, 4591]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 200000, '1': 1000, '6': 200000}, 'mode': 'shorten'}, {'months_paid': 4, 'total_interest': 0, 'final_payment': 29000, 'payments': [10000]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'34': 2000000, '15': 200000}, 'mode': 'recast'}, {'months_paid': 15, 'total_interest': 6138, 'final_payment': 63550, 'payments': [3042]}]]]
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)
Boundary fixtureActualExpectedOutcome
regression: recast remaining term{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 468], 'total_interest': 1612}{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 1104], 'total_interest': 1612}Failed
control 1{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}Passed
control 2{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}Passed
control 3{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}Passed
control 4{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}Passed
control 5{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}Passed
control 6{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}Passed

SHA-256 / d6862415a8f65d9814bf63be6f7f9c04fd4145da81cdebf984d5c5f07fe5f5a1

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n - k + 1)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: recast remaining term', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'26': 20000, '10': 5000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 3, 'total_interest': 0, 'final_payment': 56666, 'payments': [1667]}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 3445, 'final_payment': 72421, 'payments': [4432]}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'33': 200000, '21': 200000}, 'mode': 'recast'}, {'months_paid': 21, 'total_interest': 4647, 'final_payment': 28147, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'11': 20000, '19': 20000, '2': 1000}, 'mode': 'shorten'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 8994, 'payments': [1667]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'5': 2000000, '1': 200000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 1, 'total_interest': 0, 'final_payment': 60000, 'payments': [10000]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 12, 'prepay': {'4': 200000}, 'mode': 'recast'}, {'months_paid': 4, 'total_interest': 2115, 'final_payment': 46122, 'payments': [5331]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'17': 20000, '29': 20000, '5': 200000}, 'mode': 'recast'}, {'months_paid': 5, 'total_interest': 1423, 'final_payment': 54123, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'20': 5000, '18': 2000000, '15': 1000}, 'mode': 'recast'}, {'months_paid': 18, 'total_interest': 11987, 'final_payment': 31200, 'payments': [4707, 4591]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 200000, '1': 1000, '6': 200000}, 'mode': 'shorten'}, {'months_paid': 4, 'total_interest': 0, 'final_payment': 29000, 'payments': [10000]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'34': 2000000, '15': 200000}, 'mode': 'recast'}, {'months_paid': 15, 'total_interest': 6138, 'final_payment': 63550, 'payments': [3042]}]]]
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)
Boundary fixtureActualExpectedOutcome
regression: recast remaining term{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 922], 'total_interest': 1612}{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 1104], 'total_interest': 1612}Failed
control 1{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}Passed
control 2{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}Passed
control 3{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}Passed
control 4{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}Passed
control 5{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}Passed
control 6{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}Passed

SHA-256 / 9b0560da1b13ced0d51800474eebd47792380a97a789578e40ac344ff6d9efeb

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
from fractions import Fraction
N = 1
observations = []
def solve(x):
    def rnd(n, d):
        q, r = divmod(n, d)
        return q + (1 if 2 * r >= d else 0)
    def level(P, n):
        if n <= 0 or P <= 0:
            return 0
        if bp == 0:
            exact = Fraction(P, n)
        else:
            r = Fraction(bp, 120000)
            exact = P * r / (1 - (1 + r) ** -n)
        return math.floor(exact + Fraction(1, 2))
    bp = x['rate_bp']
    n = x['months']
    bal = x['principal']
    pay = level(bal, n)
    history = [pay]
    total = 0
    k = 0
    final = 0
    while bal > 0 and k < n:
        k += 1
        interest = rnd(bal * bp, 120000)
        total += interest
        if bal + interest <= pay or k == n:
            final = bal + interest
            bal = 0
            break
        bal -= pay - interest
        extra = min(x['prepay'].get(str(k), 0), bal)
        if extra:
            bal -= extra
            if x['mode'] == 'recast' and bal > 0:
                pay = level(bal, n - k)
                history.append(pay)
            if bal == 0:
                final = pay + extra
    return {'months_paid': k, 'total_interest': total, 'final_payment': final, 'payments': history}
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: recast remaining term', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000, '7': 20000}, 'mode': 'recast'}, {'months_paid': 8, 'total_interest': 1612, 'final_payment': 5464, 'payments': [5164, 1104]}], ['control 1', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'21': 5000, '12': 200000, '35': 2000000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 41663, 'payments': [1667]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'26': 20000, '10': 5000, '3': 2000000}, 'mode': 'shorten'}, {'months_paid': 3, 'total_interest': 0, 'final_payment': 56666, 'payments': [1667]}], ['control 3', {'principal': 100000, 'rate_bp': 600, 'months': 24, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 3445, 'final_payment': 72421, 'payments': [4432]}], ['control 4', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'33': 200000, '21': 200000}, 'mode': 'recast'}, {'months_paid': 21, 'total_interest': 4647, 'final_payment': 28147, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 36, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 36, 'total_interest': 0, 'final_payment': 2770, 'payments': [2778]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'10': 5000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 12595, 'final_payment': 4326, 'payments': [4707, 4323]}], ['control 1', {'principal': 250000, 'rate_bp': 0, 'months': 24, 'prepay': {'24': 200000}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 10409, 'payments': [10417]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 6, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 41665, 'payments': [41667]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 4', {'principal': 100000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 4159, 'payments': [4167]}], ['control 5', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 12979, 'final_payment': 4718, 'payments': [4707]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 36, 'prepay': {'11': 20000, '19': 20000, '2': 1000}, 'mode': 'shorten'}, {'months_paid': 19, 'total_interest': 0, 'final_payment': 8994, 'payments': [1667]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 600, 'months': 12, 'prepay': {'10': 1000}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 3272, 'final_payment': 8101, 'payments': [8607, 8101]}], ['control 1', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'12': 5000, '20': 20000, '4': 1000}, 'mode': 'shorten'}, {'months_paid': 20, 'total_interest': 11928, 'final_payment': 16495, 'payments': [4707]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'5': 2000000, '1': 200000, '3': 20000}, 'mode': 'recast'}, {'months_paid': 1, 'total_interest': 0, 'final_payment': 60000, 'payments': [10000]}], ['control 3', {'principal': 60000, 'rate_bp': 1200, 'months': 12, 'prepay': {'4': 200000}, 'mode': 'recast'}, {'months_paid': 4, 'total_interest': 2115, 'final_payment': 46122, 'payments': [5331]}], ['control 4', {'principal': 250000, 'rate_bp': 600, 'months': 36, 'prepay': {'10': 20000, '11': 1000}, 'mode': 'shorten'}, {'months_paid': 33, 'total_interest': 21026, 'final_payment': 6666, 'payments': [7605]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'17': 20000, '29': 20000, '5': 200000}, 'mode': 'recast'}, {'months_paid': 5, 'total_interest': 1423, 'final_payment': 54123, 'payments': [1825]}], ['control 6', {'principal': 100000, 'rate_bp': 0, 'months': 12, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 12, 'total_interest': 0, 'final_payment': 8337, 'payments': [8333]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 1200, 'months': 24, 'prepay': {'20': 5000, '18': 2000000, '15': 1000}, 'mode': 'recast'}, {'months_paid': 18, 'total_interest': 11987, 'final_payment': 31200, 'payments': [4707, 4591]}], ['control 1', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 36, 'total_interest': 9520, 'final_payment': 3050, 'payments': [3042]}], ['control 2', {'principal': 250000, 'rate_bp': 0, 'months': 12, 'prepay': {'7': 200000, '3': 5000}, 'mode': 'shorten'}, {'months_paid': 7, 'total_interest': 0, 'final_payment': 120002, 'payments': [20833]}], ['control 3', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {}, 'mode': 'recast'}, {'months_paid': 24, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}], ['control 4', {'principal': 60000, 'rate_bp': 600, 'months': 36, 'prepay': {'24': 2000000, '33': 1000, '12': 2000000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 3088, 'final_payment': 43013, 'payments': [1825]}], ['control 5', {'principal': 60000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 5000}, 'mode': 'shorten'}, {'months_paid': 12, 'total_interest': 1867, 'final_payment': 63, 'payments': [5164]}], ['control 6', {'principal': 60000, 'rate_bp': 0, 'months': 24, 'prepay': {'9': 5000, '24': 5000}, 'mode': 'shorten'}, {'months_paid': 22, 'total_interest': 0, 'final_payment': 2500, 'payments': [2500]}]], [['regression: recast remaining term', {'principal': 100000, 'rate_bp': 0, 'months': 6, 'prepay': {'3': 1000}, 'mode': 'recast'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 16333, 'payments': [16667, 16333]}], ['control 1', {'principal': 250000, 'rate_bp': 600, 'months': 12, 'prepay': {'8': 200000}, 'mode': 'shorten'}, {'months_paid': 8, 'total_interest': 7134, 'final_payment': 106515, 'payments': [21517]}], ['control 2', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 1000}, 'mode': 'shorten'}, {'months_paid': 6, 'total_interest': 0, 'final_payment': 9000, 'payments': [10000]}], ['control 3', {'principal': 250000, 'rate_bp': 1200, 'months': 24, 'prepay': {}, 'mode': 'shorten'}, {'months_paid': 24, 'total_interest': 32442, 'final_payment': 11778, 'payments': [11768]}], ['control 4', {'principal': 60000, 'rate_bp': 0, 'months': 6, 'prepay': {'4': 200000, '1': 1000, '6': 200000}, 'mode': 'shorten'}, {'months_paid': 4, 'total_interest': 0, 'final_payment': 29000, 'payments': [10000]}], ['control 5', {'principal': 60000, 'rate_bp': 0, 'months': 12, 'prepay': {'10': 5000}, 'mode': 'shorten'}, {'months_paid': 11, 'total_interest': 0, 'final_payment': 5000, 'payments': [5000]}], ['control 6', {'principal': 100000, 'rate_bp': 600, 'months': 36, 'prepay': {'34': 2000000, '15': 200000}, 'mode': 'recast'}, {'months_paid': 15, 'total_interest': 6138, 'final_payment': 63550, 'payments': [3042]}]]]
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)
Boundary fixtureActualExpectedOutcome
regression: recast remaining term{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 1104], 'total_interest': 1612}{'final_payment': 5464, 'months_paid': 8, 'payments': [5164, 1104], 'total_interest': 1612}Passed
control 1{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}{'final_payment': 41663, 'months_paid': 12, 'payments': [1667], 'total_interest': 0}Passed
control 2{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}{'final_payment': 56666, 'months_paid': 3, 'payments': [1667], 'total_interest': 0}Passed
control 3{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}{'final_payment': 72421, 'months_paid': 8, 'payments': [4432], 'total_interest': 3445}Passed
control 4{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}{'final_payment': 11778, 'months_paid': 24, 'payments': [11768], 'total_interest': 32442}Passed
control 5{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}{'final_payment': 28147, 'months_paid': 21, 'payments': [1825], 'total_interest': 4647}Passed
control 6{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}{'final_payment': 2770, 'months_paid': 36, 'payments': [2778], 'total_interest': 0}Passed

SHA-256 / 44e068fb2f0c16f1625086dee7ad1e475ed0dcc16bcd041cc1a86df8a62cbfd6

Verification & scope

A deterministic bounded teaching model with stipulated toy lending rules stated in the contract; money is integer cents and rates are basis points; it makes no claim of conformance to any regulation, servicing standard or product. 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:46:26.436212+00:00.

Case digest / 4e4330938623795db2c1608bcac9efbef99e48d3cc4f59a366fdc5023c1a5d56