mirror of
https://github.com/documenso/documenso.git
synced 2026-07-11 21:45:18 +10:00
b8a11df768
Detect AcroForm widgets (text, checkbox, radio, dropdown, signature) at upload time and reuse their geometry as Documenso fields instead of stripping them via form.flatten(). Imported fields land in the editor as ordinary Field rows assigned to the first signable recipient, removing the manual re-placement step users hit when preparing PDFs in Adobe Acrobat. Extraction runs before normalizePdf so widget geometry is still readable. Text fields go through a name+format heuristic that maps DATE/NUMBER/EMAIL/ NAME/INITIALS/TEXT, with AcroForm /AA format actions taking precedence over name tokens. Coordinates are converted via per-rotation transforms (0/90/180/ 270) against the rendered page dimensions; widgets fully off-page are dropped, partial overlap is clamped. Signed signatures (SignatureField. isSigned()) are detected and skip both the import and the form flatten so the signature stays valid. Encrypted PDFs, XFA hybrids, malformed PDFs, and internal extractor errors all return an empty result with skipReason set so the upload proceeds untouched. Every imported field carries fieldMeta.source = 'acroform' (new optional on ZBaseFieldMeta) for future provenance queries. DOCUMENT envelopes emit a per-field FIELD_CREATED audit entry matching create-envelope-fields.ts. Recipient assignment picks the first Recipient with role SIGNER or APPROVER sorted by (signingOrder asc nulls last, id asc); when no signable recipient exists, a placeholder Recipient 1 SIGNER is created mirroring the placeholder-pipeline behaviour.
759 lines
24 KiB
TypeScript
759 lines
24 KiB
TypeScript
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
import type { AcroFormFieldImportInfo } from '@documenso/lib/server-only/pdf/acroform-fields';
|
|
import { convertAcroFormFieldsToFieldInputs } from '@documenso/lib/server-only/pdf/acroform-fields';
|
|
import type { PlaceholderInfo } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
|
import { convertPlaceholdersToFieldInputs } from '@documenso/lib/server-only/pdf/auto-place-fields';
|
|
import { findRecipientByPlaceholder } from '@documenso/lib/server-only/pdf/helpers';
|
|
import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf';
|
|
import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients';
|
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
import { prisma } from '@documenso/prisma';
|
|
import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client';
|
|
import {
|
|
DocumentSource,
|
|
EnvelopeType,
|
|
FolderType,
|
|
RecipientRole,
|
|
SendStatus,
|
|
SigningStatus,
|
|
WebhookTriggerEvents,
|
|
} from '@prisma/client';
|
|
|
|
import type {
|
|
TDocumentAccessAuthTypes,
|
|
TDocumentActionAuthTypes,
|
|
TRecipientAccessAuthTypes,
|
|
TRecipientActionAuthTypes,
|
|
} from '../../types/document-auth';
|
|
import type { TDocumentFormValues } from '../../types/document-form-values';
|
|
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
|
|
import type { TFieldAndMeta } from '../../types/field-meta';
|
|
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
|
import { extractDerivedDocumentMeta } from '../../utils/document';
|
|
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
|
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
|
import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id';
|
|
import { getTeamSettings } from '../team/get-team-settings';
|
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
|
|
type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & {
|
|
documentDataId: string;
|
|
page: number;
|
|
positionX: number;
|
|
positionY: number;
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
type CreateEnvelopeRecipientOptions = {
|
|
email: string;
|
|
name: string;
|
|
role: RecipientRole;
|
|
signingOrder?: number;
|
|
accessAuth?: TRecipientAccessAuthTypes[];
|
|
actionAuth?: TRecipientActionAuthTypes[];
|
|
fields?: CreateEnvelopeRecipientFieldOptions[];
|
|
};
|
|
|
|
export type CreateEnvelopeOptions = {
|
|
userId: number;
|
|
teamId: number;
|
|
normalizePdf?: boolean;
|
|
internalVersion: 1 | 2;
|
|
data: {
|
|
type: EnvelopeType;
|
|
title: string;
|
|
externalId?: string;
|
|
envelopeItems: {
|
|
title?: string;
|
|
documentDataId: string;
|
|
order?: number;
|
|
placeholders?: PlaceholderInfo[];
|
|
acroFormFields?: AcroFormFieldImportInfo[];
|
|
}[];
|
|
formValues?: TDocumentFormValues;
|
|
|
|
userTimezone?: string;
|
|
|
|
templateType?: TemplateType;
|
|
publicTitle?: string;
|
|
publicDescription?: string;
|
|
|
|
visibility?: DocumentVisibility;
|
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
|
recipients?: CreateEnvelopeRecipientOptions[];
|
|
folderId?: string;
|
|
delegatedDocumentOwner?: string;
|
|
};
|
|
attachments?: Array<{
|
|
label: string;
|
|
data: string;
|
|
type?: TEnvelopeAttachmentType;
|
|
}>;
|
|
|
|
/**
|
|
* Whether to bypass adding default recipients.
|
|
*
|
|
* Defaults to false.
|
|
*/
|
|
bypassDefaultRecipients?: boolean;
|
|
|
|
meta?: Partial<Omit<DocumentMeta, 'id'>>;
|
|
requestMetadata: ApiRequestMetadata;
|
|
};
|
|
|
|
export const createEnvelope = async ({
|
|
userId,
|
|
teamId,
|
|
normalizePdf,
|
|
data,
|
|
attachments,
|
|
meta,
|
|
requestMetadata,
|
|
internalVersion,
|
|
bypassDefaultRecipients = false,
|
|
}: CreateEnvelopeOptions) => {
|
|
const {
|
|
type,
|
|
title,
|
|
externalId,
|
|
formValues,
|
|
userTimezone,
|
|
folderId,
|
|
templateType,
|
|
globalAccessAuth,
|
|
globalActionAuth,
|
|
publicTitle,
|
|
publicDescription,
|
|
visibility: visibilityOverride,
|
|
delegatedDocumentOwner,
|
|
} = data;
|
|
|
|
const team = await prisma.team.findFirst({
|
|
where: buildTeamWhereQuery({ teamId, userId }),
|
|
include: {
|
|
organisation: {
|
|
select: {
|
|
organisationClaim: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!team) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Team not found',
|
|
});
|
|
}
|
|
|
|
// Verify that the folder exists and is associated with the team.
|
|
if (folderId) {
|
|
const folder = await prisma.folder.findUnique({
|
|
where: {
|
|
id: folderId,
|
|
type: data.type === EnvelopeType.TEMPLATE ? FolderType.TEMPLATE : FolderType.DOCUMENT,
|
|
team: buildTeamWhereQuery({ teamId, userId }),
|
|
},
|
|
});
|
|
|
|
if (!folder) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Folder not found',
|
|
});
|
|
}
|
|
}
|
|
|
|
const settings = await getTeamSettings({
|
|
userId,
|
|
teamId,
|
|
});
|
|
|
|
if (data.envelopeItems.length !== 1 && internalVersion === 1) {
|
|
throw new AppError(AppErrorCode.INVALID_BODY, {
|
|
message: 'Envelope items must have exactly 1 item for version 1',
|
|
});
|
|
}
|
|
|
|
let envelopeItems = data.envelopeItems;
|
|
|
|
// Todo: Envelopes - Remove
|
|
if (normalizePdf) {
|
|
envelopeItems = await Promise.all(
|
|
data.envelopeItems.map(async (item) => {
|
|
const documentData = await prisma.documentData.findFirst({
|
|
where: {
|
|
id: item.documentDataId,
|
|
},
|
|
});
|
|
|
|
if (!documentData) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Document data not found',
|
|
});
|
|
}
|
|
|
|
const buffer = await getFileServerSide(documentData);
|
|
|
|
const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer), {
|
|
flattenForm: type !== EnvelopeType.TEMPLATE,
|
|
});
|
|
|
|
const titleToUse = item.title || title;
|
|
|
|
const { documentData: newDocumentData } = await putPdfFileServerSide({
|
|
name: titleToUse,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(normalizedPdf),
|
|
});
|
|
|
|
return {
|
|
title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
|
|
documentDataId: newDocumentData.id,
|
|
order: item.order,
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
|
|
const authOptions = createDocumentAuthOptions({
|
|
globalAccessAuth: globalAccessAuth || [],
|
|
globalActionAuth: globalActionAuth || [],
|
|
});
|
|
|
|
const recipientsHaveActionAuth = data.recipients?.some(
|
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
|
);
|
|
|
|
// Check if user has permission to set the global action auth.
|
|
if (
|
|
(authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) &&
|
|
!team.organisation.organisationClaim.flags.cfr21
|
|
) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'You do not have permission to set the action auth',
|
|
});
|
|
}
|
|
|
|
const visibility = visibilityOverride || settings.documentVisibility;
|
|
|
|
const emailId = meta?.emailId;
|
|
|
|
// Validate that the email ID belongs to the organisation.
|
|
if (emailId) {
|
|
const email = await prisma.organisationEmail.findFirst({
|
|
where: {
|
|
id: emailId,
|
|
organisationId: team.organisationId,
|
|
},
|
|
});
|
|
|
|
if (!email) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Email not found',
|
|
});
|
|
}
|
|
}
|
|
|
|
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
|
// for uploads from the frontend
|
|
const timezoneToUse = meta?.timezone || settings.documentTimezone || userTimezone;
|
|
|
|
const getValidatedDelegatedOwner = async () => {
|
|
if (!settings.delegateDocumentOwnership || !delegatedDocumentOwner || requestMetadata.source === 'app') {
|
|
return null;
|
|
}
|
|
|
|
const delegatedOwner = await prisma.user.findFirst({
|
|
where: {
|
|
email: delegatedDocumentOwner,
|
|
},
|
|
});
|
|
|
|
if (!delegatedOwner) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'Delegated document owner must be a member of the team',
|
|
});
|
|
}
|
|
|
|
const isTeamMember = await prisma.team.findFirst({
|
|
where: buildTeamWhereQuery({ teamId, userId: delegatedOwner.id }),
|
|
});
|
|
|
|
if (!isTeamMember) {
|
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
|
message: 'Delegated document owner must be a member of the team',
|
|
});
|
|
}
|
|
|
|
return delegatedOwner;
|
|
};
|
|
|
|
const [documentMeta, secondaryId, delegatedOwner] = await Promise.all([
|
|
prisma.documentMeta.create({
|
|
data: extractDerivedDocumentMeta(settings, {
|
|
...meta,
|
|
timezone: timezoneToUse,
|
|
}),
|
|
}),
|
|
type === EnvelopeType.DOCUMENT
|
|
? incrementDocumentId().then((v) => v.formattedDocumentId)
|
|
: incrementTemplateId().then((v) => v.formattedTemplateId),
|
|
getValidatedDelegatedOwner(),
|
|
]);
|
|
const envelopeOwnerId = delegatedOwner?.id ?? userId;
|
|
|
|
const createdEnvelope = await prisma.$transaction(async (tx) => {
|
|
const envelope = await tx.envelope.create({
|
|
data: {
|
|
id: prefixedId('envelope'),
|
|
secondaryId,
|
|
internalVersion,
|
|
type,
|
|
title,
|
|
qrToken: prefixedId('qr'),
|
|
externalId,
|
|
envelopeItems: {
|
|
createMany: {
|
|
data: envelopeItems.map((item, i) => ({
|
|
id: prefixedId('envelope_item'),
|
|
title: item.title || title,
|
|
order: item.order !== undefined ? item.order : i + 1,
|
|
documentDataId: item.documentDataId,
|
|
})),
|
|
},
|
|
},
|
|
envelopeAttachments: {
|
|
createMany: {
|
|
data: (attachments || []).map((attachment) => ({
|
|
label: attachment.label,
|
|
data: attachment.data,
|
|
type: attachment.type ?? 'link',
|
|
})),
|
|
},
|
|
},
|
|
userId: envelopeOwnerId,
|
|
teamId,
|
|
authOptions,
|
|
visibility,
|
|
folderId,
|
|
formValues,
|
|
source: type === EnvelopeType.DOCUMENT ? DocumentSource.DOCUMENT : DocumentSource.TEMPLATE,
|
|
documentMetaId: documentMeta.id,
|
|
|
|
// Template specific fields.
|
|
templateType: type === EnvelopeType.TEMPLATE ? templateType : undefined,
|
|
publicTitle: type === EnvelopeType.TEMPLATE ? publicTitle : undefined,
|
|
publicDescription: type === EnvelopeType.TEMPLATE ? publicDescription : undefined,
|
|
},
|
|
include: {
|
|
envelopeItems: true,
|
|
},
|
|
});
|
|
|
|
const firstEnvelopeItem = envelope.envelopeItems[0];
|
|
|
|
const defaultRecipients =
|
|
settings.defaultRecipients && !bypassDefaultRecipients
|
|
? ZDefaultRecipientsSchema.parse(settings.defaultRecipients)
|
|
: [];
|
|
|
|
const mappedDefaultRecipients: CreateEnvelopeRecipientOptions[] = defaultRecipients.map((recipient) => ({
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
}));
|
|
|
|
const allRecipients = [...(data.recipients || []), ...mappedDefaultRecipients];
|
|
|
|
await Promise.all(
|
|
allRecipients.map(async (recipient) => {
|
|
const recipientAuthOptions = createRecipientAuthOptions({
|
|
accessAuth: recipient.accessAuth ?? [],
|
|
actionAuth: recipient.actionAuth ?? [],
|
|
});
|
|
|
|
const recipientFieldsToCreate = (recipient.fields || []).map((field) => {
|
|
let envelopeItemId = firstEnvelopeItem.id;
|
|
|
|
if (field.documentDataId) {
|
|
const foundEnvelopeItem = envelope.envelopeItems.find(
|
|
(item) => item.documentDataId === field.documentDataId,
|
|
);
|
|
|
|
if (!foundEnvelopeItem) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Document data not found',
|
|
});
|
|
}
|
|
|
|
envelopeItemId = foundEnvelopeItem.id;
|
|
}
|
|
|
|
return {
|
|
envelopeId: envelope.id,
|
|
envelopeItemId,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: field.fieldMeta || undefined,
|
|
};
|
|
});
|
|
|
|
await tx.recipient.create({
|
|
data: {
|
|
envelopeId: envelope.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
token: nanoid(),
|
|
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
|
signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
|
authOptions: recipientAuthOptions,
|
|
fields: {
|
|
createMany: {
|
|
data: recipientFieldsToCreate,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Create fields from PDF placeholders (extracted at upload time).
|
|
const itemsWithPlaceholders = envelopeItems.filter((item) => item.placeholders && item.placeholders.length > 0);
|
|
|
|
if (itemsWithPlaceholders.length > 0) {
|
|
// Collect all unique recipient placeholder references (e.g. "r1", "r2").
|
|
const allPlaceholders = itemsWithPlaceholders.flatMap((item) => item.placeholders ?? []);
|
|
const uniqueRecipientRefs = new Map<number, string>();
|
|
|
|
for (const p of allPlaceholders) {
|
|
const match = p.recipient.match(/^r(\d+)$/i);
|
|
|
|
if (match) {
|
|
const index = Number(match[1]);
|
|
|
|
if (!uniqueRecipientRefs.has(index)) {
|
|
uniqueRecipientRefs.set(index, `Recipient ${index}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch existing recipients (may have been created above from data.recipients or defaults).
|
|
let availableRecipients = await tx.recipient.findMany({
|
|
where: { envelopeId: envelope.id },
|
|
select: { id: true, email: true },
|
|
});
|
|
|
|
const shouldCreatePlaceholderRecipients =
|
|
(!data.recipients || data.recipients.length === 0) && uniqueRecipientRefs.size > 0;
|
|
|
|
// If recipients were not provided, create placeholder recipients even when defaults exist.
|
|
if (shouldCreatePlaceholderRecipients) {
|
|
const existingRecipientEmails = new Set(availableRecipients.map((recipient) => recipient.email.toLowerCase()));
|
|
|
|
const placeholderRecipients = Array.from(uniqueRecipientRefs.entries(), ([recipientIndex, name]) => ({
|
|
envelopeId: envelope.id,
|
|
email: `recipient.${recipientIndex}@documenso.com`,
|
|
name,
|
|
role: RecipientRole.SIGNER,
|
|
signingOrder: recipientIndex,
|
|
token: nanoid(),
|
|
sendStatus: SendStatus.NOT_SENT,
|
|
signingStatus: SigningStatus.NOT_SIGNED,
|
|
})).filter((recipient) => !existingRecipientEmails.has(recipient.email.toLowerCase()));
|
|
|
|
if (placeholderRecipients.length > 0) {
|
|
await tx.recipient.createMany({
|
|
data: placeholderRecipients,
|
|
});
|
|
|
|
// eslint-disable-next-line require-atomic-updates
|
|
availableRecipients = await tx.recipient.findMany({
|
|
where: { envelopeId: envelope.id },
|
|
select: { id: true, email: true },
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const item of itemsWithPlaceholders) {
|
|
const envelopeItem = envelope.envelopeItems.find((ei) => ei.documentDataId === item.documentDataId);
|
|
|
|
if (!envelopeItem) {
|
|
continue;
|
|
}
|
|
|
|
const fieldsToCreate = convertPlaceholdersToFieldInputs(
|
|
item.placeholders ?? [],
|
|
(recipientPlaceholder, placeholder) =>
|
|
findRecipientByPlaceholder(
|
|
recipientPlaceholder,
|
|
placeholder,
|
|
data.recipients && data.recipients.length > 0
|
|
? data.recipients.map((r) => {
|
|
const found = availableRecipients.find((cr) => cr.email === r.email);
|
|
|
|
if (!found) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: `Recipient not found for email: ${r.email}`,
|
|
});
|
|
}
|
|
|
|
return found;
|
|
})
|
|
: undefined,
|
|
availableRecipients,
|
|
),
|
|
envelopeItem.id,
|
|
);
|
|
|
|
if (fieldsToCreate.length > 0) {
|
|
await tx.field.createMany({
|
|
data: fieldsToCreate.map((field) => ({
|
|
envelopeId: envelope.id,
|
|
envelopeItemId: envelopeItem.id,
|
|
recipientId: field.recipientId,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: field.fieldMeta || undefined,
|
|
})),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create fields from imported AcroForm widgets (extracted at upload time).
|
|
// Runs after the placeholder branch so placeholder-created recipients are
|
|
// visible in `availableRecipientsForAcroForm`.
|
|
const itemsWithAcroFormFields = envelopeItems.filter(
|
|
(item) => item.acroFormFields && item.acroFormFields.length > 0,
|
|
);
|
|
|
|
if (itemsWithAcroFormFields.length > 0) {
|
|
let availableRecipientsForAcroForm = await tx.recipient.findMany({
|
|
where: { envelopeId: envelope.id },
|
|
select: { id: true, email: true, role: true, signingOrder: true },
|
|
});
|
|
|
|
const pickFirstSignableRecipient = () => {
|
|
const signable = availableRecipientsForAcroForm.filter(
|
|
(r) => r.role === RecipientRole.SIGNER || r.role === RecipientRole.APPROVER,
|
|
);
|
|
|
|
if (signable.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return [...signable].sort((a, b) => {
|
|
const aOrder = a.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
|
const bOrder = b.signingOrder ?? Number.MAX_SAFE_INTEGER;
|
|
|
|
if (aOrder !== bOrder) {
|
|
return aOrder - bOrder;
|
|
}
|
|
|
|
return a.id - b.id;
|
|
})[0];
|
|
};
|
|
|
|
let firstSignableRecipient = pickFirstSignableRecipient();
|
|
|
|
if (!firstSignableRecipient) {
|
|
// No signable recipient yet — create a placeholder Recipient 1 SIGNER
|
|
// mirroring the placeholder branch's behavior.
|
|
const placeholderEmail = 'recipient.1@documenso.com';
|
|
const existingPlaceholder = availableRecipientsForAcroForm.find(
|
|
(r) => r.email.toLowerCase() === placeholderEmail,
|
|
);
|
|
|
|
if (!existingPlaceholder) {
|
|
await tx.recipient.create({
|
|
data: {
|
|
envelopeId: envelope.id,
|
|
email: placeholderEmail,
|
|
name: 'Recipient 1',
|
|
role: RecipientRole.SIGNER,
|
|
signingOrder: 1,
|
|
token: nanoid(),
|
|
sendStatus: SendStatus.NOT_SENT,
|
|
signingStatus: SigningStatus.NOT_SIGNED,
|
|
},
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line require-atomic-updates
|
|
availableRecipientsForAcroForm = await tx.recipient.findMany({
|
|
where: { envelopeId: envelope.id },
|
|
select: { id: true, email: true, role: true, signingOrder: true },
|
|
});
|
|
|
|
firstSignableRecipient = pickFirstSignableRecipient();
|
|
}
|
|
|
|
if (!firstSignableRecipient) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Could not resolve a signable recipient for AcroForm import.',
|
|
});
|
|
}
|
|
|
|
const acroFormRecipient = firstSignableRecipient;
|
|
|
|
for (const item of itemsWithAcroFormFields) {
|
|
const envelopeItem = envelope.envelopeItems.find((ei) => ei.documentDataId === item.documentDataId);
|
|
|
|
if (!envelopeItem) {
|
|
continue;
|
|
}
|
|
|
|
const fieldsToCreate = convertAcroFormFieldsToFieldInputs(
|
|
item.acroFormFields ?? [],
|
|
() => acroFormRecipient,
|
|
envelopeItem.id,
|
|
);
|
|
|
|
if (fieldsToCreate.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const createdFields = await tx.field.createManyAndReturn({
|
|
data: fieldsToCreate.map((field) => ({
|
|
envelopeId: envelope.id,
|
|
envelopeItemId: envelopeItem.id,
|
|
recipientId: field.recipientId,
|
|
type: field.type,
|
|
page: field.page,
|
|
positionX: field.positionX,
|
|
positionY: field.positionY,
|
|
width: field.width,
|
|
height: field.height,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: field.fieldMeta || undefined,
|
|
})),
|
|
});
|
|
|
|
if (type === EnvelopeType.DOCUMENT) {
|
|
await tx.documentAuditLog.createMany({
|
|
data: createdFields.map((createdField) =>
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
|
envelopeId: envelope.id,
|
|
metadata: requestMetadata,
|
|
data: {
|
|
fieldId: createdField.secondaryId,
|
|
fieldRecipientEmail: acroFormRecipient.email,
|
|
fieldRecipientId: createdField.recipientId,
|
|
fieldType: createdField.type,
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const createdEnvelope = await tx.envelope.findFirst({
|
|
where: {
|
|
id: envelope.id,
|
|
},
|
|
include: {
|
|
documentMeta: true,
|
|
recipients: true,
|
|
fields: true,
|
|
folder: true,
|
|
envelopeAttachments: true,
|
|
envelopeItems: {
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!createdEnvelope) {
|
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
|
message: 'Envelope not found',
|
|
});
|
|
}
|
|
|
|
// Only create audit logs for documents.
|
|
if (type === EnvelopeType.DOCUMENT) {
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
|
envelopeId: envelope.id,
|
|
user: {
|
|
id: envelopeOwnerId,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
title,
|
|
source: {
|
|
type: DocumentSource.DOCUMENT,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
// Create audit log for delegated owner if validation passed
|
|
if (delegatedOwner) {
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELEGATED_OWNER_CREATED,
|
|
envelopeId: envelope.id,
|
|
user: {
|
|
id: userId,
|
|
},
|
|
metadata: requestMetadata,
|
|
data: {
|
|
delegatedOwnerName: delegatedOwner.name,
|
|
delegatedOwnerEmail: delegatedOwner.email,
|
|
teamName: team.name,
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
|
|
return createdEnvelope;
|
|
});
|
|
|
|
// Trigger webhook outside the transaction to avoid holding the connection
|
|
// open during network I/O.
|
|
if (type === EnvelopeType.DOCUMENT) {
|
|
await triggerWebhook({
|
|
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
|
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
|
userId,
|
|
teamId,
|
|
});
|
|
} else if (type === EnvelopeType.TEMPLATE) {
|
|
await triggerWebhook({
|
|
event: WebhookTriggerEvents.TEMPLATE_CREATED,
|
|
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)),
|
|
userId,
|
|
teamId,
|
|
});
|
|
}
|
|
|
|
return createdEnvelope;
|
|
};
|