documenso/packages/lib/server-only/document/resend-document.ts

223 lines
6.8 KiB
TypeScript
Raw Normal View History

2023-11-16 02:05:45 +00:00
import { createElement } from 'react';
2025-01-02 04:33:37 +00:00
import { msg } from '@lingui/core/macro';
2025-06-10 01:49:52 +00:00
import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client';
2023-11-16 02:05:45 +00:00
import { mailer } from '@documenso/email/mailer';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '@documenso/lib/constants/recipient-roles';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
2023-11-16 02:05:45 +00:00
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
import { prisma } from '@documenso/prisma';
2025-01-02 04:33:37 +00:00
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { isDocumentCompleted } from '../../utils/document';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
2025-06-10 01:49:52 +00:00
import { getEmailContext } from '../email/get-email-context';
import { getDocumentWhereInput } from './get-document-by-id';
2023-11-16 02:05:45 +00:00
export type ResendDocumentOptions = {
documentId: number;
userId: number;
recipients: number[];
2025-06-10 01:49:52 +00:00
teamId: number;
requestMetadata: ApiRequestMetadata;
2023-11-16 02:05:45 +00:00
};
export const resendDocument = async ({
documentId,
userId,
recipients,
teamId,
requestMetadata,
2024-12-13 16:23:35 +00:00
}: ResendDocumentOptions): Promise<void> => {
2023-11-16 02:05:45 +00:00
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
2025-06-10 01:49:52 +00:00
const { documentWhereInput } = await getDocumentWhereInput({
documentId,
userId,
teamId,
});
2023-11-16 02:05:45 +00:00
const document = await prisma.document.findUnique({
where: documentWhereInput,
2023-11-16 02:05:45 +00:00
include: {
recipients: true,
2023-11-16 02:05:45 +00:00
documentMeta: true,
team: {
select: {
teamEmail: true,
name: true,
},
},
2023-11-16 02:05:45 +00:00
},
});
const customEmail = document?.documentMeta;
if (!document) {
throw new Error('Document not found');
}
2025-01-13 02:41:53 +00:00
if (document.recipients.length === 0) {
2023-11-16 02:05:45 +00:00
throw new Error('Document has no recipients');
}
if (document.status === DocumentStatus.DRAFT) {
throw new Error('Can not send draft document');
}
if (isDocumentCompleted(document.status)) {
2023-11-16 02:05:45 +00:00
throw new Error('Can not send completed document');
}
const recipientsToRemind = document.recipients.filter(
(recipient) =>
recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED,
);
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).recipientSigningRequest;
if (!isRecipientSigningRequestEmailEnabled) {
return;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
meta: document.documentMeta,
});
2025-06-10 01:49:52 +00:00
2023-12-02 01:43:43 +00:00
await Promise.all(
recipientsToRemind.map(async (recipient) => {
if (recipient.role === RecipientRole.CC) {
return;
}
const i18n = await getI18nInstance(emailLanguage);
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
2023-11-16 02:05:45 +00:00
const { email, name } = recipient;
feat: update emails for self-signer (#1108) ## Description Updated the email content based on whether the document owner is a recipient or not. If the document owner is a recipient (self-signer): * the email subject will be `Please view/sign/approve your document` * the email header will be `Please view/sign/approve your document "<your-doc-title>"` * the email content will be `You have initiated the document "<your-doc-title>" that requires you to view/sign/approve it.` Otherwise: * the email subject will be `Please view/sign/approve this document` * the email header will be `<doc-owner> has invited you to view/sign/approve "<doc-title>"` * the email content will be `<doc-owner> has invited you to view/sign/approve the document "<doc-title>".` ## Related Issue Related to #1091 ## Testing Performed Tested the feature with a different number of recipients (including and excluding the document owner - self-signer). Tested both the sending and resending functionality. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## UI Screenshots ![CleanShot 2024-04-18 at 12 26 11@2x](https://github.com/documenso/documenso/assets/25515812/ca80f625-befb-4cbc-a541-f2186379d2e8) ![CleanShot 2024-04-18 at 12 27 40@2x](https://github.com/documenso/documenso/assets/25515812/8bcbb6fc-ba98-4fa1-8538-2d062febd27b) ![CleanShot 2024-04-18 at 12 27 53@2x](https://github.com/documenso/documenso/assets/25515812/25d77d98-b5ec-4270-8ffa-43774fe70526) ![CleanShot 2024-04-18 at 12 30 00@2x](https://github.com/documenso/documenso/assets/25515812/a90bb8e3-3ea8-42ff-9971-559b3e81ae6f) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced the document invitation components to support scenarios where the recipient is also the sender, providing customized email content and subject lines. - Introduced new properties in email templates to improve clarity and relevance based on the user's role in the document signing process. - **Refactor** - Updated components to use a more flexible `headerContent` property for displaying invitation headers, replacing previous individual inviter details. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-19 10:45:33 +00:00
const selfSigner = email === user.email;
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
.toLowerCase();
let emailMessage = customEmail?.message || '';
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
if (selfSigner) {
emailMessage = i18n._(
msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`,
);
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
}
2025-06-10 01:49:52 +00:00
if (organisationType === OrganisationType.ORGANISATION) {
emailSubject = i18n._(
msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`,
);
emailMessage =
customEmail?.message ||
i18n._(
2025-01-02 04:33:37 +00:00
msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
);
}
2023-11-16 02:05:45 +00:00
const customEmailTemplate = {
'signer.name': name,
'signer.email': email,
'document.name': document.title,
};
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
2023-11-16 02:05:45 +00:00
const template = createElement(DocumentInviteEmailTemplate, {
documentName: document.title,
inviterName: user.name || undefined,
2025-06-10 01:49:52 +00:00
inviterEmail:
organisationType === OrganisationType.ORGANISATION
? document.team?.teamEmail?.email || user.email
: user.email,
2023-11-16 02:05:45 +00:00
assetBaseUrl,
signDocumentLink,
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
role: recipient.role,
feat: update emails for self-signer (#1108) ## Description Updated the email content based on whether the document owner is a recipient or not. If the document owner is a recipient (self-signer): * the email subject will be `Please view/sign/approve your document` * the email header will be `Please view/sign/approve your document "<your-doc-title>"` * the email content will be `You have initiated the document "<your-doc-title>" that requires you to view/sign/approve it.` Otherwise: * the email subject will be `Please view/sign/approve this document` * the email header will be `<doc-owner> has invited you to view/sign/approve "<doc-title>"` * the email content will be `<doc-owner> has invited you to view/sign/approve the document "<doc-title>".` ## Related Issue Related to #1091 ## Testing Performed Tested the feature with a different number of recipients (including and excluding the document owner - self-signer). Tested both the sending and resending functionality. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## UI Screenshots ![CleanShot 2024-04-18 at 12 26 11@2x](https://github.com/documenso/documenso/assets/25515812/ca80f625-befb-4cbc-a541-f2186379d2e8) ![CleanShot 2024-04-18 at 12 27 40@2x](https://github.com/documenso/documenso/assets/25515812/8bcbb6fc-ba98-4fa1-8538-2d062febd27b) ![CleanShot 2024-04-18 at 12 27 53@2x](https://github.com/documenso/documenso/assets/25515812/25d77d98-b5ec-4270-8ffa-43774fe70526) ![CleanShot 2024-04-18 at 12 30 00@2x](https://github.com/documenso/documenso/assets/25515812/a90bb8e3-3ea8-42ff-9971-559b3e81ae6f) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced the document invitation components to support scenarios where the recipient is also the sender, providing customized email content and subject lines. - Introduced new properties in email templates to improve clarity and relevance based on the user's role in the document signing process. - **Refactor** - Updated components to use a more flexible `headerContent` property for displaying invitation headers, replacing previous individual inviter details. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-04-19 10:45:33 +00:00
selfSigner,
2025-06-10 01:49:52 +00:00
organisationType,
teamName: document.team?.name,
2023-11-16 02:05:45 +00:00
});
2025-01-02 04:33:37 +00:00
const [html, text] = await Promise.all([
renderEmailWithI18N(template, {
lang: emailLanguage,
2025-01-02 04:33:37 +00:00
branding,
}),
renderEmailWithI18N(template, {
lang: emailLanguage,
2025-01-02 04:33:37 +00:00
branding,
plainText: true,
}),
]);
await prisma.$transaction(
async (tx) => {
await mailer.sendMail({
to: {
address: email,
name,
},
from: senderEmail,
replyTo: replyToEmail,
subject: customEmail?.subject
? renderCustomEmailTemplate(
i18n._(msg`Reminder: ${customEmail.subject}`),
customEmailTemplate,
)
: emailSubject,
html,
text,
});
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
metadata: requestMetadata,
data: {
emailType: recipientEmailType,
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientRole: recipient.role,
recipientId: recipient.id,
isResending: true,
},
}),
});
},
{ timeout: 30_000 },
);
2023-11-16 02:05:45 +00:00
}),
2023-12-02 01:43:43 +00:00
);
2023-11-16 02:05:45 +00:00
};