# Invoice Email History

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 142  
**Tier:** All tiers  
**Depends on:** `invoices-core`, `activity-timeline`, `system-communications-notifications`, `foundation-auth-rbac`  
**Referenced by:** `invoices-core`, `activity-timeline`

---

## Overview

Spec 15 (`invoices-core`) stores `invoices.sent_at` (timestamp of first send) and `invoice_activities` (spec 62). There is no UI showing the full email delivery history for an invoice: who it was sent to, when, whether it was opened, or what email events occurred. This spec defines the email history panel within the invoice detail.

---

## Data Model

```sql
CREATE TABLE invoice_email_events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
  sent_by UUID NOT NULL REFERENCES users(id),       -- staff member who triggered send
  to_address TEXT NOT NULL,
  event_type TEXT NOT NULL
    CHECK (event_type IN ('sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')),
  metadata JSONB,                                    -- e.g. { "resend_id": "...", "user_agent": "..." }
  occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_invoice_email_events_invoice ON invoice_email_events(invoice_id, occurred_at DESC);
```

Email events are written by:
- `sent` / `failed`: synchronously on send (`POST /api/invoices/:id/send`)
- `delivered` / `opened` / `clicked` / `bounced`: async webhook from Resend (spec `system-communications-notifications`)

---

## Invoice Detail — Email History Tab

New **[Email history]** tab (or section) on `/invoices/:id`:

```
┌──────────────────────────────────────────────────────────────┐
│  INV-0041 — Email History                   [Send again]     │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  May 15, 2026 — 14:30                     by Yossi Levi  │ │
│  │  Sent to: dana@acme.com                                  │ │
│  │                                                          │ │
│  │  ✓ Delivered  14:31 (1 min)                              │ │
│  │  ✓ Opened     14:45 (1 device)                           │ │
│  │  ✓ Link clicked  14:47 (payment link)                    │ │
│  └─────────────────────────────────────────────────────────┘ │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  May 10, 2026 — 09:00                     by Alex Cohen  │ │
│  │  Sent to: dana@acme.com, billing@acme.com                │ │
│  │                                                          │ │
│  │  ✓ Delivered  09:01 (dana@acme.com)                      │ │
│  │  ✗ Bounced    (billing@acme.com — mailbox not found)     │ │
│  │  ✓ Opened     09:15 (dana@acme.com)                      │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```

Events grouped by send occurrence (one group per `sent` event per address). Events within a group sorted chronologically.

---

## Send Again Flow

**[Send again]** → pre-filled send modal (same as initial send from invoices-core):

```
┌──────────────────────────────────────────────────────────────┐
│  Re-send INV-0041                                            │
│                                                              │
│  To:     [dana@acme.com ✕]                                   │
│          (pre-filled from last send; editable)               │
│                                                              │
│  Subject: [Invoice INV-0041 from {business_name}___________] │
│                                                              │
│  Message: [Hi Dana, please find invoice INV-0041 attached.   │
│            ___________________________________________]       │
│                                                              │
│  [Cancel]    [Send]                                          │
└──────────────────────────────────────────────────────────────┘
```

---

## Bounce Notification

When Resend reports a bounce for an invoice email:

1. Update `invoice_email_events` row: `event_type = 'bounced'`
2. Create in-app notification for invoice owner: "Invoice INV-0041 email bounced — dana@acme.com"
3. Show bounce badge on invoice list row (small warning icon next to sent status)

---

## Resend Webhook Handler

`POST /api/webhooks/resend` (new endpoint — `system-communications-notifications` defines Telegram/WhatsApp webhooks but not Resend):

Routes by `data.tags.invoice_id` (stored as tag when sending invoice emails). For invoice emails:
- `email.delivered` → insert `delivered` event
- `email.opened` → insert `opened` event (one record per open; deduped by 1h window)
- `email.clicked` → insert `clicked` event
- `email.bounced` → insert `bounced` event + trigger notification

---

## Invoice List — Email Status Indicator

In the invoices list, `SENT` invoices show a small email delivery icon:

| Icon | Meaning |
|------|---------|
| ✉ (grey) | Sent; no delivery confirmation yet |
| ✓ (green) | Delivered |
| 👁 (blue) | Opened |
| ⚠ (amber) | Bounced or failed |

Icon reflects the best status of the most recent send to the primary recipient.

---

## API

```
GET /api/invoices/:id/email-history
    → list email events grouped by send occurrence
      Returns: [{ sent_at, sent_by_name, recipients: [{ address, events: [...] }] }]
      Requires: invoices:read

POST /api/invoices/:id/send
     → (existing) send invoice email; creates invoice_email_events 'sent' record
       Requires: invoices:write

-- New webhook endpoint (HMAC-verified from Resend):
POST /api/webhooks/resend
     → handle email delivery events; route by data.tags.invoice_id
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Separate `invoice_email_events` table | Not extend `invoice_activities` | Email delivery events come from async Resend webhooks with raw email metadata; mixing with business activities (status changes, payments) creates schema confusion and complicates webhook routing |
| Group by send occurrence | Not flat chronological | Staff need to understand "did this specific send reach the recipient?" not just "what happened over time" — grouping makes success/failure immediately readable |
| Deduplicate `opened` events | Not count all opens | Open pixels fire on every email preview load; grouping to 1-hour window prevents noise from re-downloads |
| Bounce alert in-app only | Not email | Sending an email to report a bounced email is unhelpful; in-app notification routes to the person who can act on it (staff, not the bounced address) |
