DEV Community

KunStudio
KunStudio

Posted on • Originally published at invoice-rescue.pages.dev

The Math Behind a Late-Fee: Turning "Net 30" Into an Enforceable Number

The math behind a late-fee: turning "net 30" into an enforceable number

I build small web tools, and the one that surprised me most wasn't AI-heavy at all — it was a late-fee calculator for freelancers. The hard part wasn't the arithmetic. It was deciding which arithmetic is defensible enough to put in front of a client who is already 45 days late.

This is a short write-up of the reasoning, with the actual formula I shipped in Invoice Rescue.

Simple interest, day-count, no compounding

The instinct is to reach for compound interest. Don't. For overdue B2B invoices, a client-facing fee that compounds daily reads as punitive and invites a dispute. Simple interest on a daily accrual is both easier to justify and closer to how most late-payment statutes actually describe it.

function lateFee(amount, annualRatePct, daysOverdue) {
  const dailyRate = (annualRatePct / 100) / 365;
  const fee = amount * dailyRate * Math.max(0, daysOverdue);
  return Math.round(fee * 100) / 100;
}
// $2,500 invoice, 18%/yr, 20 days late → $24.66
Enter fullscreen mode Exit fullscreen mode

Two decisions are doing the work here:

  • Math.max(0, daysOverdue) — the due date is in the future for a lot of "check the fee" sessions (people model it before it's actually late). A naive today - dueDate goes negative and produces a phantom credit. Clamp it.
  • / 365 — some contracts and jurisdictions use a 360-day "banker's year." Pick one, state it in the output, and don't silently switch. The number a client sees has to match the number in the clause you send them.

The part everyone skips: the day-count is a UX problem, not a math problem

When someone types a due date, they mean their timezone and their calendar day. If you diff two Date objects you'll get an off-by-one every time the browser TZ and the invoice TZ disagree at midnight. Compare calendar dates, not timestamps:

function daysBetween(dueISO) {
  const d = new Date(dueISO + "T00:00:00");
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  return Math.round((today - d) / 86400000);
}
Enter fullscreen mode Exit fullscreen mode

The reminder sequence is a state machine, not three templates

The other half of getting paid is the follow-up, and the mistake is treating "polite / firm / final" as three static emails. They're a state machine with one input that changes tone: how much rapport you can still afford to spend.

  • Polite (day 1–7 overdue): assume an oversight. No fee mentioned. One clear ask, one clear due date.
  • Firm (day 8–21): name the amount, the invoice number, and the days overdue as facts. Introduce the fee clause as information, not a threat.
  • Final (22+): state the next concrete step and a hard date. Still no insults — a final notice that stays professional is the one that survives being forwarded to their accounts team.

The thing that makes this convert is that every message references the specific invoice and the specific number. Generic "just following up!" emails get ignored precisely because they carry no cost signal.

What I'd tell my past self

Don't compound, clamp the day-count, compare calendar dates, and keep the final notice boring. The tool that came out of this reasoning is Invoice Rescue — the fee calculator and the three-step templates are free to use with no signup; a paid AI pack writes the full chase sequence for a specific situation. But honestly, the four rules above are the whole product. The rest is just typing.

If you've got an invoice sitting past due right now, run the numbers on Invoice Rescue and start with the polite template today — the longer you wait, the more the tone has to escalate.


Written by the KunStudio team. We build small, focused web tools. This is general information about invoicing workflows, not legal or financial advice — check your contract and local law for the enforceable rate.

Top comments (0)