diff --git a/src/app/api/v1/document-templates/[id]/generate-and-sign/route.ts b/src/app/api/v1/document-templates/[id]/generate-and-sign/route.ts index af34db21..854a73e7 100644 --- a/src/app/api/v1/document-templates/[id]/generate-and-sign/route.ts +++ b/src/app/api/v1/document-templates/[id]/generate-and-sign/route.ts @@ -26,7 +26,11 @@ export const POST = withAuth( ipAddress: ctx.ipAddress, userAgent: ctx.userAgent, }, - { dimensionUnit: body.dimensionUnit, overrides: body.overrides }, + { + dimensionUnit: body.dimensionUnit, + overrides: body.overrides, + includeYachtDetails: body.includeYachtDetails, + }, ); return NextResponse.json({ data: result }, { status: 201 }); } catch (error) { diff --git a/src/components/documents/eoi-generate-dialog.tsx b/src/components/documents/eoi-generate-dialog.tsx index 2d617277..870f22d2 100644 --- a/src/components/documents/eoi-generate-dialog.tsx +++ b/src/components/documents/eoi-generate-dialog.tsx @@ -191,6 +191,13 @@ export function EoiGenerateDialog({ const [addressOverride, setAddressOverride] = useState(null); // Phase 3c - yacht spawn flow. const [yachtSpawnOpen, setYachtSpawnOpen] = useState(false); + // Section 3 (yacht details) inclusion toggle. Defaults to ON; the toggle + // only renders when a yacht is actually linked (no toggle = always ON, + // which matches today's behaviour). Set OFF to blank yacht.* + owner.* + // merge fields on the generated PDF even though the yacht record exists + // (early-stage clients, multi-berth deals where yacht-dims don't apply, + // explicit client request to keep Section 3 off the document). + const [includeYachtDetails, setIncludeYachtDetails] = useState(true); // Resolved EOI context - the actual values the document will be // auto-filled with. Loaded only while the dialog is open so we don't @@ -564,6 +571,9 @@ export function EoiGenerateDialog({ // the yacht's own `lengthUnit` column when unspecified. dimensionUnit: effectiveDimensionUnit, ...(hasAnyOverride ? { overrides } : {}), + // Only send the flag when the rep explicitly opted out; default + // (toggle ON) keeps existing behaviour without bloating the body. + ...(ctx?.yacht && !includeYachtDetails ? { includeYachtDetails: false } : {}), }, }); // Bounce every cache that surfaces the interest's EOI state so the @@ -679,41 +689,60 @@ export function EoiGenerateDialog({
-
+

- Optional (Section 3 - left blank if absent) + Section 3 - yacht details

- {ctx.yacht ? ( -
- - -
- ) : null} +
+ {ctx.yacht ? ( + + ) : null} + {ctx.yacht && includeYachtDetails ? ( +
+ + +
+ ) : null} +
+ {ctx.yacht && !includeYachtDetails ? ( +

+ Yacht details will be omitted from the EOI even though a yacht is linked. +

+ ) : null}
diff --git a/src/lib/services/document-templates.ts b/src/lib/services/document-templates.ts index cb65221c..6f850a66 100644 --- a/src/lib/services/document-templates.ts +++ b/src/lib/services/document-templates.ts @@ -490,7 +490,7 @@ async function generateEoiFromSourcePdf( portId: string, context: GenerateInput, meta: AuditMeta, - options?: { dimensionUnit?: 'ft' | 'm' }, + options?: { dimensionUnit?: 'ft' | 'm'; includeYachtDetails?: boolean }, applied: AppliedOverrides = { resolved: {}, documentOverrideColumns: {} }, ): Promise<{ document: DbDocument; file: DbFile }> { if (!context.interestId) { @@ -501,6 +501,12 @@ async function generateEoiFromSourcePdf( await buildEoiContext(context.interestId, portId), applied, ); + // Rep opted out of Section 3 — blank the yacht slot so the AcroForm fill + // skips writing the yacht.* / owner.* fields (matching the Documenso + // pathway). + if (options?.includeYachtDetails === false) { + eoiContext.yacht = null; + } const pdfBytes = await generateEoiPdfFromTemplate(eoiContext, { dimensionUnit: options?.dimensionUnit ?? eoiContext.yacht?.lengthUnit ?? 'ft', }); @@ -597,7 +603,14 @@ export async function generateAndSign( signers: GenerateAndSignInput['signers'], pathway: 'inapp' | 'documenso-template', meta: AuditMeta, - options?: { dimensionUnit?: 'ft' | 'm'; overrides?: EoiOverridesInput }, + options?: { + dimensionUnit?: 'ft' | 'm'; + overrides?: EoiOverridesInput; + /** False = blank out Section 3 (yacht.* + owner.* merge fields) even + * when the interest carries a linked yacht. True (or unset) keeps the + * current behaviour (auto-fill from yacht record). */ + includeYachtDetails?: boolean; + }, ) { // Phase 3b - apply per-field overrides BEFORE either pathway resolves the // EOI context, so any setAsDefault contact promotion is visible to the @@ -623,7 +636,7 @@ async function generateAndSignViaInApp( context: GenerateInput, signers: GenerateAndSignInput['signers'], meta: AuditMeta, - options?: { dimensionUnit?: 'ft' | 'm' }, + options?: { dimensionUnit?: 'ft' | 'm'; includeYachtDetails?: boolean }, applied: AppliedOverrides = { resolved: {}, documentOverrideColumns: {} }, ) { const template = await getTemplateById(templateId, portId); @@ -755,7 +768,7 @@ async function generateAndSignViaDocumensoTemplate( portId: string, context: GenerateInput, meta: AuditMeta, - options?: { dimensionUnit?: 'ft' | 'm' }, + options?: { dimensionUnit?: 'ft' | 'm'; includeYachtDetails?: boolean }, applied: AppliedOverrides = { resolved: {}, documentOverrideColumns: {} }, ) { if (!context.interestId) { @@ -766,6 +779,14 @@ async function generateAndSignViaDocumensoTemplate( await buildEoiContext(context.interestId, portId), applied, ); + // Rep opted out of Section 3 (yacht details) — blank the yacht slot so + // buildDocumensoPayload + the EOI template see "no yacht linked" and + // leave yacht.* / owner.* merge fields empty. Persisted in document + // metadata below for audit (kind: 'eoi_include_yacht_details=false'). + const yachtDeclined = options?.includeYachtDetails === false; + if (yachtDeclined) { + eoiContext.yacht = null; + } const signers = await getPortEoiSigners(portId); // Per-port Documenso template + recipient IDs (with env fallback). Each // tenant pointing at its own Documenso instance has different numeric @@ -908,6 +929,10 @@ async function generateAndSignViaDocumensoTemplate( pathway: 'documenso-template', templateId: env.DOCUMENSO_TEMPLATE_ID_EOI, interestId: context.interestId, + // Rep's explicit Section-3 choice. Audit-only — Docs row has no + // metadata jsonb; the blanked yacht.* / owner.* merge fields on the + // generated PDF are the user-visible evidence. + includeYachtDetails: !yachtDeclined, }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, diff --git a/src/lib/validators/document-templates.ts b/src/lib/validators/document-templates.ts index 7810b8be..eeb22e01 100644 --- a/src/lib/validators/document-templates.ts +++ b/src/lib/validators/document-templates.ts @@ -109,6 +109,14 @@ export const generateAndSignSchema = generateSchema.extend({ * EOI's Length/Width/Draft formValues. The drawer's toggle drives this; * server defaults to the yacht's `lengthUnit` column when omitted. */ dimensionUnit: z.enum(['ft', 'm']).optional(), + /** Optional Section 3 (yacht details) inclusion. Defaults to true when a + * yacht is linked. Rep can flip OFF in the dialog to blank out yacht.* + * + owner.* merge fields even though a yacht is on file — used when the + * yacht is a placeholder, multi-berth deals where yacht-specific dims + * don't apply, or the client explicitly asked for the section to stay + * off the document. Persisted in `documents.metadata.includeYachtDetails` + * for audit. */ + includeYachtDetails: z.boolean().optional(), /** Phase 3b/3-follow-up - optional per-field overrides applied at generation. */ overrides: z .object({