Files
documenso/packages/lib/server-only/document/resend-document.ts
Lucas Smith 844af17ec2 feat: move email sending into background jobs for retry support
Each direct mailer.sendMail() call is replaced by a dedicated background
job so that email delivery failures can be retried independently.

New jobs: send-pending-email, send-completed-email, send-forgot-password-email,
send-document-super-delete-email, send-recipient-removed-email,
send-document-deleted-emails, send-2fa-token-email, send-resend-document-email,
send-direct-template-created-email.

Existing handlers (send-document-cancelled-emails, send-organisation-member-*,
send-recipient-signed-email) have io.runTask wrappers removed since they
interfere with the job scheduler. Job triggers are dispatched after
transactions commit to avoid race conditions with uncommitted data.
2026-02-20 23:25:37 +11:00

100 lines
2.7 KiB
TypeScript

import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { prisma } from '@documenso/prisma';
import { jobs } from '../../jobs/client';
import { isDocumentCompleted } from '../../utils/document';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export type ResendDocumentOptions = {
id: EnvelopeIdOptions;
userId: number;
recipients: number[];
teamId: number;
requestMetadata: ApiRequestMetadata;
};
export const resendDocument = async ({
id,
userId,
recipients,
teamId,
requestMetadata,
}: ResendDocumentOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id,
type: EnvelopeType.DOCUMENT,
userId,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: true,
documentMeta: true,
},
});
if (!envelope) {
throw new Error('Document not found');
}
if (envelope.recipients.length === 0) {
throw new Error('Document has no recipients');
}
if (envelope.status === DocumentStatus.DRAFT) {
throw new Error('Can not send draft document');
}
if (isDocumentCompleted(envelope.status)) {
throw new Error('Can not send completed document');
}
// Refresh the expiresAt on each resent recipient.
const expiresAt = resolveExpiresAt(envelope.documentMeta?.envelopeExpirationPeriod ?? null);
const recipientsToRemind = envelope.recipients.filter(
(recipient) =>
recipients.includes(recipient.id) &&
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
recipient.role !== RecipientRole.CC,
);
// Extend the expiration deadline for recipients being resent.
if (expiresAt && recipientsToRemind.length > 0) {
await prisma.recipient.updateMany({
where: {
id: {
in: recipientsToRemind.map((r) => r.id),
},
},
data: {
expiresAt,
expirationNotifiedAt: null,
},
});
}
// Dispatch the email sending to a background job so that email delivery
// failures don't block the resend operation and can be retried independently.
if (recipientsToRemind.length > 0) {
await jobs.triggerJob({
name: 'send.resend.document.email',
payload: {
envelopeId: envelope.id,
userId,
teamId: envelope.teamId,
recipientIds: recipientsToRemind.map((r) => r.id),
requestMetadata,
},
});
}
return envelope;
};