mirror of
https://github.com/documenso/documenso.git
synced 2026-07-26 18:04:55 +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.
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { EnvelopeType, RecipientRole } from '@documenso/prisma/client';
|
|
import { seedUser } from '@documenso/prisma/seed/users';
|
|
import type {
|
|
TCreateEnvelopePayload,
|
|
TCreateEnvelopeResponse,
|
|
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
|
import { expect, test } from '@playwright/test';
|
|
|
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
|
const baseUrl = `${WEBAPP_BASE_URL}/api/v2-beta`;
|
|
|
|
const ACROFORM_FIXTURE = fs.readFileSync(path.join(__dirname, '../../../../assets/acroform-import-test.pdf'));
|
|
|
|
test.describe.configure({
|
|
mode: 'parallel',
|
|
});
|
|
|
|
test.describe('AcroForm Import', () => {
|
|
test('imports AcroForm widgets as Documenso fields assigned to the provided signer', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const payload: TCreateEnvelopePayload = {
|
|
type: EnvelopeType.DOCUMENT,
|
|
title: 'AcroForm document',
|
|
recipients: [
|
|
{
|
|
email: 'signer@example.com',
|
|
name: 'Signer',
|
|
role: RecipientRole.SIGNER,
|
|
},
|
|
],
|
|
};
|
|
|
|
const formData = new FormData();
|
|
formData.append('payload', JSON.stringify(payload));
|
|
formData.append('files', new File([ACROFORM_FIXTURE], 'acroform-import-test.pdf', { type: 'application/pdf' }));
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
|
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
|
|
|
const envelope = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: response.id },
|
|
include: {
|
|
envelopeItems: { include: { documentData: true } },
|
|
recipients: true,
|
|
fields: true,
|
|
},
|
|
});
|
|
|
|
expect(envelope.recipients).toHaveLength(1);
|
|
expect(envelope.recipients[0].email).toBe('signer@example.com');
|
|
|
|
// Every imported field is assigned to the single signer.
|
|
expect(envelope.fields.length).toBeGreaterThanOrEqual(8);
|
|
expect(envelope.fields.every((f) => f.recipientId === envelope.recipients[0].id)).toBe(true);
|
|
|
|
// Every imported field has source: 'acroform' on its fieldMeta.
|
|
for (const field of envelope.fields) {
|
|
const meta = field.fieldMeta as { source?: string } | null;
|
|
expect(meta?.source).toBe('acroform');
|
|
}
|
|
|
|
// FIELD_CREATED audit log entries match the number of imported fields.
|
|
const auditEntries = await prisma.documentAuditLog.findMany({
|
|
where: { envelopeId: envelope.id, type: 'FIELD_CREATED' },
|
|
});
|
|
|
|
expect(auditEntries.length).toBe(envelope.fields.length);
|
|
});
|
|
|
|
test('creates a placeholder Recipient 1 SIGNER when no recipients are provided', async ({ request }) => {
|
|
const { user, team } = await seedUser();
|
|
const { token } = await createApiToken({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
tokenName: 'test',
|
|
expiresIn: null,
|
|
});
|
|
|
|
const payload: TCreateEnvelopePayload = {
|
|
type: EnvelopeType.DOCUMENT,
|
|
title: 'AcroForm document without recipients',
|
|
};
|
|
|
|
const formData = new FormData();
|
|
formData.append('payload', JSON.stringify(payload));
|
|
formData.append('files', new File([ACROFORM_FIXTURE], 'acroform-import-test.pdf', { type: 'application/pdf' }));
|
|
|
|
const res = await request.post(`${baseUrl}/envelope/create`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
multipart: formData,
|
|
});
|
|
|
|
expect(res.ok()).toBeTruthy();
|
|
|
|
const response = (await res.json()) as TCreateEnvelopeResponse;
|
|
|
|
const envelope = await prisma.envelope.findUniqueOrThrow({
|
|
where: { id: response.id },
|
|
include: {
|
|
recipients: true,
|
|
fields: true,
|
|
},
|
|
});
|
|
|
|
expect(envelope.recipients).toHaveLength(1);
|
|
expect(envelope.recipients[0].email).toBe('recipient.1@documenso.com');
|
|
expect(envelope.recipients[0].role).toBe(RecipientRole.SIGNER);
|
|
expect(envelope.fields.length).toBeGreaterThanOrEqual(8);
|
|
expect(envelope.fields.every((f) => f.recipientId === envelope.recipients[0].id)).toBe(true);
|
|
});
|
|
});
|