diff --git a/middleware/expenseValidator.js b/middleware/expenseValidator.js new file mode 100644 index 00000000..30501768 --- /dev/null +++ b/middleware/expenseValidator.js @@ -0,0 +1,31 @@ +const Joi = require('joi'); +const currencyService = require('../services/currencyService'); + +const expenseSchema = Joi.object({ + description: Joi.string().trim().max(100).required(), + amount: Joi.number().min(0.01).required(), + currency: Joi.string().uppercase().optional(), + category: Joi.string().valid('food', 'transport', 'entertainment', 'utilities', 'healthcare', 'shopping', 'other').required(), + type: Joi.string().valid('income', 'expense').required(), + merchant: Joi.string().trim().max(50).optional(), + date: Joi.date().optional(), + workspaceId: Joi.string().hex().length(24).optional() +}); + +const expenseValidator = async (req, res, next) => { + const { error, value } = expenseSchema.validate(req.body); + if (error) return res.status(400).json({ error: error.details[0].message }); + + const user = req.user; // Assuming auth middleware sets req.user + const expenseCurrency = value.currency || user.preferredCurrency; + + if (!currencyService.isValidCurrency(expenseCurrency)) { + return res.status(400).json({ error: 'Invalid currency code' }); + } + + req.validatedExpense = value; + req.expenseCurrency = expenseCurrency; + next(); +}; + +module.exports = expenseValidator; diff --git a/routes/expenseCreation.js b/routes/expenseCreation.js new file mode 100644 index 00000000..f2120b7f --- /dev/null +++ b/routes/expenseCreation.js @@ -0,0 +1,64 @@ +const express = require('express'); +const Expense = require('../models/Expense'); +const User = require('../models/User'); +const expenseValidator = require('../middleware/expenseValidator'); +const auth = require('../middleware/auth'); +const expenseService = require('../services/expenseService'); +const { convertExpenseAmount } = require('../utils/currencyUtils'); + +const router = express.Router(); + +// POST new expense for authenticated user +router.post('/', auth, expenseValidator, async (req, res) => { + try { + const user = await User.findById(req.user._id); + const { validatedExpense: value, expenseCurrency } = req; + + // Handle auto-categorization + const { finalCategory, autoCategorized } = await expenseService.handleAutoCategorization(value, req.user._id); + + // Store original amount and currency + const expenseData = { + ...value, + category: finalCategory, + user: value.workspaceId ? req.user._id : req.user._id, + addedBy: req.user._id, + workspace: value.workspaceId || null, + originalAmount: value.amount, + originalCurrency: expenseCurrency, + amount: value.amount, + autoCategorized + }; + + // Handle currency conversion if needed + const conversionData = await expenseService.handleCurrencyConversion(value.amount, expenseCurrency, user.preferredCurrency); + Object.assign(expenseData, conversionData); + + const expense = new Expense(expenseData); + await expense.save(); + + // Handle approval submission + const { requiresApproval, workflow } = await expenseService.handleApprovalSubmission(expense, req.user._id); + + // Handle budget update + const amountForBudget = expenseData.convertedAmount || value.amount; + await expenseService.handleBudgetUpdate(req.user._id, value.type, amountForBudget, value.category); + + // Emit real-time update + const io = req.app.get('io'); + const expenseForSocket = expenseService.prepareExpenseResponse(expense, user.preferredCurrency); + expenseService.emitRealTimeUpdate(io, req.user._id, 'expense_created', expenseForSocket); + + const response = { + ...expenseService.prepareExpenseResponse(expense, user.preferredCurrency), + requiresApproval, + workflow: workflow ? { _id: workflow._id, status: workflow.status } : null + }; + + res.status(201).json(response); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/routes/expenseExport.js b/routes/expenseExport.js new file mode 100644 index 00000000..833dc831 --- /dev/null +++ b/routes/expenseExport.js @@ -0,0 +1,43 @@ +const express = require('express'); +const exportService = require('../services/exportService'); +const auth = require('../middleware/auth'); + +const router = express.Router(); + +// GET export expenses to CSV +router.get('/export', auth, async (req, res) => { + try { + const { format, startDate, endDate, category } = req.query; + + // Validate format + if (format && format !== 'csv') { + return res.status(400).json({ error: 'Only CSV format is supported' }); + } + + // Get expenses using export service + const expenses = await exportService.getExpensesForExport(req.user._id, { + startDate, + endDate, + category, + type: 'all' // Include both income and expenses + }); + + if (expenses.length === 0) { + return res.status(404).json({ error: 'No expenses found for the selected filters' }); + } + + // Generate CSV using ExportService + const csv = exportService.generateCSV(expenses); + + // Set CSV headers + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename="expenses.csv"'); + + res.send(csv); + } catch (error) { + console.error('Export error:', error); + res.status(500).json({ error: 'Failed to export expenses' }); + } +}); + +module.exports = router; diff --git a/routes/expenseUpdate.js b/routes/expenseUpdate.js new file mode 100644 index 00000000..5671efd3 --- /dev/null +++ b/routes/expenseUpdate.js @@ -0,0 +1,51 @@ +const express = require('express'); +const Expense = require('../models/Expense'); +const User = require('../models/User'); +const expenseValidator = require('../middleware/expenseValidator'); +const auth = require('../middleware/auth'); +const expenseService = require('../services/expenseService'); + +const router = express.Router(); + +// PUT update expense for authenticated user +router.put('/:id', auth, expenseValidator, async (req, res) => { + try { + const user = await User.findById(req.user._id); + const { validatedExpense: value, expenseCurrency } = req; + + // Prepare update data + const updateData = { + ...value, + originalAmount: value.amount, + originalCurrency: expenseCurrency, + amount: value.amount + }; + + // Handle currency conversion if needed + const conversionData = await expenseService.handleCurrencyConversion(value.amount, expenseCurrency, user.preferredCurrency); + Object.assign(updateData, conversionData); + + const expense = await Expense.findOneAndUpdate( + { _id: req.params.id, user: req.user._id }, + updateData, + { new: true } + ); + if (!expense) return res.status(404).json({ error: 'Expense not found' }); + + // Handle budget update + await expenseService.handleBudgetUpdate(req.user._id, value.type, updateData.convertedAmount || value.amount, value.category); + + // Emit real-time update + const io = req.app.get('io'); + const expenseForSocket = expenseService.prepareExpenseResponse(expense, user.preferredCurrency); + expenseService.emitRealTimeUpdate(io, req.user._id, 'expense_updated', expenseForSocket); + + const response = expenseService.prepareExpenseResponse(expense, user.preferredCurrency); + + res.json(response); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server.js b/server.js index e23e30f2..60dc9224 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,9 @@ require('dotenv').config(); const authRoutes = require('./routes/auth'); const expenseRoutes = require('./routes/expenses'); +const expenseCreationRoutes = require('./routes/expenseCreation'); +const expenseUpdateRoutes = require('./routes/expenseUpdate'); +const expenseExportRoutes = require('./routes/expenseExport'); const syncRoutes = require('./routes/sync'); const splitsRoutes = require('./routes/splits'); const groupsRoutes = require('./routes/groups'); diff --git a/utils/currencyUtils.js b/utils/currencyUtils.js new file mode 100644 index 00000000..9c0caca4 --- /dev/null +++ b/utils/currencyUtils.js @@ -0,0 +1,32 @@ +const currencyService = require('../services/currencyService'); + +const convertExpenseAmount = async (amount, fromCurrency, toCurrency) => { + if (fromCurrency === toCurrency) { + return { convertedAmount: amount, exchangeRate: 1 }; + } + try { + const conversion = await currencyService.convertCurrency(amount, fromCurrency, toCurrency); + return { convertedAmount: conversion.convertedAmount, exchangeRate: conversion.exchangeRate }; + } catch (error) { + console.error('Currency conversion failed:', error.message); + return null; // Or throw error, but for now return null to handle gracefully + } +}; + +const prepareExpenseWithDisplayAmounts = (expense, userPreferredCurrency) => { + const expenseObj = expense.toObject ? expense.toObject() : expense; + if (expenseObj.originalCurrency !== userPreferredCurrency) { + // Assuming convertedAmount is already set if conversion happened + expenseObj.displayAmount = expenseObj.convertedAmount || expenseObj.amount; + expenseObj.displayCurrency = userPreferredCurrency; + } else { + expenseObj.displayAmount = expenseObj.amount; + expenseObj.displayCurrency = expenseObj.originalCurrency; + } + return expenseObj; +}; + +module.exports = { + convertExpenseAmount, + prepareExpenseWithDisplayAmounts +};