import { requireSalesOrAdmin } from '~/server/utils/auth'; import { getNocoDbConfiguration, normalizePersonName } from '~/server/utils/nocodb'; import { findDuplicates, createExpenseConfig } from '~/server/utils/duplicate-detection'; import type { Expense } from '~/utils/types'; export default defineEventHandler(async (event) => { console.log('[EXPENSES] Find duplicate expenses request'); try { // Require sales or admin access await requireSalesOrAdmin(event); const query = getQuery(event); const dateRange = query.dateRange as string || '30'; // Default to last 30 days // Calculate date range const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - parseInt(dateRange)); // Get expenses from NocoDB const config = getNocoDbConfiguration(); const expenseTableId = "mxfcefkk4dqs6uq"; const response = await $fetch(`${config.url}/api/v2/tables/${expenseTableId}/records`, { headers: { 'xc-token': config.token }, params: { limit: 5000, where: `(Time,gte,${startDate.toISOString().split('T')[0]})~and(Time,lte,${endDate.toISOString().split('T')[0]})`, sort: '-Time' } }) as any; const expenses = response.list || []; console.log('[EXPENSES] Analyzing', expenses.length, 'expenses for duplicates'); // Find duplicate groups using the new centralized utility const duplicateConfig = createExpenseConfig(); const duplicateGroups = findDuplicates(expenses, duplicateConfig); // Convert to the expected format const formattedGroups = duplicateGroups.map(group => ({ id: group.id, expenses: group.items, matchReason: group.matchReason, confidence: group.confidence, masterCandidate: group.masterCandidate })); // Also find payer name variations const payerVariations = findPayerNameVariations(expenses); console.log('[EXPENSES] Found', formattedGroups.length, 'duplicate groups and', payerVariations.length, 'payer variations'); return { success: true, data: { duplicateGroups: formattedGroups, payerVariations, totalExpenses: expenses.length, duplicateCount: formattedGroups.reduce((sum, group) => sum + group.expenses.length, 0), dateRange: { start: startDate.toISOString().split('T')[0], end: endDate.toISOString().split('T')[0] } } }; } catch (error: any) { console.error('[EXPENSES] Failed to find duplicates:', error); if (error.statusCode === 403) { throw createError({ statusCode: 403, statusMessage: 'Access denied. Sales or admin role required.' }); } throw createError({ statusCode: 500, statusMessage: 'Failed to find duplicate expenses' }); } }); /** * Find payer name variations (like "Abbie" vs "abbie") */ function findPayerNameVariations(expenses: any[]) { const payerMap = new Map>(); // Group payers by normalized name expenses.forEach(expense => { if (expense.Payer) { const normalized = normalizePersonName(expense.Payer); if (!payerMap.has(normalized)) { payerMap.set(normalized, new Set()); } payerMap.get(normalized)!.add(expense.Payer); } }); // Find variations const variations: Array<{ normalizedName: string; variations: string[]; expenseCount: number; }> = []; payerMap.forEach((variationSet, normalized) => { if (variationSet.size > 1) { const variationArray = Array.from(variationSet); const expenseCount = expenses.filter(e => e.Payer && normalizePersonName(e.Payer) === normalized ).length; variations.push({ normalizedName: normalized, variations: variationArray, expenseCount }); } }); return variations.sort((a, b) => b.expenseCount - a.expenseCount); }