feat(eoi): seed Standard EOI in-app template per port
Adds a new per-port document_templates row of type 'eoi' containing an
HTML EOI / Letter of Intent body with {{section.field}} merge tokens
that mirror the EoiContext shape. Enables the in-app pdfme PDF path as
an alternative to the Documenso template flow.
- New getStandardEoiTemplateHtml() returns the Letter-sized HTML body
with Applicant / Yacht / Owner / Berth / Interest / Signatures blocks
- STANDARD_EOI_MERGE_FIELDS exported for resolveTemplate wiring (11.4)
- seed-data.ts inserts one document_templates row per port inside the
existing withTransaction block, between ownership transfers and
interests, using SEED_USER_ID for audit consistency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
337
src/lib/pdf/templates/eoi-standard-inapp.ts
Normal file
337
src/lib/pdf/templates/eoi-standard-inapp.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 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 “EOI”) is entered into between
|
||||
<strong>{{port.name}}</strong> and the Applicant named below, and records the
|
||||
Applicant’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 / {{yacht.lengthM}} m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Beam (ft / m)</td>
|
||||
<td>{{yacht.widthFt}} ft / {{yacht.widthM}} m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label">Draft (ft / m)</td>
|
||||
<td>{{yacht.draftFt}} ft / {{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}} · Expression of Interest · {{date.year}}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user