AR aging + collections — get paid on time
A 4-bucket aging snapshot per client + firm-wide. Auto-overdue cron flips sent → overdue. Dunning chain via M17. Collection workflow with last-contact tracking. Dashboard tile shows total AR + aged. The single biggest predictor of firm cashflow health.
The 4 aging buckets
| Bucket | Days past due | Colour | Action |
|---|---|---|---|
| Current | not yet due | Green | None — invoice sent, payment within terms expected |
| 1-30 days | 1-30 | Yellow | 1st reminder · friendly nudge |
| 31-60 days | 31-60 | Orange | 2nd reminder · firmer · phone call |
| 60+ days | 61+ | Red | Partner escalation · legal review · payment plan or write-off |
Where AR aging surfaces
- Dashboard → Pulse panel — total AR + bucket breakdown
- Client detail → Billing card — 4-bucket bar per client + balance + advance
- M11 invoices list → Aging filter — quick view of overdue items
- Reports → AR Aging Detail — full report, exportable to CSV/PDF
- Reports → AR Aging Summary by Client — one row per client, 4 columns
Auto-overdue cron
InvoiceStateService::markOverdueDue() runs nightly at 02:00 Asia/Muscat. Logic:
FOR EACH invoice WHERE
doc_type = 'tax_invoice' AND
status IN ('sent', 'partially_paid') AND
balance_due > 0 AND
due_date < CURDATE() AND
overdue_flagged_at IS NULL
UPDATE status TO 'overdue'
STAMP overdue_flagged_at = NOW()
QUEUE M17 dunning email (template: invoice.payment_reminder)
NOTIFY manager + accountant + partner
AUDIT LOG entry
Dunning chain
The reminder cascade kicks in once an invoice goes overdue:
| Days overdue | Email template | Tone |
|---|---|---|
| 1 (auto on flip) | invoice.payment_reminder | Friendly: "We notice payment is now past due..." |
| 15 | invoice.payment_reminder.firm | Firmer: "Payment remains outstanding. Please confirm a settlement date." |
| 30 | invoice.payment_reminder.escalation | Escalation: "Partner-level review pending. Settle within 7 days to avoid further action." |
| 60+ | (manual partner letter) | Legal review · payment plan · write-off discussion |
Idempotent: each cron checks invoice.last_dunning_sent_at and skips if < 14 days ago. Configurable per firm via m11.dunning_intervals_days.
Collection workflow
Each overdue invoice gets a "Collection log" panel:
- Last contacted — when, by whom, method (call/email/whatsapp/visit)
- Promised payment date — what client committed to
- Notes — what they said, blockers, escalation history
- Next action — call back, send formal letter, escalate to partner
Roll-up across clients = the firm's collection pipeline.
Step-by-step — handling an overdue invoice
Cron flips status to overdue
2am Asia/Muscat run. Status auto-flips. Notification fires. M17 reminder queued.
Accountant reviews next morning
Reports → AR Aging Detail → 1-30 bucket. Click into each invoice.
Contact client
Pick method (phone preferred for first chase). Note the call's outcome in Collection log.
Client commits to a date
Set "Promised payment date" + notes ("Client confirmed payment by 15-Apr per call with their CFO").
Track
If date passes without payment → escalate. Re-contact. Update notes. Eventually escalate to partner.
Resolution
Either: payment arrives → allocate via normal flow → status auto-flips back to paid. Or: write-off after exhausting collection → reason captured + audit-logged.
Dashboard AR tile
The cashflow forecast (13-week)
Dashboard → 13-week cashflow forecast (Chart.js stacked bar):
- Committed band (navy) — outstanding balance_due on sent/partially_paid/overdue tax invoices, bucketed by due_date into Sunday-anchored weeks
- Expected band (teal) — sent/revised quotes weighted at 50% probability, bucketed by valid_until (or issue_date+30 if no valid_until)
- Overdue collapse — overdue invoices show as week 0 (the past)
Partner uses this for cashflow planning. If week 4 shows OMR 50k expected against OMR 70k operating costs, decisions get made (chase collections, defer expenses, draw credit line).
Open Reports → AR Aging Detail. Sort by days_overdue descending. The top 5 rows are your firm's risk concentration. Walk each one with the partner — for each, decide: chase / payment plan / write-off / legal. Every Friday morning. This is the single highest-leverage hour in the firm.
The overdue cron only runs nightly. If you need same-day flagging (e.g. Monday morning after a Sunday deadline), run php bin/cron-m11-overdue.php manually. Idempotent — running twice doesn't double-flag. Useful when partners want fresh AR data before Monday partner meetings.
Healthy audit-firm AR: < 15% in 31-60 bucket, < 5% in 60+ bucket. Above those = pricing or collection issue. Track week-over-week. If 60+ creeps up 3 weeks in a row, it's now a collections crisis — escalate to partner, change collection cadence.