Skip to content

How LOC billing cycles work

A line of credit billing cycle is fundamentally different from an installment loan. Understanding this structure is critical for migration because Peach must initialize balance buckets correctly at cutover — and post-migration, these mechanics govern how balances move, when interest accrues, and when a borrower becomes overdue.

This page explains how LOC billing cycles work. To run a migration, see the LOC migration procedure.

Billing period structure

Every LOC billing cycle is divided into periods defined by four key dates:

DateWhat happens
startDatePeriod begins. New balance tracking starts.
endDatePeriod ends. Last day of the billing period.
statementDateStatement generated. Always endDate + 1. Non-due balances move to due.
dueDatePayment deadline. Full balance must be paid by this date for grace (if applicable).
dueDate + 1Enforcement. Unpaid due amounts move to overdue. Grace eligibility checked.

In Peach, statementDate must always equal endDate + 1 day. This is a hard validation rule — if they don't match, migration will fail with "Period statement date should be one day after end date."

Balance movement: non-due → due → overdue

Unlike installment loans where a single payment obligation comes due on one date, LOCs move balances through buckets on specific dates.

Non-due to due (statement date)

Between startDate and statementDate, all accrued interest and posted transactions sit in the non-due bucket. On statementDate, the system moves the remaining non-due balance to the due bucket. The amount moved equals the remainingAmount from the obligation for that period.

During this step, outstanding non-due interest is truncated to the nearest cent. Any fractional cents are moved to a forgone_interest_rounding bucket and are not recovered. This is a key difference from systems that round interest at a different cadence.

Due to overdue (due date + 1)

After statementDate, the balance sits in the due bucket. For LOCs, this is typically 2–3 weeks (dueDate - statementDate + 1 days). On dueDate + 1, any remaining due balance moves to overdue.

Loan typeNon-due → dueTime in "due"Due → overdue
Line of creditStatement date2–3 weeksDue date + 1
InstallmentDue date1 dayDue date + 1

This is the most important structural difference between LOCs and installment loans. For installment loans, balances move from non-due to due on the due date and become overdue just one day later. For LOCs, there's a substantial window between "due" and "overdue."

Obligations: tracking what the borrower owes

An obligation tracks how much the borrower must pay in a specific billing period. It answers: "How much is owed, how much has been paid, and how much remains?"

The obligation amount is computed from expected payments — forecasts of what the borrower should pay based on the loan's schedule and fee structure:

obligationAmount = sum(expectedPayment.amount)
  where expectedPayment.period_id = obligation.period_id

Expected payments include: one PeriodicPayment (principal + interest), any number of DynamicFee entries (e.g., periodic servicing fees), and optionally one OriginationFee or DrawFee.

The obligation also tracks payment progress:

fulfilledAmount = paymentsAmount + serviceCreditsAmount
remainingAmount = obligationAmount - fulfilledAmount

The fulfilledAmount calculation respects the allowPrepayments loan type setting. If prepayments are allowed, a borrower's early payment can satisfy future obligations. If prepayments are not allowed, excess payments are tracked as overpaymentsAmount but don't reduce future remainingAmount.

The refresh_obligations() function recomputes all obligations whenever a relevant event occurs (payment, service credit, refund, etc.).

Why this matters for migration

During migration, you must seed Peach's ledger with the correct balance buckets. Your legacy system's balances must be broken down into non-due, due, and overdue — per fee type (principal, interest, periodic fees, late fees, origination fees, draw fees, etc.). If these breakdowns are incorrect, post-migration DLM will move the wrong amounts at statement date, causing balance discrepancies.

You also need to provide the correct obligationAmount for the migration period (reconstructed from your legacy system's billing data). For overdue loans, you must include migratedDaysOverdue and migratedOverdueAmount so that Peach can correctly track the borrower's delinquency status going forward.

Timeline example

Day 1 (Aug 1):    startDate — Period begins
                   ├─ Interest accrues daily in non-due bucket
                   ├─ Purchases post to non-due bucket
                   └─ Borrower can make payments (reduce non-due)

Day 31 (Aug 31):  endDate — Period ends

Day 32 (Sep 1):   statementDate — Statement generated
                   ├─ Non-due balance moves to due bucket
                   ├─ Interest truncated (fractional cents → forgone_interest_rounding)
                   ├─ Obligation sealed (obligationAmount set)
                   └─ Statement mailed/emailed to borrower

Days 32–53:       Due period (balance in "due" for ~3 weeks)
                   ├─ Borrower makes payments (reduce due)
                   └─ Grace: payments effectiveDate → statementDate if eligible

Day 53 (Sep 22):  dueDate — Payment deadline
                   └─ Last day to pay for grace eligibility

Day 54 (Sep 23):  dueDate + 1 — Enforcement
                   ├─ Remaining due balance moves to overdue
                   ├─ Grace eligibility checked (revoke or reinstate)
                   ├─ loan_overdue event fires (if overdue)
                   ├─ Late fees may be charged
                   └─ Acceleration/charge-off threshold checked

See also