fix: move document complete email to a job (#2835)

This commit is contained in:
David Nguyen
2026-05-22 14:21:26 +10:00
committed by GitHub
parent f1b235819e
commit fec5d55250
4 changed files with 69 additions and 36 deletions
+2
View File
@@ -2,6 +2,7 @@ import { JobClient } from './client/client';
import { SEND_ADMIN_USER_CREATED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-admin-user-created-email';
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails';
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
@@ -41,6 +42,7 @@ export const jobsClient = new JobClient([
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION,
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION,
SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION,
@@ -5,29 +5,26 @@ import { msg } from '@lingui/core/macro';
import { DocumentSource, EnvelopeType } from '@prisma/client';
import { createElement } from 'react';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../utils/recipients';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
import { getEmailContext } from '../email/get-email-context';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { getFileServerSide } from '../../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../../utils/teams';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSendDocumentCompletedEmailsJobDefinition } from './send-document-completed-emails';
export interface SendDocumentOptions {
id: EnvelopeIdOptions;
requestMetadata?: RequestMetadata;
}
export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmailsJobDefinition; io: JobRunIO }) => {
const { envelopeId, requestMetadata } = payload;
export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOptions) => {
const envelope = await prisma.envelope.findUnique({
where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
where: unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT),
include: {
envelopeItems: {
include: {
@@ -0,0 +1,33 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import type { JobDefinition } from '../../client/_internal/job';
const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID = 'send.document.completed.emails';
const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
envelopeId: z.string(),
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSendDocumentCompletedEmailsJobDefinition = z.infer<
typeof SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA
>;
export const SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION = {
id: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
name: 'Send Document Completed Emails',
version: '1.0.0',
trigger: {
name: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
schema: SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-document-completed-emails.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION_ID,
TSendDocumentCompletedEmailsJobDefinition
>;
@@ -14,7 +14,6 @@ import { groupBy } from 'remeda';
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { insertFieldInPDFV1 } from '../../../server-only/pdf/insert-field-in-pdf-v1';
@@ -31,6 +30,7 @@ import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fiel
import { isDocumentCompleted } from '../../../utils/document';
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
import { mapDocumentIdToSecondaryId } from '../../../utils/envelope';
import { jobs } from '../../client';
import type { JobRunIO } from '../../client/_internal/job';
import type { TSealDocumentJobDefinition } from './seal-document';
@@ -294,21 +294,6 @@ export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition
};
});
await io.runTask('send-completed-email', async () => {
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await sendCompletedEmail({
id: { type: 'envelopeId', id: envelopeId },
requestMetadata,
});
}
});
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
where: {
id: envelopeId,
@@ -325,6 +310,22 @@ export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition
userId: updatedEnvelope.userId,
teamId: updatedEnvelope.teamId ?? undefined,
});
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
shouldSendCompletedEmail = sendEmail;
}
if (shouldSendCompletedEmail) {
await jobs.triggerJob({
name: 'send.document.completed.emails',
payload: {
envelopeId,
requestMetadata,
},
});
}
};
type DecorateAndSignPdfOptions = {