port-nimara-client-portal/server/api/email/generate-eoi-document.ts

431 lines
16 KiB
TypeScript

import { requireAuth } from '~/server/utils/auth';
import { getInterestById, updateInterest } from '~/server/utils/nocodb';
// Helper function to create embedded signing URLs
const createEmbeddedSigningUrl = (documensoUrl: string, signerType: 'client' | 'cc' | 'developer'): string => {
if (!documensoUrl) return '';
const token = documensoUrl.split('/').pop();
return `https://portnimara.com/sign/${signerType}/${token}`;
};
interface DocumensoRecipient {
id: number;
name: string;
email: string;
role: 'SIGNER' | 'APPROVER';
signingOrder: number;
signingUrl?: string;
}
interface DocumensoResponse {
documentId: number;
recipients: Array<{
recipientId: number;
name: string;
email: string;
token: string;
role: 'SIGNER' | 'APPROVER';
signingOrder: number;
signingUrl: string;
}>;
}
export default defineEventHandler(async (event) => {
console.log('[generate-eoi] ========== EOI GENERATION REQUEST RECEIVED ==========');
// Check authentication (x-tag header OR Keycloak session)
await requireAuth(event);
console.log('[generate-eoi] Request authenticated');
try {
const body = await readBody(event);
const { interestId } = body;
console.log('[generate-eoi] Interest ID received:', interestId);
if (!interestId) {
throw createError({ statusCode: 400, statusMessage: "Interest ID is required" });
}
// Get the interest data
console.log('[generate-eoi] Fetching interest data for ID:', interestId);
const interest = await getInterestById(interestId);
if (!interest) {
throw createError({ statusCode: 404, statusMessage: "Interest not found" });
}
console.log('[generate-eoi] Interest data retrieved successfully');
// Documenso API configuration - moved to top for use throughout
const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY;
const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL;
const templateId = process.env.NUXT_DOCUMENSO_TEMPLATE_ID || '1';
const clientRecipientId = parseInt(process.env.NUXT_DOCUMENSO_CLIENT_RECIPIENT_ID || '1');
const davidRecipientId = parseInt(process.env.NUXT_DOCUMENSO_DAVID_RECIPIENT_ID || '2');
const approvalRecipientId = parseInt(process.env.NUXT_DOCUMENSO_APPROVAL_RECIPIENT_ID || '3');
if (!documensoApiKey || !documensoBaseUrl) {
throw createError({
statusCode: 500,
statusMessage: "Documenso configuration missing. Please check NUXT_DOCUMENSO_API_KEY and NUXT_DOCUMENSO_BASE_URL environment variables."
});
}
// Check if uploaded EOI documents exist - prevent generation if they do
const eoiDocuments = interest['EOI Document'] || [];
if (eoiDocuments.length > 0) {
throw createError({
statusCode: 400,
statusMessage: "Cannot generate EOI - uploaded documents already exist. Please remove uploaded documents first."
});
}
// Check if EOI already exists (has signature links)
if (interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']) {
console.log('[generate-eoi] EOI already exists, checking for embedded URLs');
// Check if embedded URLs already exist
const hasEmbeddedUrls = interest['EmbeddedSignatureLinkClient'] &&
interest['EmbeddedSignatureLinkCC'] &&
interest['EmbeddedSignatureLinkDeveloper'];
if (!hasEmbeddedUrls) {
console.log('[generate-eoi] Embedded URLs missing, creating them from existing signature links');
// Create embedded URLs from existing signature links
const updateData: any = {};
if (interest['Signature Link Client']) {
const embeddedClientUrl = createEmbeddedSigningUrl(interest['Signature Link Client'], 'client');
updateData['EmbeddedSignatureLinkClient'] = embeddedClientUrl;
console.log('[EMBEDDED] Retroactive Client URL:', interest['Signature Link Client'], '-> Embedded:', embeddedClientUrl);
}
if (interest['Signature Link CC']) {
const embeddedCCUrl = createEmbeddedSigningUrl(interest['Signature Link CC'], 'cc');
updateData['EmbeddedSignatureLinkCC'] = embeddedCCUrl;
console.log('[EMBEDDED] Retroactive CC URL:', interest['Signature Link CC'], '-> Embedded:', embeddedCCUrl);
}
if (interest['Signature Link Developer']) {
const embeddedDevUrl = createEmbeddedSigningUrl(interest['Signature Link Developer'], 'developer');
updateData['EmbeddedSignatureLinkDeveloper'] = embeddedDevUrl;
console.log('[EMBEDDED] Retroactive Developer URL:', interest['Signature Link Developer'], '-> Embedded:', embeddedDevUrl);
}
console.log('[EMBEDDED] Updating existing EOI with embedded URLs:', updateData);
// Update the database with embedded URLs
await updateInterest(interestId, updateData);
console.log('[generate-eoi] Embedded URLs successfully added to existing EOI');
} else {
console.log('[generate-eoi] Embedded URLs already exist for this EOI');
}
return {
success: true,
documentId: 'existing',
clientSigningUrl: interest['Signature Link Client'],
signingLinks: {
'Client': interest['Signature Link Client'],
'CC': interest['Signature Link CC'],
'Developer': interest['Signature Link Developer']
}
};
}
// If there's an existing generated document, delete it from Documenso first
if (interest['documensoID']) {
console.log('Existing generated document found, deleting from Documenso first');
try {
const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${interest['documensoID']}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${documensoApiKey}`,
'Content-Type': 'application/json'
}
});
if (deleteResponse.ok) {
console.log('Successfully deleted old document from Documenso');
} else {
console.warn('Failed to delete old document from Documenso, continuing with new generation');
}
} catch (error) {
console.warn('Error deleting old document from Documenso:', error);
// Continue with generation even if deletion fails
}
}
// Validate required fields
const requiredFields = [
{ field: 'Full Name', value: interest['Full Name'] },
{ field: 'Email Address', value: interest['Email Address'] },
{ field: 'Yacht Name', value: interest['Yacht Name'] },
{ field: 'Length', value: interest['Length'] },
{ field: 'Width', value: interest['Width'] },
{ field: 'Depth', value: interest['Depth'] }
];
// Address is optional - use a default if not provided
const address = interest['Address'] || 'Not Provided';
const missingFields = requiredFields.filter(f => !f.value).map(f => f.field);
if (missingFields.length > 0) {
throw createError({
statusCode: 400,
statusMessage: `Please fill in the following required fields before generating EOI: ${missingFields.join(', ')}. You can update these fields in the interest details form.`
});
}
// Get linked berths - forward the authentication cookies for internal API call
const cookies = getRequestHeader(event, "cookie");
const requestHeaders: Record<string, string> = {};
if (cookies) {
requestHeaders["cookie"] = cookies;
}
console.log('[generate-eoi] Making internal API call to get-interest-berths with forwarded cookies');
const berthsResponse = await $fetch<{ list: Array<{ 'Mooring Number': string }> }>(
"/api/get-interest-berths",
{
headers: requestHeaders,
params: {
interestId: interestId,
linkType: "berths",
},
}
);
const berths = berthsResponse.list || [];
if (berths.length === 0) {
throw createError({
statusCode: 400,
statusMessage: "No berths linked to this interest. Please link at least one berth."
});
}
// Concatenate berth numbers
const berthNumbers = berths.map(b => b['Mooring Number']).join(', ');
// 1. Get template (optional - just to verify it exists)
try {
const templateResponse = await fetch(`${documensoBaseUrl}/api/v1/templates/${templateId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${documensoApiKey}`
}
});
if (!templateResponse.ok) {
throw new Error(`Template not found: ${templateResponse.statusText}`);
}
} catch (error) {
console.error('Failed to verify template:', error);
throw createError({
statusCode: 500,
statusMessage: "Failed to verify template. Please check Documenso configuration."
});
}
// 2. Create document
const createDocumentPayload = {
meta: {
message: `Dear ${interest['Full Name']},\n\nThank you for your interest in a berth at Port Nimara. Please click the link above to sign your LOI.\n\nBest Regards,\nPort Nimara Team`,
subject: "Your LOI is ready to be signed",
redirectUrl: "https://portnimara.com",
distributionMethod: "NONE"
},
title: `${interest['Full Name']}-EOI-NDA`,
externalId: `loi-${interestId}`,
formValues: {
"Name": interest['Full Name'],
"Draft": interest['Depth'],
"Email": interest['Email Address'],
"Width": interest['Width'],
"Length": interest['Length'],
"Address": address,
"Lease_10": false,
"Purchase": true,
"Yacht Name": interest['Yacht Name'],
"Berth Number": berthNumbers
},
recipients: [
{
id: clientRecipientId,
name: interest['Full Name'],
role: "SIGNER",
email: interest['Email Address'],
signingOrder: 1
},
{
id: davidRecipientId,
name: "David Mizrahi",
role: "SIGNER",
email: "dm@portnimara.com",
signingOrder: 3
},
{
id: approvalRecipientId,
name: "Approval",
role: "APPROVER",
email: "sales@portnimara.com",
signingOrder: 2
}
]
};
let documentResponse: DocumensoResponse;
try {
const response = await fetch(`${documensoBaseUrl}/api/v1/templates/${templateId}/generate-document`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${documensoApiKey}`
},
body: JSON.stringify(createDocumentPayload)
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to create document:', errorText);
throw new Error(`Failed to create document: ${response.statusText}`);
}
documentResponse = await response.json();
} catch (error) {
console.error('Document creation error:', error);
throw createError({
statusCode: 500,
statusMessage: "Failed to create EOI document. Please try again."
});
}
// 3. Send document (moves from draft to active and sends emails)
try {
const sendResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documentResponse.documentId}/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${documensoApiKey}`
},
body: JSON.stringify({
sendEmail: true,
sendCompletionEmails: true
})
});
if (!sendResponse.ok) {
const errorText = await sendResponse.text();
console.error('Failed to send document:', errorText);
throw new Error(`Failed to send document: ${sendResponse.statusText}`);
}
console.log('Document sent successfully');
} catch (error) {
console.error('Document send error:', error);
throw createError({
statusCode: 500,
statusMessage: "Document created but failed to send. Please check Documenso dashboard."
});
}
// Extract signing URLs from recipients
const signingLinks: Record<string, string> = {};
if (documentResponse.recipients) {
documentResponse.recipients.forEach(recipient => {
if (recipient.signingUrl) {
if (recipient.email === interest['Email Address']) {
signingLinks['Client'] = recipient.signingUrl;
} else if (recipient.email === 'dm@portnimara.com') {
signingLinks['David Mizrahi'] = recipient.signingUrl;
} else if (recipient.email === 'sales@portnimara.com') {
signingLinks['Approval'] = recipient.signingUrl;
}
}
});
}
// 4. Update interest record
const currentDate = new Date();
const dateTimeString = currentDate.toLocaleString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
const extraComments = interest['Extra Comments'] || '';
const updatedComments = extraComments + (extraComments ? '\n\n' : '') + `EOI Generated ${dateTimeString}`;
const updateData: any = {
'EOI Status': 'Waiting for Signatures',
'Sales Process Level': 'LOI and NDA Sent',
// Don't set EOI Time Sent here - only when email is sent or link is copied
'Extra Comments': updatedComments,
'documensoID': documentResponse.documentId.toString()
};
// DEBUG: Log the documensoID being saved
console.log('[generate-eoi] DEBUGGING documensoID save:', {
interestId: interestId,
documentId: documentResponse.documentId,
documentId_type: typeof documentResponse.documentId,
documensoID_string: documentResponse.documentId.toString(),
documensoID_string_type: typeof documentResponse.documentId.toString(),
updateData_documensoID: updateData['documensoID'],
updateData_documensoID_type: typeof updateData['documensoID'],
full_updateData: updateData
});
// Add signing links to update data with new column names
console.log('[EMBEDDED] Available signing links:', signingLinks);
if (signingLinks['Client']) {
updateData['Signature Link Client'] = signingLinks['Client'];
const embeddedClientUrl = createEmbeddedSigningUrl(signingLinks['Client'], 'client');
updateData['EmbeddedSignatureLinkClient'] = embeddedClientUrl;
console.log('[EMBEDDED] Client URL:', signingLinks['Client'], '-> Embedded:', embeddedClientUrl);
}
if (signingLinks['David Mizrahi']) {
updateData['Signature Link Developer'] = signingLinks['David Mizrahi'];
const embeddedDevUrl = createEmbeddedSigningUrl(signingLinks['David Mizrahi'], 'developer');
updateData['EmbeddedSignatureLinkDeveloper'] = embeddedDevUrl;
console.log('[EMBEDDED] Developer URL:', signingLinks['David Mizrahi'], '-> Embedded:', embeddedDevUrl);
}
if (signingLinks['Approval']) {
updateData['Signature Link CC'] = signingLinks['Approval'];
const embeddedCCUrl = createEmbeddedSigningUrl(signingLinks['Approval'], 'cc');
updateData['EmbeddedSignatureLinkCC'] = embeddedCCUrl;
console.log('[EMBEDDED] CC URL:', signingLinks['Approval'], '-> Embedded:', embeddedCCUrl);
}
console.log('[EMBEDDED] Final updateData being sent to NocoDB:', updateData);
await updateInterest(interestId, updateData);
return {
success: true,
documentId: documentResponse.documentId,
documensoID: documentResponse.documentId.toString(), // Include this for immediate UI update
clientSigningUrl: signingLinks['Client'] || '',
signingLinks: signingLinks
};
} catch (error) {
console.error('Failed to generate EOI document:', error);
if (error instanceof Error) {
throw createError({
statusCode: 500,
statusMessage: error.message || "Failed to generate EOI document"
});
} else {
throw createError({
statusCode: 500,
statusMessage: "An unexpected error occurred",
});
}
}
});