Automating Accounts Receivable in a Custom ERP: Reminders, Aging Reports, and Payment Tracking

Photo by Unsplash

Photo by Unsplash
In most Indonesian SMEs, accounts receivable management is handled through a combination of Excel spreadsheets and manual WhatsApp follow-ups. The finance team tracks who owes what, sends reminders by hand, and spends hours each month reconciling payment receipts against outstanding invoices. When I built the AR module for Commsult Indonesia's custom ERP, the goal was to automate the entire receivables lifecycle — from invoice generation to overdue escalation — so the finance team could focus on exceptions rather than routine follow-up.
A complete AR automation covers four stages: invoice creation and delivery, automated payment reminders at defined intervals, overdue detection and escalation, and payment recording with reconciliation. Each stage produces events that trigger the next, creating a self-running loop that only requires human intervention for exceptions — disputed invoices, partial payments that don't match, or customers who need a custom payment arrangement.
The first step is generating a professional PDF invoice and delivering it to the customer via email. In our NestJS implementation, we use Puppeteer to render an HTML template to PDF — this gives full design control without the limitations of PDF libraries. The HTML template pulls data from the invoice entity and renders it via a headless Chrome instance. The resulting PDF is stored in Google Cloud Storage and a download link is included in the delivery email.
The reminder schedule is configurable per customer payment term. For a NET_30 customer: a friendly reminder at T-7, a firmer reminder at T-3, an overdue notice at T+0, a follow-up at T+7, and a credit hold notification at T+30 overdue. Different email templates are used at each stage — the tone escalates from friendly to formal, and the T+30 email copies the sales account manager to prompt manual intervention.
Accounts Receivable Automation Flow
Invoice Created
│
▼
┌──────────────────┐
│ Generate PDF │◄── NestJS PDF service (Puppeteer)
│ & Send Email │──► Customer inbox
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Due Date T-7 │──► Reminder email #1 (automated)
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Due Date T-3 │──► Reminder email #2 (escalation tone)
└────────┬─────────┘
│
▼
┌──────────────────┐ ┌─────────────────────┐
│ Due Date T+0 │─────►│ Mark as OVERDUE │
└────────┬─────────┘ │ Notify AR Manager │
│ └─────────────────────┘
▼
┌──────────────────┐
│ T+7 Overdue │──► Manual follow-up flag
│ T+30 Overdue │──► Credit hold trigger
└──────────────────┘Always include a direct payment link in reminder emails. We integrated with a local payment gateway (Midtrans) so customers can pay immediately from the email. Invoices with payment links get paid 40% faster than those requiring a manual bank transfer with a reference number.
NestJS's @nestjs/schedule module provides a cron-based scheduler that runs inside the application process. We run the AR reminder job every morning at 8 AM WIB (UTC+7). The job queries for invoices due in 7 days, 3 days, and 0 days, and for invoices already overdue. For each matched invoice, it sends the appropriate email template, updates the invoice status, and logs the action to the audit trail.
The aging report categorizes all outstanding invoices by how overdue they are: current (not yet due), 1–30 days, 31–60 days, 61–90 days, and 90+ days. We generate this report on demand (real-time query) and also pre-compute a daily snapshot stored in a summary table for performance. The PostgreSQL query uses CASE WHEN on the date difference between today and the due date to bucket each invoice.
// NestJS: AR automated reminder scheduler
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThanOrEqual, In } from 'typeorm';
import { Invoice } from './invoice.entity';
import { EmailService } from '../email/email.service';
import { addDays, startOfDay } from 'date-fns';
@Injectable()
export class ArReminderService {
private readonly logger = new Logger(ArReminderService.name);
constructor(
@InjectRepository(Invoice)
private invoiceRepo: Repository<Invoice>,
private emailService: EmailService,
) {}
@Cron(CronExpression.EVERY_DAY_AT_8AM)
async sendReminders() {
const today = startOfDay(new Date());
// T-7: first reminder
const upcomingInvoices = await this.invoiceRepo.find({
where: {
dueDate: addDays(today, 7),
status: In(['SENT', 'PARTIALLY_PAID']),
},
relations: ['customer'],
});
for (const invoice of upcomingInvoices) {
await this.emailService.sendArReminder(invoice, 'REMINDER_7_DAYS');
this.logger.log(`Sent T-7 reminder for invoice ${invoice.invoiceNumber}`);
}
// T+0: mark overdue
const overdueInvoices = await this.invoiceRepo.find({
where: {
dueDate: LessThanOrEqual(today),
status: In(['SENT', 'PARTIALLY_PAID']),
},
relations: ['customer'],
});
for (const invoice of overdueInvoices) {
await this.invoiceRepo.update(invoice.id, { status: 'OVERDUE' });
await this.emailService.sendArReminder(invoice, 'OVERDUE_NOTICE');
}
this.logger.log(
`Processed ${upcomingInvoices.length} reminders, ${overdueInvoices.length} overdue`
);
}
}When a customer makes a payment, finance staff records it in the ERP against the invoice. The system supports partial payments — an invoice can have multiple payment records until it's fully settled. When an invoice reaches zero balance, it automatically transitions to PAID status and a receipt is emailed to the customer. For bank transfers, reconciliation is semi-automated via bank statement import.
Be careful with timezone handling in the scheduler. Indonesia has three time zones (WIB, WITA, WIT) and server time is often UTC. If your reminder emails go out at 3 AM local time because the cron runs at 08:00 UTC, customers will receive them before their workday starts — which looks unprofessional. Always store due dates as DATE (not TIMESTAMP) in PostgreSQL and explicitly convert to the customer's timezone when computing overdue status.
Beyond the aging report, the AR module feeds into the finance dashboard with three key metrics: Days Sales Outstanding (DSO), collection rate by period, and overdue amount by customer tier. DSO is the most important metric — it tells you how many days on average it takes to collect payment after invoicing. A rising DSO signals a collections problem before it becomes a cash flow crisis.
After running the automated AR system in production at Commsult Indonesia for 6 months: the finance team's time spent on manual follow-up dropped by roughly 70%. Late payment rate decreased from around 45% to 22%. Average collection time improved from 38 days to 26 days. The biggest win was not the automation itself but the visibility — finance managers could see the full AR picture in real time rather than compiling it manually in Excel at month-end.