mirror of
https://github.com/documenso/documenso.git
synced 2026-07-13 22:37:24 +10:00
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
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,
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
};
|