217 lines
7.3 KiB
TypeScript
217 lines
7.3 KiB
TypeScript
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<T>(
|
|
operation: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
baseDelay: number = 1000
|
|
): Promise<T> {
|
|
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.'
|
|
});
|
|
}
|
|
});
|