All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m30s
123 lines
5.0 KiB
TypeScript
123 lines
5.0 KiB
TypeScript
// src/services/db/budget.db.ts
|
|
import type { Pool, PoolClient } from 'pg';
|
|
import { getPool, withTransaction } from './connection.db';
|
|
import { ForeignKeyConstraintError, NotFoundError } from './errors.db';
|
|
import { logger } from '../logger.server';
|
|
import { Budget, SpendingByCategory } from '../../types';
|
|
|
|
export class BudgetRepository {
|
|
private db: Pool | PoolClient;
|
|
|
|
constructor(db: Pool | PoolClient = getPool()) {
|
|
this.db = db;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all budgets for a specific user.
|
|
* @param userId The UUID of the user.
|
|
* @returns A promise that resolves to an array of Budget objects.
|
|
*/
|
|
async getBudgetsForUser(userId: string): Promise<Budget[]> {
|
|
try {
|
|
const res = await this.db.query<Budget>(
|
|
'SELECT * FROM public.budgets WHERE user_id = $1 ORDER BY start_date DESC',
|
|
[userId]
|
|
);
|
|
return res.rows;
|
|
} catch (error) {
|
|
logger.error('Database error in getBudgetsForUser:', { error, userId });
|
|
throw new Error('Failed to retrieve budgets.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new budget for a user.
|
|
* @param userId The ID of the user creating the budget.
|
|
* @param budgetData The data for the new budget.
|
|
* @returns A promise that resolves to the newly created Budget object.
|
|
*/
|
|
async createBudget(userId: string, budgetData: Omit<Budget, 'budget_id' | 'user_id'>): Promise<Budget> {
|
|
const { name, amount_cents, period, start_date } = budgetData;
|
|
try {
|
|
return await withTransaction(async (client) => {
|
|
const res = await client.query<Budget>(
|
|
'INSERT INTO public.budgets (user_id, name, amount_cents, period, start_date) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
|
[userId, name, amount_cents, period, start_date]
|
|
);
|
|
|
|
// After successfully creating the budget, try to award the 'First Budget Created' achievement.
|
|
// The award_achievement function handles checking if the user already has it.
|
|
await client.query("SELECT public.award_achievement($1, 'First Budget Created')", [userId]);
|
|
return res.rows[0];
|
|
});
|
|
} catch (error) {
|
|
// The patch requested this specific error handling.
|
|
if ((error as any).code === '23503') {
|
|
throw new ForeignKeyConstraintError('The specified user does not exist.');
|
|
}
|
|
logger.error('Database error in createBudget:', { error });
|
|
throw new Error('Failed to create budget.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates an existing budget.
|
|
* @param budgetId The ID of the budget to update.
|
|
* @param userId The ID of the user who owns the budget (for verification).
|
|
* @param budgetData The data to update.
|
|
* @returns A promise that resolves to the updated Budget object.
|
|
*/
|
|
async updateBudget(budgetId: number, userId: string, budgetData: Partial<Omit<Budget, 'budget_id' | 'user_id'>>): Promise<Budget> {
|
|
const { name, amount_cents, period, start_date } = budgetData;
|
|
try {
|
|
const res = await this.db.query<Budget>(
|
|
`UPDATE public.budgets SET
|
|
name = COALESCE($1, name),
|
|
amount_cents = COALESCE($2, amount_cents),
|
|
period = COALESCE($3, period),
|
|
start_date = COALESCE($4, start_date)
|
|
WHERE budget_id = $5 AND user_id = $6 RETURNING *`,
|
|
[name, amount_cents, period, start_date, budgetId, userId],
|
|
);
|
|
if (res.rowCount === 0) throw new NotFoundError('Budget not found or user does not have permission to update.');
|
|
return res.rows[0];
|
|
} catch (error) {
|
|
logger.error('Database error in updateBudget:', { error, budgetId, userId });
|
|
throw new Error('Failed to update budget.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes a budget.
|
|
* @param budgetId The ID of the budget to delete.
|
|
* @param userId The ID of the user who owns the budget (for verification).
|
|
*/
|
|
async deleteBudget(budgetId: number, userId: string): Promise<void> {
|
|
try {
|
|
const result = await this.db.query('DELETE FROM public.budgets WHERE budget_id = $1 AND user_id = $2', [budgetId, userId]);
|
|
if (result.rowCount === 0) {
|
|
throw new NotFoundError('Budget not found or user does not have permission to delete.');
|
|
}
|
|
} catch (error) {
|
|
logger.error('Database error in deleteBudget:', { error, budgetId, userId });
|
|
throw new Error('Failed to delete budget.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the database function to get a user's spending breakdown by category.
|
|
* @param userId The ID of the user.
|
|
* @param startDate The start of the date range.
|
|
* @param endDate The end of the date range.
|
|
* @returns A promise that resolves to an array of spending data.
|
|
*/
|
|
async getSpendingByCategory(userId: string, startDate: string, endDate: string): Promise<SpendingByCategory[]> {
|
|
try {
|
|
const res = await this.db.query<SpendingByCategory>('SELECT * FROM public.get_spending_by_category($1, $2, $3)', [userId, startDate, endDate]);
|
|
return res.rows;
|
|
} catch (error) {
|
|
logger.error('Database error in getSpendingByCategory:', { error, userId });
|
|
throw new Error('Failed to get spending analysis.');
|
|
}
|
|
}
|
|
} |