Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
234 lines
7.9 KiB
TypeScript
234 lines
7.9 KiB
TypeScript
// src/controllers/budget.controller.ts
|
|
// ============================================================================
|
|
// BUDGET CONTROLLER
|
|
// ============================================================================
|
|
// Provides endpoints for managing user budgets, including CRUD operations
|
|
// and spending analysis. All endpoints require authentication.
|
|
//
|
|
// Implements ADR-028 (API Response Format) via BaseController.
|
|
// ============================================================================
|
|
|
|
import {
|
|
Get,
|
|
Post,
|
|
Put,
|
|
Delete,
|
|
Route,
|
|
Tags,
|
|
Security,
|
|
Body,
|
|
Path,
|
|
Query,
|
|
Request,
|
|
SuccessResponse,
|
|
Response,
|
|
} from 'tsoa';
|
|
import type { Request as ExpressRequest } from 'express';
|
|
import { BaseController } from './base.controller';
|
|
import type { SuccessResponse as SuccessResponseType, ErrorResponse } from './types';
|
|
import { budgetRepo } from '../services/db/index.db';
|
|
import type { Budget, SpendingByCategory, UserProfile } from '../types';
|
|
|
|
// ============================================================================
|
|
// REQUEST/RESPONSE TYPES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Request body for creating a new budget.
|
|
*/
|
|
interface CreateBudgetRequest {
|
|
/** Budget name */
|
|
name: string;
|
|
/** Budget amount in cents (must be positive) */
|
|
amount_cents: number;
|
|
/** Budget period - weekly or monthly */
|
|
period: 'weekly' | 'monthly';
|
|
/** Budget start date in YYYY-MM-DD format */
|
|
start_date: string;
|
|
}
|
|
|
|
/**
|
|
* Request body for updating a budget.
|
|
* All fields are optional, but at least one must be provided.
|
|
*/
|
|
interface UpdateBudgetRequest {
|
|
/** Budget name */
|
|
name?: string;
|
|
/** Budget amount in cents (must be positive) */
|
|
amount_cents?: number;
|
|
/** Budget period - weekly or monthly */
|
|
period?: 'weekly' | 'monthly';
|
|
/** Budget start date in YYYY-MM-DD format */
|
|
start_date?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// BUDGET CONTROLLER
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Controller for managing user budgets.
|
|
*
|
|
* All endpoints require JWT authentication. Users can only access
|
|
* their own budgets - the user ID is extracted from the JWT token.
|
|
*/
|
|
@Route('budgets')
|
|
@Tags('Budgets')
|
|
@Security('bearerAuth')
|
|
export class BudgetController extends BaseController {
|
|
// ==========================================================================
|
|
// LIST BUDGETS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get all budgets for the authenticated user.
|
|
*
|
|
* Returns a list of all budgets owned by the authenticated user,
|
|
* ordered by start date descending (newest first).
|
|
*
|
|
* @summary Get all budgets
|
|
* @param request Express request with authenticated user
|
|
* @returns List of user budgets
|
|
*/
|
|
@Get()
|
|
@SuccessResponse(200, 'List of user budgets')
|
|
@Response<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
|
|
public async getBudgets(
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<Budget[]>> {
|
|
const userProfile = request.user as UserProfile;
|
|
const budgets = await budgetRepo.getBudgetsForUser(userProfile.user.user_id, request.log);
|
|
return this.success(budgets);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// CREATE BUDGET
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Create a new budget for the authenticated user.
|
|
*
|
|
* Creates a budget with the specified name, amount, period, and start date.
|
|
* The budget is automatically associated with the authenticated user.
|
|
*
|
|
* @summary Create budget
|
|
* @param request Express request with authenticated user
|
|
* @param body Budget creation data
|
|
* @returns The newly created budget
|
|
*/
|
|
@Post()
|
|
@SuccessResponse(201, 'Budget created')
|
|
@Response<ErrorResponse>(400, 'Validation error')
|
|
@Response<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
|
|
public async createBudget(
|
|
@Request() request: ExpressRequest,
|
|
@Body() body: CreateBudgetRequest,
|
|
): Promise<SuccessResponseType<Budget>> {
|
|
const userProfile = request.user as UserProfile;
|
|
const newBudget = await budgetRepo.createBudget(userProfile.user.user_id, body, request.log);
|
|
return this.created(newBudget);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// UPDATE BUDGET
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Update an existing budget.
|
|
*
|
|
* Updates the specified budget with the provided fields. At least one
|
|
* field must be provided. The user must own the budget to update it.
|
|
*
|
|
* @summary Update budget
|
|
* @param id Budget ID
|
|
* @param request Express request with authenticated user
|
|
* @param body Fields to update
|
|
* @returns The updated budget
|
|
*/
|
|
@Put('{id}')
|
|
@SuccessResponse(200, 'Budget updated')
|
|
@Response<ErrorResponse>(400, 'Validation error - at least one field required')
|
|
@Response<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
|
|
@Response<ErrorResponse>(404, 'Budget not found')
|
|
public async updateBudget(
|
|
@Path() id: number,
|
|
@Request() request: ExpressRequest,
|
|
@Body() body: UpdateBudgetRequest,
|
|
): Promise<SuccessResponseType<Budget>> {
|
|
const userProfile = request.user as UserProfile;
|
|
|
|
// Validate at least one field is provided
|
|
if (Object.keys(body).length === 0) {
|
|
this.setStatus(400);
|
|
throw new Error('At least one field to update must be provided.');
|
|
}
|
|
|
|
const updatedBudget = await budgetRepo.updateBudget(
|
|
id,
|
|
userProfile.user.user_id,
|
|
body,
|
|
request.log,
|
|
);
|
|
return this.success(updatedBudget);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// DELETE BUDGET
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Delete a budget.
|
|
*
|
|
* Permanently deletes the specified budget. The user must own
|
|
* the budget to delete it.
|
|
*
|
|
* @summary Delete budget
|
|
* @param id Budget ID
|
|
* @param request Express request with authenticated user
|
|
*/
|
|
@Delete('{id}')
|
|
@SuccessResponse(204, 'Budget deleted')
|
|
@Response<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
|
|
@Response<ErrorResponse>(404, 'Budget not found')
|
|
public async deleteBudget(@Path() id: number, @Request() request: ExpressRequest): Promise<void> {
|
|
const userProfile = request.user as UserProfile;
|
|
await budgetRepo.deleteBudget(id, userProfile.user.user_id, request.log);
|
|
return this.noContent();
|
|
}
|
|
|
|
// ==========================================================================
|
|
// SPENDING ANALYSIS
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Get spending analysis by category.
|
|
*
|
|
* Returns a breakdown of spending by category for the specified date range.
|
|
* This helps users understand their spending patterns relative to their budgets.
|
|
*
|
|
* @summary Get spending analysis
|
|
* @param startDate Start date in YYYY-MM-DD format
|
|
* @param endDate End date in YYYY-MM-DD format
|
|
* @param request Express request with authenticated user
|
|
* @returns Spending breakdown by category
|
|
*/
|
|
@Get('spending-analysis')
|
|
@SuccessResponse(200, 'Spending breakdown by category')
|
|
@Response<ErrorResponse>(400, 'Invalid date format')
|
|
@Response<ErrorResponse>(401, 'Unauthorized - invalid or missing token')
|
|
public async getSpendingAnalysis(
|
|
@Query() startDate: string,
|
|
@Query() endDate: string,
|
|
@Request() request: ExpressRequest,
|
|
): Promise<SuccessResponseType<SpendingByCategory[]>> {
|
|
const userProfile = request.user as UserProfile;
|
|
const spendingData = await budgetRepo.getSpendingByCategory(
|
|
userProfile.user.user_id,
|
|
startDate,
|
|
endDate,
|
|
request.log,
|
|
);
|
|
return this.success(spendingData);
|
|
}
|
|
}
|