import { prisma } from '@documenso/prisma'; import { SigningStatus, WebhookTriggerEvents } from '@prisma/client'; import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs'; import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../../types/webhook-payload'; import { createDocumentAuditLogData } from '../../../utils/document-audit-logs'; import { jobs } from '../../client'; import type { JobRunIO } from '../../client/_internal/job'; import type { TProcessRecipientExpiredJobDefinition } from './process-recipient-expired'; export const run = async ({ payload, io }: { payload: TProcessRecipientExpiredJobDefinition; io: JobRunIO }) => { const { recipientId } = payload; // Atomic idempotency guard — only one concurrent worker wins. // Wrapping in runTask caches the result so that on retry the claim is not // re-evaluated and subsequent steps can still proceed. const claimedCount = await io.runTask('claim-recipient', async () => { const result = await prisma.recipient.updateMany({ where: { id: recipientId, expirationNotifiedAt: null, signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] }, }, data: { expirationNotifiedAt: new Date() }, }); return result.count; }); if (claimedCount === 0) { io.logger.info(`Recipient ${recipientId} already processed or no longer eligible, skipping`); return; } // Fetch recipient (now marked) with its envelope for downstream steps. // Re-fetch after claiming so that expirationNotifiedAt reflects the updated value // and webhook consumers see consistent state. const recipient = await prisma.recipient.findUniqueOrThrow({ where: { id: recipientId }, include: { envelope: { include: { recipients: true, documentMeta: true }, }, }, }); const { envelope } = recipient; io.logger.info(`Recipient ${recipientId} (${recipient.email}) expired on envelope ${recipient.envelopeId}`); // Create audit log entry — wrapped so a retry skips this if it already succeeded. await io.runTask('create-audit-log', async () => { await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRED, envelopeId: recipient.envelopeId, data: { recipientEmail: recipient.email, recipientName: recipient.name, recipientId: recipient.id, }, }), }); }); // Trigger webhook for recipient expiration. await triggerWebhook({ event: WebhookTriggerEvents.RECIPIENT_EXPIRED, data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), userId: envelope.userId, teamId: envelope.teamId, }); // Trigger email notification to the document owner. await jobs.triggerJob({ name: 'send.owner.recipient.expired.email', payload: { recipientId: recipient.id, envelopeId: recipient.envelopeId, }, }); };