Fines, Mora Interest & Payment Methods¶
MoneyWarp models late payments realistically: overdue installments incur fines (a flat percentage of the missed amount) and mora interest (extra daily-compounded interest for the days beyond the due date). Payments are always allocated in strict priority: fines first, then interest, then principal.
All payment methods return a Settlement object showing exactly how the payment was allocated (see Installments & Settlements below).
Payment Methods¶
MoneyWarp provides two sugar methods for recording payments, plus a low-level method for full control.
pay_installment() — The Common Case¶
Records a payment at the current date (self.now()). Interest accrual depends on timing:
- Early or on-time: interest accrues up to the due date (no discount)
- Late: interest accrues up to
self.now(), charging extra interest for the overdue days
from datetime import datetime
from money_warp import Loan, Money, InterestRate, Warp, generate_monthly_dates
from money_warp.tz import to_date
loan = Loan(
Money("10000"),
InterestRate("5% a"),
[to_date(d) for d in generate_monthly_dates(datetime(2024, 2, 1), 12)],
)
# Pay on time using the Time Machine
with Warp(loan, datetime(2024, 2, 1)) as warped:
warped.pay_installment(Money("856.07"), "February payment")
# Pay late — mora interest is automatically charged
with Warp(loan, datetime(2024, 3, 15)) as warped:
warped.pay_installment(Money("900.00"), "March payment (late)")
anticipate_payment() — Early Payment with Discount¶
Records a payment at the current date, but calculates interest only up to self.now(). Fewer elapsed days means less interest charged — the borrower gets a discount.
# Anticipate a payment before the due date — pay less interest
with Warp(loan, datetime(2024, 3, 20)) as warped:
warped.anticipate_payment(Money("800.00"), "Early April payment")
record_payment() — Explicit Date Control¶
Sets both payment_date and interest_date to the given date. Useful for batch processing and tests where you need explicit dates.
loan.record_payment(Money("856.07"), datetime(2024, 2, 1), "February payment")
loan.record_payment(Money("856.07"), datetime(2024, 3, 1), "March payment")
Fine Configuration¶
Configure fines and grace periods when creating a loan:
from datetime import datetime
from money_warp import InterestRate, Loan, Money, generate_monthly_dates
from money_warp.tz import to_date
_due = [to_date(d) for d in generate_monthly_dates(datetime(2024, 2, 1), 12)]
# Default: 2% fine, no grace period
loan = Loan(
Money("10000"),
InterestRate("5% a"),
_due,
)
# Custom: 5% fine with a 7-day grace period
loan = Loan(
Money("10000"),
InterestRate("5% a"),
_due,
fine_rate=InterestRate("5% annual"),
grace_period_days=7,
)
| Parameter | Default | Meaning |
|---|---|---|
fine_rate |
InterestRate("2% annual") |
Fine rate applied to the expected installment amount |
grace_period_days |
0 |
Days after the due date before fines are applied |
How Fines Work¶
Checking for Late Payments¶
from datetime import date, datetime
from money_warp import Loan, Money, InterestRate, generate_monthly_dates
from money_warp.tz import to_date
loan = Loan(
Money("10000"),
InterestRate("5% a"),
[to_date(d) for d in generate_monthly_dates(datetime(2024, 2, 1), 3)],
)
# Is the February payment late as of February 15?
is_late = loan.is_payment_late(date(2024, 2, 1), as_of_date=datetime(2024, 2, 15))
print(f"Late? {is_late}") # True — no payment was made
Calculating and Applying Fines¶
calculate_late_fines() scans all due dates up to the given date and applies one fine per missed due date. Fines are never duplicated — each due date can only be fined once.
# Apply fines as of March 15 (both Feb and March installments missed)
new_fines = loan.calculate_late_fines(as_of_date=datetime(2024, 3, 15))
print(f"New fines applied: {new_fines}")
print(f"Total fines: {loan.total_fines}")
print(f"Fine balance: {loan.fine_balance}")
Fine Amounts Come from the Original Schedule¶
Fines are calculated as fine_rate * expected_payment_amount, where the expected payment comes from the original amortization schedule — not the rebuilt schedule. This ensures fine amounts are predictable and don't change as payments are recorded.
Mora Interest¶
When a borrower pays late, they are charged extra interest for the days beyond the due date. The interest is automatically split into two separate cash flow items:
- Regular interest (
"interest", kind=HAPPENED) — accrued from the last payment to the due date - Mora interest (
"mora_interest", kind=HAPPENED) — accrued from the due date to the payment date
On-time and early payments produce only a regular interest item (no mora).
from datetime import date, datetime
from money_warp import InterestRate, Loan, Money, Warp
loan = Loan(
Money("10000"),
InterestRate("6% a"),
[date(2024, 2, 1), date(2024, 3, 1), date(2024, 4, 1)],
disbursement_date=datetime(2024, 1, 1),
)
# Miss installment 1, pay 2 weeks late
# The borrower pays:
# 1. A fine (2% of the expected installment)
# 2. Regular interest for 31 days (disbursement to due date)
# 3. Mora interest for 14 days (due date to payment date)
# 4. The remaining amount reduces principal
with Warp(loan, datetime(2024, 2, 15)) as warped:
warped.pay_installment(Money("3600.00"), "Late payment")
print(f"Fine balance: {warped.fine_balance}")
print(f"Remaining balance: {warped.current_balance}")
Payment Allocation Priority¶
All payment methods allocate funds in the same strict order:
- Outstanding fines — paid off first
- Accrued interest — daily-compounded since last payment, up to the interest date
- Principal — whatever remains reduces the loan balance
A large late payment naturally covers the missed installment and eats into future installments through this allocation pipeline. There is no special-casing — the allocation priority and principal balance tracking handle overpayments automatically.
Grace Periods¶
A grace period delays when fines are applied. If grace_period_days=7, a payment due on February 1st is not considered late until February 8th.
from datetime import date, datetime
from money_warp import InterestRate, Loan, Money
loan = Loan(
Money("10000"),
InterestRate("5% a"),
[date(2024, 2, 1)],
grace_period_days=7,
)
# Not late on February 5 (within grace period)
print(loan.is_payment_late(date(2024, 2, 1), datetime(2024, 2, 5))) # False
# Late on February 9 (grace period expired)
print(loan.is_payment_late(date(2024, 2, 1), datetime(2024, 2, 9))) # True
Note: the grace period only affects fines. Mora interest always accrues for every day past the due date, regardless of the grace period.
Tracking Fines and Mora via Settlements¶
Settlements are derived views that show how each payment was allocated. Use loan.settlements to inspect fine and mora detail:
for s in loan.settlements:
print(f"{s.payment_date.date()}: fine={s.fine_paid}, mora={s.mora_paid}")
for a in s.allocations:
print(f" inst {a.installment_number}: fine={a.fine_allocated}, mora={a.mora_allocated}")
The raw cashflow (loan.cashflow) contains expected schedule items and undifferentiated payment items. The breakdown into fine, interest, mora, and principal is a derived view available through loan.settlements.
CashFlow Categories¶
| Category | Kind | Meaning |
|---|---|---|
"disbursement" |
EXPECTED | Loan disbursement |
"interest" |
EXPECTED | Scheduled interest payment |
"principal" |
EXPECTED | Scheduled principal payment |
"payment" |
HAPPENED | Actual payment (allocation is derived via loan.settlements) |
Installments & Settlements¶
A loan is not a group of installments. Installments are consequences of the loan — they describe how the borrower repays. Settlements are consequences of making a payment — they describe how the money was allocated.
Installments¶
Access the repayment plan via loan.installments. Each Installment shows expected amounts, actual paid amounts, and detailed per-payment allocations.
from datetime import datetime
from money_warp import Loan, Money, InterestRate, generate_monthly_dates
from money_warp.tz import to_date, to_datetime
loan = Loan(
Money("10000"),
InterestRate("6% a"),
[to_date(d) for d in generate_monthly_dates(datetime(2025, 2, 1), 3)],
disbursement_date=datetime(2025, 1, 1),
)
# Before any payments: all unpaid
for inst in loan.installments:
print(f"#{inst.number} due {inst.due_date}: "
f"{inst.expected_payment} — paid: {inst.is_fully_paid}")
# After a payment: first installment is paid (payment_date is a datetime)
schedule = loan.get_original_schedule()
loan.record_payment(schedule[0].payment_amount, to_datetime(schedule[0].due_date))
inst = loan.installments[0]
print(f"#{inst.number} is_fully_paid={inst.is_fully_paid}")
print(f" principal_paid={inst.principal_paid}, interest_paid={inst.interest_paid}")
print(f" allocations: {len(inst.allocations)}")
Installments are Warp-aware — inside a Warp context, is_fully_paid reflects only payments made by the warped date.
Settlements¶
Every call to record_payment(), pay_installment(), or anticipate_payment() returns a Settlement. You can also access all past settlements via loan.settlements.
settlement = loan.record_payment(Money("5000"), datetime(2025, 3, 1))
# Settlement-level totals
print(f"Payment: {settlement.payment_amount}")
print(f"Fine: {settlement.fine_paid}")
print(f"Interest: {settlement.interest_paid}")
print(f"Mora: {settlement.mora_paid}")
print(f"Principal: {settlement.principal_paid}")
print(f"Remaining: {settlement.remaining_balance}")
# Per-installment detail
for alloc in settlement.allocations:
print(f" Installment #{alloc.installment_number}: "
f"principal={alloc.principal_allocated}, "
f"interest={alloc.interest_allocated}, "
f"covered={alloc.is_fully_covered}")
# Access all settlements
for s in loan.settlements:
print(f"{s.payment_date.date()}: {s.payment_amount}")
Settlements are not stored as separate state — they are reconstructed by querying the loan's cash flow data. This means loan.settlements is always consistent with the actual payment history.
Key Properties¶
| Property | Type | Meaning |
|---|---|---|
total_fines |
Money |
Sum of all fines ever applied |
fine_balance |
Money |
Unpaid fines (total minus what's been paid off) |
fines_applied |
Dict[datetime, Money] |
Fine amount applied per due date |
is_paid_off |
bool |
True only when principal and fines are zero |
installments |
List[Installment] |
Repayment plan with expected/actual amounts (Warp-aware) |
settlements |
List[Settlement] |
Payment allocation history (Warp-aware) |