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

@@ -1,337 +0,0 @@
/**
* Standard in-app EOI (Letter of Intent) template.
*
* Rendered in-app via pdfme (HTML → PDF pipeline) for ports that prefer the
* in-app PDF generation path over the Documenso template flow.
*
* Merge tokens use the {{section.field}} convention and match the
* `EoiContext` shape produced by `buildEoiContext` in
* `src/lib/services/eoi-context.ts`. The tokens are resolved by
* `resolveTemplate` (Task 11.4 wires the expanded resolver).
*
* Related:
* - Field mapping: docs/eoi-documenso-field-mapping.md
* - Context builder: src/lib/services/eoi-context.ts
* - Schema: document_templates (src/lib/db/schema/documents.ts)
*/
export const STANDARD_EOI_MERGE_FIELDS: string[] = [
'date.today',
'date.year',
'port.name',
'port.defaultCurrency',
'client.fullName',
'client.nationality',
'client.primaryEmail',
'client.primaryPhone',
'client.address.street',
'client.address.city',
'client.address.country',
'yacht.name',
'yacht.hullNumber',
'yacht.flag',
'yacht.yearBuilt',
'yacht.lengthFt',
'yacht.widthFt',
'yacht.draftFt',
'yacht.lengthM',
'yacht.widthM',
'yacht.draftM',
'company.name',
'company.legalName',
'company.taxId',
'company.billingAddress',
'owner.type',
'owner.name',
'owner.legalName',
'berth.mooringNumber',
'berth.area',
'berth.lengthFt',
'berth.price',
'berth.priceCurrency',
'berth.tenureType',
'interest.stage',
'interest.leadCategory',
'interest.dateFirstContact',
'interest.notes',
];
export function getStandardEoiTemplateHtml(): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Expression of Interest - Letter of Intent</title>
<style>
@page {
size: letter;
margin: 0.9in 0.9in 1.0in 0.9in;
}
html, body {
margin: 0;
padding: 0;
font-family: "Times New Roman", Georgia, serif;
font-size: 12pt;
color: #111;
line-height: 1.45;
}
.header {
border-bottom: 2px solid #111;
padding-bottom: 8pt;
margin-bottom: 18pt;
}
.header .port-name {
font-size: 18pt;
font-weight: 700;
letter-spacing: 0.5pt;
}
.header .doc-title {
margin-top: 4pt;
font-size: 14pt;
font-weight: 600;
font-style: italic;
}
.meta {
display: flex;
justify-content: space-between;
margin-bottom: 18pt;
font-size: 11pt;
}
h2.section {
font-size: 12pt;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8pt;
border-bottom: 1px solid #555;
padding-bottom: 2pt;
margin: 16pt 0 8pt 0;
}
table.fields {
width: 100%;
border-collapse: collapse;
margin-bottom: 6pt;
}
table.fields td {
padding: 3pt 6pt 3pt 0;
vertical-align: top;
}
table.fields td.label {
width: 34%;
font-weight: 600;
color: #333;
}
.addr-line {
margin: 0;
padding: 0;
}
.signatures {
margin-top: 36pt;
display: table;
width: 100%;
}
.signatures .slot {
display: table-cell;
width: 50%;
padding-right: 18pt;
vertical-align: top;
}
.signatures .slot:last-child {
padding-right: 0;
padding-left: 18pt;
}
.sig-line {
border-top: 1px solid #111;
margin-top: 42pt;
padding-top: 4pt;
font-size: 10pt;
color: #333;
}
.footer {
position: fixed;
bottom: 0.4in;
left: 0.9in;
right: 0.9in;
text-align: center;
font-size: 9pt;
color: #666;
border-top: 1px solid #ccc;
padding-top: 4pt;
}
.muted {
color: #666;
}
</style>
</head>
<body>
<div class="header">
<div class="port-name">{{port.name}}</div>
<div class="doc-title">Expression of Interest - Letter of Intent</div>
</div>
<div class="meta">
<div><strong>Date:</strong> {{date.today}}</div>
<div><strong>Port:</strong> {{port.name}}</div>
</div>
<p>
This Expression of Interest (the &ldquo;EOI&rdquo;) is entered into between
<strong>{{port.name}}</strong> and the Applicant named below, and records the
Applicant&rsquo;s non-binding intent to proceed toward a berth acquisition at
the port. It is subject to subsequent definitive documentation.
</p>
<h2 class="section">1. Applicant</h2>
<table class="fields">
<tr>
<td class="label">Full name</td>
<td>{{client.fullName}}</td>
</tr>
<tr>
<td class="label">Nationality</td>
<td>{{client.nationality}}</td>
</tr>
<tr>
<td class="label">Email</td>
<td>{{client.primaryEmail}}</td>
</tr>
<tr>
<td class="label">Phone</td>
<td>{{client.primaryPhone}}</td>
</tr>
<tr>
<td class="label">Address</td>
<td>
<p class="addr-line">{{client.address.street}}</p>
<p class="addr-line">{{client.address.city}}</p>
<p class="addr-line">{{client.address.country}}</p>
</td>
</tr>
</table>
<h2 class="section">2. Yacht</h2>
<table class="fields">
<tr>
<td class="label">Name</td>
<td>{{yacht.name}}</td>
</tr>
<tr>
<td class="label">Hull number</td>
<td>{{yacht.hullNumber}}</td>
</tr>
<tr>
<td class="label">Flag</td>
<td>{{yacht.flag}}</td>
</tr>
<tr>
<td class="label">Year built</td>
<td>{{yacht.yearBuilt}}</td>
</tr>
<tr>
<td class="label">Length (ft / m)</td>
<td>{{yacht.lengthFt}} ft &nbsp;/&nbsp; {{yacht.lengthM}} m</td>
</tr>
<tr>
<td class="label">Beam (ft / m)</td>
<td>{{yacht.widthFt}} ft &nbsp;/&nbsp; {{yacht.widthM}} m</td>
</tr>
<tr>
<td class="label">Draft (ft / m)</td>
<td>{{yacht.draftFt}} ft &nbsp;/&nbsp; {{yacht.draftM}} m</td>
</tr>
</table>
<h2 class="section">3. Owner</h2>
<p>
Owner type: <strong>{{owner.type}}</strong><br />
Owner name: <strong>{{owner.name}}</strong>
<span class="muted"> (legal: {{owner.legalName}})</span>
</p>
<table class="fields">
<tr>
<td class="label">Company name</td>
<td>{{company.name}}</td>
</tr>
<tr>
<td class="label">Legal name</td>
<td>{{company.legalName}}</td>
</tr>
<tr>
<td class="label">Tax ID</td>
<td>{{company.taxId}}</td>
</tr>
<tr>
<td class="label">Billing address</td>
<td>{{company.billingAddress}}</td>
</tr>
</table>
<p class="muted" style="font-size:10pt;">
The company block is populated only where the yacht is company-owned; for
client-owned yachts these fields render empty.
</p>
<h2 class="section">4. Berth</h2>
<table class="fields">
<tr>
<td class="label">Mooring number</td>
<td>{{berth.mooringNumber}}</td>
</tr>
<tr>
<td class="label">Area</td>
<td>{{berth.area}}</td>
</tr>
<tr>
<td class="label">Length</td>
<td>{{berth.lengthFt}} ft</td>
</tr>
<tr>
<td class="label">Price</td>
<td>{{berth.price}} {{berth.priceCurrency}}</td>
</tr>
<tr>
<td class="label">Tenure type</td>
<td>{{berth.tenureType}}</td>
</tr>
</table>
<h2 class="section">5. Interest Summary</h2>
<table class="fields">
<tr>
<td class="label">Pipeline stage</td>
<td>{{interest.stage}}</td>
</tr>
<tr>
<td class="label">Lead category</td>
<td>{{interest.leadCategory}}</td>
</tr>
<tr>
<td class="label">First contact</td>
<td>{{interest.dateFirstContact}}</td>
</tr>
<tr>
<td class="label">Notes</td>
<td>{{interest.notes}}</td>
</tr>
</table>
<h2 class="section">6. Signatures</h2>
<div class="signatures">
<div class="slot">
<div class="sig-line">
Applicant - {{client.fullName}}
</div>
<div class="muted" style="font-size:10pt; margin-top:2pt;">Date: __________________</div>
</div>
<div class="slot">
<div class="sig-line">
For and on behalf of {{port.name}}
</div>
<div class="muted" style="font-size:10pt; margin-top:2pt;">Date: __________________</div>
</div>
</div>
<div class="footer">
{{port.name}} &middot; Expression of Interest &middot; {{date.year}}
</div>
</body>
</html>`;
}