Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 41s
156 lines
6.0 KiB
TypeScript
156 lines
6.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 type { Logger } from 'pino';
|
|
import type { Budget, SpendingByCategory } from '../../types';
|
|
import { GamificationRepository } from './gamification.db';
|
|
|
|
export class BudgetRepository {
|
|
// The repository only needs an object with a `query` method, matching the Pool/PoolClient interface.
|
|
// Using `Pick` makes this dependency explicit and simplifies testing by reducing the mock surface.
|
|
private db: Pick<Pool | PoolClient, 'query'>;
|
|
|
|
constructor(db: Pick<Pool | PoolClient, 'query'> = 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, logger: Logger): 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({ err: error, userId }, 'Database error in getBudgetsForUser');
|
|
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' | 'created_at' | 'updated_at'>,
|
|
logger: Logger,
|
|
): 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.
|
|
const gamificationRepo = new GamificationRepository(client);
|
|
await gamificationRepo.awardAchievement(userId, 'First Budget Created', logger);
|
|
return res.rows[0];
|
|
});
|
|
} catch (error) {
|
|
// The patch requested this specific error handling.
|
|
// Type-safe check for a PostgreSQL error code.
|
|
// This ensures 'error' is an object with a 'code' property before we access it.
|
|
if (error instanceof Error && 'code' in error && error.code === '23503') {
|
|
throw new ForeignKeyConstraintError('The specified user does not exist.');
|
|
}
|
|
logger.error({ err: error, budgetData, userId }, 'Database error in createBudget');
|
|
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' | 'created_at' | 'updated_at'>>,
|
|
logger: Logger,
|
|
): 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) {
|
|
if (error instanceof NotFoundError) throw error;
|
|
logger.error({ err: error, budgetId, userId }, 'Database error in updateBudget');
|
|
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, logger: Logger): 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) {
|
|
if (error instanceof NotFoundError) throw error;
|
|
logger.error({ err: error, budgetId, userId }, 'Database error in deleteBudget');
|
|
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,
|
|
logger: Logger,
|
|
): 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(
|
|
{ err: error, userId, startDate, endDate },
|
|
'Database error in getSpendingByCategory',
|
|
);
|
|
throw new Error('Failed to get spending analysis.');
|
|
}
|
|
}
|
|
}
|