fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-01-30 11:28:05 +00:00
322 changed files with 19265 additions and 7182 deletions

View File

@ -6,6 +6,7 @@ import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-sig
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
import { SEND_TEAM_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-joined-email';
import { SEND_TEAM_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-member-left-email';
import { BULK_SEND_TEMPLATE_JOB_DEFINITION } from './definitions/internal/bulk-send-template';
import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document';
/**
@ -21,6 +22,7 @@ export const jobsClient = new JobClient([
SEAL_DOCUMENT_JOB_DEFINITION,
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
BULK_SEND_TEMPLATE_JOB_DEFINITION,
] as const);
export const jobs = jobsClient;

View File

@ -0,0 +1,39 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID = 'send.bulk.complete.email';
const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
userId: z.number(),
templateId: z.number(),
templateName: z.string(),
totalProcessed: z.number(),
successCount: z.number(),
failedCount: z.number(),
errors: z.array(z.string()),
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TSendBulkCompleteEmailJobDefinition = z.infer<
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA
>;
export const SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION = {
id: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
name: 'Send Bulk Complete Email',
version: '1.0.0',
trigger: {
name: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
schema: SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./send-bulk-complete-email.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof SEND_BULK_COMPLETE_EMAIL_JOB_DEFINITION_ID,
TSendBulkCompleteEmailJobDefinition
>;

View File

@ -36,19 +36,19 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
const document = await prisma.document.findFirst({
where: {
id: documentId,
Recipient: {
recipients: {
some: {
id: recipientId,
},
},
},
include: {
Recipient: {
recipients: {
where: {
id: recipientId,
},
},
User: true,
user: true,
documentMeta: true,
team: {
include: {
@ -62,7 +62,7 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
throw new Error('Document not found');
}
if (document.Recipient.length === 0) {
if (document.recipients.length === 0) {
throw new Error('Document has no recipients');
}
@ -74,9 +74,9 @@ export const SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION = {
return;
}
const [recipient] = document.Recipient;
const [recipient] = document.recipients;
const { email: recipientEmail, name: recipientName } = recipient;
const { User: owner } = document;
const { user: owner } = document;
const recipientReference = recipientName || recipientEmail;

View File

@ -33,7 +33,7 @@ export const run = async ({
id: documentId,
},
include: {
User: true,
user: true,
documentMeta: true,
team: {
select: {
@ -53,7 +53,7 @@ export const run = async ({
}),
]);
const { documentMeta, team, User: documentOwner } = document;
const { documentMeta, user: documentOwner } = document;
const isEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
@ -70,7 +70,7 @@ export const run = async ({
const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, {
recipientName: recipient.name,
documentName: document.title,
documentOwnerName: document.User.name || document.User.email,
documentOwnerName: document.user.name || document.user.email,
reason: recipient.rejectionReason || '',
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});

View File

@ -0,0 +1,208 @@
import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { parse } from 'csv-parse/sync';
import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { BulkSendCompleteEmail } from '@documenso/email/templates/bulk-send-complete';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { prisma } from '@documenso/prisma';
import type { TeamGlobalSettings } from '@documenso/prisma/client';
import { getI18nInstance } from '../../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { AppError } from '../../../errors/app-error';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../../utils/team-global-settings-to-branding';
import type { JobRunIO } from '../../client/_internal/job';
import type { TBulkSendTemplateJobDefinition } from './bulk-send-template';
const ZRecipientRowSchema = z.object({
name: z.string().optional(),
email: z.union([
z.string().email({ message: 'Value must be a valid email or empty string' }),
z.string().max(0, { message: 'Value must be a valid email or empty string' }),
]),
});
export const run = async ({
payload,
io,
}: {
payload: TBulkSendTemplateJobDefinition;
io: JobRunIO;
}) => {
const { userId, teamId, templateId, csvContent, sendImmediately, requestMetadata } = payload;
const template = await getTemplateById({
id: templateId,
userId,
teamId,
});
if (!template) {
throw new Error('Template not found');
}
const rows = parse(csvContent, { columns: true, skip_empty_lines: true });
if (rows.length > 100) {
throw new Error('Maximum 100 rows allowed per upload');
}
const { recipients } = template;
// Validate CSV structure
const csvHeaders = Object.keys(rows[0]);
const requiredHeaders = recipients.map((_, index) => `recipient_${index + 1}_email`);
for (const header of requiredHeaders) {
if (!csvHeaders.includes(header)) {
throw new Error(`Missing required column: ${header}`);
}
}
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
select: {
email: true,
name: true,
},
});
const results = {
success: 0,
failed: 0,
errors: Array<string>(),
};
// Process each row
for (const [rowIndex, row] of rows.entries()) {
try {
for (const [recipientIndex] of recipients.entries()) {
const nameKey = `recipient_${recipientIndex + 1}_name`;
const emailKey = `recipient_${recipientIndex + 1}_email`;
const parsed = ZRecipientRowSchema.safeParse({
name: row[nameKey],
email: row[emailKey],
});
if (!parsed.success) {
throw new Error(
`Invalid recipient data provided for ${emailKey}, ${nameKey}: ${parsed.error.issues?.[0]?.message}`,
);
}
}
const document = await io.runTask(`create-document-${rowIndex}`, async () => {
return await createDocumentFromTemplate({
templateId: template.id,
userId,
teamId,
recipients: recipients.map((recipient, index) => {
return {
id: recipient.id,
email: row[`recipient_${index + 1}_email`] || recipient.email,
name: row[`recipient_${index + 1}_name`] || recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
};
}),
requestMetadata: {
source: 'app',
auth: 'session',
requestMetadata: requestMetadata || {},
},
});
});
if (sendImmediately) {
await io.runTask(`send-document-${rowIndex}`, async () => {
await sendDocument({
documentId: document.id,
userId,
teamId,
requestMetadata: {
source: 'app',
auth: 'session',
requestMetadata: requestMetadata || {},
},
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
});
}
results.success += 1;
} catch (error) {
results.failed += 1;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.errors.push(`Row ${rowIndex + 1}: Was unable to be processed - ${errorMessage}`);
}
}
await io.runTask('send-completion-email', async () => {
const completionTemplate = createElement(BulkSendCompleteEmail, {
userName: user.name || user.email,
templateName: template.title,
totalProcessed: rows.length,
successCount: results.success,
failedCount: results.failed,
errors: results.errors,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
let teamGlobalSettings: TeamGlobalSettings | undefined | null;
if (template.teamId) {
teamGlobalSettings = await prisma.teamGlobalSettings.findUnique({
where: {
teamId: template.teamId,
},
});
}
const branding = teamGlobalSettings
? teamGlobalSettingsToBranding(teamGlobalSettings)
: undefined;
const i18n = await getI18nInstance(teamGlobalSettings?.documentLanguage);
const [html, text] = await Promise.all([
renderEmailWithI18N(completionTemplate, {
lang: teamGlobalSettings?.documentLanguage,
branding,
}),
renderEmailWithI18N(completionTemplate, {
lang: teamGlobalSettings?.documentLanguage,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: {
name: user.name || '',
address: user.email,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: i18n._(msg`Bulk Send Complete: ${template.title}`),
html,
text,
});
});
};

View File

@ -0,0 +1,37 @@
import { z } from 'zod';
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
import { type JobDefinition } from '../../client/_internal/job';
const BULK_SEND_TEMPLATE_JOB_DEFINITION_ID = 'internal.bulk-send-template';
const BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA = z.object({
userId: z.number(),
teamId: z.number().optional(),
templateId: z.number(),
csvContent: z.string(),
sendImmediately: z.boolean(),
requestMetadata: ZRequestMetadataSchema.optional(),
});
export type TBulkSendTemplateJobDefinition = z.infer<
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA
>;
export const BULK_SEND_TEMPLATE_JOB_DEFINITION = {
id: BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
name: 'Bulk Send Template',
version: '1.0.0',
trigger: {
name: BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
schema: BULK_SEND_TEMPLATE_JOB_DEFINITION_SCHEMA,
},
handler: async ({ payload, io }) => {
const handler = await import('./bulk-send-template.handler');
await handler.run({ payload, io });
},
} as const satisfies JobDefinition<
typeof BULK_SEND_TEMPLATE_JOB_DEFINITION_ID,
TBulkSendTemplateJobDefinition
>;

View File

@ -20,7 +20,10 @@ import { insertFieldInPDF } from '../../../server-only/pdf/insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../../../server-only/pdf/normalize-signature-appearances';
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
import { ZWebhookDocumentSchema } from '../../../types/webhook-payload';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../../types/webhook-payload';
import { getFile } from '../../../universal/upload/get-file';
import { putPdfFile } from '../../../universal/upload/put-file';
import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers';
@ -40,7 +43,7 @@ export const run = async ({
const document = await prisma.document.findFirstOrThrow({
where: {
id: documentId,
Recipient: {
recipients: {
every: {
signingStatus: SigningStatus.SIGNED,
},
@ -48,7 +51,7 @@ export const run = async ({
},
include: {
documentMeta: true,
Recipient: true,
recipients: true,
team: {
select: {
teamGlobalSettings: {
@ -102,7 +105,7 @@ export const run = async ({
documentId: document.id,
},
include: {
Signature: true,
signature: true,
},
});
@ -258,13 +261,13 @@ export const run = async ({
include: {
documentData: true,
documentMeta: true,
Recipient: true,
recipients: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
data: ZWebhookDocumentSchema.parse(updatedDocument),
data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)),
userId: updatedDocument.userId,
teamId: updatedDocument.teamId ?? undefined,
});