Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m10s
329 lines
9.6 KiB
TypeScript
329 lines
9.6 KiB
TypeScript
// src/routes/budget.ts
|
|
import express, { Request, Response, NextFunction } from 'express';
|
|
import { z } from 'zod';
|
|
import passport from '../config/passport';
|
|
import { budgetRepo } from '../services/db/index.db';
|
|
import type { UserProfile } from '../types';
|
|
import { validateRequest } from '../middleware/validation.middleware';
|
|
import { requiredString, numericIdParam } from '../utils/zodUtils';
|
|
import { budgetUpdateLimiter } from '../config/rateLimiters';
|
|
import { sendSuccess, sendNoContent } from '../utils/apiResponse';
|
|
|
|
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 }));
|
|
|
|
// Apply rate limiting to all subsequent budget routes
|
|
router.use(budgetUpdateLimiter);
|
|
|
|
/**
|
|
* @openapi
|
|
* /budgets:
|
|
* get:
|
|
* tags: [Budgets]
|
|
* summary: Get all budgets
|
|
* description: Retrieve all budgets for the authenticated user.
|
|
* security:
|
|
* - bearerAuth: []
|
|
* responses:
|
|
* 200:
|
|
* description: List of user budgets
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/SuccessResponse'
|
|
* 401:
|
|
* description: Unauthorized - invalid or missing token
|
|
*/
|
|
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);
|
|
sendSuccess(res, budgets);
|
|
} catch (error) {
|
|
req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching budgets');
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @openapi
|
|
* /budgets:
|
|
* post:
|
|
* tags: [Budgets]
|
|
* summary: Create budget
|
|
* description: Create a new budget for the authenticated user.
|
|
* security:
|
|
* - bearerAuth: []
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* required:
|
|
* - name
|
|
* - amount_cents
|
|
* - period
|
|
* - start_date
|
|
* properties:
|
|
* name:
|
|
* type: string
|
|
* description: Budget name
|
|
* amount_cents:
|
|
* type: integer
|
|
* minimum: 1
|
|
* description: Budget amount in cents
|
|
* period:
|
|
* type: string
|
|
* enum: [weekly, monthly]
|
|
* description: Budget period
|
|
* start_date:
|
|
* type: string
|
|
* format: date
|
|
* description: Budget start date (YYYY-MM-DD)
|
|
* responses:
|
|
* 201:
|
|
* description: Budget created
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/SuccessResponse'
|
|
* 400:
|
|
* description: Validation error
|
|
* 401:
|
|
* description: Unauthorized - invalid or missing token
|
|
*/
|
|
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);
|
|
sendSuccess(res, newBudget, 201);
|
|
} catch (error: unknown) {
|
|
req.log.error({ error, userId: userProfile.user.user_id, body }, 'Error creating budget');
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /budgets/{id}:
|
|
* put:
|
|
* tags: [Budgets]
|
|
* summary: Update budget
|
|
* description: Update an existing budget.
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Budget ID
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* name:
|
|
* type: string
|
|
* description: Budget name
|
|
* amount_cents:
|
|
* type: integer
|
|
* minimum: 1
|
|
* description: Budget amount in cents
|
|
* period:
|
|
* type: string
|
|
* enum: [weekly, monthly]
|
|
* description: Budget period
|
|
* start_date:
|
|
* type: string
|
|
* format: date
|
|
* description: Budget start date (YYYY-MM-DD)
|
|
* responses:
|
|
* 200:
|
|
* description: Budget updated
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/SuccessResponse'
|
|
* 400:
|
|
* description: Validation error - at least one field required
|
|
* 401:
|
|
* description: Unauthorized - invalid or missing token
|
|
* 404:
|
|
* description: Budget not found
|
|
*/
|
|
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,
|
|
);
|
|
sendSuccess(res, updatedBudget);
|
|
} catch (error: unknown) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, budgetId: params.id },
|
|
'Error updating budget',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /budgets/{id}:
|
|
* delete:
|
|
* tags: [Budgets]
|
|
* summary: Delete budget
|
|
* description: Delete a budget by ID.
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: id
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Budget ID
|
|
* responses:
|
|
* 204:
|
|
* description: Budget deleted
|
|
* 401:
|
|
* description: Unauthorized - invalid or missing token
|
|
* 404:
|
|
* description: Budget not found
|
|
*/
|
|
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);
|
|
sendNoContent(res);
|
|
} catch (error: unknown) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, budgetId: params.id },
|
|
'Error deleting budget',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
/**
|
|
* @openapi
|
|
* /budgets/spending-analysis:
|
|
* get:
|
|
* tags: [Budgets]
|
|
* summary: Get spending analysis
|
|
* description: Get spending breakdown by category for a date range.
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: query
|
|
* name: startDate
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* format: date
|
|
* description: Start date (YYYY-MM-DD)
|
|
* - in: query
|
|
* name: endDate
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* format: date
|
|
* description: End date (YYYY-MM-DD)
|
|
* responses:
|
|
* 200:
|
|
* description: Spending breakdown by category
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/SuccessResponse'
|
|
* 400:
|
|
* description: Invalid date format
|
|
* 401:
|
|
* description: Unauthorized - invalid or missing token
|
|
*/
|
|
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,
|
|
);
|
|
sendSuccess(res, spendingData);
|
|
} catch (error) {
|
|
req.log.error(
|
|
{ error, userId: userProfile.user.user_id, startDate, endDate },
|
|
'Error fetching spending analysis',
|
|
);
|
|
next(error);
|
|
}
|
|
},
|
|
);
|
|
|
|
export default router;
|