# Multi-Signatory Coordination

**Date:** 2026-05-31  
**Status:** Draft  
**Spec:** 90  
**Tier:** All tiers  
**Depends on:** `contracts-esignature`, `contract-signing-page`, `foundation-auth-rbac`  
**Referenced by:** `contracts-esignature`

---

## Overview

Spec 48 (`contracts-esignature`) defines `contract_signatories` with an `"order"` column and up to 3 signatories, but does not specify the UI for managing signing order, sending individual reminder emails, or tracking per-signatory progress. This spec fills those gaps.

---

## Data Model

No new tables. Schema delta on `contract_signatories`:

```sql
ALTER TABLE contract_signatories ADD COLUMN reminder_sent_at TIMESTAMPTZ;
ALTER TABLE contract_signatories ADD COLUMN reminder_count INTEGER DEFAULT 0;
```

`"order"` (already in spec 48) controls sequential signing: signatory 2 cannot sign until signatory 1 has signed. Value `1` = first to sign.

---

## Signing Order Rules

- `"order"` values must be unique per contract (1, 2, 3... or all `1` for simultaneous)
- **Sequential**: when orders differ, only the current signer's link is active; subsequent signatories receive their email only after the previous one signs
- **Simultaneous**: when all orders are `1`, all signatories receive emails at once
- Default on contract creation: all signatories get `"order" = 1` (simultaneous)

---

## Signatory Management UI

In contract detail view (`/contracts/:id`), DRAFT status, "Signatories" section:

```
┌──────────────────────────────────────────────────────────────┐
│  Signatories                                [+ Add signatory] │
│                                                              │
│  Signing order:  ● Simultaneously   ○ In sequence           │
│                                                              │
│  1.  Dana Cohen          dana@acme.com         [Edit] [✕]   │
│  2.  (drag to reorder)                                       │
│                                                              │
│  ── When "In sequence": ────────────────────────────────── │
│                                                              │
│  ① Dana Cohen  →  ② Ronen Bar  →  ③ Legal Dept             │
│  Drag rows to change order                                   │
└──────────────────────────────────────────────────────────────┘
```

Reordering: drag-and-drop in DRAFT. After SENT, order is locked (editing requires void + re-send).

---

## Signatory Status: SENT/VIEWED/SIGNED

In contract detail, SENT or later, each signatory shows live status:

```
┌──────────────────────────────────────────────────────────────┐
│  Signatories                                                 │
│                                                              │
│  ① Dana Cohen       ✅ Signed      2026-05-28 14:22         │
│  ② Ronen Bar        👁 Viewed      2026-05-30 09:11         │
│     [Send reminder]   [Resend link]                          │
│  ③ Legal Dept       ⏳ Waiting (sequential — waiting for ②) │
│                                                              │
│  Overall: 1 of 3 signed                                     │
└──────────────────────────────────────────────────────────────┘
```

Status per signatory:
- ✅ Signed: `signed_at` set
- 👁 Viewed: `viewed_at` set, `signed_at` null
- 📬 Sent: email dispatched, not yet viewed
- ⏳ Waiting: sequential and earlier signatories not done
- ❌ Declined: `declined_at` set

---

## Per-Signatory Actions

### Send Reminder

`POST /api/contracts/:id/signatories/:signatoryId/reminder`

Resends the signing invitation email. Updates `reminder_sent_at` and increments `reminder_count`. Rate-limited: max 1 reminder per signatory per 24 hours (returns 429 if attempted sooner).

### Resend Signing Link

`POST /api/contracts/:id/signatories/:signatoryId/resend`

Regenerates token (new UUID) and `token_expires_at` (30 days from now), resends email. Invalidates old token. OWNER/ADMIN only.

### Replace Signatory

`PATCH /api/contracts/:id/signatories/:signatoryId`

Update signatory name/email while contract is in SENT state. Regenerates token and resends. Records event in `contract_audit_log`. Only allowed if signatory has not yet signed.

---

## Auto-Reminder

Cron: `contract-signing-reminders` — daily 09:00 UTC (10:00 Israel):

```sql
SELECT cs.id
FROM contract_signatories cs
JOIN contracts c ON c.id = cs.contract_id
WHERE c.status IN ('SENT', 'VIEWED')
  AND cs.signed_at IS NULL
  AND cs.declined_at IS NULL
  AND (cs.reminder_sent_at IS NULL OR cs.reminder_sent_at < now() - interval '7 days')
  AND (
    c.id NOT IN (SELECT contract_id FROM contract_signatories WHERE signed_at IS NULL AND "order" < cs."order")
  )
```

Last condition: only send to signatories whose turn it currently is (sequential order check).

---

## Completion: All Signed

When `signed_at` is set for all signatories (trigger logic in spec 48):
- `contracts.status` → `SIGNED`
- `contracts.signed_at` = now()
- Composite PDF generated with all signature blocks appended
- Webhook: `contract.signed` emitted

---

## API

```
GET /api/contracts/:id/signatories
    → list signatories with status, reminder counts
      Requires: contracts:read

PATCH /api/contracts/:id/signatories
      → update all signatories + order (DRAFT only)
        body: { signatories: [{ id?, name, email, order }] }
        Requires: contracts:write

POST /api/contracts/:id/signatories/:sigId/reminder
     → send reminder email to this signatory
       Requires: contracts:write

POST /api/contracts/:id/signatories/:sigId/resend
     → regenerate token + resend
       Requires: contracts:write

PATCH /api/contracts/:id/signatories/:sigId
      → replace name/email (SENT, not yet signed)
        Requires: contracts:write
```

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Sequential via `"order"` column | Not state machine table | Order is a simple integer; the rule (don't email order N until N-1 signs) is enforced at send time, not a complex FSM |
| Per-signatory reminder limit 24h | Not unlimited | Spamming signatories damages relationships; 24h minimum gap with manual override if needed |
| Simultaneous = all order=1 | Not a separate boolean | Single field, consistent query; "all order=1" means send all at once — no ambiguity |
| Replace signatory (not void+redo) | Not force-void | Practical case: typo in email; voiding + restarting wastes previous signatures |
