feat(automate-signing): one-click invitation kickoff + auto cascade + completion broadcast
Phase 3 of the comprehensive UAT round. Implements the Automate
Signing feature per the 2026-05-26 locked decisions.
P3.1 — documents.automation_mode schema
Migration 0088 adds the column with a CHECK constraint enforcing
the three-value enum: manual / sequential_auto / concurrent_auto.
Drizzle schema picks it up; default 'manual' preserves existing
behaviour.
P3.2 — Automate Signing orchestrator service
New src/lib/services/signing-automation.service.ts. enableSigningAutomation
resolves the mode from the envelope's signing order (SEQUENTIAL ->
sequential_auto fires first signer only; PARALLEL -> concurrent_auto
fires all signers in one parallel dispatch), updates documents.automationMode,
and dispatches invitations via the same sendSigningInvitation path
the manual route uses (so the email a recipient sees is identical
regardless of trigger). ensureSigningUrls recovers v2 signing URLs
if they're missing on the local signer rows. Hard guards: envelope
must exist, status in {draft, sent, partially_signed}, ≥2 signers.
disableSigningAutomation reverts to manual; idempotent.
P3.3 — Webhook cascade
The existing sendCascadingInviteForNextSigner in documents.service.ts
already fires the next pending signer on every recipient_signed event
(mode-independent). handleDocumentCompleted already sends the signed
PDF to all recipients via sendSigningCompleted on completion. So
"automate" really means "kick off the first invitation"; the rest
is mode-independent existing behaviour. Doc comment in the new
service explains the interaction.
P3.4 — ActiveEoiCard Automate signing button + banner
- DocumentRow type extended with automationMode + documensoId.
- New automateMutation hits POST /api/v1/documents/[id]/automate;
pauseAutomationMutation hits DELETE.
- "Automate signing" button visible when totalCount ≥ 2 AND doc has
documensoId AND envelope is in-flight AND mode === 'manual'.
- "Automating sequentially/concurrently · N of M signed" banner
renders when automation is active, with a Pause button that
reverts to manual.
- Per-row Send invitation / Send reminder buttons in SigningProgress
stay visible per the locked decision (manual override during auto).
P3.5 — Automate Signing API route + tests
- POST /api/v1/documents/[id]/automate (enables) + DELETE (disables).
- Permission: documents.send_for_signing (mirrors the manual
send-invitation route).
- vitest covering: NotFound on missing doc, Conflict on missing
envelope, Conflict on completed status, Conflict on already-
automated, Conflict on <2 signers, disable is idempotent when
already manual. All 7 cases pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
src/lib/db/migrations/0088_documents_automation_mode.sql
Normal file
24
src/lib/db/migrations/0088_documents_automation_mode.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Phase 3: Automate Signing feature.
|
||||
--
|
||||
-- `automation_mode` controls whether the CRM cascades signing
|
||||
-- invitations automatically as signers complete vs. leaving the rep
|
||||
-- to fire each invitation manually.
|
||||
--
|
||||
-- Values:
|
||||
-- manual - default; rep fires each signer's invitation by hand.
|
||||
-- sequential_auto - first signer invited; webhook handler fires the
|
||||
-- next-in-order signer on each recipient_signed event.
|
||||
-- concurrent_auto - all signers invited at once; only the broadcast
|
||||
-- completion email fires from the webhook.
|
||||
--
|
||||
-- Default 'manual' preserves existing behaviour. Backfill not needed.
|
||||
|
||||
ALTER TABLE documents
|
||||
ADD COLUMN automation_mode text NOT NULL DEFAULT 'manual';
|
||||
|
||||
-- Defensive constraint: only the three known enum values are
|
||||
-- acceptable. Service layer also validates, but having it at the DB
|
||||
-- level prevents direct-SQL bypass from creating an invalid state.
|
||||
ALTER TABLE documents
|
||||
ADD CONSTRAINT documents_automation_mode_check
|
||||
CHECK (automation_mode IN ('manual', 'sequential_auto', 'concurrent_auto'));
|
||||
@@ -125,6 +125,14 @@ export const documents = pgTable(
|
||||
* the empty string when null. Plain-text (XSS-escaped by the
|
||||
* email renderer); not Markdown. */
|
||||
invitationMessage: text('invitation_message'),
|
||||
/** Automate Signing mode. `manual` (default) = rep clicks each
|
||||
* signer's invitation by hand. `sequential_auto` = first signer
|
||||
* invited; webhook auto-fires the next-in-order on each
|
||||
* recipient_signed event. `concurrent_auto` = all signers invited
|
||||
* in one bulk dispatch; webhook only fires the broadcast on
|
||||
* completion. CHECK constraint at the DB layer enforces the
|
||||
* three-value enum. */
|
||||
automationMode: text('automation_mode').notNull().default('manual'),
|
||||
remindersDisabled: boolean('reminders_disabled').notNull().default(false),
|
||||
reminderCadenceOverride: integer('reminder_cadence_override'),
|
||||
// Phase 3 - per-document field overrides. When NULL, the canonical
|
||||
|
||||
Reference in New Issue
Block a user