Skip to content

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:

  1. Outstanding fines — paid off first
  2. Accrued interest — daily-compounded since last payment, up to the interest date
  3. 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)