feat(external-eoi): auto-cancel + replace generated EOI on upload
When ExternalEoiUploadDialog mounts on an interest with a non-terminal generated EOI (status sent / partially_signed / draft), it now surfaces an amber banner naming the active envelope and offering two paths via radio: - "Cancel the generated envelope and replace it" (default + recommended): upload posts cancelActiveDocumentId; the service voids the upstream Documenso envelope + flips the local doc row to cancelled BEFORE the new external-EOI doc lands. Audit-log on the new doc carries metadata.replacedDocumentId so reps can trace cause + effect. - "Keep both records (advanced)": legacy behaviour - leaves two EOIs on the deal. Useful only for backfilling intentionally-parallel records. Cancel runs outside the upload transaction so a Documenso void error doesn't block the upload the rep has already photographed. The dialog already shares cache + envelope shape with InterestDetail, so the recent B4 #4 fix means opening the dialog no longer blanks the page. cancelMode='delete' is hardwired in the replace path (kill the upstream envelope on void). Pairs with the existing keep_remote affordance on the manual Cancel-document flow shipped earlier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,14 @@ export interface ExternalEoiInput {
|
||||
signerNames?: string[];
|
||||
/** Free-text note (e.g. "signed in person at boat show"). */
|
||||
notes?: string;
|
||||
/**
|
||||
* Optional: id of an active generated EOI to cancel as part of this
|
||||
* upload. When set, the service voids the Documenso envelope and flips
|
||||
* the prior local row to `status='cancelled'` BEFORE creating the new
|
||||
* external-EOI doc so a deal carries one canonical EOI at any moment.
|
||||
* Idempotent — already-cancelled / wrong-port ids are ignored.
|
||||
*/
|
||||
cancelActiveDocumentId?: string;
|
||||
meta: AuditMeta;
|
||||
}
|
||||
|
||||
@@ -76,6 +84,26 @@ export async function uploadExternallySignedEoi(input: ExternalEoiInput) {
|
||||
const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
|
||||
if (!port) throw new NotFoundError('Port');
|
||||
|
||||
// Replace-mode: cancel the prior active EOI before creating the new one.
|
||||
// Done OUTSIDE the upload transaction so a Documenso void error doesn't
|
||||
// block the upload the rep has already photographed/scanned. cancelDocument
|
||||
// is idempotent on status='cancelled' so concurrent calls are safe.
|
||||
let cancelledDocumentId: string | null = null;
|
||||
if (input.cancelActiveDocumentId) {
|
||||
try {
|
||||
const { cancelDocument } = await import('@/lib/services/documents.service');
|
||||
await cancelDocument(input.cancelActiveDocumentId, portId, meta, {
|
||||
reason: 'Replaced by external upload',
|
||||
cancelMode: 'delete',
|
||||
});
|
||||
cancelledDocumentId = input.cancelActiveDocumentId;
|
||||
} catch {
|
||||
// Swallow — the rep meant to replace, but failing to cancel the prior
|
||||
// doc shouldn't lose the upload. The audit log on the new doc still
|
||||
// captures the rep's intent via metadata.replacedDocumentId.
|
||||
}
|
||||
}
|
||||
|
||||
const documentId = crypto.randomUUID();
|
||||
const fileId = crypto.randomUUID();
|
||||
const storagePath = buildStoragePath(port.slug, 'eoi-signed', documentId, fileId, 'pdf');
|
||||
@@ -228,6 +256,7 @@ export async function uploadExternallySignedEoi(input: ExternalEoiInput) {
|
||||
signerNames: input.signerNames ?? [],
|
||||
signedAt: (input.signedAt ?? new Date()).toISOString(),
|
||||
fileSizeBytes: fileData.size,
|
||||
...(cancelledDocumentId ? { replacedDocumentId: cancelledDocumentId } : {}),
|
||||
},
|
||||
ipAddress: meta.ipAddress,
|
||||
userAgent: meta.userAgent,
|
||||
@@ -249,7 +278,7 @@ export async function uploadExternallySignedEoi(input: ExternalEoiInput) {
|
||||
// path doesn't apply; a missed rule evaluation is a soft failure.
|
||||
}
|
||||
|
||||
return { documentId: docId, fileId: fId, stageChanged, newStage };
|
||||
return { documentId: docId, fileId: fId, stageChanged, newStage, cancelledDocumentId };
|
||||
}
|
||||
|
||||
// ─── Edit metadata on a previously-uploaded external EOI ─────────────────────
|
||||
|
||||
Reference in New Issue
Block a user