Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m21s
158 lines
5.0 KiB
TypeScript
158 lines
5.0 KiB
TypeScript
// src/routes/budget.ts
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import passport from './passport.routes';
|
|
import { budgetRepo } from '../services/db/index.db';
|
|
import type { UserProfile } from '../types';
|
|
import { validateRequest } from '../middleware/validation.middleware';
|
|
import { requiredString, numericIdParam } from '../utils/zodUtils';
|
|
|
|
const router = express.Router();
|
|
|
|
// --- Zod Schemas for Budget Routes (as per ADR-003) ---
|
|
const budgetIdParamSchema = numericIdParam('id', "Invalid ID for parameter 'id'. Must be a number.");
|
|
|
|
const createBudgetSchema = z.object({
|
|
body: z.object({
|
|
name: requiredString('Budget name is required.'),
|
|
amount_cents: z.number().int().positive('Amount must be a positive integer.'),
|
|
period: z.enum(['weekly', 'monthly']),
|
|
start_date: z.string().date('Start date must be a valid date in YYYY-MM-DD format.'),
|
|
}),
|
|
});
|
|
|
|
const updateBudgetSchema = budgetIdParamSchema.extend({
|
|
body: createBudgetSchema.shape.body.partial().refine((data) => Object.keys(data).length > 0, {
|
|
message: 'At least one field to update must be provided.',
|
|
}),
|
|
});
|
|
|
|
const spendingAnalysisSchema = z.object({
|
|
query: z.object({
|
|
startDate: z.string().date('startDate must be a valid date in YYYY-MM-DD format.'),
|
|
endDate: z.string().date('endDate must be a valid date in YYYY-MM-DD format.'),
|
|
}),
|
|
});
|
|
|
|
// Middleware to ensure user is authenticated for all budget routes
|
|
router.use(passport.authenticate('jwt', { session: false }));
|
|
|
|
/**
|
|
* GET /api/budgets - Get all budgets for the authenticated user.
|
|
*/
|
|
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
|
const userProfile = req.user as UserProfile;
|
|
try {
|
|
const budgets = await budgetRepo.getBudgetsForUser(userProfile.user.user_id, req.log);
|
|
res.json(budgets);
|
|
} catch (error) {
|
|
req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching budgets');
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/budgets - Create a new budget for the authenticated user.
|
|
*/
|
|
router.post(
|
|
'/',
|
|
validateRequest(createBudgetSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const userProfile = req.user as UserProfile;
|
|
type CreateBudgetRequest = z.infer<typeof createBudgetSchema>;
|
|
const { body } = req as unknown as CreateBudgetRequest;
|
|
try {
|
|
const newBudget = await budgetRepo.createBudget(userProfile.user.user_id, body, req.log);
|
|
res.status(201).json(newBudget);
|
|
} catch (error: unknown) {
|
|
req.log.error({ error, userId: userProfile.user.user_id, body }, 'Error creating budget');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* PUT /api/budgets/:id - Update an existing budget.
|
|
*/
|
|
router.put(
|
|
'/:id',
|
|
validateRequest(updateBudgetSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const userProfile = req.user as UserProfile;
|
|
type UpdateBudgetRequest = z.infer<typeof updateBudgetSchema>;
|
|
const { params, body } = req as unknown as UpdateBudgetRequest;
|
|
try {
|
|
const updatedBudget = await budgetRepo.updateBudget(
|
|
params.id,
|
|
userProfile.user.user_id,
|
|
body,
|
|
req.log,
|
|
);
|
|
res.json(updatedBudget);
|
|
} catch (error: unknown) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, budgetId: params.id },
|
|
'Error updating budget',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* DELETE /api/budgets/:id - Delete a budget.
|
|
*/
|
|
router.delete(
|
|
'/:id',
|
|
validateRequest(budgetIdParamSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const userProfile = req.user as UserProfile;
|
|
type DeleteBudgetRequest = z.infer<typeof budgetIdParamSchema>;
|
|
const { params } = req as unknown as DeleteBudgetRequest;
|
|
try {
|
|
await budgetRepo.deleteBudget(params.id, userProfile.user.user_id, req.log);
|
|
res.status(204).send(); // No Content
|
|
} catch (error: unknown) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, budgetId: params.id },
|
|
'Error deleting budget',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* GET /api/spending-analysis - Get spending breakdown by category for a date range.
|
|
* Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD)
|
|
*/
|
|
router.get(
|
|
'/spending-analysis',
|
|
validateRequest(spendingAnalysisSchema),
|
|
async (req: Request, res: Response, next: NextFunction) => {
|
|
const userProfile = req.user as UserProfile;
|
|
type SpendingAnalysisRequest = z.infer<typeof spendingAnalysisSchema>;
|
|
const {
|
|
query: { startDate, endDate },
|
|
} = req as unknown as SpendingAnalysisRequest;
|
|
|
|
try {
|
|
const spendingData = await budgetRepo.getSpendingByCategory(
|
|
userProfile.user.user_id,
|
|
startDate,
|
|
endDate,
|
|
req.log,
|
|
);
|
|
res.json(spendingData);
|
|
} catch (error) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, startDate, endDate },
|
|
'Error fetching spending analysis',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
export default router;
|