Files
flyer-crawler.projectium.com/src/services/db/budget.db.ts
Torben Sorensen 117f034b2b
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m30s
ADR1-3 on routes + db files
2025-12-12 16:09:59 -08:00

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.');
}
}
}