feat: Enhance receipt processing in PDF generation with overall receipt numbering and improved path extraction for MinIO

This commit is contained in:
Matt 2025-07-10 12:57:43 -04:00
parent a00b3918be
commit 6e99f4f783
1 changed files with 124 additions and 36 deletions

View File

@ -478,6 +478,7 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
let totalReceiptImages = 0;
let processedImages = 0;
let currentReceiptNumber = 0; // Track overall receipt number across all expenses
// Count total receipt images for progress tracking
expensesWithReceipts.forEach(expense => {
@ -495,9 +496,11 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
// 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) {
currentReceiptNumber++; // Increment overall receipt counter
if (receipt.url || receipt.signedUrl || 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}`);
console.log(`[expenses/generate-pdf] Fetching receipt ${currentReceiptNumber}/${totalReceiptImages} (expense ${expense.Id}, receipt ${receiptIndex + 1}/${expense.Receipt.length})`);
const imageBuffer = await fetchReceiptImage(receipt);
if (imageBuffer) {
@ -517,10 +520,10 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
doc.fillColor('#000000');
// Receipt header content
// Receipt header content with overall numbering
doc.fontSize(16)
.font('Helvetica-Bold')
.text(`Receipt Image ${receiptIndex + 1}${expense.Receipt.length > 1 ? ` of ${expense.Receipt.length}` : ''}`,
.text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`,
70, 80, { align: 'left' });
doc.fontSize(14)
@ -584,14 +587,14 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
}
} else {
console.warn(`[expenses/generate-pdf] No image buffer received for receipt ${receiptIndex + 1} of expense ${expense.Id}`);
console.warn(`[expenses/generate-pdf] No image buffer received for receipt ${currentReceiptNumber} of expense ${expense.Id}`);
// Add page with error message
doc.addPage();
doc.fontSize(16)
.font('Helvetica-Bold')
.text(`Receipt Image ${receiptIndex + 1}${expense.Receipt.length > 1 ? ` of ${expense.Receipt.length}` : ''}`,
.text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`,
{ align: 'center' });
doc.fontSize(14)
@ -609,14 +612,14 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
}
} catch (imageError: any) {
console.error(`[expenses/generate-pdf] Error processing receipt ${receiptIndex + 1} for expense ${expense.Id}:`, imageError);
console.error(`[expenses/generate-pdf] Error processing receipt ${currentReceiptNumber} 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}` : ''}`,
.text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`,
{ align: 'center' });
doc.fontSize(14)
@ -637,7 +640,7 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) {
doc.fillColor('#000000');
}
} else {
console.log(`[expenses/generate-pdf] Skipping receipt ${receiptIndex + 1} for expense ${expense.Id} - no valid file path`);
console.log(`[expenses/generate-pdf] Skipping receipt ${currentReceiptNumber} for expense ${expense.Id} - no valid file path`);
}
}
}
@ -655,48 +658,60 @@ async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {
const client = getMinioClient();
const bucketName = useRuntimeConfig().minio.bucketName;
// Determine the file path - try multiple possible locations
let filePath = null;
// Determine the file path - try multiple possible sources
let rawPath = null;
// Try different receipt data structures
if (receipt.url) {
filePath = receipt.url;
// Try different receipt data structures - prioritize signedUrl for S3 URLs
if (receipt.signedUrl) {
rawPath = receipt.signedUrl;
} else if (receipt.url) {
rawPath = receipt.url;
} else if (receipt.directus_files_id?.filename_download) {
filePath = receipt.directus_files_id.filename_download;
rawPath = receipt.directus_files_id.filename_download;
} else if (receipt.filename_download) {
filePath = receipt.filename_download;
rawPath = receipt.filename_download;
} else if (receipt.id && receipt.filename_disk) {
filePath = receipt.filename_disk;
rawPath = receipt.filename_disk;
} else if (typeof receipt === 'string') {
filePath = receipt;
rawPath = receipt;
}
if (!filePath) {
if (!rawPath) {
console.log('[expenses/generate-pdf] No file path found for receipt:', JSON.stringify(receipt, null, 2));
return null;
}
console.log('[expenses/generate-pdf] Fetching receipt image from path:', filePath);
console.log('[expenses/generate-pdf] Raw path from receipt:', rawPath);
// Remove any URL prefixes if present
if (filePath.includes('/files/')) {
const parts = filePath.split('/files/');
if (parts.length > 1) {
filePath = parts[parts.length - 1];
}
// Extract MinIO path from S3 URL or use as-is if it's already a path
let minioPath = extractMinioPath(rawPath);
if (!minioPath) {
console.log('[expenses/generate-pdf] Could not extract MinIO path from:', rawPath);
return null;
}
// Ensure we're looking in the right place - sometimes files are in receipts/ folder
console.log('[expenses/generate-pdf] Extracted MinIO path:', minioPath);
// Try multiple possible locations in MinIO
const possiblePaths = [
filePath,
`receipts/${filePath}`,
`expenses/${filePath}`,
filePath.startsWith('receipts/') ? filePath : `receipts/${filePath}`
minioPath,
`receipts/${minioPath}`,
`expenses/${minioPath}`,
// Try without any folder prefix
minioPath.split('/').pop() || minioPath,
// Try in receipts folder with just filename
`receipts/${minioPath.split('/').pop() || minioPath}`,
// Try in expenses folder with just filename
`expenses/${minioPath.split('/').pop() || minioPath}`
];
for (const testPath of possiblePaths) {
// Remove duplicates
const uniquePaths = [...new Set(possiblePaths)];
for (const testPath of uniquePaths) {
try {
console.log('[expenses/generate-pdf] Trying path:', testPath);
console.log('[expenses/generate-pdf] Trying MinIO path:', testPath);
// Check if object exists first
await client.statObject(bucketName, testPath);
@ -713,16 +728,16 @@ async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {
dataStream.on('error', reject);
});
console.log('[expenses/generate-pdf] Successfully fetched image from:', testPath, 'Size:', imageBuffer.length);
console.log('[expenses/generate-pdf] Successfully fetched image from MinIO path:', testPath, 'Size:', imageBuffer.length);
return imageBuffer;
} catch (pathError) {
console.log('[expenses/generate-pdf] Path not found:', testPath);
console.log('[expenses/generate-pdf] MinIO path not found:', testPath);
continue;
}
}
console.log('[expenses/generate-pdf] Could not find image in any of the attempted paths');
console.log('[expenses/generate-pdf] Could not find image in any of the attempted MinIO paths:', uniquePaths);
return null;
} catch (error) {
@ -731,6 +746,79 @@ async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {
}
}
/**
* Extract the MinIO path from an S3 URL or return the path as-is
*/
function extractMinioPath(urlOrPath: string): string | null {
try {
// If it's already just a path, return it
if (!urlOrPath.startsWith('http')) {
return urlOrPath;
}
// Parse the URL
const url = new URL(urlOrPath);
// Extract the pathname (removes query parameters)
let pathname = decodeURIComponent(url.pathname);
console.log('[expenses/generate-pdf] URL pathname:', pathname);
// For S3 URLs, we need to extract the part after the bucket name
// Pattern: /database/nc/uploads/path/to/file.jpg
// We want: uploads/path/to/file.jpg
// Remove leading slash
if (pathname.startsWith('/')) {
pathname = pathname.substring(1);
}
// Look for common patterns
if (pathname.includes('/uploads/')) {
// Extract everything from 'uploads/' onwards
const uploadsIndex = pathname.indexOf('uploads/');
const extractedPath = pathname.substring(uploadsIndex);
console.log('[expenses/generate-pdf] Extracted path from uploads pattern:', extractedPath);
return extractedPath;
}
if (pathname.includes('/nc/uploads/')) {
// Extract everything from 'uploads/' onwards
const uploadsIndex = pathname.indexOf('uploads/');
const extractedPath = pathname.substring(uploadsIndex);
console.log('[expenses/generate-pdf] Extracted path from nc/uploads pattern:', extractedPath);
return extractedPath;
}
if (pathname.includes('/database/')) {
// Remove the database prefix
const databaseIndex = pathname.indexOf('database/');
const withoutDatabase = pathname.substring(databaseIndex + 'database/'.length);
console.log('[expenses/generate-pdf] Extracted path after removing database prefix:', withoutDatabase);
return withoutDatabase;
}
// If no specific pattern found, return the pathname as-is
console.log('[expenses/generate-pdf] Using pathname as-is:', pathname);
return pathname;
} catch (error) {
console.error('[expenses/generate-pdf] Error parsing URL:', error);
// If URL parsing fails, try to extract manually
// Remove query parameters manually
const withoutQuery = urlOrPath.split('?')[0];
// Look for uploads pattern
if (withoutQuery.includes('/uploads/')) {
const uploadsIndex = withoutQuery.indexOf('/uploads/');
return withoutQuery.substring(uploadsIndex + 1); // +1 to remove the leading slash
}
return null;
}
}
function addFooter(doc: PDFKit.PDFDocument) {
doc.fontSize(10)
.fillColor('#666666')