Plan §4.6 + §1: a render function that compresses every berth marked
is_in_eoi_bundle=true on an interest into a compact range string
("A1-A3, B5-B7"), wired into both EOI generation paths (the Documenso
template-generate call and the in-app pdf-lib AcroForm fill).
- src/lib/templates/berth-range.ts: pure formatBerthRange() with the
full edge-case set from §4.6 - empty, single, run, gap, multiple
prefixes, sort/dedup, multi-letter prefixes, non-canonical
passthrough, long ranges. Sorts by (prefix, number); dedupes; passes
non-canonical inputs through with a logger warning.
- src/lib/templates/merge-fields.ts: new {{eoi.berthRange}} token
added to VALID_MERGE_TOKENS allow-list under a fresh `eoi` scope so
unknown-token validation at template creation time still rejects
typos.
- src/lib/services/eoi-context.ts: EoiContext gains eoiBerthRange.
Resolved by joining interest_berths (is_in_eoi_bundle=true) →
berths and feeding the mooring numbers through formatBerthRange.
- src/lib/services/documenso-payload.ts: formValues now includes
"Berth Range" alongside the legacy "Berth Number". Multi-berth EOIs
surface here; single-berth EOIs duplicate the primary.
- src/lib/pdf/fill-eoi-form.ts: in-app AcroForm fill mirrors the
Documenso payload by populating "Berth Range". Falls back silently
when older PDFs don't have the field (setText is no-op-on-missing).
15 unit tests on the formatter; existing EoiContext + Documenso
payload tests updated to assert the new field. 1022 -> 1037 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep CRM mooring numbers from the legacy hyphen+padded form ("A-01")
to the canonical bare form ("A1") used by NocoDB, the public website,
the per-berth PDFs, and the Documenso EOI templates. Drift was
introduced by the original load-berths-to-port-nimara.ts seed; this
gates the Phase 3 public-website cutover where /berths/A1 URLs would
404 against a CRM still storing "A-01".
- 0024 data migration: idempotent regexp_replace + post-update sanity
check that surfaces any non-conforming rows for manual triage.
- Invert normalizeLegacyMooring in dedup/migration-apply: it now
canonicalizes ("D-32" -> "D32") instead of legacy-izing.
- Update tiptap-to-pdfme example tokens, EOI fixture moorings, and
smoke-test seed moorings.
- Refresh seed-data/berths.json to canonical form; drop the now-
redundant legacyMooringNumber field.
- Delete scripts/load-berths-to-port-nimara.ts (superseded in 0c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the in-app pathway is used for EOI templates, we now load the same
source PDF that the Documenso template uploads and fill its AcroForm
fields with values from EoiContext via pdf-lib. Field names mirror the
Documenso template's formValues keys exactly (Name, Email, Address,
Yacht Name, Length, Width, Draft, Berth Number + Lease_10 / Purchase
checkboxes), so both pathways produce equivalent legal documents — only
the renderer differs.
The form is left interactive (not flattened) so a recipient can still
adjust values before signing. Non-EOI templates (welcome letters,
acknowledgments, etc.) keep using the existing HTML→pdfme path.
Adds:
- pdf-lib direct dep
- src/lib/pdf/fill-eoi-form.ts — load + fill helpers, EOI_TEMPLATE_PDF_PATH
env override
- assets/ + README documenting the expected source PDF
- next.config outputFileTracingIncludes so the asset is bundled in the
standalone build
Tests: 8 new (4 fill-form unit + 2 source-PDF route + 2 fallback);
645/645 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>