Menu /
The Worst Year for Late Payers Hasn’t Happened Yet
SQL / Blog

The Worst Year for Late Payers Hasn’t Happened Yet

Clearstone’s worst year for late payers, according to its own ledger, is 2040.

Clearstone, before anyone phones their accountant, is made up. Its books are practice data on LucidPlexus: a budget, a record of cash, five departments and a ledger of 1,000 invoices, built to look like a real finance team’s files, mess included, so that learners can practise on something with teeth. The ledger has one invoice a week, each with what a customer owes Clearstone, what Clearstone owes a supplier, a due date and a status.

Practise on the same dataFree to read · a free account previews and downloads
Clearstone — Invoices receivables1,000 rows · 4 columns · CSV →
Every query below runs in DuckDB once the file is a table: CREATE TABLE receivables AS SELECT * FROM read_csv('receivables.csv'); It is the same file this article used, faults included, so your results will match ours.

So we brought it the question every finance director gets round to asking: when does our cash pressure peak? The first query most of us would write groups the invoices marked overdue by the year they fall due:

SELECT year(due_date)         AS year_due,
       COUNT(*)               AS invoices,
       SUM(receivable_amount) AS labelled_overdue
FROM receivables
WHERE payment_status = 'Overdue'
GROUP BY year_due
ORDER BY labelled_overdue DESC
LIMIT 3;
year_dueinvoiceslabelled_overdue
204019644,001
203020598,510
203721579,825

Clearstone’s customers, it seems, are planning to be late thirteen years ahead. Most of us can’t plan a dentist’s appointment that far out. Drawn as a chart, the full result looks worse still.

Amounts labelled Overdue by the year they fall due, 2022 to 2041. The tallest bar is 2040, at €644,001, and most of the labelled money falls due after the review date of 1 January 2027.Labelled Overdue, by the year it falls duefalls due after the review date200k400k600k800k20222027203220372040: €644,001dashed line: the review date, 1 Jan 2027324 invoices labelled Overdue, 249 of them not due yet
Fig. 1 — The status column taken at its word. Everything right of the dashed line falls due after the review date of 1 January 2027, and that is where most of the “overdue” money sits.

Overdue is two dates, not one word

So the label is a rumour: most of what it calls Overdue isn’t even due on the date the books are reviewed to, 1 January 2027. Overdue is two facts, unpaid and past due, and the query needs that review date pinned as a literal to stand on, not CURRENT_DATE, which hands you a different answer every morning:

SELECT year(due_date)         AS year_due,
       COUNT(*)               AS invoices,
       SUM(receivable_amount) AS overdue
FROM receivables
WHERE payment_status <> 'Paid'
  AND due_date < DATE '2027-01-01'
GROUP BY year_due
ORDER BY year_due;
year_dueinvoicesoverdue (€, rounded)
202228739k
202334923k
202431709k
202534746k
202632898k

By the dates, about €4.0 million is overdue, and the year it piles up most is 2023, which at least has the decency to be in the past. (The overdue-by-date amounts in this article are rounded: the exact figures are yours to find in the Follow the Money guided project, where they’re the answers its checks look for.)

So cash pressure peaked in 2023? Not quite. That table answers “which year’s invoices are unpaid?”, and the question was when the pressure peaks. Pressure isn’t one year’s slice. It’s the whole pile, on the day you look at it.

Ask on more than one day

Here’s the move that turns one number into a history: treat the review date as a parameter, and ask the same question on many dates. DuckDB’s generate_series builds the dates, one every six months, and a join with an inequality in it (due_date < as_of_date) attaches every unpaid invoice to every review date it was already late on.

WITH as_of AS (
  SELECT CAST(d AS DATE) AS as_of_date
  FROM generate_series(DATE '2022-07-01', DATE '2027-01-01',
                       INTERVAL 6 MONTH) AS t(d)
)
SELECT a.as_of_date,
       COUNT(r.due_date)             AS still_unpaid,
       SUM(r.receivable_amount)      AS overdue
FROM as_of AS a
LEFT JOIN receivables AS r
       ON r.due_date < a.as_of_date
      AND r.payment_status <> 'Paid'
GROUP BY a.as_of_date
ORDER BY a.as_of_date;
as_of_datestill_unpaidoverdue (€, rounded)
2022-07-0116379k
2023-01-0128739k
2023-07-01431,179k
2024-01-01621,662k
2024-07-01771,944k
2025-01-01932,371k
2025-07-011122,780k
2026-01-011273,117k
2026-07-011453,667k
2027-01-011594,016k

Before reading anything into that last column, notice the one thing it can’t do: go down. The ledger holds each invoice’s status as it stands today, not the day it was paid, so “unpaid now and due before the review date” can only grow as the review date moves later. An invoice paid late in 2024 was overdue in 2023 and simply isn’t there. A line that rises every time is built into the method, so it proves nothing. What the method can show is how steeply it rises.

Unpaid invoices already past due, on ten review dates from July 2022 to January 2027. The amount rises every time, from about €0.38M to about €4.0M.Overdue and still unpaid, on each review date€1M€2M€3M€4M20232024202520262027≈ €4.0M1 Jan 2027first review date, 1 Jul 2022: about €0.38Mbefore 2027 a floor: invoices paid late since are not in it
Fig. 2 — The same question asked on ten review dates. With only today’s statuses the line can’t fall; what it shows is the slope, about €0.8 million of still-unpaid invoices for every year of due dates.

And the slope is steady: about €0.8 million for every year of due dates, with no year far out of line. A crisis would show up as a bump. This is a ramp, and the question it raises is the one the slide can’t answer, because it can’t see payments: does late money ever come in?

Do late payers ever pay?

If they did, you could see it without a single payment date. An invoice that fell due five years ago has had five years to be paid; one that fell due in November has had a few weeks. Customers who pay late, but do pay, leave the old invoices mostly settled and the young ones mostly open. So take every invoice that has fallen due, group it by how long ago, and count what’s still unpaid:

SELECT CASE
         WHEN DATE '2027-01-01' - due_date <= 365  THEN '0-1 years'
         WHEN DATE '2027-01-01' - due_date <= 730  THEN '1-2 years'
         WHEN DATE '2027-01-01' - due_date <= 1095 THEN '2-3 years'
         WHEN DATE '2027-01-01' - due_date <= 1461 THEN '3-4 years'
         ELSE '4-5 years'
       END                                       AS overdue_for,
       COUNT(*)                                  AS fell_due,
       COUNT(*) FILTER (WHERE payment_status <> 'Paid') AS unpaid,
       ROUND(100.0 * COUNT(*) FILTER (WHERE payment_status <> 'Paid')
             / COUNT(*), 1)                      AS pct_unpaid
FROM receivables
WHERE due_date < DATE '2027-01-01'
GROUP BY overdue_for
ORDER BY overdue_for;
overdue_forfell_dueunpaidpct_unpaid
0-1 years523261.5
1-2 years523465.4
2-3 years523159.6
3-4 years533464.2
4-5 years522853.8

Now read the last column from top to bottom: 61.5, 65.4, 59.6, 64.2 and 53.8 per cent. Invoices that have had five years to be paid are about as likely to be unpaid as invoices that have had one. The very first quarter of the ledger, January to March 2022, still has 9 of its 13 invoices unpaid. The last quarter of 2026 has 6 of 13.

In this ledger, late and never look exactly alike.

Share of invoices still unpaid, by how many years ago they fell due: under 1 years 61.5%, 1 to 2 years 65.4%, 2 to 3 years 59.6%, 3 to 4 years 64.2%, 4 to 5 years 53.8%. All five sit close to the overall 60.9 per cent.Still unpaid, by how long ago it fell due25%50%75%100%61.5%under 132 of 5265.4%1 to 234 of 5259.6%2 to 331 of 5264.2%3 to 434 of 5353.8%4 to 528 of 52dashed: all 261 fallen due, 60.9% unpaidyears since the due date, on 1 Jan 2027
Fig. 3 — If late payers paid late, these bars would shrink from left to right. They sit within a few points of the overall 60.9%, whatever their age.

That changes how to read the €4.0 million. Taken at face value, it’s sediment rather than a queue of payments on their way: each year of due dates has left between €0.71 million and €0.92 million behind, and nothing in the ledger suggests any of it has moved since. About €3.1 million, more than three-quarters of the pile, is over a year late. On that reading, Clearstone’s cash pressure doesn’t peak at all; it accumulates.

Three habits for anything “as of”
  1. Pin the as-of date in the query, as a literal or a parameter. CURRENT_DATE makes every report unrepeatable.
  2. Work status out from the dates. Treat the label as a claim to test, not a column to sum.
  3. Before you call something a peak, slide the as-of date, and remember that a snapshot of today’s statuses can only pile up as you slide it.
-- DuckDB: subtract the dates
DATE '2027-01-01' - due_date

-- SQLite: dates are text, so go through julianday()
julianday('2027-01-01') - julianday(due_date)

Date arithmetic is a dialect. We ran every query here in DuckDB 2.0.0 and cross-checked the overdue totals and the age table in SQLite 3.45.1; both gave the same numbers.

The other explanation

There is a second way to read Fig. 3, and the ledger lends it plenty of support. Look at how the three labels are spread on either side of the review date:

SELECT CASE WHEN due_date < DATE '2027-01-01'
            THEN 'fallen due' ELSE 'not yet due' END AS side,
       COUNT(*) AS invoices,
       ROUND(100.0 * COUNT(*) FILTER (WHERE payment_status = 'Paid')
             / COUNT(*), 1) AS pct_paid,
       ROUND(100.0 * COUNT(*) FILTER (WHERE payment_status = 'Pending')
             / COUNT(*), 1) AS pct_pending,
       ROUND(100.0 * COUNT(*) FILTER (WHERE payment_status = 'Overdue')
             / COUNT(*), 1) AS pct_overdue
FROM receivables
GROUP BY side
ORDER BY side;
sideinvoicespct_paidpct_pendingpct_overdue
fallen due26139.132.228.7
not yet due73931.035.333.7

Paid covers 39% of the invoices that have fallen due and 31% of those that haven’t, some of which aren’t due until 2041. Each label covers roughly a third on both sides of the line. A status column that looks much the same for last year’s invoices and for ones fourteen years off isn’t tracking payments closely, and a team that keeps it like that might never change Pending to Paid when a late payment finally lands.

If so, part of that €4.0 million is already in Clearstone’s bank under the wrong word, and Fig. 3 is a picture of a spreadsheet nobody updates. If not, it’s money the company will never see, sitting on the balance sheet as though it might turn up.

The ledger can’t tell those two stories apart. The bank statement could, but it isn’t in the files. So which is it at Clearstone: cash that never came, or cash that came and nobody wrote down?

References

On LucidPlexus
Elsewhere

Comments

No comments yet. Be the first to share your thoughts.