# Overdue invoice follow-up

Published by Croft · Category: Finance

Every Monday, find overdue invoices and email capped reminders to clients.

Runs each Monday at 9:00 a.m. America/New_York. It checks invoice due dates, finds up to 100 overdue invoices, and emails clients with an address on file. Requires an email connection and does not modify invoice records.

## Requirements

Requires an Email connection. Runs Mondays at 9am America/New_York.

### Connections

- Email — send (write)

### Record streams

- An unspecified record stream (read)

### Not statically known

- Line 54: the iterated value does not trace back to a recorded step result, so the branch count is unknown until run time

## Source

#### `index.ts`

```typescript
import { schedule, workflow } from "@croft/sdk";

export default workflow("overdue-invoice-follow-up", {
  trigger: schedule("0 9 * * MON", { timezone: "America/New_York" }),
  connections: ["email"],
  description: "Every Monday morning, finds overdue invoices and emails a follow-up reminder to each client with an email address. It does not change invoice records.",
}, async (ctx) => {
  const asOf = ctx.run.date;
  const asOfDate = new Date(asOf + "T00:00:00.000Z");
  const minimumDaysOverdue = 1;
  const maxResults = 100;
  const queryLimit = maxResults + 1;

  const result = await ctx.step("Find overdue invoices", async (ctx) => {
    return await ctx.records.query(
      `SELECT i.id, i.number, i.due_on, i.status,
              c.name AS client_name, c.email AS client_email
       FROM invoices AS i
       LEFT JOIN clients AS c ON c.id = i.client_id
       WHERE i.due_on IS NOT NULL
         AND CAST(i.due_on AS DATE) <= CAST('${asOf}' AS DATE) - ${minimumDaysOverdue}
         AND LOWER(COALESCE(i.status, '')) NOT IN ('paid', 'cancelled', 'canceled', 'void', 'draft')
         AND COALESCE(i._op, 'live') = 'live'
       ORDER BY i.due_on ASC
       LIMIT ${queryLimit}`,
    );
  });

  const hasMore = result.rows.length > maxResults;
  const columnIndex = Object.fromEntries(result.columns.map((column, index) => [column, index]));
  const reminders = result.rows.slice(0, maxResults).map((row) => {
    const invoiceId = String(row[columnIndex.id]);
    const invoiceNumber = String(row[columnIndex.number] ?? invoiceId);
    const clientName = String(row[columnIndex.client_name] ?? "there");
    const clientEmail = row[columnIndex.client_email] == null ? null : String(row[columnIndex.client_email]).trim();
    const dueOn = String(row[columnIndex.due_on]);
    const dueDate = new Date(dueOn + "T00:00:00.000Z");
    const daysOverdue = Math.max(0, Math.floor((asOfDate.getTime() - dueDate.getTime()) / 86400000));
    return {
      invoiceId,
      invoiceNumber,
      clientName,
      clientEmail,
      dueOn,
      daysOverdue,
      subject: `Follow-up: Invoice ${invoiceNumber} is overdue`,
      bodyMarkdown: `Hello ${clientName},\n\nOur records show that invoice ${invoiceNumber}, due ${dueOn}, is ${daysOverdue} day(s) overdue. If you have already sent payment, please disregard this note. Otherwise, please let us know if you need another copy of the invoice.\n\nThank you.`,
    };
  });

  const delivery = await ctx.step("Send reminder emails", async (ctx) => {
    const sent: string[] = [];
    const skipped: string[] = [];
    for (const reminder of reminders) {
      if (!reminder.clientEmail) {
        skipped.push(reminder.invoiceNumber);
        continue;
      }
      await ctx.email.send("email", {
        to: reminder.clientEmail,
        subject: reminder.subject,
        bodyMarkdown: reminder.bodyMarkdown,
      });
      sent.push(reminder.invoiceNumber);
    }
    return { sent, skipped };
  });

  return {
    asOf,
    minimumDaysOverdue,
    returnedCount: reminders.length,
    hasMore,
    emailsSent: delivery.sent.length,
    sentInvoices: delivery.sent,
    skippedMissingEmail: delivery.skipped,
    note: "Scheduled follow-up emails were sent to clients with an email address. No invoice records were changed.",
  };
});
```

## Install

Install this blueprint: https://croft.now/blueprints/croft/overdue-invoice-follow-up/install

---
View online: https://croft.now/blueprints/croft/overdue-invoice-follow-up
