2025-06-15 16:32:34 +02:00
import { requireAuth } from '~/server/utils/auth' ;
2025-06-09 22:40:37 +02:00
import { getInterestById , updateInterest } from '~/server/utils/nocodb' ;
2025-06-11 17:57:58 +02:00
// Helper function to create embedded signing URLs
const createEmbeddedSigningUrl = ( documensoUrl : string , signerType : 'client' | 'cc' | 'developer' ) : string = > {
if ( ! documensoUrl ) return '' ;
const token = documensoUrl . split ( '/' ) . pop ( ) ;
2025-06-11 18:46:18 +02:00
return ` https://portnimara.com/sign/ ${ signerType } / ${ token } ` ;
2025-06-11 17:57:58 +02:00
} ;
2025-06-09 22:40:37 +02:00
interface DocumensoRecipient {
id : number ;
name : string ;
email : string ;
role : 'SIGNER' | 'APPROVER' ;
signingOrder : number ;
signingUrl? : string ;
}
interface DocumensoResponse {
2025-06-09 23:09:32 +02:00
documentId : number ;
recipients : Array < {
recipientId : number ;
name : string ;
email : string ;
token : string ;
role : 'SIGNER' | 'APPROVER' ;
signingOrder : number ;
signingUrl : string ;
} > ;
2025-06-09 22:40:37 +02:00
}
export default defineEventHandler ( async ( event ) = > {
2025-06-11 18:29:56 +02:00
console . log ( '[generate-eoi] ========== EOI GENERATION REQUEST RECEIVED ==========' ) ;
2025-06-15 16:32:34 +02:00
// Check authentication (x-tag header OR Keycloak session)
await requireAuth ( event ) ;
console . log ( '[generate-eoi] Request authenticated' ) ;
2025-06-09 22:40:37 +02:00
try {
const body = await readBody ( event ) ;
const { interestId } = body ;
2025-06-11 18:29:56 +02:00
console . log ( '[generate-eoi] Interest ID received:' , interestId ) ;
2025-06-09 22:40:37 +02:00
if ( ! interestId ) {
throw createError ( { statusCode : 400 , statusMessage : "Interest ID is required" } ) ;
}
// Get the interest data
2025-06-11 18:29:56 +02:00
console . log ( '[generate-eoi] Fetching interest data for ID:' , interestId ) ;
2025-06-09 22:40:37 +02:00
const interest = await getInterestById ( interestId ) ;
if ( ! interest ) {
throw createError ( { statusCode : 404 , statusMessage : "Interest not found" } ) ;
}
2025-06-11 18:29:56 +02:00
console . log ( '[generate-eoi] Interest data retrieved successfully' ) ;
2025-06-09 22:40:37 +02:00
2025-06-11 14:08:28 +02:00
// 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 ;
2025-07-10 03:51:42 +02:00
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' ) ;
2025-06-11 14:08:28 +02:00
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."
} ) ;
}
2025-06-10 00:37:43 +02:00
// Check if EOI already exists (has signature links)
2025-06-11 13:54:04 +02:00
if ( interest [ 'Signature Link Client' ] && interest [ 'Signature Link CC' ] && interest [ 'Signature Link Developer' ] ) {
2025-06-11 19:22:50 +02:00
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' ) ;
}
2025-06-11 13:54:04 +02:00
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' ]
2025-06-10 00:37:43 +02:00
}
2025-06-11 13:54:04 +02:00
} ;
2025-06-10 00:37:43 +02:00
}
2025-06-11 14:08:28 +02:00
// 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
}
}
2025-06-09 22:40:37 +02:00
// 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' ] }
] ;
2025-06-09 22:56:10 +02:00
// Address is optional - use a default if not provided
const address = interest [ 'Address' ] || 'Not Provided' ;
2025-06-09 22:40:37 +02:00
const missingFields = requiredFields . filter ( f = > ! f . value ) . map ( f = > f . field ) ;
if ( missingFields . length > 0 ) {
throw createError ( {
statusCode : 400 ,
2025-06-09 22:56:10 +02:00
statusMessage : ` Please fill in the following required fields before generating EOI: ${ missingFields . join ( ', ' ) } . You can update these fields in the interest details form. `
2025-06-09 22:40:37 +02:00
} ) ;
}
2025-06-15 17:48:40 +02:00
// Get linked berths - forward the authentication cookies for internal API call
const cookies = getRequestHeader ( event , "cookie" ) ;
2025-06-15 16:32:34 +02:00
const requestHeaders : Record < string , string > = { } ;
2025-06-15 17:48:40 +02:00
if ( cookies ) {
requestHeaders [ "cookie" ] = cookies ;
2025-06-15 16:32:34 +02:00
}
2025-06-15 17:48:40 +02:00
console . log ( '[generate-eoi] Making internal API call to get-interest-berths with forwarded cookies' ) ;
2025-06-09 22:40:37 +02:00
const berthsResponse = await $fetch < { list : Array < { 'Mooring Number' : string } > } > (
"/api/get-interest-berths" ,
{
2025-06-15 16:32:34 +02:00
headers : requestHeaders ,
2025-06-09 22:40:37 +02:00
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 : "SEQUENTIAL"
} ,
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' ] ,
2025-06-09 22:56:10 +02:00
"Address" : address ,
2025-06-09 22:40:37 +02:00
"Lease_10" : false ,
"Purchase" : true ,
"Yacht Name" : interest [ 'Yacht Name' ] ,
"Berth Number" : berthNumbers
} ,
recipients : [
{
2025-07-10 03:51:42 +02:00
id : clientRecipientId ,
2025-06-09 22:40:37 +02:00
name : interest [ 'Full Name' ] ,
role : "SIGNER" ,
email : interest [ 'Email Address' ] ,
signingOrder : 1
} ,
{
2025-07-10 03:51:42 +02:00
id : davidRecipientId ,
2025-06-09 22:40:37 +02:00
name : "David Mizrahi" ,
role : "SIGNER" ,
email : "dm@portnimara.com" ,
signingOrder : 3
} ,
{
2025-07-10 03:51:42 +02:00
id : approvalRecipientId ,
name : "Approval" ,
2025-06-09 22:40:37 +02:00
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."
} ) ;
}
2025-06-09 23:09:32 +02:00
// 3. Send document (moves from draft to active and sends emails)
2025-06-09 22:40:37 +02:00
try {
2025-06-09 23:09:32 +02:00
const sendResponse = await fetch ( ` ${ documensoBaseUrl } /api/v1/documents/ ${ documentResponse . documentId } /send ` , {
2025-06-09 22:40:37 +02:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Authorization' : ` Bearer ${ documensoApiKey } `
} ,
body : JSON.stringify ( {
2025-06-09 23:09:32 +02:00
sendEmail : true ,
2025-06-09 22:40:37 +02:00
sendCompletionEmails : true
} )
} ) ;
2025-06-09 23:09:32 +02:00
if ( ! sendResponse . ok ) {
const errorText = await sendResponse . text ( ) ;
console . error ( 'Failed to send document:' , errorText ) ;
throw new Error ( ` Failed to send document: ${ sendResponse . statusText } ` ) ;
2025-06-09 22:40:37 +02:00
}
2025-06-09 23:09:32 +02:00
console . log ( 'Document sent successfully' ) ;
2025-06-09 22:40:37 +02:00
} catch ( error ) {
2025-06-09 23:09:32 +02:00
console . error ( 'Document send error:' , error ) ;
throw createError ( {
statusCode : 500 ,
statusMessage : "Document created but failed to send. Please check Documenso dashboard."
} ) ;
2025-06-09 22:40:37 +02:00
}
// 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' ) {
2025-07-10 03:51:42 +02:00
signingLinks [ 'Approval' ] = recipient . signingUrl ;
2025-06-09 22:40:37 +02:00
}
}
} ) ;
}
// 4. Update interest record
const currentDate = new Date ( ) ;
2025-06-10 00:53:49 +02:00
const dateTimeString = currentDate . toLocaleString ( 'en-GB' , {
2025-06-09 22:40:37 +02:00
day : '2-digit' ,
2025-06-10 00:53:49 +02:00
month : '2-digit' ,
year : 'numeric' ,
2025-06-09 22:40:37 +02:00
hour : '2-digit' ,
minute : '2-digit' ,
2025-06-10 00:53:49 +02:00
hour12 : false
2025-06-09 22:40:37 +02:00
} ) ;
const extraComments = interest [ 'Extra Comments' ] || '' ;
2025-06-11 16:50:31 +02:00
const updatedComments = extraComments + ( extraComments ? '\n\n' : '' ) + ` EOI Generated ${ dateTimeString } ` ;
2025-06-09 22:40:37 +02:00
const updateData : any = {
'EOI Status' : 'Waiting for Signatures' ,
'Sales Process Level' : 'LOI and NDA Sent' ,
2025-06-10 00:53:49 +02:00
// Don't set EOI Time Sent here - only when email is sent or link is copied
2025-06-11 14:08:28 +02:00
'Extra Comments' : updatedComments ,
'documensoID' : documentResponse . documentId . toString ( )
2025-06-09 22:40:37 +02:00
} ;
2025-06-11 16:05:19 +02:00
// 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
} ) ;
2025-06-10 00:37:43 +02:00
// Add signing links to update data with new column names
2025-06-11 18:14:10 +02:00
console . log ( '[EMBEDDED] Available signing links:' , signingLinks ) ;
2025-06-09 22:40:37 +02:00
if ( signingLinks [ 'Client' ] ) {
2025-06-10 00:37:43 +02:00
updateData [ 'Signature Link Client' ] = signingLinks [ 'Client' ] ;
2025-06-11 18:14:10 +02:00
const embeddedClientUrl = createEmbeddedSigningUrl ( signingLinks [ 'Client' ] , 'client' ) ;
updateData [ 'EmbeddedSignatureLinkClient' ] = embeddedClientUrl ;
console . log ( '[EMBEDDED] Client URL:' , signingLinks [ 'Client' ] , '-> Embedded:' , embeddedClientUrl ) ;
2025-06-09 22:40:37 +02:00
}
if ( signingLinks [ 'David Mizrahi' ] ) {
2025-06-10 00:37:43 +02:00
updateData [ 'Signature Link Developer' ] = signingLinks [ 'David Mizrahi' ] ;
2025-06-11 18:14:10 +02:00
const embeddedDevUrl = createEmbeddedSigningUrl ( signingLinks [ 'David Mizrahi' ] , 'developer' ) ;
updateData [ 'EmbeddedSignatureLinkDeveloper' ] = embeddedDevUrl ;
console . log ( '[EMBEDDED] Developer URL:' , signingLinks [ 'David Mizrahi' ] , '-> Embedded:' , embeddedDevUrl ) ;
2025-06-09 22:40:37 +02:00
}
2025-07-10 03:51:42 +02:00
if ( signingLinks [ 'Approval' ] ) {
updateData [ 'Signature Link CC' ] = signingLinks [ 'Approval' ] ;
const embeddedCCUrl = createEmbeddedSigningUrl ( signingLinks [ 'Approval' ] , 'cc' ) ;
2025-06-11 18:14:10 +02:00
updateData [ 'EmbeddedSignatureLinkCC' ] = embeddedCCUrl ;
2025-07-10 03:51:42 +02:00
console . log ( '[EMBEDDED] CC URL:' , signingLinks [ 'Approval' ] , '-> Embedded:' , embeddedCCUrl ) ;
2025-06-09 22:40:37 +02:00
}
2025-06-11 18:14:10 +02:00
console . log ( '[EMBEDDED] Final updateData being sent to NocoDB:' , updateData ) ;
2025-06-09 22:40:37 +02:00
await updateInterest ( interestId , updateData ) ;
return {
success : true ,
2025-06-09 23:09:32 +02:00
documentId : documentResponse.documentId ,
2025-06-12 17:52:57 +02:00
documensoID : documentResponse.documentId.toString ( ) , // Include this for immediate UI update
2025-06-09 22:40:37 +02:00
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" ,
} ) ;
}
}
} ) ;