# Invoice Payment Link Generation

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 126  
**Tier:** All tiers  
**Depends on:** `invoices-core`, `invoice-payment-ux`, `payment-gateway-adapters`, `foundation-auth-rbac`  
**Referenced by:** `invoices-core`, `invoice-payment-ux`

---

## Overview

Spec 53 (`invoice-payment-ux`) defines the customer-facing `/pay/{invoiceToken}` page. Spec 49 defines payment gateway adapters. But no spec documents how a tenant generates and distributes payment links from within the invoice management UI. This spec fills that gap: the link generation, copy UI, expiry rules, and email integration.

---

## Payment Link Basics

A payment link is a stateless HMAC-signed URL derived from the invoice ID:

```
https://zync.is/pay/{invoiceToken}
```

`invoiceToken` = `HMAC-SHA256(INVOICE_PAYMENT_LINK_KEY, invoiceId + tenantId)` encoded as URL-safe base64. Token is stateless — no additional DB row needed. Expiry enforced server-side: tokens for `PAID`, `VOID`, or `CANCELLED` invoices return 410 Gone.

Token is valid as long as the invoice is in a payable status: `SENT | TAX_ISSUED | PARTIALLY_PAID`.

---

## Entry Points for Payment Links

### 1. Invoice Detail — Payment Link Button

On the invoice detail view, for invoices in `SENT | TAX_ISSUED | PARTIALLY_PAID` status:

```
┌──────────────────────────────────────────────────────────────┐
│  INV-0051  ·  Acme Corp  ·  ₪2,000  ·  SENT                  │
│                                                              │
│  [Preview PDF]  [Mark paid]  [Send reminder]  [⋮ More]       │
│                                                              │
│  💳 Payment link                                              │
│  zync.is/pay/abc123xyz...                [Copy] [Open] [📧]  │
│                                                              │
│  Link is active until invoice is paid or voided.             │
└──────────────────────────────────────────────────────────────┘
```

**[Copy]** — copies URL to clipboard, shows "Copied!" toast.  
**[Open]** — opens `/pay/{token}` in a new tab (preview as customer sees it).  
**[📧]** — opens "Send payment link" email compose (see below).

### 2. Invoice List — Inline Copy

In the `/invoices` list, for payable invoices: hover row → action buttons include a copy-link icon.

### 3. Send Invoice — Includes Link Automatically

When `PATCH /api/invoices/:id/send` is called (status → SENT), the outbound email includes the payment link if `tenant_settings.invoice_show_payment_link = true` (spec 125). Staff do not need to manually attach a link.

---

## Send Payment Link — Email Compose

**[📧]** on invoice detail → inline compose panel (not a new page):

```
┌──────────────────────────────────────────────────────────────┐
│  Send payment link to Acme Corp                              │
│                                                              │
│  To:  dana@acme.com  [+ Add recipient]                       │
│                                                              │
│  Subject: [Your invoice INV-0051 is ready to pay_________]   │
│                                                              │
│  Message:                                                    │
│  [Hi Dana,                                                   │
│   Invoice INV-0051 for ₪2,000 is due by Jun 30.             │
│   You can pay securely here: {{payment_link}}                │
│   Thank you,                                                 │
│   {business_name}___________]                                │
│                                                              │
│  [Cancel]        [Send now]                                  │
└──────────────────────────────────────────────────────────────┘
```

Pre-populated from `tenant_email_templates.payment_link` template (spec 66). `{{payment_link}}` interpolated at send time.

Default To: customer's primary email from `customers` table.

---

## Link Status Indicators

| Invoice status | Link state |
|---------------|------------|
| `DRAFT` | No link shown (not yet sent) |
| `APPROVED` | No link shown (internal approval, not yet sent to customer) |
| `SENT` | Link active — shown with copy button |
| `TAX_ISSUED` | Link active |
| `PARTIALLY_PAID` | Link active — shows remaining balance on payment page |
| `PAID` | Link shown as inactive: "Invoice paid — payment link expired" |
| `VOID` | Link shown as inactive: "Invoice void" |
| `REJECTED` | Link shown as inactive: "Invoice rejected" |

---

## Gateway Required

Payment link only works if the tenant has configured at least one payment gateway (spec 49). If no gateway configured, payment link section shows:

```
│  💳 Payment link — inactive                                   │
│  Set up a payment gateway in Settings → Payment to enable.   │
│  [Go to Payment Settings →]                                  │
```

---

## Schema Delta

No new table. Token is stateless. Add to `invoices`:

```sql
ALTER TABLE invoices ADD COLUMN payment_link_sent_at TIMESTAMPTZ;
-- Timestamp of last payment link email send (audit trail, not enforcement).
```

---

## API

```
GET /api/invoices/:id/payment-link
    → generate/return payment link URL + token for this invoice
      Returns: { url: string, token: string, status: 'active'|'inactive', reason?: string }
      Requires: invoices:read

POST /api/invoices/:id/payment-link/send
     → send payment link email to recipient(s)
       body: { to: string[], subject?: string, message?: string }
       Returns: { sent: true }
       Requires: invoices:write
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Stateless HMAC token | Not stored token with expiry | Stateless = no DB row per link; token validity is derived from invoice status (always current); avoids stale link records piling up |
| Link included in SENT email automatically | Not opt-in per invoice | Default = frictionless payment; `invoice_show_payment_link = false` is the escape hatch for tenants who prefer bank transfer |
| Inline compose | Not separate email route | Sending a payment link is a quick action from the invoice detail; opening a new page breaks context |
| `payment_link_sent_at` column | Not full send log | Full email log is in `invoice_activities` (spec 62); timestamp is sufficient for "last sent" display |
