feat(documenso): rejection reason + poll fallback + rollback hardening + recipient UX
Documenso reliability + signer-UX bundle from the 2026-05-26 live UAT. Each piece detailed in docs/superpowers/audits/active-uat.md with full file:line + root cause + alternatives. Webhook + poll convergence - DocumensoRecipient (webhook payload type) gains rejectionReason + declineReason. The DOCUMENT_REJECTED / DOCUMENT_DECLINED handler coalesces them at the boundary so downstream code sees one stable field. Empty/whitespace normalised to null. - DocumensoDocument.recipients[] (normalized client output) gains rejectionReason. normalizeDocument coalesces v2 + v1 field names the same way so poller consumers see identical shape. - handleDocumentRejected signature gains rejectionReason. Stored on document_events.eventData, persisted in audit_logs metadata, quoted inline in the in-CRM rep notification (truncated 120 chars; full reason still on the audit row). New 'transfer' AuditAction added alongside. - signature-poll job now handles REJECTED / DECLINED. Previously only SIGNED / COMPLETED / EXPIRED were reconciled, so a missed rejection webhook (stale tunnel URL is the typical dev cause) left documents stuck in 'sent' forever. The 5-min poll cycle now closes that gap — webhook becomes an optimisation, not a correctness requirement. placeFields rollback gap - custom-document-upload.service moved the synchronous field-placement map() INSIDE the same try/catch that wraps placeFields(). Previously the map's throw bubbled past the catch-and-rollback block, leaving Documenso with a live envelope + recipients but no fields, and the CRM document row stuck in 'sent' with no signing UI for the signers. Logger captures looked-up email + map keys on miss for diagnosis. - Comment documents Documenso's by-email dedupe semantic so future readers don't reintroduce the per-recipient-row map assumption. UploadForSigningDialog recipient UX - New RECIPIENT_ROLE_META + RecipientRoleBadge helpers. Placement-step sidebar list rebuilt as a two-line layout (name + role badge / email on its own line) so duplicate-named recipients are visually distinguishable. FieldSidePanel dropdown SelectItem mirrors the same stacked shape. - "Recipient" label renamed to "Assign this field to" with an explainer paragraph below. SigningProgress copy-link parity - Copy-link button now always renders for pending signers (disabled + explainer tooltip when signingUrl not yet issued). Reps can copy even when the URL hasn't been distributed via email yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
handleRecipientSigned,
|
||||
handleDocumentCompleted,
|
||||
handleDocumentExpired,
|
||||
handleDocumentRejected,
|
||||
} from '@/lib/services/documents.service';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
@@ -74,6 +75,35 @@ export async function processDocumensoPoll(): Promise<void> {
|
||||
'Reconciling expired document from poll',
|
||||
);
|
||||
await handleDocumentExpired({ documentId: doc.documensoId, portId: doc.portId });
|
||||
} else if (
|
||||
(remoteDoc.status === 'REJECTED' || remoteDoc.status === 'DECLINED') &&
|
||||
doc.status !== 'rejected'
|
||||
) {
|
||||
// Rejection / decline reconcile — mirrors the webhook handler's
|
||||
// logic so a missed `DOCUMENT_REJECTED` / `DOCUMENT_DECLINED`
|
||||
// webhook delivery (stale tunnel URL, signature mismatch, etc.)
|
||||
// still converges within one poll cycle. Finds the rejecting
|
||||
// recipient by status, plucks the free-text reason if Documenso
|
||||
// carried one, hands off to the same handler the webhook uses
|
||||
// — so document_events, audit log, notification, and UI all
|
||||
// see the same shape regardless of delivery path.
|
||||
const rejecting = remoteDoc.recipients.find(
|
||||
(r) => r.status === 'REJECTED' || r.status === 'DECLINED',
|
||||
);
|
||||
logger.info(
|
||||
{
|
||||
documentId: doc.id,
|
||||
portId: doc.portId,
|
||||
rejectingEmail: rejecting?.email ?? null,
|
||||
},
|
||||
'Reconciling rejected document from poll',
|
||||
);
|
||||
await handleDocumentRejected({
|
||||
documentId: doc.documensoId,
|
||||
recipientEmail: rejecting?.email,
|
||||
rejectionReason: rejecting?.rejectionReason ?? null,
|
||||
portId: doc.portId,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
|
||||
Reference in New Issue
Block a user