Files
documenso/packages/lib/utils/document.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

155 lines
6.0 KiB
TypeScript

import type { DocumentMeta, Envelope, OrganisationGlobalSettings, Recipient, Team, User } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
import { resolveSigningOrder } from '../server-only/signature-level/resolve-signing-order';
import type { TDocumentLite, TDocumentMany } from '../types/document';
import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
import { SignatureLevel } from '../types/signature-level';
import { mapSecondaryIdToDocumentId } from './envelope';
import { mapRecipientToLegacyRecipient } from './recipients';
export const isDocumentCompleted = (document: Pick<Envelope, 'status'> | DocumentStatus) => {
const status = typeof document === 'string' ? document : document.status;
return (
status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED || status === DocumentStatus.CANCELLED
);
};
/**
* Extracts the derived document meta which should be used when creating a document
* from scratch, or from a template.
*
* Uses the following, the lower number overrides the higher number:
* 1. Merged organisation/team settings
* 2. Meta overrides
*
* @param settings - The merged organisation/team settings.
* @param overrideMeta - The meta to override the settings with.
* @param signatureLevel - The envelope's signature level. Optional; defaults
* to `SES` for backward compatibility, which preserves the legacy `PARALLEL`
* signing-order default. New callers should pass the resolved level so the
* TSP envelopes get the `SEQUENTIAL` default + assertion against explicit
* `PARALLEL`.
* @returns The derived document meta.
*/
export const extractDerivedDocumentMeta = (
settings: Omit<OrganisationGlobalSettings, 'id'>,
overrideMeta: Partial<DocumentMeta> | undefined | null,
signatureLevel: string = SignatureLevel.SES,
) => {
const meta = overrideMeta ?? {};
// Note: If you update this you will also need to update `create-document-from-template.ts`
// since there is custom work there which allows 3 overrides.
return {
language: meta.language || settings.documentLanguage,
timezone: meta.timezone || settings.documentTimezone || DEFAULT_DOCUMENT_TIME_ZONE,
dateFormat: meta.dateFormat || settings.documentDateFormat,
message: meta.message || null,
subject: meta.subject || null,
redirectUrl: meta.redirectUrl || null,
signingOrder: resolveSigningOrder({ signatureLevel, requested: meta.signingOrder }),
allowDictateNextSigner: meta.allowDictateNextSigner ?? false,
distributionMethod: meta.distributionMethod || DocumentDistributionMethod.EMAIL, // Todo: Make this a setting.
// Signature settings.
typedSignatureEnabled: meta.typedSignatureEnabled ?? settings.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled ?? settings.drawSignatureEnabled,
// Email settings.
emailId: meta.emailId ?? settings.emailId,
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
emailSettings: meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
// Envelope expiration.
envelopeExpirationPeriod: meta.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod ?? null,
// Reminder settings.
reminderSettings: meta.reminderSettings ?? settings.reminderSettings ?? null,
} satisfies Omit<DocumentMeta, 'id'>;
};
/**
* Map an envelope to a legacy document lite response entity.
*
* Do not use spread operator here to avoid unexpected behavior.
*/
export const mapEnvelopeToDocumentLite = (envelope: Envelope): TDocumentLite => {
const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
id: documentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
externalId: envelope.externalId,
userId: envelope.userId,
authOptions: envelope.authOptions,
formValues: envelope.formValues,
title: envelope.title,
createdAt: envelope.createdAt,
documentDataId: '', // Backwards compatibility.
updatedAt: envelope.updatedAt,
completedAt: envelope.completedAt,
deletedAt: envelope.deletedAt,
teamId: envelope.teamId,
folderId: envelope.folderId,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
templateId: envelope.templateId,
};
};
type MapEnvelopeToDocumentManyOptions = Envelope & {
user: Pick<User, 'id' | 'name' | 'email'>;
team: Pick<Team, 'id' | 'url'>;
recipients: Recipient[];
};
/**
* Map an envelope to a legacy document many response entity.
*
* Do not use spread operator here to avoid unexpected behavior.
*/
export const mapEnvelopesToDocumentMany = (envelope: MapEnvelopeToDocumentManyOptions): TDocumentMany => {
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
return {
id: legacyDocumentId, // Use legacy ID.
envelopeId: envelope.id,
internalVersion: envelope.internalVersion,
visibility: envelope.visibility,
status: envelope.status,
source: envelope.source,
externalId: envelope.externalId,
userId: envelope.userId,
authOptions: envelope.authOptions,
formValues: envelope.formValues,
title: envelope.title,
createdAt: envelope.createdAt,
documentDataId: '', // Backwards compatibility.
updatedAt: envelope.updatedAt,
completedAt: envelope.completedAt,
deletedAt: envelope.deletedAt,
teamId: envelope.teamId,
folderId: envelope.folderId,
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
templateId: envelope.templateId,
user: {
id: envelope.userId,
name: envelope.user.name,
email: envelope.user.email,
},
team: {
id: envelope.teamId,
url: envelope.team.url,
},
recipients: envelope.recipients.map((recipient) => mapRecipientToLegacyRecipient(recipient, envelope)),
};
};