Files
flyer-crawler.projectium.com/src/routes/budget.routes.ts
Torben Sorensen 11aeac5edd
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m10s
whoa - so much - new features (UPC,etc) - Sentry for app logging! so much more !
2026-01-11 19:07:02 -08:00

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;