port-nimara-client-portal/server/api/expenses/duplicates/find.ts

130 lines
3.9 KiB
TypeScript

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<string, Set<string>>();
// 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);
}