diff --git a/nuxt.config.ts b/nuxt.config.ts index 452f86b..0b7f993 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -79,6 +79,7 @@ export default defineNuxtConfig({ workbox: { navigateFallback: '/', globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'], + navigateFallbackDenylist: [/^\/api\//], runtimeCaching: [ { urlPattern: /^https:\/\/cms\.portnimara\.dev\/.*/i, @@ -94,7 +95,9 @@ export default defineNuxtConfig({ } } } - ] + ], + skipWaiting: true, + clientsClaim: true }, client: { installPrompt: true, diff --git a/pages/dashboard/expenses.vue b/pages/dashboard/expenses.vue index b3f59c7..62b10aa 100644 --- a/pages/dashboard/expenses.vue +++ b/pages/dashboard/expenses.vue @@ -413,7 +413,17 @@ const fetchExpenses = async () => { } catch (err: any) { console.error('[expenses] Error fetching expenses:', err); - error.value = err.message || 'Failed to fetch expenses'; + + // Better error messages based on status codes + if (err.statusCode === 401) { + error.value = 'Authentication required. Please refresh the page and log in again.'; + } else if (err.statusCode === 403) { + error.value = 'Access denied. You need proper permissions to view expenses.'; + } else if (err.statusCode === 503) { + error.value = 'Service temporarily unavailable. Please try again in a few moments.'; + } else { + error.value = err.data?.message || err.message || 'Failed to fetch expenses. Please check your connection and try again.'; + } } finally { loading.value = false; } diff --git a/server/api/eoi/delete-generated-document.ts b/server/api/eoi/delete-generated-document.ts index f926350..84960b5 100644 --- a/server/api/eoi/delete-generated-document.ts +++ b/server/api/eoi/delete-generated-document.ts @@ -12,6 +12,7 @@ export default defineEventHandler(async (event) => { try { const body = await readBody(event); const { interestId } = body; + const query = getQuery(event); console.log('[Delete Generated EOI] Interest ID:', interestId); @@ -77,51 +78,132 @@ export default defineEventHandler(async (event) => { console.log('[Delete Generated EOI] Deleting document from Documenso'); let documensoDeleteSuccessful = false; + let retryCount = 0; + const maxRetries = 3; - try { - const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${documensoApiKey}`, - 'Content-Type': 'application/json' - } - }); + // Retry logic for temporary failures + while (!documensoDeleteSuccessful && retryCount < maxRetries) { + try { + const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${documensoApiKey}`, + 'Content-Type': 'application/json' + } + }); - if (!deleteResponse.ok) { - const errorText = await deleteResponse.text(); - console.error('[Delete Generated EOI] Documenso deletion failed:', errorText); + const responseStatus = deleteResponse.status; + let errorDetails = ''; - // If it's a 404, the document is already gone, which is what we want - if (deleteResponse.status === 404) { - console.log('[Delete Generated EOI] Document already deleted from Documenso (404) - proceeding with database cleanup'); - documensoDeleteSuccessful = true; - } else { - throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`); + try { + errorDetails = await deleteResponse.text(); + } catch { + errorDetails = 'No error details available'; } - } else { - console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); - documensoDeleteSuccessful = true; - } - } catch (error: any) { - console.error('[Delete Generated EOI] Documenso deletion error:', error); - - // Check if it's a network error or 404 - in those cases, proceed with cleanup - if (error.message?.includes('404') || error.status === 404) { - console.log('[Delete Generated EOI] Document not found in Documenso - proceeding with database cleanup'); - documensoDeleteSuccessful = true; - } else { + + if (!deleteResponse.ok) { + console.error(`[Delete Generated EOI] Documenso deletion failed (attempt ${retryCount + 1}/${maxRetries}):`, { + status: responseStatus, + statusText: deleteResponse.statusText, + details: errorDetails + }); + + // Handle specific status codes + switch (responseStatus) { + case 404: + // Document already deleted - this is fine + console.log('[Delete Generated EOI] Document already deleted from Documenso (404) - proceeding with database cleanup'); + documensoDeleteSuccessful = true; + break; + + case 403: + // Permission denied - document might be in a protected state + console.warn('[Delete Generated EOI] Permission denied (403) - document may be in a protected state'); + throw createError({ + statusCode: 403, + statusMessage: 'Cannot delete document - it may be fully signed or in a protected state', + }); + + case 500: + case 502: + case 503: + case 504: + // Server errors - retry if we haven't exceeded retries + if (retryCount < maxRetries - 1) { + console.log(`[Delete Generated EOI] Server error (${responseStatus}) - retrying in ${(retryCount + 1) * 2} seconds...`); + await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 2000)); // Exponential backoff + retryCount++; + continue; + } else { + console.error('[Delete Generated EOI] Max retries exceeded for server error'); + // Allow proceeding with cleanup for server errors after retries + if (query.forceCleanup === 'true') { + console.warn('[Delete Generated EOI] Force cleanup enabled - proceeding despite Documenso error'); + documensoDeleteSuccessful = true; + break; + } + throw new Error(`Documenso server error after ${maxRetries} attempts (${responseStatus}): ${errorDetails}`); + } + + default: + // Other errors - don't retry + throw new Error(`Documenso API error (${responseStatus}): ${errorDetails || deleteResponse.statusText}`); + } + } else { + console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); + documensoDeleteSuccessful = true; + } + } catch (error: any) { + console.error(`[Delete Generated EOI] Documenso deletion error (attempt ${retryCount + 1}/${maxRetries}):`, error); + + // Network errors - retry if we haven't exceeded retries + if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') { + if (retryCount < maxRetries - 1) { + console.log(`[Delete Generated EOI] Network error - retrying in ${(retryCount + 1) * 2} seconds...`); + await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 2000)); + retryCount++; + continue; + } + } + + // Check if it's a 404 error wrapped in another error + if (error.message?.includes('404') || error.status === 404 || error.statusCode === 404) { + console.log('[Delete Generated EOI] Document not found in Documenso - proceeding with database cleanup'); + documensoDeleteSuccessful = true; + break; + } + + // Check if force cleanup is enabled + const query = getQuery(event); + if (query.forceCleanup === 'true') { + console.warn('[Delete Generated EOI] Force cleanup enabled - proceeding despite Documenso error:', error.message); + documensoDeleteSuccessful = true; + break; + } + + // Don't wrap error messages multiple times + if (error.statusCode) { + throw error; + } + throw createError({ statusCode: 500, - statusMessage: `Failed to delete document from Documenso: ${error.message}`, + statusMessage: error.message || 'Failed to communicate with Documenso API', }); } } if (!documensoDeleteSuccessful) { - throw createError({ - statusCode: 500, - statusMessage: 'Failed to delete document from Documenso', - }); + const query = getQuery(event); + if (query.forceCleanup === 'true') { + console.warn('[Delete Generated EOI] Force cleanup enabled - proceeding with database cleanup despite Documenso failure'); + documensoDeleteSuccessful = true; + } else { + throw createError({ + statusCode: 500, + statusMessage: 'Failed to delete document from Documenso after multiple attempts. You can add ?forceCleanup=true to force database cleanup.', + }); + } } // Reset interest fields diff --git a/server/api/expenses/duplicates/find.ts b/server/api/expenses/duplicates/find.ts index d640643..0cab3ed 100644 --- a/server/api/expenses/duplicates/find.ts +++ b/server/api/expenses/duplicates/find.ts @@ -78,6 +78,8 @@ export default defineEventHandler(async (event) => { * Find duplicate expenses based on multiple criteria */ function findDuplicateExpenses(expenses: any[]) { + console.log('[EXPENSES] Starting duplicate detection for', expenses.length, 'expenses'); + const duplicateGroups: Array<{ id: string; expenses: any[]; @@ -87,6 +89,7 @@ function findDuplicateExpenses(expenses: any[]) { }> = []; const processedIds = new Set(); + let comparisons = 0; for (let i = 0; i < expenses.length; i++) { const expense1 = expenses[i]; @@ -102,8 +105,13 @@ function findDuplicateExpenses(expenses: any[]) { if (processedIds.has(expense2.Id)) continue; const similarity = calculateExpenseSimilarity(expense1, expense2); + comparisons++; - if (similarity.score >= 0.8) { + console.log(`[EXPENSES] Comparing ${expense1.Id} vs ${expense2.Id}: score=${similarity.score.toFixed(3)}, threshold=0.7`); + + if (similarity.score >= 0.7) { // Lower threshold for expenses + console.log(`[EXPENSES] MATCH FOUND! ${expense1.Id} vs ${expense2.Id} (score: ${similarity.score.toFixed(3)})`); + console.log('[EXPENSES] Match reasons:', similarity.reasons); matches.push(expense2); processedIds.add(expense2.Id); similarity.reasons.forEach(r => matchReasons.add(r)); diff --git a/server/api/expenses/generate-pdf.ts b/server/api/expenses/generate-pdf.ts index 25ded80..267f890 100644 --- a/server/api/expenses/generate-pdf.ts +++ b/server/api/expenses/generate-pdf.ts @@ -452,81 +452,202 @@ function groupExpenses(expenses: Expense[], groupBy: string): Record { + console.log(`[expenses/generate-pdf] Expense ${index + 1} (ID: ${expense.Id}):`, { + establishment: expense['Establishment Name'], + hasReceipt: !!expense.Receipt, + receiptType: typeof expense.Receipt, + receiptLength: Array.isArray(expense.Receipt) ? expense.Receipt.length : 'N/A', + receiptData: expense.Receipt + }); + }); const expensesWithReceipts = expenses.filter(expense => expense.Receipt && Array.isArray(expense.Receipt) && expense.Receipt.length > 0 ); + console.log('[expenses/generate-pdf] Expenses with receipts:', expensesWithReceipts.length); + if (expensesWithReceipts.length === 0) { console.log('[expenses/generate-pdf] No receipts found to include'); return; } - // Add new page for receipts - doc.addPage(); + let totalReceiptImages = 0; + let processedImages = 0; - doc.fontSize(18) - .font('Helvetica-Bold') - .text('Receipt Images', { align: 'center' }); + // Count total receipt images for progress tracking + expensesWithReceipts.forEach(expense => { + if (expense.Receipt && Array.isArray(expense.Receipt)) { + totalReceiptImages += expense.Receipt.length; + } + }); - doc.y += 20; + console.log('[expenses/generate-pdf] Total receipt images to process:', totalReceiptImages); for (const expense of expensesWithReceipts) { try { - // Add expense header - doc.fontSize(14) - .font('Helvetica-Bold') - .text(`Receipt for: ${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, - { align: 'left' }); + console.log('[expenses/generate-pdf] Processing receipts for expense:', expense.Id, expense['Establishment Name']); - doc.fontSize(12) - .font('Helvetica') - .text(`Date: ${expense.Time ? formatDate(expense.Time) : 'N/A'}`, { align: 'left' }); - - doc.y += 10; - - // Process receipt images - if (expense.Receipt) { - for (const receipt of expense.Receipt) { - if (receipt.url || receipt.directus_files_id?.filename_download) { + // Process receipt images - each gets its own page + if (expense.Receipt && Array.isArray(expense.Receipt)) { + for (const [receiptIndex, receipt] of expense.Receipt.entries()) { + if (receipt.url || receipt.directus_files_id?.filename_download || receipt.filename_download) { try { + console.log(`[expenses/generate-pdf] Fetching receipt ${receiptIndex + 1}/${expense.Receipt.length} for expense ${expense.Id}`); const imageBuffer = await fetchReceiptImage(receipt); if (imageBuffer) { - // Check if we need a new page - if (doc.y > doc.page.height - 400) { - doc.addPage(); - doc.y = 60; + // Add new page for each receipt image + doc.addPage(); + + // Add header section for this receipt + const headerHeight = 100; + + // Header background + doc.rect(60, 60, doc.page.width - 120, headerHeight) + .fillColor('#f8f9fa') + .fill() + .strokeColor('#dee2e6') + .lineWidth(1) + .stroke(); + + doc.fillColor('#000000'); + + // Receipt header content + doc.fontSize(16) + .font('Helvetica-Bold') + .text(`Receipt Image ${receiptIndex + 1}${expense.Receipt.length > 1 ? ` of ${expense.Receipt.length}` : ''}`, + 70, 80, { align: 'left' }); + + doc.fontSize(14) + .font('Helvetica-Bold') + .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, + 70, 105, { align: 'left' }); + + doc.fontSize(12) + .font('Helvetica') + .text(`Date: ${expense.Time ? formatDate(expense.Time) : 'N/A'}`, + 70, 125, { align: 'left' }); + + doc.fontSize(10) + .fillColor('#666666') + .text(`Payer: ${expense.Payer || 'N/A'} | Category: ${expense.Category || 'N/A'}`, + 70, 140, { align: 'left' }); + + doc.fillColor('#000000'); + + // Calculate available space for image (full page minus header and margins) + const pageWidth = doc.page.width; + const pageHeight = doc.page.height; + const margin = 60; + const imageStartY = 60 + headerHeight + 20; // Header + spacing + + const maxImageWidth = pageWidth - (margin * 2); + const maxImageHeight = pageHeight - imageStartY - margin; + + console.log(`[expenses/generate-pdf] Adding large image - Max size: ${maxImageWidth}x${maxImageHeight}, Buffer size: ${imageBuffer.length} bytes`); + + // Add the receipt image with maximum size + try { + doc.image(imageBuffer, margin, imageStartY, { + fit: [maxImageWidth, maxImageHeight], + align: 'center', + valign: 'center' + }); + + processedImages++; + console.log(`[expenses/generate-pdf] Successfully added receipt image ${processedImages}/${totalReceiptImages}`); + + } catch (imageEmbedError: any) { + console.error('[expenses/generate-pdf] Error embedding image in PDF:', imageEmbedError); + + // Add error message on the page + doc.fontSize(14) + .fillColor('#dc3545') + .text('Receipt image could not be embedded', margin, imageStartY + 50, { + align: 'center', + width: maxImageWidth + }); + + doc.fontSize(12) + .fillColor('#6c757d') + .text(`Error: ${imageEmbedError.message || 'Unknown error'}`, margin, imageStartY + 80, { + align: 'center', + width: maxImageWidth + }); + + doc.fillColor('#000000'); } - // Add image - const maxWidth = 400; - const maxHeight = 300; + } else { + console.warn(`[expenses/generate-pdf] No image buffer received for receipt ${receiptIndex + 1} of expense ${expense.Id}`); - doc.image(imageBuffer, { - fit: [maxWidth, maxHeight], - align: 'center' - }); + // Add page with error message + doc.addPage(); - doc.y += 20; + doc.fontSize(16) + .font('Helvetica-Bold') + .text(`Receipt Image ${receiptIndex + 1}${expense.Receipt.length > 1 ? ` of ${expense.Receipt.length}` : ''}`, + { align: 'center' }); + + doc.fontSize(14) + .font('Helvetica') + .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, + { align: 'center' }); + + doc.y += 50; + + doc.fontSize(12) + .fillColor('#dc3545') + .text('Receipt image could not be loaded from storage', { align: 'center' }); + + doc.fillColor('#000000'); } - } catch (imageError) { - console.error('[expenses/generate-pdf] Error adding receipt image:', imageError); + + } catch (imageError: any) { + console.error(`[expenses/generate-pdf] Error processing receipt ${receiptIndex + 1} for expense ${expense.Id}:`, imageError); + + // Add page with error information + doc.addPage(); + + doc.fontSize(16) + .font('Helvetica-Bold') + .text(`Receipt Image ${receiptIndex + 1}${expense.Receipt.length > 1 ? ` of ${expense.Receipt.length}` : ''}`, + { align: 'center' }); + + doc.fontSize(14) + .font('Helvetica') + .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, + { align: 'center' }); + + doc.y += 50; + + doc.fontSize(12) + .fillColor('#dc3545') + .text('Error loading receipt image', { align: 'center' }); + doc.fontSize(10) - .fillColor('#666666') - .text('Receipt image could not be loaded', { align: 'center' }); + .fillColor('#6c757d') + .text(`${imageError.message || 'Unknown error'}`, { align: 'center' }); + doc.fillColor('#000000'); - doc.y += 10; } + } else { + console.log(`[expenses/generate-pdf] Skipping receipt ${receiptIndex + 1} for expense ${expense.Id} - no valid file path`); } } } - doc.y += 20; } catch (error) { - console.error('[expenses/generate-pdf] Error processing receipt for expense:', expense.Id, error); + console.error('[expenses/generate-pdf] Error processing receipts for expense:', expense.Id, error); } } + + console.log(`[expenses/generate-pdf] Completed processing ${processedImages}/${totalReceiptImages} receipt images`); } async function fetchReceiptImage(receipt: any): Promise { diff --git a/server/api/get-expenses.ts b/server/api/get-expenses.ts index 885eeb1..44ce6a6 100644 --- a/server/api/get-expenses.ts +++ b/server/api/get-expenses.ts @@ -7,9 +7,13 @@ export default defineEventHandler(async (event) => { console.log('[get-expenses] API called with query:', getQuery(event)); try { - // Check authentication + // Set proper headers + setHeader(event, 'Cache-Control', 'no-cache'); + setHeader(event, 'Content-Type', 'application/json'); + // Check authentication first try { await requireSalesOrAdmin(event); + console.log('[get-expenses] Authentication successful'); } catch (authError: any) { console.error('[get-expenses] Authentication failed:', authError); @@ -127,14 +131,34 @@ export default defineEventHandler(async (event) => { statusMessage: 'Unable to fetch expense data. Please try again later.' }); } - } catch (authError: any) { - if (authError.statusCode === 403) { + } catch (error: any) { + console.error('[get-expenses] Top-level error:', error); + + // If it's already a proper H3 error, re-throw it + if (error.statusCode) { + throw error; + } + + // Handle authentication errors specifically + if (error.message?.includes('authentication') || error.message?.includes('auth')) { throw createError({ - statusCode: 403, - statusMessage: 'Access denied. This feature requires sales team or administrator privileges.' + statusCode: 401, + statusMessage: 'Authentication required. Please log in again.' }); } - throw authError; + // Handle database connection errors + if (error.message?.includes('database') || error.message?.includes('connection')) { + throw createError({ + statusCode: 503, + statusMessage: 'Database temporarily unavailable. Please try again later.' + }); + } + + // Generic server error for anything else + throw createError({ + statusCode: 500, + statusMessage: 'An unexpected error occurred. Please try again later.' + }); } }); diff --git a/server/api/interests/duplicates/find.ts b/server/api/interests/duplicates/find.ts index 51b6179..d1ca86d 100644 --- a/server/api/interests/duplicates/find.ts +++ b/server/api/interests/duplicates/find.ts @@ -86,6 +86,9 @@ export default defineEventHandler(async (event) => { * Find duplicate interests based on multiple criteria */ function findDuplicateInterests(interests: any[], threshold: number = 0.8) { + console.log('[INTERESTS] Starting duplicate detection with threshold:', threshold); + console.log('[INTERESTS] Total interests to analyze:', interests.length); + const duplicateGroups: Array<{ id: string; interests: any[]; @@ -95,6 +98,7 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) { }> = []; const processedIds = new Set(); + let comparisons = 0; for (let i = 0; i < interests.length; i++) { const interest1 = interests[i]; @@ -109,14 +113,21 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) { if (processedIds.has(interest2.Id)) continue; const similarity = calculateSimilarity(interest1, interest2); + comparisons++; + + console.log(`[INTERESTS] Comparing ${interest1.Id} vs ${interest2.Id}: score=${similarity.score.toFixed(3)}, threshold=${threshold}`); if (similarity.score >= threshold) { + console.log(`[INTERESTS] MATCH FOUND! ${interest1.Id} vs ${interest2.Id} (score: ${similarity.score.toFixed(3)})`); + console.log('[INTERESTS] Match details:', similarity.details); matches.push(interest2); processedIds.add(interest2.Id); } } if (matches.length > 1) { + console.log(`[INTERESTS] Creating duplicate group with ${matches.length} matches`); + // Mark all as processed matches.forEach(match => processedIds.add(match.Id)); @@ -138,6 +149,7 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) { } } + console.log(`[INTERESTS] Completed ${comparisons} comparisons, found ${duplicateGroups.length} duplicate groups`); return duplicateGroups; } @@ -146,37 +158,68 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) { */ function calculateSimilarity(interest1: any, interest2: any) { const scores: Array<{ type: string; score: number; weight: number }> = []; + + console.log(`[INTERESTS] Calculating similarity between:`, { + id1: interest1.Id, + name1: interest1['Full Name'], + email1: interest1['Email Address'], + phone1: interest1['Phone Number'], + id2: interest2.Id, + name2: interest2['Full Name'], + email2: interest2['Email Address'], + phone2: interest2['Phone Number'] + }); - // Email similarity (highest weight) + // Email similarity (highest weight) - exact match required if (interest1['Email Address'] && interest2['Email Address']) { - const emailScore = normalizeEmail(interest1['Email Address']) === normalizeEmail(interest2['Email Address']) ? 1.0 : 0.0; - scores.push({ type: 'email', score: emailScore, weight: 0.4 }); + const email1 = normalizeEmail(interest1['Email Address']); + const email2 = normalizeEmail(interest2['Email Address']); + const emailScore = email1 === email2 ? 1.0 : 0.0; + scores.push({ type: 'email', score: emailScore, weight: 0.5 }); + console.log(`[INTERESTS] Email comparison: "${email1}" vs "${email2}" = ${emailScore}`); } - // Phone similarity + // Phone similarity - exact match on normalized numbers if (interest1['Phone Number'] && interest2['Phone Number']) { const phone1 = normalizePhone(interest1['Phone Number']); const phone2 = normalizePhone(interest2['Phone Number']); - const phoneScore = phone1 === phone2 ? 1.0 : 0.0; - scores.push({ type: 'phone', score: phoneScore, weight: 0.3 }); + const phoneScore = phone1 === phone2 && phone1.length >= 8 ? 1.0 : 0.0; // Require at least 8 digits + scores.push({ type: 'phone', score: phoneScore, weight: 0.4 }); + console.log(`[INTERESTS] Phone comparison: "${phone1}" vs "${phone2}" = ${phoneScore}`); } - // Name similarity + // Name similarity - fuzzy matching if (interest1['Full Name'] && interest2['Full Name']) { const nameScore = calculateNameSimilarity(interest1['Full Name'], interest2['Full Name']); - scores.push({ type: 'name', score: nameScore, weight: 0.2 }); + scores.push({ type: 'name', score: nameScore, weight: 0.3 }); + console.log(`[INTERESTS] Name comparison: "${interest1['Full Name']}" vs "${interest2['Full Name']}" = ${nameScore.toFixed(3)}`); } // Address similarity if (interest1.Address && interest2.Address) { const addressScore = calculateStringSimilarity(interest1.Address, interest2.Address); - scores.push({ type: 'address', score: addressScore, weight: 0.1 }); + scores.push({ type: 'address', score: addressScore, weight: 0.2 }); + console.log(`[INTERESTS] Address comparison: ${addressScore.toFixed(3)}`); } - // Calculate weighted average + // Special case: if we have exact email OR phone match, give high score regardless of other fields + const hasExactEmailMatch = scores.find(s => s.type === 'email' && s.score === 1.0); + const hasExactPhoneMatch = scores.find(s => s.type === 'phone' && s.score === 1.0); + + if (hasExactEmailMatch || hasExactPhoneMatch) { + console.log('[INTERESTS] Exact email or phone match found - high confidence'); + return { + score: 0.95, // High confidence for exact email/phone match + details: scores + }; + } + + // Calculate weighted average for other cases const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0); const weightedScore = scores.reduce((sum, s) => sum + (s.score * s.weight), 0) / (totalWeight || 1); + console.log(`[INTERESTS] Weighted score: ${weightedScore.toFixed(3)} (weights: ${totalWeight})`); + return { score: weightedScore, details: scores