feat(document-templates): delete TipTap-to-pdfme bridge

Phase 1 / commit 12 of 14 — strips out the 571-line tiptap-to-pdfme
serializer and every code path that depended on it. TipTap document
templates remain as Documenso-template seed bodies; the CRM no longer
renders them to PDF in-app.

Deleted:
  src/lib/pdf/tiptap-to-pdfme.ts                                (571 LOC)
  src/lib/pdf/templates/eoi-standard-inapp.ts                   (337 LOC)
  src/app/api/v1/admin/templates/preview/route.ts
  src/app/api/v1/document-templates/[id]/generate/route.ts
  src/app/api/v1/document-templates/[id]/generate-and-send/route.ts
  src/lib/services/document-templates.ts:generateFromTemplate (~140 LOC)
  src/lib/services/document-templates.ts:generateAndSend       (~40 LOC)
  src/lib/validators/document-templates.ts:generateAndSendSchema
  src/lib/validators/document-templates.ts:previewAdminTemplateSchema
  tests/unit/tiptap-serializer.test.ts (old bridge tests)

Preserved as src/lib/pdf/tiptap-validation.ts (~70 LOC):
  - validateTipTapDocument()  — still used to reject unsupported nodes
    on save in the admin template editor
  - TEMPLATE_VARIABLES        — drives the merge-token picker in the
    admin template form + preview UI

generateAndSign() now throws a clear ValidationError when a non-EOI
template tries the in-app pathway. Use a Documenso template, or wait
for the deferred AcroForm-fill admin-upload feature.

seed-data.ts: "Standard EOI (in-app)" template row now seeds with stub
bodyHtml + small MERGE_FIELDS array; the deleted HTML helper was never
actually rendered (in-app EOI is pdf-lib AcroForm fill on the source
PDF — generateEoiPdfFromTemplate, unchanged).

After this commit, pdfme has zero callers left. Commit 14 drops the
deps and the generate.ts shim.

1298/1298 vitest green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 21:11:23 +02:00
parent ed2424cc68
commit 411d0764e8
14 changed files with 137 additions and 1497 deletions

View File

@@ -17,7 +17,7 @@ import { documentTemplates } from '@/lib/db/schema/documents';
import { auditLogs } from '@/lib/db/schema/system';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { validateTipTapDocument } from '@/lib/pdf/tiptap-to-pdfme';
import { validateTipTapDocument } from '@/lib/pdf/tiptap-validation';
import type {
CreateAdminTemplateInput,
UpdateAdminTemplateInput,

View File

@@ -16,8 +16,6 @@ import { emitToRoom } from '@/lib/socket/server';
import { buildStoragePath } from '@/lib/minio';
import { getStorageBackend } from '@/lib/storage';
import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
import { generatePdf } from '@/lib/pdf/generate';
import { getCountryName } from '@/lib/i18n/countries';
import {
createDocument as documensoCreate,
@@ -30,7 +28,6 @@ import { generateEoiPdfFromTemplate } from '@/lib/pdf/fill-eoi-form';
import { MERGE_FIELDS, type MergeFieldCatalog } from '@/lib/templates/merge-fields';
import { buildEoiContext } from '@/lib/services/eoi-context';
import { getPrimaryBerth } from '@/lib/services/interest-berths.service';
import { sendEmail } from '@/lib/email';
import type {
CreateTemplateInput,
UpdateTemplateInput,
@@ -457,184 +454,20 @@ export async function resolveTemplate(
return resolved;
}
// ─── Generate From Template ───────────────────────────────────────────────────
/**
* BR-142: Resolve template → HTML → PDF. Store in MinIO + create file/document records.
*/
export async function generateFromTemplate(
templateId: string,
portId: string,
context: GenerateInput,
meta: AuditMeta,
): Promise<{ document: unknown; file: unknown }> {
const template = await getTemplateById(templateId, portId);
const resolvedHtml = await resolveTemplate(templateId, { ...context, portId });
const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
// Wrap HTML in a minimal full-page document for pdfme text block
const wrappedContent = resolvedHtml
.replace(/<[^>]+>/g, ' ') // strip HTML tags for plain-text PDF rendering
.replace(/\s+/g, ' ')
.trim();
// Use a simple single-field pdfme template for the HTML body
const pdfTemplate = {
basePdf: 'BLANK_PDF' as unknown as string,
schemas: [
[
{
name: 'portName',
type: 'text' as const,
position: { x: 20, y: 15 },
width: 170,
height: 10,
fontSize: 14,
},
{
name: 'body',
type: 'text' as const,
position: { x: 20, y: 30 },
width: 170,
height: 230,
fontSize: 9,
},
{
name: 'generatedAt',
type: 'text' as const,
position: { x: 20, y: 275 },
width: 170,
height: 6,
fontSize: 7,
},
],
],
};
const pdfBytes = await generatePdf(pdfTemplate, [
{
portName: `${port?.name ?? 'Port Nimara'} - ${template.name}`,
body: wrappedContent,
generatedAt: `Generated: ${new Date().toLocaleString('en-GB')}`,
},
]);
// Store in MinIO
const fileId = crypto.randomUUID();
const storagePath = buildStoragePath(
port?.slug ?? portId,
'document-templates',
templateId,
fileId,
'pdf',
);
{
const buffer = Buffer.from(pdfBytes);
const backend = await getStorageBackend();
await backend.put(storagePath, buffer, {
contentType: 'application/pdf',
sizeBytes: buffer.length,
});
}
// Create file record
const [fileRecord] = await db
.insert(files)
.values({
portId,
clientId: context.clientId ?? null,
filename: `${template.name.toLowerCase().replace(/\s+/g, '-')}.pdf`,
originalName: `${template.name}.pdf`,
mimeType: 'application/pdf',
sizeBytes: String(pdfBytes.byteLength),
storagePath,
storageBucket: env.MINIO_BUCKET,
category: 'correspondence',
uploadedBy: meta.userId,
})
.returning();
// Create document record
const [documentRecord] = await db
.insert(documents)
.values({
portId,
clientId: context.clientId ?? null,
interestId: context.interestId ?? null,
documentType: template.templateType,
title: template.name,
status: 'draft',
fileId: fileRecord!.id,
isManualUpload: false,
createdBy: meta.userId,
})
.returning();
void createAuditLog({
userId: meta.userId,
portId,
action: 'create',
entityType: 'document',
entityId: documentRecord!.id,
newValue: {
templateId,
templateName: template.name,
clientId: context.clientId,
interestId: context.interestId,
berthId: context.berthId,
},
ipAddress: meta.ipAddress,
userAgent: meta.userAgent,
});
emitToRoom(`port:${portId}`, 'document:created', { documentId: documentRecord!.id });
return { document: documentRecord!, file: fileRecord! };
}
// ─── Generate and Send ────────────────────────────────────────────────────────
export async function generateAndSend(
templateId: string,
portId: string,
context: GenerateInput,
recipientEmail: string,
meta: AuditMeta,
) {
const { document, file } = await generateFromTemplate(templateId, portId, context, meta);
const template = await getTemplateById(templateId, portId);
const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
// Send email with PDF as attachment (base64 encoded body)
try {
const resolvedHtml = await resolveTemplate(templateId, { ...context, portId });
await sendEmail(
recipientEmail,
template.name,
`<p>Please find the attached document: <strong>${template.name}</strong></p><hr/>${resolvedHtml}`,
`${port?.name ?? 'Port Nimara'} <noreply@${env.SMTP_HOST}>`,
);
} catch (err) {
logger.error({ err, templateId, recipientEmail }, 'Failed to send template email');
// Don't throw - document was created successfully; email failure is non-fatal
}
void createAuditLog({
userId: meta.userId,
portId,
action: 'create',
entityType: 'documentTemplate',
entityId: templateId,
metadata: { action: 'generate_and_send', recipientEmail },
ipAddress: meta.ipAddress,
userAgent: meta.userAgent,
});
return { document, file };
}
// ─── Generate from template (REMOVED) ─────────────────────────────────────────
//
// The in-app TipTap-to-PDF rendering path (`generateFromTemplate` and the
// public `generateAndSend` wrapper) was removed in the PDF stack overhaul
// (see docs/superpowers/specs/2026-05-12-pdf-stack-overhaul-design.md).
// Only the EOI in-app pathway survives, and it renders via pdf-lib AcroForm
// fill on the source PDF (`generateEoiFromSourcePdf` below). All other
// template types must go through Documenso.
//
// The old API routes `/api/v1/document-templates/[id]/generate` and
// `/api/v1/document-templates/[id]/generate-and-send` have been deleted.
// `generateAndSign` (the EOI signing entry point) now throws a clear
// ValidationError when a non-EOI template is requested through the in-app
// pathway.
// ─── EOI from source PDF (in-app pathway, EOI templates only) ─────────────────
@@ -807,15 +640,22 @@ async function generateAndSignViaInApp(
}
// EOI templates fill the same source PDF as the Documenso template (so both
// pathways yield the same document). Other template types stay on the
// HTML→pdfme rendering path.
const { document: documentRecord, file } =
template.templateType === 'eoi'
? await generateEoiFromSourcePdf(template, portId, context, meta)
: ((await generateFromTemplate(templateId, portId, context, meta)) as {
document: DbDocument;
file: DbFile;
});
// pathways yield the same document). The HTML→pdfme rendering path for
// non-EOI templates was removed in the PDF stack overhaul (see the design
// spec). Send non-EOI documents via the Documenso pathway, OR — once it
// ships — the admin-uploaded AcroForm-fill template feature.
if (template.templateType !== 'eoi') {
throw new ValidationError(
`In-app PDF rendering for templates of type "${template.templateType}" is not supported. ` +
'Use a Documenso template, or upload a custom PDF (AcroForm-fill feature is deferred).',
);
}
const { document: documentRecord, file } = await generateEoiFromSourcePdf(
template,
portId,
context,
meta,
);
// Fetch PDF bytes from the active storage backend to send to Documenso.
const pdfStream = await (await getStorageBackend()).get(file.storagePath);