Files
documenso/packages/trpc/server/envelope-router/bulk-cancel-envelopes.ts
T
Lucas Smith 4f346d3c2d 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.
2026-06-18 13:52:35 +10:00

96 lines
2.6 KiB
TypeScript

import { cancelDocument } from '@documenso/lib/server-only/document/cancel-document';
import { getMultipleEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelopes-by-ids';
import { prisma } from '@documenso/prisma';
import { EnvelopeType } from '@prisma/client';
import pMap from 'p-map';
import { authenticatedProcedure } from '../trpc';
import { ZBulkCancelEnvelopesRequestSchema, ZBulkCancelEnvelopesResponseSchema } from './bulk-cancel-envelopes.types';
export const bulkCancelEnvelopesRoute = authenticatedProcedure
.input(ZBulkCancelEnvelopesRequestSchema)
.output(ZBulkCancelEnvelopesResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { envelopeIds, reason } = input;
ctx.logger.info({
input: {
envelopeIds,
},
});
// Cancelling only applies to documents. Templates are filtered out here so
// selecting a template never counts towards a cancellation.
const { envelopeWhereInput } = await getMultipleEnvelopeWhereInput({
ids: {
type: 'envelopeId',
ids: envelopeIds,
},
userId: user.id,
teamId,
type: EnvelopeType.DOCUMENT,
});
const envelopes = await prisma.envelope.findMany({
where: envelopeWhereInput,
select: {
id: true,
},
});
const results = await pMap(
envelopes,
async (envelope) => {
const { id: envelopeId } = envelope;
try {
await cancelDocument({
id: {
type: 'envelopeId',
id: envelopeId,
},
userId: user.id,
teamId,
reason,
requestMetadata: ctx.metadata,
});
return {
success: true,
envelopeId,
};
} catch (err) {
ctx.logger.warn(
{
envelopeId,
error: err,
},
'Failed to cancel envelope during bulk cancel',
);
return {
success: false,
envelopeId,
};
}
},
{
concurrency: 10,
stopOnError: false,
},
);
const cancelledCount = results.filter((r) => r.success).length;
const failedIds = results.filter((r) => !r.success).map((r) => r.envelopeId);
// Include envelope IDs that were not attempted (unauthorized/not found/not a document).
const attemptedIds = new Set(envelopes.map((e) => e.id));
const unattemptedIds = envelopeIds.filter((id) => !attemptedIds.has(id));
return {
cancelledCount,
failedIds: [...failedIds, ...unattemptedIds],
};
});