import { kyselyPrisma, sql } from '@documenso/prisma'; import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client'; import { DateTime } from 'luxon'; import { mapSecondaryIdToDocumentId } from '../../../utils/envelope'; import { jobs } from '../../client'; import type { JobRunIO } from '../../client/_internal/job'; import type { TSealDocumentSweepJobDefinition } from './seal-document-sweep'; export const run = async ({ io }: { payload: TSealDocumentSweepJobDefinition; io: JobRunIO }) => { const now = DateTime.now(); const fifteenMinutesAgo = now.minus({ minutes: 15 }).toJSDate(); const sixHoursAgo = now.minus({ hours: 6 }).toJSDate(); // Find all PENDING envelopes that should have been sealed but weren't. // // A document is ready to seal when either: // 1. All recipients are SIGNED or have role CC (normal completion) // 2. Any recipient has REJECTED (rejection triggers immediate seal) // // We only look at documents where the last action was between 15 minutes // and 6 hours ago. The lower bound avoids racing with the normal seal-document // job that fires on completion. The upper bound stops us from endlessly retrying // documents that are stuck due to a deeper issue (e.g. corrupt PDF). const unsealedEnvelopes = await kyselyPrisma.$kysely .selectFrom('Envelope') .select(['Envelope.id', 'Envelope.secondaryId']) .where('Envelope.status', '=', sql.lit(DocumentStatus.PENDING)) .where('Envelope.type', '=', sql.lit(EnvelopeType.DOCUMENT)) .where('Envelope.deletedAt', 'is', null) // Ensure there is at least one recipient. .where((eb) => eb.exists(eb.selectFrom('Recipient').whereRef('Recipient.envelopeId', '=', 'Envelope.id'))) // Document is ready to seal: all recipients are SIGNED/CC, or any recipient REJECTED. .where((eb) => eb.or([ // Case 1: All recipients are either SIGNED or CC. eb.not( eb.exists( eb .selectFrom('Recipient') .whereRef('Recipient.envelopeId', '=', 'Envelope.id') .where('Recipient.signingStatus', '!=', sql.lit(SigningStatus.SIGNED)) .where('Recipient.role', '!=', sql.lit(RecipientRole.CC)), ), ), // Case 2: Any recipient has rejected. eb.exists( eb .selectFrom('Recipient') .whereRef('Recipient.envelopeId', '=', 'Envelope.id') .where('Recipient.signingStatus', '=', sql.lit(SigningStatus.REJECTED)), ), ]), ) // Exclude envelopes where a recipient signed/rejected within the last 15 minutes // to avoid racing with the standard completion flow. .where((eb) => eb.not( eb.exists( eb .selectFrom('Recipient') .whereRef('Recipient.envelopeId', '=', 'Envelope.id') .where('Recipient.signedAt', '>', fifteenMinutesAgo), ), ), ) // Exclude envelopes where all activity is older than 6 hours. // These are likely stuck due to a deeper issue and should not be retried. .where((eb) => eb.exists( eb .selectFrom('Recipient') .whereRef('Recipient.envelopeId', '=', 'Envelope.id') .where('Recipient.signedAt', '>', sixHoursAgo), ), ) .limit(100) .execute(); if (unsealedEnvelopes.length === 0) { io.logger.info('No unsealed documents found'); return; } io.logger.info(`Found ${unsealedEnvelopes.length} unsealed documents`); await Promise.allSettled( unsealedEnvelopes.map(async (envelope) => { const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId); io.logger.info(`Triggering seal for document ${documentId} (${envelope.id})`); await jobs.triggerJob({ name: 'internal.seal-document', payload: { documentId, isResealing: true, }, }); }), ); };