import { requireSalesOrAdmin } from '@/server/utils/auth'; import { getExpenses, getCurrentMonthExpenses } from '@/server/utils/nocodb'; import { processExpenseWithCurrency } from '@/server/utils/currency'; import type { ExpenseFilters } from '@/utils/types'; // Retry operation wrapper for database calls async function retryOperation( operation: () => Promise, maxRetries: number = 3, baseDelay: number = 1000 ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error: any) { console.log(`[get-expenses] Attempt ${attempt}/${maxRetries} failed:`, error.message); // Don't retry on authentication/authorization errors if (error.statusCode === 401 || error.statusCode === 403) { throw error; } // Don't retry on client errors (4xx except 404) if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 404) { throw error; } // If this is the last attempt, throw the error if (attempt === maxRetries) { throw error; } // For retryable errors (5xx, network errors, timeouts), wait before retry const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff console.log(`[get-expenses] Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error('Retry operation failed unexpectedly'); } export default defineEventHandler(async (event) => { console.log('[get-expenses] API called with query:', getQuery(event)); try { // Set proper headers setHeader(event, 'Cache-Control', 'no-cache'); setHeader(event, 'Content-Type', 'application/json'); // Check authentication first try { await requireSalesOrAdmin(event); console.log('[get-expenses] Authentication successful'); } catch (authError: any) { console.error('[get-expenses] Authentication failed:', authError); // Return proper error status if (authError.statusCode === 403) { throw createError({ statusCode: 403, statusMessage: 'Access denied. You need sales or admin role to view expenses.' }); } // Re-throw other auth errors throw authError; } const query = getQuery(event); // If no date filters provided, default to current month if (!query.startDate && !query.endDate) { console.log('[get-expenses] No date filters provided, defaulting to current month'); try { const result = await retryOperation(() => getCurrentMonthExpenses()); // Process expenses with currency conversion const processedExpenses = await Promise.all( result.list.map((expense: any) => processExpenseWithCurrency(expense)) ); return { ...result, list: processedExpenses }; } catch (dbError: any) { console.error('[get-expenses] Database error (current month):', dbError); if (dbError.statusCode === 403) { throw createError({ statusCode: 503, statusMessage: 'Expense database is currently unavailable. Please contact your administrator or try again later.' }); } if (dbError.statusCode === 404) { throw createError({ statusCode: 404, statusMessage: 'No expense records found for the current month.' }); } throw createError({ statusCode: 500, statusMessage: 'Unable to fetch expense data. Please try again later.' }); } } // Build filters from query parameters const filters: ExpenseFilters = {}; if (query.startDate && typeof query.startDate === 'string') { filters.startDate = query.startDate; } if (query.endDate && typeof query.endDate === 'string') { filters.endDate = query.endDate; } if (query.payer && typeof query.payer === 'string') { filters.payer = query.payer; } if (query.category && typeof query.category === 'string') { filters.category = query.category as any; // Cast to ExpenseCategory } console.log('[get-expenses] Fetching expenses with filters:', filters); try { const result = await retryOperation(() => getExpenses(filters)); // Process expenses with currency conversion const processedExpenses = await Promise.all( result.list.map((expense: any) => processExpenseWithCurrency(expense)) ); // Add formatted dates const transformedExpenses = processedExpenses.map(expense => ({ ...expense, FormattedDate: new Date(expense.Time).toLocaleDateString(), FormattedTime: new Date(expense.Time).toLocaleTimeString() })); // Calculate summary with USD totals const usdTotal = transformedExpenses.reduce((sum, e) => sum + (e.PriceUSD || e.PriceNumber || 0), 0); const originalTotal = transformedExpenses.reduce((sum, e) => sum + (e.PriceNumber || 0), 0); return { expenses: transformedExpenses, PageInfo: result.PageInfo, totalCount: result.PageInfo?.totalRows || transformedExpenses.length, summary: { total: originalTotal, // Original currency total (mixed currencies) totalUSD: usdTotal, // USD converted total count: transformedExpenses.length, uniquePayers: [...new Set(transformedExpenses.map(e => e.Payer))].length, currencies: [...new Set(transformedExpenses.map(e => e.currency))].filter(Boolean) } }; } catch (dbError: any) { console.error('[get-expenses] Database error (filtered):', dbError); if (dbError.statusCode === 403) { throw createError({ statusCode: 503, statusMessage: 'Expense database is currently unavailable. Please contact your administrator or try again later.' }); } if (dbError.statusCode === 404) { throw createError({ statusCode: 404, statusMessage: 'No expense records found matching the specified criteria.' }); } throw createError({ statusCode: 500, statusMessage: 'Unable to fetch expense data. Please try again later.' }); } } catch (error: any) { 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({ statusCode: 401, statusMessage: 'Authentication required. Please log in again.' }); } // 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.' }); } });