feat: Enhance error handling and logging in expense and interest duplicate detection, add retry logic for document deletion, and improve PDF generation with detailed receipt processing

This commit is contained in:
Matt 2025-07-10 09:59:17 -04:00
parent 06500a614d
commit a00b3918be
7 changed files with 384 additions and 93 deletions

View File

@ -79,6 +79,7 @@ export default defineNuxtConfig({
workbox: { workbox: {
navigateFallback: '/', navigateFallback: '/',
globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'], globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,svg,ico}'],
navigateFallbackDenylist: [/^\/api\//],
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: /^https:\/\/cms\.portnimara\.dev\/.*/i, urlPattern: /^https:\/\/cms\.portnimara\.dev\/.*/i,
@ -94,7 +95,9 @@ export default defineNuxtConfig({
} }
} }
} }
] ],
skipWaiting: true,
clientsClaim: true
}, },
client: { client: {
installPrompt: true, installPrompt: true,

View File

@ -413,7 +413,17 @@ const fetchExpenses = async () => {
} catch (err: any) { } catch (err: any) {
console.error('[expenses] Error fetching expenses:', err); 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 { } finally {
loading.value = false; loading.value = false;
} }

View File

@ -12,6 +12,7 @@ export default defineEventHandler(async (event) => {
try { try {
const body = await readBody(event); const body = await readBody(event);
const { interestId } = body; const { interestId } = body;
const query = getQuery(event);
console.log('[Delete Generated EOI] Interest ID:', interestId); 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'); console.log('[Delete Generated EOI] Deleting document from Documenso');
let documensoDeleteSuccessful = false; let documensoDeleteSuccessful = false;
let retryCount = 0;
const maxRetries = 3;
try { // Retry logic for temporary failures
const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, { while (!documensoDeleteSuccessful && retryCount < maxRetries) {
method: 'DELETE', try {
headers: { const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, {
'Authorization': `Bearer ${documensoApiKey}`, method: 'DELETE',
'Content-Type': 'application/json' headers: {
} 'Authorization': `Bearer ${documensoApiKey}`,
}); 'Content-Type': 'application/json'
}
});
if (!deleteResponse.ok) { const responseStatus = deleteResponse.status;
const errorText = await deleteResponse.text(); let errorDetails = '';
console.error('[Delete Generated EOI] Documenso deletion failed:', errorText);
// If it's a 404, the document is already gone, which is what we want try {
if (deleteResponse.status === 404) { errorDetails = await deleteResponse.text();
console.log('[Delete Generated EOI] Document already deleted from Documenso (404) - proceeding with database cleanup'); } catch {
documensoDeleteSuccessful = true; errorDetails = 'No error details available';
} else {
throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`);
} }
} else {
console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); if (!deleteResponse.ok) {
documensoDeleteSuccessful = true; console.error(`[Delete Generated EOI] Documenso deletion failed (attempt ${retryCount + 1}/${maxRetries}):`, {
} status: responseStatus,
} catch (error: any) { statusText: deleteResponse.statusText,
console.error('[Delete Generated EOI] Documenso deletion error:', error); details: errorDetails
});
// Check if it's a network error or 404 - in those cases, proceed with cleanup
if (error.message?.includes('404') || error.status === 404) { // Handle specific status codes
console.log('[Delete Generated EOI] Document not found in Documenso - proceeding with database cleanup'); switch (responseStatus) {
documensoDeleteSuccessful = true; case 404:
} else { // 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({ throw createError({
statusCode: 500, statusCode: 500,
statusMessage: `Failed to delete document from Documenso: ${error.message}`, statusMessage: error.message || 'Failed to communicate with Documenso API',
}); });
} }
} }
if (!documensoDeleteSuccessful) { if (!documensoDeleteSuccessful) {
throw createError({ const query = getQuery(event);
statusCode: 500, if (query.forceCleanup === 'true') {
statusMessage: 'Failed to delete document from Documenso', 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 // Reset interest fields

View File

@ -78,6 +78,8 @@ export default defineEventHandler(async (event) => {
* Find duplicate expenses based on multiple criteria * Find duplicate expenses based on multiple criteria
*/ */
function findDuplicateExpenses(expenses: any[]) { function findDuplicateExpenses(expenses: any[]) {
console.log('[EXPENSES] Starting duplicate detection for', expenses.length, 'expenses');
const duplicateGroups: Array<{ const duplicateGroups: Array<{
id: string; id: string;
expenses: any[]; expenses: any[];
@ -87,6 +89,7 @@ function findDuplicateExpenses(expenses: any[]) {
}> = []; }> = [];
const processedIds = new Set<number>(); const processedIds = new Set<number>();
let comparisons = 0;
for (let i = 0; i < expenses.length; i++) { for (let i = 0; i < expenses.length; i++) {
const expense1 = expenses[i]; const expense1 = expenses[i];
@ -102,8 +105,13 @@ function findDuplicateExpenses(expenses: any[]) {
if (processedIds.has(expense2.Id)) continue; if (processedIds.has(expense2.Id)) continue;
const similarity = calculateExpenseSimilarity(expense1, expense2); 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); matches.push(expense2);
processedIds.add(expense2.Id); processedIds.add(expense2.Id);
similarity.reasons.forEach(r => matchReasons.add(r)); similarity.reasons.forEach(r => matchReasons.add(r));

View File

@ -452,81 +452,202 @@ function groupExpenses(expenses: Expense[], groupBy: string): Record<string, Exp
async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) { async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
console.log('[expenses/generate-pdf] Adding receipt images...'); console.log('[expenses/generate-pdf] Adding receipt images...');
console.log('[expenses/generate-pdf] Total expenses to check:', expenses.length);
// Log receipt data structure for debugging
expenses.forEach((expense, index) => {
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 => const expensesWithReceipts = expenses.filter(expense =>
expense.Receipt && Array.isArray(expense.Receipt) && expense.Receipt.length > 0 expense.Receipt && Array.isArray(expense.Receipt) && expense.Receipt.length > 0
); );
console.log('[expenses/generate-pdf] Expenses with receipts:', expensesWithReceipts.length);
if (expensesWithReceipts.length === 0) { if (expensesWithReceipts.length === 0) {
console.log('[expenses/generate-pdf] No receipts found to include'); console.log('[expenses/generate-pdf] No receipts found to include');
return; return;
} }
// Add new page for receipts let totalReceiptImages = 0;
doc.addPage(); let processedImages = 0;
doc.fontSize(18) // Count total receipt images for progress tracking
.font('Helvetica-Bold') expensesWithReceipts.forEach(expense => {
.text('Receipt Images', { align: 'center' }); 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) { for (const expense of expensesWithReceipts) {
try { try {
// Add expense header console.log('[expenses/generate-pdf] Processing receipts for expense:', expense.Id, expense['Establishment Name']);
doc.fontSize(14)
.font('Helvetica-Bold')
.text(`Receipt for: ${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`,
{ align: 'left' });
doc.fontSize(12) // Process receipt images - each gets its own page
.font('Helvetica') if (expense.Receipt && Array.isArray(expense.Receipt)) {
.text(`Date: ${expense.Time ? formatDate(expense.Time) : 'N/A'}`, { align: 'left' }); for (const [receiptIndex, receipt] of expense.Receipt.entries()) {
if (receipt.url || receipt.directus_files_id?.filename_download || receipt.filename_download) {
doc.y += 10;
// Process receipt images
if (expense.Receipt) {
for (const receipt of expense.Receipt) {
if (receipt.url || receipt.directus_files_id?.filename_download) {
try { try {
console.log(`[expenses/generate-pdf] Fetching receipt ${receiptIndex + 1}/${expense.Receipt.length} for expense ${expense.Id}`);
const imageBuffer = await fetchReceiptImage(receipt); const imageBuffer = await fetchReceiptImage(receipt);
if (imageBuffer) { if (imageBuffer) {
// Check if we need a new page // Add new page for each receipt image
if (doc.y > doc.page.height - 400) { doc.addPage();
doc.addPage();
doc.y = 60; // 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 } else {
const maxWidth = 400; console.warn(`[expenses/generate-pdf] No image buffer received for receipt ${receiptIndex + 1} of expense ${expense.Id}`);
const maxHeight = 300;
doc.image(imageBuffer, { // Add page with error message
fit: [maxWidth, maxHeight], doc.addPage();
align: 'center'
});
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) doc.fontSize(10)
.fillColor('#666666') .fillColor('#6c757d')
.text('Receipt image could not be loaded', { align: 'center' }); .text(`${imageError.message || 'Unknown error'}`, { align: 'center' });
doc.fillColor('#000000'); 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) { } 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<Buffer | null> { async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {

View File

@ -7,9 +7,13 @@ export default defineEventHandler(async (event) => {
console.log('[get-expenses] API called with query:', getQuery(event)); console.log('[get-expenses] API called with query:', getQuery(event));
try { try {
// Check authentication // Set proper headers
setHeader(event, 'Cache-Control', 'no-cache');
setHeader(event, 'Content-Type', 'application/json');
// Check authentication first
try { try {
await requireSalesOrAdmin(event); await requireSalesOrAdmin(event);
console.log('[get-expenses] Authentication successful');
} catch (authError: any) { } catch (authError: any) {
console.error('[get-expenses] Authentication failed:', authError); 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.' statusMessage: 'Unable to fetch expense data. Please try again later.'
}); });
} }
} catch (authError: any) { } catch (error: any) {
if (authError.statusCode === 403) { 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({ throw createError({
statusCode: 403, statusCode: 401,
statusMessage: 'Access denied. This feature requires sales team or administrator privileges.' 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.'
});
} }
}); });

View File

@ -86,6 +86,9 @@ export default defineEventHandler(async (event) => {
* Find duplicate interests based on multiple criteria * Find duplicate interests based on multiple criteria
*/ */
function findDuplicateInterests(interests: any[], threshold: number = 0.8) { 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<{ const duplicateGroups: Array<{
id: string; id: string;
interests: any[]; interests: any[];
@ -95,6 +98,7 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) {
}> = []; }> = [];
const processedIds = new Set<number>(); const processedIds = new Set<number>();
let comparisons = 0;
for (let i = 0; i < interests.length; i++) { for (let i = 0; i < interests.length; i++) {
const interest1 = interests[i]; const interest1 = interests[i];
@ -109,14 +113,21 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) {
if (processedIds.has(interest2.Id)) continue; if (processedIds.has(interest2.Id)) continue;
const similarity = calculateSimilarity(interest1, interest2); 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) { 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); matches.push(interest2);
processedIds.add(interest2.Id); processedIds.add(interest2.Id);
} }
} }
if (matches.length > 1) { if (matches.length > 1) {
console.log(`[INTERESTS] Creating duplicate group with ${matches.length} matches`);
// Mark all as processed // Mark all as processed
matches.forEach(match => processedIds.add(match.Id)); 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; return duplicateGroups;
} }
@ -146,37 +158,68 @@ function findDuplicateInterests(interests: any[], threshold: number = 0.8) {
*/ */
function calculateSimilarity(interest1: any, interest2: any) { function calculateSimilarity(interest1: any, interest2: any) {
const scores: Array<{ type: string; score: number; weight: number }> = []; 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']) { if (interest1['Email Address'] && interest2['Email Address']) {
const emailScore = normalizeEmail(interest1['Email Address']) === normalizeEmail(interest2['Email Address']) ? 1.0 : 0.0; const email1 = normalizeEmail(interest1['Email Address']);
scores.push({ type: 'email', score: emailScore, weight: 0.4 }); 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']) { if (interest1['Phone Number'] && interest2['Phone Number']) {
const phone1 = normalizePhone(interest1['Phone Number']); const phone1 = normalizePhone(interest1['Phone Number']);
const phone2 = normalizePhone(interest2['Phone Number']); const phone2 = normalizePhone(interest2['Phone Number']);
const phoneScore = phone1 === phone2 ? 1.0 : 0.0; const phoneScore = phone1 === phone2 && phone1.length >= 8 ? 1.0 : 0.0; // Require at least 8 digits
scores.push({ type: 'phone', score: phoneScore, weight: 0.3 }); 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']) { if (interest1['Full Name'] && interest2['Full Name']) {
const nameScore = calculateNameSimilarity(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 // Address similarity
if (interest1.Address && interest2.Address) { if (interest1.Address && interest2.Address) {
const addressScore = calculateStringSimilarity(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 totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
const weightedScore = scores.reduce((sum, s) => sum + (s.score * s.weight), 0) / (totalWeight || 1); 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 { return {
score: weightedScore, score: weightedScore,
details: scores details: scores