feat: add cancellable document status (#2992)

Adds a CANCELLED envelope status that privileged members (owner or team
admin/manager) can move a pending document into. Sending recipient
notifications via a background job while retaining the document in the
dashboard as proof of distribution.

Includes a dedicated Cancelled tab, single and bulk cancel actions,
the ENVELOPE_CANCELLED mutability guard, and e2e coverage for
permissions
and visibility.
This commit is contained in:
Lucas Smith
2026-06-18 13:52:35 +10:00
committed by GitHub
parent d5ce222482
commit 4f346d3c2d
45 changed files with 1806 additions and 45 deletions
@@ -3,7 +3,7 @@ import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelop
import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import type { DocumentData } from '@prisma/client';
import { DocumentDataType, EnvelopeType } from '@prisma/client';
import { DocumentDataType, DocumentStatus, EnvelopeType } from '@prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
@@ -58,7 +58,12 @@ export const downloadDocumentBetaRoute = authenticatedProcedure
});
}
if (version === 'signed' && !isDocumentCompleted(envelope.status)) {
// A cancelled document was never sealed, so its data is the unsigned original.
// Treat it as not-completed here so a "signed" version is never served for it.
// REJECTED and COMPLETED keep their prior behavior.
const hasSignedArtifact = isDocumentCompleted(envelope.status) && envelope.status !== DocumentStatus.CANCELLED;
if (version === 'signed' && !hasSignedArtifact) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document is not completed yet.',
});
@@ -4,7 +4,7 @@ import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-e
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { authenticatedProcedure } from '../trpc';
import {
@@ -60,7 +60,9 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure
});
}
if (!isDocumentCompleted(envelope.status)) {
// A cancelled document was never sealed/completed, so a signing certificate
// must not be generated for it. REJECTED and COMPLETED keep their prior behavior.
if (!isDocumentCompleted(envelope.status) || envelope.status === DocumentStatus.CANCELLED) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
@@ -19,6 +19,7 @@ export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
[ExtendedDocumentStatus.PENDING]: z.number(),
[ExtendedDocumentStatus.COMPLETED]: z.number(),
[ExtendedDocumentStatus.REJECTED]: z.number(),
[ExtendedDocumentStatus.CANCELLED]: z.number(),
[ExtendedDocumentStatus.INBOX]: z.number(),
[ExtendedDocumentStatus.ALL]: z.number(),
}),