← all case studies · Sebenza · Billing Cron · PR #388 + #393

Frozen for Your Customer's Sins

In a multi-tenant system, one invoices table holds two different things with the same organization_id: the platform's charges to the business, and the business's invoices to its own customers. A daily dunning cron forgot the one column that tells them apart — and suspended businesses because their customers paid late. Run it yourself, in both modes.

1 · Run the daily cron

Meet Dlamini Plumbing: their Sebenza subscription is paid, and they've issued two invoices to their own customers — one of which is 6 days overdue, because Mrs Naidoo is slow with EFTs. The cron scans for unpaid invoices older than the 5-day grace period. Watch which rows it matches.

-- before: which org has unpaid, overdue invoices? (NO is_platform filter) JOIN invoices i ON i.organization_id = o.id WHERE i.status_id IN ('sent','overdue') AND i.due_date < NOW() - INTERVAL '5 days' -- matches the tenant's CUSTOMER invoices → subscription_status = 'frozen'

2 · Why the bug was invisible

TRAP 1

Same table, same org id

Platform invoices and the tenant's customer invoices are rows in one table, joined by the same organization_id. Only is_platform separates “what you owe us” from “what your customers owe you”.

TRAP 2

The statuses never overlapped

Platform invoices are inserted paid or pending — never sent. So the cron's status IN ('sent','overdue') could only ever match customer invoices. The wrong behaviour was the only behaviour.

TRAP 3

Silent dunning death

The first fix scoped the join to is_platform = true — correct, but since platform rows are never sent, the cron now matched nothing: no dunning at all. Adding pending to the status set brought it back.

FIX 4

Warn before you freeze

The only email on this path fired at the moment of suspension. Now the owner gets a dated warning every day of the grace period — amount, days overdue, the exact freeze date, and “your data is not deleted”.

The rule this minted: every scheduled query that touches invoices states its is_platform stance explicitly, in the SQL, with a comment. In a multi-tenant table, forgetting the discriminator doesn't fail loudly — it targets someone else's data and succeeds.

3 · The receipts

What shipped · Sebenza PRs #388 + #393 · 2026-08-27
foundcheck-overdue (daily 09:00 SAST, live in prod) froze tenants over customer invoices; child orgs cascade-frozen; “Account Frozen” email sent
foundsibling cron emailed tenants' CUSTOMERS with the platform's own dunning copy + a Pay-Now link they cannot log into
fixedis_platform = true + 'pending' in the status set — dunning works, tenants untouched
fixedcustomerInvoiceReminder template — speaks as the business, threatens nothing
fixedplatformPaymentOverdue warning — every grace day, before any freeze; 12 tests pin warn-before-freeze
Sebenza · billing-cron check-overdue · found by guide research, proven by tests, shipped same day