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:
2026-05-25 13:15:22 +02:00
parent cd6b19e173
commit 3a1c16ae71
3 changed files with 115 additions and 1 deletions

View File

@@ -75,6 +75,10 @@ export const POST = withAuth(
throw new ValidationError('Invalid signedAt');
}
const cancelActiveDocumentIdRaw =
(form.get('cancelActiveDocumentId') as string | null) ?? null;
const cancelActiveDocumentId = cancelActiveDocumentIdRaw?.trim() || undefined;
const result = await uploadExternallySignedEoi({
interestId,
portId: ctx.portId,
@@ -89,6 +93,7 @@ export const POST = withAuth(
signerNames,
signatories,
notes,
cancelActiveDocumentId,
meta: {
userId: ctx.userId,
portId: ctx.portId,