Compare commits

...
5 changed files with 87 additions and 25 deletions
@@ -169,12 +169,28 @@ export const run = async ({
}); });
} }
const envelopeCompletedAuditLog = createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
envelopeId: envelope.id,
requestMetadata,
user: null,
data: {
transactionId: nanoid(),
...(isRejected ? { isRejected: true, rejectionReason: rejectionReason } : {}),
},
});
const finalEnvelopeStatus = isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED;
let certificateDoc: PDF | null = null; let certificateDoc: PDF | null = null;
let auditLogDoc: PDF | null = null; let auditLogDoc: PDF | null = null;
if (settings.includeSigningCertificate || settings.includeAuditLog) { if (settings.includeSigningCertificate || settings.includeAuditLog) {
const certificatePayload = { const certificatePayload = {
envelope, envelope: {
...envelope,
status: finalEnvelopeStatus,
},
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients. recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
fields, fields,
language: envelope.documentMeta.language, language: envelope.documentMeta.language,
@@ -185,6 +201,7 @@ export const run = async ({
envelopeItems: envelopeItems.map((item) => item.title), envelopeItems: envelopeItems.map((item) => item.title),
pageWidth: PDF_SIZE_A4_72PPI.width, pageWidth: PDF_SIZE_A4_72PPI.width,
pageHeight: PDF_SIZE_A4_72PPI.height, pageHeight: PDF_SIZE_A4_72PPI.height,
envelopeCompletedAuditLog,
}; };
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation. // Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
@@ -263,22 +280,13 @@ export const run = async ({
id: envelope.id, id: envelope.id,
}, },
data: { data: {
status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED, status: finalEnvelopeStatus,
completedAt: new Date(), completedAt: new Date(),
}, },
}); });
await tx.documentAuditLog.create({ await tx.documentAuditLog.create({
data: createDocumentAuditLogData({ data: envelopeCompletedAuditLog,
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED,
envelopeId: envelope.id,
requestMetadata,
user: null,
data: {
transactionId: nanoid(),
...(isRejected ? { isRejected: true, rejectionReason: rejectionReason } : {}),
},
}),
}); });
}); });
@@ -1,6 +1,7 @@
import { PDF } from '@libpdf/core'; import { PDF } from '@libpdf/core';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { type TDocumentAuditLog } from '@documenso/lib/types/document-audit-logs';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n'; import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
@@ -15,12 +16,20 @@ type GenerateAuditLogPdfOptions = GenerateCertificatePdfOptions & {
}; };
export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => { export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) => {
const { envelope, envelopeOwner, envelopeItems, recipients, language, pageWidth, pageHeight } = const {
options; envelope,
envelopeOwner,
envelopeItems,
recipients,
language,
pageWidth,
pageHeight,
envelopeCompletedAuditLog,
} = options;
const documentLanguage = ZSupportedLanguageCodeSchema.parse(language); const documentLanguage = ZSupportedLanguageCodeSchema.parse(language);
const [organisationClaim, auditLogs, messages] = await Promise.all([ const [organisationClaim, partialAuditLogs, messages] = await Promise.all([
getOrganisationClaimByTeamId({ teamId: envelope.teamId }), getOrganisationClaimByTeamId({ teamId: envelope.teamId }),
getAuditLogs(envelope.id), getAuditLogs(envelope.id),
getTranslations(documentLanguage), getTranslations(documentLanguage),
@@ -31,6 +40,17 @@ export const generateAuditLogPdf = async (options: GenerateAuditLogPdfOptions) =
messages, messages,
}); });
const auditLogs: TDocumentAuditLog[] = [...partialAuditLogs];
if (envelopeCompletedAuditLog) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
auditLogs.unshift({
...envelopeCompletedAuditLog,
id: '',
createdAt: new Date(),
} satisfies Omit<TDocumentAuditLog, 'type'> as TDocumentAuditLog);
}
const auditLogPages = await renderAuditLogs({ const auditLogPages = await renderAuditLogs({
envelope, envelope,
envelopeOwner, envelopeOwner,
@@ -7,6 +7,8 @@ import { FieldType } from '@prisma/client';
import { prop, sortBy } from 'remeda'; import { prop, sortBy } from 'remeda';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n'; import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs'; import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs';
import { extractDocumentAuthMethods } from '../../utils/document-auth'; import { extractDocumentAuthMethods } from '../../utils/document-auth';
@@ -16,7 +18,14 @@ import { getOrganisationClaimByTeamId } from '../organisation/get-organisation-c
import { renderCertificate } from './render-certificate'; import { renderCertificate } from './render-certificate';
export type GenerateCertificatePdfOptions = { export type GenerateCertificatePdfOptions = {
envelope: Envelope & { /**
* Note: completedAt is not included since it's not real at this point in time.
*
* If we actually need it here in the future, we will need to preserve the
* completedAt value and pass it to the final `envelope.update` function when
* the document is initially sealed.
*/
envelope: Omit<Envelope, 'completedAt'> & {
documentMeta: DocumentMeta; documentMeta: DocumentMeta;
}; };
envelopeOwner: { envelopeOwner: {
@@ -30,6 +39,7 @@ export type GenerateCertificatePdfOptions = {
language?: string; language?: string;
pageWidth: number; pageWidth: number;
pageHeight: number; pageHeight: number;
envelopeCompletedAuditLog?: CreateDocumentAuditLogDataResponse;
}; };
export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => { export const generateCertificatePdf = async (options: GenerateCertificatePdfOptions) => {
@@ -30,7 +30,7 @@ export type AuditLogRecipient = {
}; };
type GenerateAuditLogsOptions = { type GenerateAuditLogsOptions = {
envelope: Envelope & { envelope: Omit<Envelope, 'completedAt'> & {
documentMeta: DocumentMeta; documentMeta: DocumentMeta;
}; };
envelopeItems: string[]; envelopeItems: string[];
@@ -168,7 +168,7 @@ const renderVerticalLabelAndText = (options: RenderVerticalLabelAndTextOptions)
}; };
type RenderOverviewCardOptions = { type RenderOverviewCardOptions = {
envelope: Envelope & { envelope: Omit<Envelope, 'completedAt'> & {
documentMeta: DocumentMeta; documentMeta: DocumentMeta;
}; };
envelopeItems: string[]; envelopeItems: string[];
@@ -78,6 +78,7 @@ const getDevice = (userAgent?: string | null): string => {
const textMutedForegroundLight = '#929DAE'; const textMutedForegroundLight = '#929DAE';
const textForeground = '#000'; const textForeground = '#000';
const textMutedForeground = '#64748B'; const textMutedForeground = '#64748B';
const textRejectedRed = '#dc2626';
const textBase = 10; const textBase = 10;
const textSm = 9; const textSm = 9;
const textXs = 8; const textXs = 8;
@@ -97,6 +98,8 @@ type RenderLabelAndTextOptions = {
text: string; text: string;
width: number; width: number;
y?: number; y?: number;
labelFill?: string;
valueFill?: string;
}; };
const renderLabelAndText = (options: RenderLabelAndTextOptions) => { const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
@@ -106,13 +109,16 @@ const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
y, y,
}); });
const labelFill = options.labelFill ?? textMutedForeground;
const valueFill = options.valueFill ?? textMutedForeground;
const label = new Konva.Text({ const label = new Konva.Text({
x: 0, x: 0,
y: 0, y: 0,
text: `${options.label}: `, text: `${options.label}: `,
fontStyle: fontMedium, fontStyle: fontMedium,
fontFamily: 'Inter', fontFamily: 'Inter',
fill: textMutedForeground, fill: labelFill,
fontSize: textSm, fontSize: textSm,
}); });
@@ -124,7 +130,7 @@ const renderLabelAndText = (options: RenderLabelAndTextOptions) => {
width: width - label.width(), width: width - label.width(),
fontFamily: 'Inter', fontFamily: 'Inter',
text: options.text, text: options.text,
fill: textMutedForeground, fill: valueFill,
wrap: 'char', wrap: 'char',
fontSize: textSm, fontSize: textSm,
}); });
@@ -269,6 +275,8 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
const columnWidth = width - columnPadding; const columnWidth = width - columnPadding;
const isRejected = Boolean(recipient.logs.rejected);
if (recipient.signatureField?.secondaryId) { if (recipient.signatureField?.secondaryId) {
// Signature container with green border // Signature container with green border
const signatureContainer = new Konva.Group({ x: 0, y: 0 }); const signatureContainer = new Konva.Group({ x: 0, y: 0 });
@@ -313,7 +321,10 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
signatureContainer.add(typedSig); signatureContainer.add(typedSig);
} }
// Do not add the signature container for rejected recipients.
if (!isRejected) {
column.add(signatureContainer); column.add(signatureContainer);
}
const signatureHeight = Math.max(signatureContainer.getClientRect().height, minSignatureHeight); const signatureHeight = Math.max(signatureContainer.getClientRect().height, minSignatureHeight);
@@ -342,7 +353,7 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
// Signature ID // Signature ID
const sigIdLabel = new Konva.Text({ const sigIdLabel = new Konva.Text({
x: 0, x: 0,
y: signatureHeight + 10, y: isRejected ? 0 : signatureHeight + 10,
text: `${i18n._(msg`Signature ID`)}:`, text: `${i18n._(msg`Signature ID`)}:`,
fill: textMutedForeground, fill: textMutedForeground,
width: columnWidth, width: columnWidth,
@@ -376,9 +387,11 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
column.add(naText); column.add(naText);
} }
const relevantLog = isRejected ? recipient.logs.rejected : recipient.logs.completed;
const ipLabelAndText = renderLabelAndText({ const ipLabelAndText = renderLabelAndText({
label: i18n._(msg`IP Address`), label: i18n._(msg`IP Address`),
text: recipient.logs.completed?.ipAddress ?? i18n._(msg`Unknown`), text: relevantLog?.ipAddress ?? i18n._(msg`Unknown`),
width, width,
y: column.getClientRect().height + 6, y: column.getClientRect().height + 6,
}); });
@@ -386,7 +399,7 @@ const renderColumnTwo = (options: RenderColumnOptions) => {
const deviceLabelAndText = renderLabelAndText({ const deviceLabelAndText = renderLabelAndText({
label: i18n._(msg`Device`), label: i18n._(msg`Device`),
text: getDevice(recipient.logs.completed?.userAgent), text: getDevice(relevantLog?.userAgent),
width, width,
y: column.getClientRect().height + 6, y: column.getClientRect().height + 6,
}); });
@@ -400,7 +413,14 @@ const renderColumnThree = (options: RenderColumnOptions) => {
const column = new Konva.Group(); const column = new Konva.Group();
const itemsToRender = [ type DetailItem = {
label: string;
value: string;
labelFill?: string;
valueFill?: string;
};
const itemsToRender: DetailItem[] = [
{ {
label: i18n._(msg`Sent`), label: i18n._(msg`Sent`),
value: recipient.logs.emailed value: recipient.logs.emailed
@@ -429,6 +449,8 @@ const renderColumnThree = (options: RenderColumnOptions) => {
value: DateTime.fromJSDate(recipient.logs.rejected.createdAt) value: DateTime.fromJSDate(recipient.logs.rejected.createdAt)
.setLocale(APP_I18N_OPTIONS.defaultLocale) .setLocale(APP_I18N_OPTIONS.defaultLocale)
.toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)'), .toFormat('yyyy-MM-dd hh:mm:ss a (ZZZZ)'),
labelFill: textRejectedRed,
valueFill: textRejectedRed,
}); });
} else { } else {
itemsToRender.push({ itemsToRender.push({
@@ -459,6 +481,8 @@ const renderColumnThree = (options: RenderColumnOptions) => {
text: item.value, text: item.value,
width, width,
y: column.getClientRect().height + (index === 0 ? 0 : 8), y: column.getClientRect().height + (index === 0 ? 0 : 8),
labelFill: item.labelFill,
valueFill: item.valueFill,
}); });
column.add(labelAndText); column.add(labelAndText);
} }