mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 01:45:08 +10:00
feat: signing reminders (#1749)
This commit is contained in:
@@ -442,6 +442,7 @@ export const completeDocumentWithToken = async ({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
sentAt: new Date(),
|
||||
...(nextSigner && envelope.documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
|
||||
@@ -69,6 +69,8 @@ export const viewedDocument = async ({
|
||||
// This handles cases where distribution is done manually
|
||||
sendStatus: SendStatus.SENT,
|
||||
readStatus: ReadStatus.OPENED,
|
||||
// Only set sentAt if not already set (email may have been sent before they opened).
|
||||
...(!recipient.sentAt ? { sentAt: new Date() } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -92,6 +94,9 @@ export const viewedDocument = async ({
|
||||
});
|
||||
});
|
||||
|
||||
// Don't schedule reminders for manually distributed documents —
|
||||
// there's no email pathway to send them through.
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: {
|
||||
id: recipient.envelopeId,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams';
|
||||
import { recomputeNextReminderForEnvelope } from '../recipient/update-recipient-next-reminder';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { getEnvelopeWhereInput } from './get-envelope-by-id';
|
||||
|
||||
@@ -354,6 +355,11 @@ export const updateEnvelope = async ({
|
||||
return result;
|
||||
});
|
||||
|
||||
// Recompute reminders for active recipients when reminder settings change.
|
||||
if (meta && 'reminderSettings' in meta) {
|
||||
await recomputeNextReminderForEnvelope(envelope.id);
|
||||
}
|
||||
|
||||
if (envelope.type === EnvelopeType.TEMPLATE) {
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.TEMPLATE_UPDATED,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
DocumentDistributionMethod,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
ZEnvelopeReminderSettings,
|
||||
resolveNextReminderAt,
|
||||
} from '../../constants/envelope-reminder';
|
||||
|
||||
/**
|
||||
* Compute and store `nextReminderAt` for a single recipient.
|
||||
*
|
||||
* Call this after:
|
||||
* - Sending the signing email (sentAt is set)
|
||||
* - Sending a reminder (lastReminderSentAt is updated)
|
||||
*
|
||||
* If `reminderSettings` is provided it's used directly, avoiding a query.
|
||||
* Otherwise it's read from the envelope's documentMeta (already resolved
|
||||
* from the org/team cascade at envelope creation time).
|
||||
*/
|
||||
export const updateRecipientNextReminder = async (options: {
|
||||
recipientId: number;
|
||||
envelopeId: string;
|
||||
sentAt: Date;
|
||||
lastReminderSentAt: Date | null;
|
||||
reminderSettings?: ReturnType<typeof ZEnvelopeReminderSettings.parse> | null;
|
||||
}) => {
|
||||
const { recipientId, envelopeId, sentAt, lastReminderSentAt } = options;
|
||||
|
||||
let settings = options.reminderSettings;
|
||||
|
||||
if (settings === undefined) {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: { id: envelopeId },
|
||||
select: { documentMeta: { select: { reminderSettings: true } } },
|
||||
});
|
||||
|
||||
settings = envelope?.documentMeta?.reminderSettings
|
||||
? ZEnvelopeReminderSettings.parse(envelope.documentMeta.reminderSettings)
|
||||
: null;
|
||||
}
|
||||
|
||||
const nextReminderAt = resolveNextReminderAt({
|
||||
config: settings,
|
||||
sentAt,
|
||||
lastReminderSentAt,
|
||||
});
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: { id: recipientId },
|
||||
data: { nextReminderAt },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Recompute `nextReminderAt` for all active (unsigned, sent) recipients
|
||||
* of a given envelope. Call when document-level reminder settings change.
|
||||
*/
|
||||
export const recomputeNextReminderForEnvelope = async (envelopeId: string) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: { id: envelopeId },
|
||||
select: {
|
||||
documentMeta: {
|
||||
select: { reminderSettings: true, distributionMethod: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// No reminders for manually distributed documents.
|
||||
const isEmailDistribution =
|
||||
envelope?.documentMeta?.distributionMethod !== DocumentDistributionMethod.NONE;
|
||||
|
||||
const settings =
|
||||
isEmailDistribution && envelope?.documentMeta?.reminderSettings
|
||||
? ZEnvelopeReminderSettings.parse(envelope.documentMeta.reminderSettings)
|
||||
: null;
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
envelopeId,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
sendStatus: SendStatus.SENT,
|
||||
sentAt: { not: null },
|
||||
role: { not: RecipientRole.CC },
|
||||
},
|
||||
select: { id: true, sentAt: true, lastReminderSentAt: true },
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
recipients.map(async (recipient) => {
|
||||
if (!recipient.sentAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextReminderAt = resolveNextReminderAt({
|
||||
config: settings,
|
||||
sentAt: recipient.sentAt,
|
||||
lastReminderSentAt: recipient.lastReminderSentAt,
|
||||
});
|
||||
|
||||
await prisma.recipient.update({
|
||||
where: { id: recipient.id },
|
||||
data: { nextReminderAt },
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -705,6 +705,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
sentAt: new Date(),
|
||||
...(nextSigner && documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
|
||||
Reference in New Issue
Block a user