Key Takeaway
Automating accounts receivable in a custom ERP comes down to four linked jobs: send every invoice with a payment link, run a time-zone-aware reminder schedule from T-7 to T+30, bucket open invoices into an aging report, and track DSO. That turns spreadsheet and WhatsApp chasing into configuration, leaving the finance team only the exceptions.
In many of the Indonesian SMEs I have worked with, 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 a client's custom ERP as part of my freelance ERP consulting work, 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.
The AR Lifecycle: What Needs to Be Automated
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.
Invoice Generation and PDF Delivery
The first step is generating a professional PDF invoice and delivering it to the customer via email. In the NestJS implementation I built, I 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. For a PKP seller this is not the tax invoice: PP 44/2022 Article 26 also requires a Faktur Pajak at delivery, issued through DJP's systems (Coretax since 1 January 2025), so the AR record stores its number.
Reminder Schedule Design
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. In my freelance ERP consulting work, I integrated a local payment gateway (Midtrans): customers pay from the email by virtual account, QRIS, e-wallet or card, and Midtrans notifies the ERP, so nobody matches transfers by reference number. Midtrans accepts each order_id only once, so create one link per invoice and reuse it in every reminder.
Implementing the Scheduler in NestJS
NestJS's @nestjs/schedule module provides a cron-based scheduler that runs inside the application process. I set it up to run the AR reminder job every morning at 8 AM WIB (UTC+7) by passing timeZone: 'Asia/Jakarta' to the @Cron decorator; without it, the underlying cron library uses the server's local time. 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.
Aging Report Generation
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. I 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, { timeZone: 'Asia/Jakarta' })
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`
);
}
}Payment Recording and Reconciliation
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 (UTC+7), WITA (UTC+8) and WIT (UTC+9) — and server time is often UTC. A cron meant for 08:00 that runs on UTC fires at 15:00 WIB, 16:00 WITA and 17:00 WIT, late in the customer's working day, and between 17:00 and midnight UTC the UTC date is already a day behind WIB. Always store due dates as DATE (not TIMESTAMP) in PostgreSQL and explicitly convert to the customer's timezone when computing overdue status.
Reporting and Dashboard Integration
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.
Results After 6 Months of Production
After running the automated AR system in production for a client for six months, the finance team spent far less time on manual follow-up, fewer invoices slipped past their due date, and collection time shortened. One client's percentages will not transfer to your business, so record DSO, the share of invoices paid late and weekly follow-up hours before go-live, then compare. 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.
Sources & Further Reading
- NestJS Docs — Task Scheduling with @nestjs/schedule
- TypeORM — Official Documentation for PostgreSQL integration
- Midtrans — Create Payment Link API reference
- cron (kelektiv/node-cron) — timeZone option, default local time
- JDIH BPK — PP No. 44 Tahun 2022 (Article 26: Faktur Pajak)
- DJP — Keterangan Tertulis Implementasi Coretax DJP

