2026-05-02 00:19:55 +02:00
|
|
|
import { and, eq } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { systemSettings } from '@/lib/db/schema/system';
|
2026-04-24 16:09:27 +02:00
|
|
|
import type { EoiContext } from '@/lib/services/eoi-context';
|
|
|
|
|
|
|
|
|
|
export interface DocumensoTemplatePayload {
|
|
|
|
|
title: string;
|
|
|
|
|
externalId: string;
|
|
|
|
|
meta: {
|
|
|
|
|
message: string;
|
|
|
|
|
subject: string;
|
|
|
|
|
redirectUrl: string;
|
|
|
|
|
distributionMethod: 'NONE' | 'EMAIL';
|
|
|
|
|
};
|
|
|
|
|
formValues: {
|
|
|
|
|
Name: string;
|
|
|
|
|
Email: string;
|
|
|
|
|
Address: string;
|
|
|
|
|
'Yacht Name': string;
|
|
|
|
|
Length: string;
|
|
|
|
|
Width: string;
|
|
|
|
|
Draft: string;
|
|
|
|
|
'Berth Number': string;
|
|
|
|
|
Lease_10: boolean;
|
|
|
|
|
Purchase: boolean;
|
|
|
|
|
};
|
|
|
|
|
recipients: Array<{
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
email: string;
|
|
|
|
|
role: 'SIGNER' | 'APPROVER';
|
|
|
|
|
signingOrder: number;
|
|
|
|
|
}>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DocumensoPayloadOptions {
|
|
|
|
|
/** `interestId` used to build `externalId` and Documenso referencing. */
|
|
|
|
|
interestId: string;
|
|
|
|
|
/** Documenso recipient IDs — come from env vars. */
|
|
|
|
|
clientRecipientId: number;
|
|
|
|
|
developerRecipientId: number;
|
|
|
|
|
approvalRecipientId: number;
|
|
|
|
|
/** Hardcoded developer + approver names/emails (legacy). */
|
|
|
|
|
developerName?: string;
|
|
|
|
|
developerEmail?: string;
|
|
|
|
|
approverName?: string;
|
|
|
|
|
approverEmail?: string;
|
|
|
|
|
/** Redirect URL after signing. Defaults to the app URL. */
|
|
|
|
|
redirectUrl?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_DEVELOPER_NAME = 'David Mizrahi';
|
|
|
|
|
const DEFAULT_DEVELOPER_EMAIL = 'dm@portnimara.com';
|
|
|
|
|
const DEFAULT_APPROVER_NAME = 'Abbie May';
|
|
|
|
|
const DEFAULT_APPROVER_EMAIL = 'sales@portnimara.com';
|
|
|
|
|
const DEFAULT_REDIRECT_URL = 'https://portnimara.com';
|
|
|
|
|
|
2026-05-02 00:19:55 +02:00
|
|
|
export interface EoiSignerConfig {
|
|
|
|
|
developer: { name: string; email: string };
|
|
|
|
|
approver: { name: string; email: string };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_EOI_SIGNERS: EoiSignerConfig = {
|
|
|
|
|
developer: { name: DEFAULT_DEVELOPER_NAME, email: DEFAULT_DEVELOPER_EMAIL },
|
|
|
|
|
approver: { name: DEFAULT_APPROVER_NAME, email: DEFAULT_APPROVER_EMAIL },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function isSignerEntry(v: unknown): v is { name: string; email: string } {
|
|
|
|
|
return (
|
|
|
|
|
!!v &&
|
|
|
|
|
typeof v === 'object' &&
|
|
|
|
|
typeof (v as Record<string, unknown>).name === 'string' &&
|
|
|
|
|
typeof (v as Record<string, unknown>).email === 'string' &&
|
|
|
|
|
!!(v as Record<string, string>).name &&
|
|
|
|
|
!!(v as Record<string, string>).email
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Read the per-port `eoi_signers` setting, fall back to legacy hardcoded
|
|
|
|
|
* defaults if missing or malformed. The fallback exists to keep older
|
|
|
|
|
* ports working until an admin saves the setting; once saved, the DB row
|
|
|
|
|
* always wins. */
|
|
|
|
|
export async function getPortEoiSigners(portId: string): Promise<EoiSignerConfig> {
|
|
|
|
|
const row = await db.query.systemSettings.findFirst({
|
|
|
|
|
where: and(eq(systemSettings.key, 'eoi_signers'), eq(systemSettings.portId, portId)),
|
|
|
|
|
});
|
|
|
|
|
const value = row?.value as Record<string, unknown> | undefined;
|
|
|
|
|
if (value && isSignerEntry(value.developer) && isSignerEntry(value.approver)) {
|
|
|
|
|
return {
|
|
|
|
|
developer: value.developer,
|
|
|
|
|
approver: value.approver,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return DEFAULT_EOI_SIGNERS;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:09:27 +02:00
|
|
|
function formatAddress(address: EoiContext['client']['address']): string {
|
|
|
|
|
if (!address) return '';
|
|
|
|
|
return [address.street, address.city, address.country].filter(Boolean).join(', ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildMessage(context: EoiContext): string {
|
|
|
|
|
const greeting = `Dear ${context.client.fullName},`;
|
|
|
|
|
const body = `Thank you for your interest in a berth at ${context.port.name}. Please click the link above to sign your LOI.`;
|
|
|
|
|
const onBehalf =
|
|
|
|
|
context.owner.type === 'company' && context.company
|
|
|
|
|
? `\n\nOn behalf of ${context.company.legalName ?? context.company.name} (representing the yacht's owner).`
|
|
|
|
|
: '';
|
|
|
|
|
const footer = `\n\nBest Regards,\n${context.port.name} Team`;
|
|
|
|
|
return `${greeting}\n\n${body}${onBehalf}${footer}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildDocumensoPayload(
|
|
|
|
|
context: EoiContext,
|
|
|
|
|
options: DocumensoPayloadOptions,
|
|
|
|
|
): DocumensoTemplatePayload {
|
|
|
|
|
return {
|
|
|
|
|
title: `${context.client.fullName}-EOI-NDA`,
|
|
|
|
|
externalId: `loi-${options.interestId}`,
|
|
|
|
|
meta: {
|
|
|
|
|
message: buildMessage(context),
|
|
|
|
|
subject: 'Your LOI is ready to be signed',
|
|
|
|
|
redirectUrl: options.redirectUrl ?? DEFAULT_REDIRECT_URL,
|
|
|
|
|
distributionMethod: 'NONE',
|
|
|
|
|
},
|
|
|
|
|
formValues: {
|
|
|
|
|
Name: context.client.fullName,
|
|
|
|
|
Email: context.client.primaryEmail ?? '',
|
|
|
|
|
Address: formatAddress(context.client.address),
|
2026-05-02 03:11:14 +02:00
|
|
|
// Yacht + berth are optional EOI fields; when not linked, render as
|
|
|
|
|
// empty strings so the corresponding template inputs stay blank.
|
|
|
|
|
'Yacht Name': context.yacht?.name ?? '',
|
|
|
|
|
Length: context.yacht?.lengthFt ?? '',
|
|
|
|
|
Width: context.yacht?.widthFt ?? '',
|
|
|
|
|
Draft: context.yacht?.draftFt ?? '',
|
|
|
|
|
'Berth Number': context.berth?.mooringNumber ?? '',
|
2026-04-24 16:09:27 +02:00
|
|
|
Lease_10: false,
|
|
|
|
|
Purchase: true,
|
|
|
|
|
},
|
|
|
|
|
recipients: [
|
|
|
|
|
{
|
|
|
|
|
id: options.clientRecipientId,
|
|
|
|
|
name: context.client.fullName,
|
|
|
|
|
email: context.client.primaryEmail ?? '',
|
|
|
|
|
role: 'SIGNER',
|
|
|
|
|
signingOrder: 1,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: options.developerRecipientId,
|
|
|
|
|
name: options.developerName ?? DEFAULT_DEVELOPER_NAME,
|
|
|
|
|
email: options.developerEmail ?? DEFAULT_DEVELOPER_EMAIL,
|
|
|
|
|
role: 'SIGNER',
|
|
|
|
|
signingOrder: 2,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: options.approvalRecipientId,
|
|
|
|
|
name: options.approverName ?? DEFAULT_APPROVER_NAME,
|
|
|
|
|
email: options.approverEmail ?? DEFAULT_APPROVER_EMAIL,
|
|
|
|
|
role: 'APPROVER',
|
|
|
|
|
signingOrder: 3,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
}
|