Smart Reminders & Late Fees
A multi-step dunning workflow on top of the basic "X days before due" reminder that already ships in Pro. Add steps, customize the email template per step, auto-apply a late fee after N days overdue, and optionally offer an early-payment discount.
When to use it
- You're losing cashflow to overdue invoices
- You don't have time to manually chase clients
- You want fees / discounts handled consistently across every invoice
The single highest-ROI feature in the Pro plan. Typical recovery on previously-written-off invoices is 15–25% in the first month.
How it differs from the built-in reminder
Easy Invoice Pro already ships a basic "remind me X days before due date" feature. Smart Reminders adds three things that don't exist in core:
| Feature | Core reminder | Smart Reminders addon |
|---|---|---|
| Steps | 1 (pre-due only) | Unlimited (pre + post due) |
| Per-step email template | No | Yes |
| Auto-apply late fee | No | Yes (% or flat) |
| Early-payment discount | No | Yes |
| "Run now" button | No | Yes |
| Per-invoice progress tracking | No | Yes (last step + late-fee flags) |
You can run both simultaneously — they don't conflict on cron hook names or meta keys.
Enabling
- Easy Invoice → Addons
- Find Smart Reminders & Late Fees
- Click Activate
- Open via the Settings → link or navigate to Easy Invoice → (sidebar) → Smart Reminders (slug:
easy-invoice-addon-dunning)
The addon registers one daily WP-Cron event: easy_invoice_dunning_daily_tick.
Configuring the workflow
Reminder Schedule
Each row in the schedule is one step. A step is "send this email to the client when the invoice is X days from its due date." Offset days are relative to the invoice's due date — negative = before, positive = after.
Default schedule shipped with the addon:
| Step | Offset | Tone |
|---|---|---|
| 1 | -3 days | "Friendly reminder — your invoice is due in 3 days" |
| 2 | +1 day | "Your invoice is overdue" |
| 3 | +7 days | "Second notice" |
| 4 | +14 days | "Final notice" |
Each step has a Subject and a Body with smart-tag interpolation. Available tags:
{{invoice_number}} {{client_name}} {{total_amount}}
{{due_date}} {{invoice_url}} {{company_name}}Add as many steps as you want with Add step, reorder by changing offset values (rows are sorted on save). Each step fires exactly once per invoice — the addon records the last-sent step in _easy_invoice_dunning_last_step so a step is never re-sent.
Late Fee
Auto-apply a fee after N days overdue:
| Option | Default | What it does |
|---|---|---|
| Enable late fees | off | Master switch |
| Fee type | percent | percent of invoice total, or flat currency amount |
| Amount | 5 | Percent (if percent) or flat currency (if flat) |
| Apply after (days) | 7 | Days overdue before the fee is calculated and stored |
The calculated fee is stored in invoice meta _easy_invoice_late_fee_amount and _easy_invoice_late_fee_applied_at. To surface it on the rendered invoice, the addon hooks two filters automatically:
easy_invoice_calculate_total ⟶ adds fee to total
easy_invoice_extra_line_items ⟶ appends fee as a line itemThat means the late fee shows up automatically on the invoice — no extra wiring needed.
Early-Payment Discount
Encourage clients to pay before due date:
| Option | Default | What it does |
|---|---|---|
| Enable discount | off | Master switch |
| Discount percent (%) | 2 | Percentage off |
| Within (days of issue) | 7 | Pay within N days of the issue date to qualify |
The discount is offered in your reminder emails (use a smart-tag in the body) and recorded as discount-related meta when honoured — full automation of discount enforcement requires payment-gateway integration and is on the roadmap.
Running it
Daily cron
The hook easy_invoice_dunning_daily_tick runs once per day. WP-Cron is "pseudo-cron" — it runs on real page loads, so a quiet site may run a day late. If you have real cron set up (wp-cron.php from system cron), the run is exactly on schedule.
"Run now"
The Run now button (top-right of the addon page) calls the same Service::runDailyTick() synchronously. Useful when you've just changed the schedule and want to see immediate results. The redirect shows a banner like:
Dunning run complete. Scanned 247 invoice(s) — sent 12 reminder(s), applied 3 late fee(s).
Behaviour at scale
The tick is designed for thousands of invoices:
- Batched query: invoices are loaded 50 at a time (
posts_per_page=50,paged=N). Neverposts_per_page=-1. - SQL-level filter: only invoices whose
_easy_invoice_due_date <= today + max_pre_offset_daysare loaded — the runner never touches paid invoices, cancelled invoices, or invoices whose first reminder is still weeks away. - Wall-clock budget: by default 25 seconds. If the runner can't finish the queue in that time, it stops at a batch boundary and the next cron picks up where it left off (per-invoice progress is durable via
_easy_invoice_dunning_last_step). - Per-invoice progress: re-runs are idempotent — a step that already fired for an invoice is skipped.
Filters for tuning:
// How many invoices to load per query (default 50)
add_filter('easy_invoice_dunning_batch_size', fn() => 100);
// How long each tick may run (default 25s)
add_filter('easy_invoice_dunning_tick_budget_seconds', fn() => 45);Permissions
Anyone with manage_options can use the addon. With Team Members & Audit Log enabled, every reminder send and fee application is logged.
Hooks for developers
| Hook | Type | When |
|---|---|---|
easy_invoice_dunning_email_sent (invoice_id, step, recipient_email) | action | Just after a reminder email is wp_mail()'d |
easy_invoice_dunning_late_fee_applied (invoice_id, fee, settings) | action | Just after a late fee is stored on an invoice |
easy_invoice_dunning_after_tick (reminders_sent, fees_applied, scanned) | action | End of every daily tick |
easy_invoice_dunning_batch_size | filter | Override the per-batch invoice count |
easy_invoice_dunning_tick_budget_seconds | filter | Override the tick wall-clock budget |
Example — Slack notification on every late fee:
add_action('easy_invoice_dunning_late_fee_applied', function ($invoice_id, $fee, $settings) {
$client = get_post_meta($invoice_id, '_easy_invoice_customer_name', true);
$msg = sprintf('Late fee of %.2f applied to invoice #%d (%s)', $fee, $invoice_id, $client);
wp_remote_post('https://hooks.slack.com/services/…', [
'body' => wp_json_encode(['text' => $msg]),
]);
}, 10, 3);Smart tag reference
The body and subject of each reminder step support these tags. They're interpolated server-side just before wp_mail():
| Tag | Source |
|---|---|
| _easy_invoice_number meta, or #{ID} if missing |
| _easy_invoice_customer_name meta |
| _easy_invoice_total, formatted with 2 decimals |
| _easy_invoice_due_date, formatted via the site's date_format |
| get_permalink($invoice_id) |
| easy_invoice_company_name option |
The body is run through nl2br() so plain-text newlines become <br>. HTML in the body is allowed (kept by wp_kses_post on save).
Coexistence with the built-in Pro reminder
The basic Pro reminder uses cron hook easy_invoice_send_payment_reminders and option easy_invoice_payment_reminder_settings. Smart Reminders uses cron hook easy_invoice_dunning_daily_tick and option easy_invoice_dunning_settings. No conflict — but you might double-send if both are configured to email at the same offset. If you've enabled the addon, consider disabling the built-in reminder under Settings → Email → Payment Reminder.
Troubleshooting
"Run now" reports 0 reminders sent
Most common causes:
- The schedule's offset has no step matching today's
today − due_datefor any invoice - Invoices are missing a
_easy_invoice_due_datemeta value - Invoices are already
paid/cancelled/refunded(excluded by design) - Each matching step has already been sent for the matching invoice (per
_easy_invoice_dunning_last_stepmeta)
The "Scanned N invoice(s)" count in the banner tells you how many invoices the SQL pre-filter let through — if Scanned is 0, your filter cut everything out (often a missing due date).
Cron isn't firing
WP-Cron only runs when someone visits the site. Add real cron:
*/5 * * * * curl -fsS https://your-site.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1Then disable WP's built-in cron:
// wp-config.php
define('DISABLE_WP_CRON', true);See Troubleshooting → WP-Cron.
Late fee doesn't show on invoice
The fee writes to _easy_invoice_late_fee_amount meta and the addon's two filters surface it. If you've replaced the invoice render pipeline with a custom template, you may need to call those filters yourself:
$total = apply_filters('easy_invoice_calculate_total', $base_total, $invoice_id);
$items = apply_filters('easy_invoice_extra_line_items', $items, $invoice_id);See also
- Email & notifications — base email config the reminders ride on
- Payments — how status flips to
paid(which stops dunning) - Webhooks & Zapier Bridge — fire a webhook when a reminder is sent
- Addons overview