# Invoice Payment Reminders

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 79  
**Tier:** All tiers  
**Depends on:** `invoices-core`, `email-template-editor`, `system-communications-notifications`, `invoice-payment-link-generation`, `foundation-auth-rbac`  
**Referenced by:** `invoices-core`, `email-template-editor`

---

## Overview

Automated reminder emails for unpaid invoices. Tenants configure a reminder schedule; cron checks daily and sends reminder emails via the email system. Covers three standard reminders (before due, on due, overdue) plus a configurable sequence. Staff can also send one-off manual reminders from the invoice detail view.

---

## Trigger Conditions

An invoice is eligible for automated reminders when:
- `status IN ('SENT', 'APPROVED', 'TAX_ISSUED', 'PARTIALLY_PAID')` — `PARTIALLY_PAID` is treated like `TAX_ISSUED` (still chasing the balance; per spec 80)
- `invoices.due_date IS NOT NULL`
- Tenant has `reminder_enabled = true` in `tenant_settings`
- `invoices.reminders_disabled = false` (per-invoice opt-out)

---

## Reminder Schedule

Default schedule (all relative to `due_date`):

| Stage | When | Subject |
|-------|------|---------|
| Pre-due reminder | 3 days before `due_date` | "Invoice due in 3 days — {invoiceNumber}" |
| Due-day reminder | On `due_date` | "Invoice due today — {invoiceNumber}" |
| Overdue reminder 1 | 7 days after `due_date` | "Invoice overdue — {invoiceNumber}" |
| Overdue reminder 2 | 14 days after `due_date` | "Follow-up: Invoice overdue — {invoiceNumber}" |
| Overdue reminder 3 | 30 days after `due_date` | "Final notice: Invoice overdue — {invoiceNumber}" |

Tenant can enable/disable individual stages and change day offsets in `/settings/invoicing`.

### Boundary with Dunning (spec 93)

This spec and `payment-retry-dunning` (spec 93) both email customers about unpaid invoices. To prevent a single invoice triggering **both** engines on the same day, the boundary is **by offset relative to `due_date`**:

- **Reminders (this spec) own the pre-due / at-due window** — gentle nudges at `offset_days <= 0` (the `-3` and `0` stages above). These always run on all tiers.
- **Dunning (spec 93, Business+) owns the post-due escalation window** — `offset_days > 0` (its `+3 / +7 / +14 / +21` schedule).

**Dedup guard:** the overdue stages of *this* spec (`+7 / +14 / +30`) are **only active when dunning is not running for the tenant** — i.e. the tenant is on a tier without dunning (Freelancer) **or** has `dunning` disabled (no `dunning_schedules` rows). When dunning is active, this spec's cron skips every `offset_days > 0` stage, leaving post-due escalation entirely to spec 93. This keeps Freelancer tenants covered for overdue follow-up while guaranteeing Business+ invoices are never double-emailed. The guard is implemented in the cron query below.

---

## Data Model

```sql
ALTER TABLE invoices ADD COLUMN reminder_last_sent_at TIMESTAMPTZ;
ALTER TABLE invoices ADD COLUMN reminder_count INTEGER DEFAULT 0;
ALTER TABLE invoices ADD COLUMN reminders_disabled BOOLEAN DEFAULT false;
ALTER TABLE invoices ADD COLUMN next_reminder_at TIMESTAMPTZ;
-- next_reminder_at computed on each send; NULL when all reminders sent or reminders_disabled
ALTER TABLE invoices ADD COLUMN next_reminder_offset INTEGER;
-- offset_days of the stage next_reminder_at points to; used by the dunning dedup guard
-- (<= 0 = pre/at-due, always sent; > 0 = post-due, suppressed when dunning is active)
```

Reminder settings added to `tenant_settings` table (spec 17 module config):

```sql
-- Schema delta on tenant_settings:
ALTER TABLE tenant_settings ADD COLUMN invoice_reminders_enabled BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE tenant_settings ADD COLUMN invoice_reminder_schedule JSONB NOT NULL DEFAULT
  '[{"offset_days":-3,"enabled":true},{"offset_days":0,"enabled":true},{"offset_days":7,"enabled":true},{"offset_days":14,"enabled":true},{"offset_days":30,"enabled":true}]'::jsonb;
```

---

## Settings UI: `/settings/invoicing` (extension of existing page)

New section "Payment Reminders":

```
┌────────────────────────────────────────────────────────────┐
│  Payment Reminders                                         │
│                                                            │
│  ● Enabled    ○ Disabled                                   │
│                                                            │
│  Send reminders to customers with unpaid invoices:        │
│                                                            │
│  ☑  3 days before due date                                │
│  ☑  On due date                                           │
│  ☑  7 days overdue                                        │
│  ☑  14 days overdue                                       │
│  ☑  30 days overdue (final notice)                        │
│                                                            │
│  Reply-to: [billing@acme.com_____________]                 │
│  (uses tenant From address from email settings)           │
└────────────────────────────────────────────────────────────┘
```

---

## Invoice Detail: Reminders Panel

In `/invoices/:id`, new "Reminders" section visible when invoice is payable:

```
┌──────────────────────────────────────────────────────────────┐
│  Payment Reminders                                           │
│                                                              │
│  Last sent:  7 days overdue — 2026-05-24                    │
│  Next:       14 days overdue — scheduled for 2026-05-31     │
│  Sent count: 3                                               │
│                                                              │
│  [Send now]   [Disable reminders for this invoice]          │
└──────────────────────────────────────────────────────────────┘
```

"Send now" → `POST /api/invoices/:id/reminders/send` — sends current stage immediately, advances schedule.  
"Disable reminders" → sets `invoices.reminders_disabled = true`.

---

## Email Content

Uses spec 66 `tenant_email_templates` with template key `invoice_reminder`. Available variables:
- `{{invoice_number}}`, `{{invoice_total}}`, `{{due_date}}`, `{{days_overdue}}`, `{{payment_link}}`

Default body (fallback if no custom template):
> "This is a reminder that invoice {invoice_number} for {invoice_total} is due {days_overdue > 0 ? '{days_overdue} days overdue' : 'on {due_date}'}. Pay now: {payment_link}"

---

## Cron

`invoice-reminders` — Daily 07:00 UTC (runs before the 08:00 UTC `invoice-dunning` cron). Queries:
```sql
SELECT i.id, i.due_date FROM invoices i
WHERE i.status IN ('SENT', 'APPROVED', 'TAX_ISSUED', 'PARTIALLY_PAID')
  AND i.reminders_disabled = false
  AND i.due_date IS NOT NULL
  AND i.next_reminder_at <= now()
  AND i.tenant_id IN (
    SELECT tenant_id FROM tenant_settings
    WHERE invoice_reminders_enabled = true
  )
  -- Dedup guard vs dunning (spec 93): only send a POST-due stage (next stage
  -- offset_days > 0) when dunning is NOT active for this tenant. Pre-due / at-due
  -- stages (offset_days <= 0) always send.
  AND (
    i.next_reminder_offset <= 0
    OR NOT EXISTS (
      SELECT 1 FROM dunning_schedules ds WHERE ds.tenant_id = i.tenant_id
    )
  )
```

`next_reminder_offset` is the `offset_days` of the stage `next_reminder_at` points to (stored alongside it). The `NOT EXISTS` clause is the boundary: a tenant with any `dunning_schedules` row (Business+ with dunning configured) has all post-due stages handled by spec 93, so this cron emits only its `-3` and `0` stages for them. Tenants without dunning (Freelancer, or Business+ who deleted their dunning schedule) keep the full `+7 / +14 / +30` overdue series here.

For each matching invoice: sends email, increments `reminder_count`, updates `reminder_last_sent_at`, computes and sets `next_reminder_at` + `next_reminder_offset` (or NULL if all eligible stages exhausted).

---

## API

```
POST /api/invoices/:id/reminders/send
     → send reminder now (manual trigger)
       Requires: invoices:write
       Returns: { sentAt, recipientEmail, subject }

PATCH /api/invoices/:id/reminders
     → update invoice reminder settings
       body: { disabled?: bool }
       Requires: invoices:write

GET /api/settings/invoicing/reminders
     → get tenant reminder config

PATCH /api/settings/invoicing/reminders
     → update tenant reminder config
       body: { enabled, schedule: [...] }
       Requires: settings:write
```

---

## Foundation Deltas

**Schema delta on `invoices`:** `reminder_last_sent_at`, `reminder_count`, `reminders_disabled`, `next_reminder_at`

**New cron:** `invoice-reminders` — Daily 07:00 UTC, enqueues reminder send jobs.

**New email template key:** `invoice_reminder` (spec 66 template system)

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `next_reminder_at` column | Not cron-computed lookup | Precomputed column with index makes daily cron a single indexed query — no per-invoice schedule math at cron time |
| Per-invoice opt-out | Not tenant-level override only | Some invoices are under dispute; staff needs to suppress without disabling globally |
| 5-stage default | Not simple overdue-only | Pre-due reminders reduce overdue rate; final notice creates urgency without being aggressive |
| Cron time 07:00 UTC | Not midnight | 07:00 UTC = 09:00 Israel time — business hours delivery improves open rates |
