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:
2025-07-10 09:59:17 -04:00
parent 06500a614d
commit a00b3918be
7 changed files with 384 additions and 93 deletions

View File

@@ -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<number>();
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));

View File

@@ -452,81 +452,202 @@ function groupExpenses(expenses: Expense[], groupBy: string): Record<string, Exp
async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
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 =>
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<Buffer | null> {