πΌ Business Problem
Many finance teams still manually send overdue invoice reminders to customers. This leads to inconsistent follow-ups, cash flow delays, and missed collections.
NetSuite already provides aging reports, but thereβs no built-in automated dunning email system unless you use the Advanced Dunning feature (available in SuiteBilling). For many businesses, a simple scripted automation can achieve 90% of that power with no extra licensing.
Goal:
Automatically send email reminders to customers with overdue balances based on their aging bucket (e.g., 30, 60, 90+ days) and include a summary or attached invoices for easy payment.
π§ Approach
- Saved Search finds customers with open invoices and overdue balances.
- Map/Reduce Script (or Scheduled Script) loops through results and groups invoices by customer.
- Based on aging bucket (1β30, 31β60, 61β90, >90), the script:
- Chooses an email template (polite β firm).
- Optionally attaches overdue invoices as PDFs.
- Sends the email from a subsidiary-specific sender.
- Updates a custom field like
custentity_last_dunning_date.
- Optional: log summary in a custom Dunning Log record for audit trail.
π§Ύ Saved Search Example
Name: Overdue Invoices by Aging Bucket
Type: Transaction
Filters:
- Type = Invoice
- Status = Open
- Amount Remaining > 0
- Main Line = T
Results:
- Customer Internal ID
- Customer Email
- Aging (Days Overdue)
- Amount Remaining
- Tran ID
- Due Date
- Subsidiary
Search ID: customsearch_overdue_invoices_aging
π§© SuiteScript 2.1 β Map/Reduce Example
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
* Title: MR | Auto-Dunning Emails by Aging Bucket
* Author: The NetSuite Pro
*/
define(['N/search','N/email','N/runtime','N/render','N/record','N/log'], (search, email, runtime, render, record, log) => {
const SEARCH_ID = 'customsearch_overdue_invoices_aging';
const PARAM_SENDER = 'custscript_dunning_sender_id';
const getInputData = () => search.load({ id: SEARCH_ID });
const map = (ctx) => {
const r = JSON.parse(ctx.value);
const custId = r.values['internalid.CUSTENTITY_CUSTOMER'] || r.values.entity?.value || null;
const emailAddr = r.values.email || '';
const invoiceId = r.id;
const aging = Number(r.values.formulanumeric || r.values.daysoverdue || 0);
ctx.write({
key: custId,
value: { invoiceId, emailAddr, aging }
});
};
const reduce = (ctx) => {
const custId = ctx.key;
const data = ctx.values.map(JSON.parse);
const recipient = data[0].emailAddr;
if (!recipient) return;
const agingMax = Math.max(...data.map(d => d.aging));
const bucket = getBucket(agingMax);
const subject = buildSubject(bucket);
const body = buildBody(bucket, data.length);
try {
const sender = Number(runtime.getCurrentScript().getParameter({ name: PARAM_SENDER })) || runtime.getCurrentUser().id;
const attachments = data.map(d => render.transaction({ entityId: d.invoiceId, printMode: render.PrintMode.PDF }));
email.send({
author: sender,
recipients: recipient,
subject,
body,
attachments
});
record.submitFields({
type: 'customer',
id: custId,
values: { custentity_last_dunning_date: new Date() },
options: { enableSourcing: false, ignoreMandatoryFields: true }
});
log.audit(`Dunning sent to Customer ${custId}`, { bucket, recipient });
} catch (e) {
log.error('Reduce Error', { custId, e });
}
};
const summarize = (summary) => {
log.audit('Dunning Summary', {
usage: summary.usage,
yields: summary.yields,
concurrency: summary.concurrency
});
};
// ---------- Helpers ----------
function getBucket(days) {
if (days <= 30) return '1-30';
if (days <= 60) return '31-60';
if (days <= 90) return '61-90';
return '90+';
}
function buildSubject(bucket) {
return `Payment Reminder (${bucket} Days Overdue)`;
}
function buildBody(bucket, count) {
const tone = {
'1-30': 'friendly',
'31-60': 'gentle',
'61-90': 'firm',
'90+': 'urgent'
}[bucket] || 'friendly';
return `
<p>Dear Customer,</p>
<p>This is a ${tone} reminder that you have <b>${count}</b> outstanding invoice(s) now ${bucket} days past due.</p>
<p>Please find the invoices attached and remit payment at your earliest convenience.</p>
<p>Thank you for your prompt attention to this matter.<br/>Accounts Receivable</p>
`;
}
return { getInputData, map, reduce, summarize };
});
β Testing Checklist
- Create a few test customers with open invoices aged beyond 30, 60, 90 days.
- Set up a test email (e.g., your sandbox Gmail).
- Run the script manually β confirm proper bucket and email tone.
- Ensure PDFs attach correctly.
- Add logging via
custentity_last_dunning_dateand verify it updates.
π§© Optional Enhancements
| Feature | Description |
|---|---|
| Dunning Templates by Subsidiary | Create different email tone templates by subsidiary or language. |
| Custom Record Log | Save each sent dunning event for compliance tracking. |
| Aging Tiers Auto-Escalation | If the same customer appears in β90+β twice, escalate to finance manager. |
| Payment Links | Embed payment links directly into email (e.g., Stripe/PayPal). |
| Bulk PDF Summary | Attach a single PDF listing all overdue invoices instead of multiple files. |
| Email CC Rules | Add cc for Sales Rep or Finance Manager automatically. |
π Summary
| Goal | Result |
|---|---|
| Automatically notify customers with overdue invoices | β Dunning emails sent by bucket |
| Include invoice PDFs and tailored messaging | β Branded reminders per customer |
| Improve AR cash flow & team efficiency | β Consistent follow-up without manual effort |