Files
pn-new-crm/docs/eoi-documenso-field-mapping.md
Matt Ciaccio 312ebf1a88 docs(eoi): document multi-berth Berth Range field + legacy parity
The user asked us to confirm we copied the Documenso template's
auto-fill schema verbatim from the legacy system. Confirmed and
documented in the canonical mapping file:

- Every legacy formValues key (Name, Email, Address, Yacht Name,
  Length, Width, Draft, Berth Number, Lease_10, Purchase) is still
  emitted with identical names and types — single-berth EOIs are
  byte-for-byte compatible with template id 8.
- Phase 5 added one new field: `Berth Range` (compact range string
  for multi-berth EOIs from the is_in_eoi_bundle junction rows).
  Documenso silently drops unknown formValues, so the live template
  will simply not render the range until someone adds the field. The
  doc now flags this explicitly.
- Verified buildDocumensoPayload() populates all 11 fields from the
  resolved EoiContext; tests at tests/unit/services/documenso-payload
  cover every field.

The "rest is handled inside Documenso" (signature, date, terms) -
those fields live on the template itself and don't appear in our
formValues map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 04:13:32 +02:00

82 lines
13 KiB
Markdown

# Documenso EOI Template — Field Mapping
**Purpose:** This doc is the canonical reference for mapping the Documenso EOI template's `formValues` keys to the new data model's `EoiContext` shape. It drives `buildDocumensoPayload()` (Task 11.2), the in-app Standard EOI HTML tokens (Task 11.3), and the Spec 2 importer's yacht/company hydration.
## Source
The legacy field list comes from `client-portal/server/api/eoi/generate-quick-eoi.ts`, specifically the POST body sent to `POST /api/v1/templates/{templateId}/generate-document` (Documenso template 8). The relevant lines in that file are around the `createDocumentPayload.formValues` object.
## Documenso template `formValues` keys
Documenso template IDs and recipient IDs are configured via env vars:
- `NUXT_DOCUMENSO_TEMPLATE_ID` (default: `8`)
- `NUXT_DOCUMENSO_CLIENT_RECIPIENT_ID` (default: `192`) — signing order 1
- `NUXT_DOCUMENSO_DEVELOPER_RECIPIENT_ID` (default: `193`) — signing order 2
- `NUXT_DOCUMENSO_APPROVAL_RECIPIENT_ID` (default: `194`) — APPROVER, signing order 3
The template exposes eight text fields (`formValues` keys) and two boolean checkboxes.
## Field mapping
The legacy template (Documenso template `8`, configured in production) auto-fills exactly the fields below. All eight text fields + two booleans are populated by `buildDocumensoPayload()` from the resolved `EoiContext`. Anything else on the form (signature, date, terms acknowledgment) is filled in by the client inside Documenso.
| Documenso key | Type | Legacy source | New `EoiContext` path | Notes |
| -------------- | ------- | --------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Name` | text | `interest['Full Name']` | `context.client.fullName` | The interest's point-of-contact client (billing signer). |
| `Email` | text | `interest['Email Address']` | `context.client.primaryEmail` | Primary email contact from `client_contacts`. |
| `Address` | text | `interest['Address']` | concat `context.client.address.{street,city,country}` | Concatenate street, city, country with `', '`. Empty if address is null. |
| `Yacht Name` | text | `interest['Yacht Name']` | `context.yacht.name` | Yacht is now a first-class row; pulled via `interest.yachtId`. Empty string when no yacht is linked yet. |
| `Length` | text | `interest['Length']` | `context.yacht.lengthFt` | Boat dimension. Send as string. Documenso doesn't enforce numeric format. Empty string when not applicable. |
| `Width` | text | `interest['Width']` | `context.yacht.widthFt` | Same. |
| `Draft` | text | `interest['Depth']` | `context.yacht.draftFt` | Legacy field was named "Depth" in NocoDB; Documenso key is "Draft". |
| `Berth Number` | text | `berthNumbers` (joined) | `context.berth.mooringNumber` | The interest's PRIMARY berth (resolved via `interest_berths.is_primary=true`). Empty string when no primary set. |
| `Berth Range` | text | (new) | `context.eoiBerthRange` | **NEW IN PHASE 5** — compact range string for multi-berth EOIs (e.g. `"A1-A3, B5-B7"`) covering every junction row marked `is_in_eoi_bundle=true`. Empty string when the bundle is empty. **The live Documenso template (id `8`) does NOT yet have this field. Add a `Berth Range` text field to the template before multi-berth EOIs render the range; until then Documenso silently drops the value and only `Berth Number` (the primary mooring) renders.** |
| `Lease_10` | boolean | hardcoded `false` | `false` | Hardcoded — legacy flow defaults to Purchase (not Lease). |
| `Purchase` | boolean | hardcoded `true` | `true` | Hardcoded — legacy flow defaults to Purchase. |
**Backwards-compatibility guarantee**: every legacy `formValues` key is still emitted with the same name and type. The only addition is `Berth Range` (Phase 5). Documenso silently ignores unknown formValues keys, so old templates that don't have `Berth Range` will simply not render it — single-berth EOIs continue to work identically. No template changes are required for legacy use.
## Document `meta` fields (non-`formValues`)
| Documenso key | Type | Legacy source | New source |
| ------------------------- | ---- | ---------------------------------------- | ----------------------------------------------------------------- |
| `meta.message` | text | `Dear ${interest['Full Name']}...` | `Dear ${context.client.fullName}, ...port name interpolated` |
| `meta.subject` | text | `"Your LOI is ready to be signed"` | Same — constant. |
| `meta.redirectUrl` | text | `"https://portnimara.com"` | `context.port.redirectUrl` if per-port; otherwise global app URL. |
| `meta.distributionMethod` | text | `"NONE"` | Same — constant. We use manual send flow (Documenso webhook). |
| `title` | text | `` `${interest['Full Name']}-EOI-NDA` `` | `` `${context.client.fullName}-EOI-NDA` `` |
| `externalId` | text | `` `loi-${interestId}` `` | Same. |
## Recipients (non-`formValues`)
| Recipient | Role | Name | Email | Signing order |
| ------------------- | -------- | ------------------------- | ----------------------------- | ------------- |
| Client (signer) | SIGNER | `context.client.fullName` | `context.client.primaryEmail` | 1 |
| Developer (signer) | SIGNER | `"David Mizrahi"` | `"dm@portnimara.com"` | 2 |
| Approval (approver) | APPROVER | `"Abbie May"` | `"sales@portnimara.com"` | 3 |
The Developer and Approval recipients are currently hardcoded in the legacy flow. In the new system these should eventually come from port-level settings (e.g., `ports.settings.eoi.developerName` + email). For Task 11.2, keep them hardcoded as the legacy system does — tracking as TODO: "Replace hardcoded Developer/Approval recipients with port-level configuration."
## Company-owned yacht handling
The legacy flow has no concept of company ownership — the signer is always the interest's client. In the new system:
- If `context.yacht.ownerType === 'client'`: behavior unchanged.
- If `context.yacht.ownerType === 'company'`: the interest's point-of-contact client still signs (they're the representative of the yacht's owning company), but an extra block should appear in the message body: `"On behalf of ${context.company.legalName ?? context.company.name} (representing the yacht's owner)."`. This isn't a separate Documenso field — it's woven into `meta.message`.
Tracking this in the mapping doc rather than as a hard TODO because company-owned EOIs were rare in the legacy system and need product input before committing to the final wording.
## Deprecated fields (no longer sourced from `clients`)
The legacy system read these fields from the client row. They are now sourced elsewhere:
| Legacy source | New source |
| ------------------------- | --------------------------------------------------- |
| `client.yachtName` | `yachts.name` via `interest.yachtId` |
| `client.yachtLengthFt` | `yachts.lengthFt` via `interest.yachtId` |
| `client.yachtWidthFt` | `yachts.widthFt` via `interest.yachtId` |
| `client.yachtDraftFt` | `yachts.draftFt` via `interest.yachtId` |
| `client.companyName` | `companies.name` via polymorphic owner resolution |
| `client.berthSizeDesired` | Removed. Berth is picked via reservation, not text. |