diff --git a/CHANGELOG.md b/CHANGELOG.md index da5b269..84d40e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Backend 0.7.5 / Shared 0.3.1] - 2026-08-20 + +### Added + +- Creating a category rule now applies it atomically to all matching unconfirmed transactions and returns the affected count. + ## [Backend 0.7.4] - 2026-08-20 ### Fixed diff --git a/backend/package.json b/backend/package.json index adf4d82..8d3a241 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/backend", - "version": "0.7.4", + "version": "0.7.5", "private": true, "scripts": { "dev": "tsx watch src/app.ts", diff --git a/backend/src/services/categoryRules.ts b/backend/src/services/categoryRules.ts index 870a37b..d3617b5 100644 --- a/backend/src/services/categoryRules.ts +++ b/backend/src/services/categoryRules.ts @@ -55,22 +55,38 @@ export async function getRules(params: GetCategoryRulesParams): Promise { +): Promise { const pattern = body.pattern.trim(); const matchType = body.matchType ?? 'contains'; const priority = body.priority ?? 100; const requiresConfirmation = body.requiresConfirmation ?? false; - const { rows } = await pool.query( - `INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation) - VALUES ($1, $2, $3, $4, $5) - RETURNING *`, - [pattern, matchType, body.categoryId, priority, requiresConfirmation], - ); - - const catResult = await pool.query('SELECT name FROM categories WHERE id = $1', [body.categoryId]); - rows[0].category_name = catResult.rows[0].name; - return toRule(rows[0]); + const client = await pool.connect(); + try { + await client.query('BEGIN'); + const { rows } = await client.query( + `INSERT INTO category_rules (pattern, match_type, category_id, priority, requires_confirmation) + VALUES ($1, $2, $3, $4, $5) RETURNING *`, + [pattern, matchType, body.categoryId, priority, requiresConfirmation], + ); + const match = matchType === 'starts_with' + ? `t.description ILIKE $1 || '%'` + : `t.description ILIKE '%' || $1 || '%'`; + const applied = await client.query( + `UPDATE transactions t SET category_id = $2, is_category_confirmed = $3, updated_at = NOW() + WHERE (t.category_id IS NULL OR t.is_category_confirmed = FALSE) AND ${match}`, + [pattern, body.categoryId, !requiresConfirmation], + ); + const catResult = await client.query('SELECT name FROM categories WHERE id = $1', [body.categoryId]); + rows[0].category_name = catResult.rows[0].name; + await client.query('COMMIT'); + return { ...toRule(rows[0]), applied: applied.rowCount ?? 0 }; + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } } export async function updateRule( diff --git a/frontend/src/api/rules.ts b/frontend/src/api/rules.ts index 37e9d41..59bb975 100644 --- a/frontend/src/api/rules.ts +++ b/frontend/src/api/rules.ts @@ -1,5 +1,6 @@ import type { CategoryRule, + CreateCategoryRuleResponse, GetCategoryRulesParams, CreateCategoryRuleRequest, UpdateCategoryRuleRequest, @@ -24,8 +25,8 @@ export async function getCategoryRules( export async function createCategoryRule( data: CreateCategoryRuleRequest, -): Promise { - return api.post('/api/category-rules', data); +): Promise { + return api.post('/api/category-rules', data); } export async function updateCategoryRule( diff --git a/shared/package.json b/shared/package.json index 44f6336..e8be15f 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,6 +1,6 @@ { "name": "@family-budget/shared", - "version": "0.3.0", + "version": "0.3.1", "private": true, "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/shared/src/types/category-rule.ts b/shared/src/types/category-rule.ts index 14d3e19..f3d48b0 100644 --- a/shared/src/types/category-rule.ts +++ b/shared/src/types/category-rule.ts @@ -26,6 +26,10 @@ export interface CreateCategoryRuleRequest { requiresConfirmation?: boolean; } +export interface CreateCategoryRuleResponse extends CategoryRule { + applied: number; +} + export interface UpdateCategoryRuleRequest { pattern?: string; categoryId?: number; diff --git a/shared/src/types/index.ts b/shared/src/types/index.ts index 0cb5aca..eaed6fa 100644 --- a/shared/src/types/index.ts +++ b/shared/src/types/index.ts @@ -24,6 +24,7 @@ export type { export type { CategoryRule, + CreateCategoryRuleResponse, GetCategoryRulesParams, CreateCategoryRuleRequest, UpdateCategoryRuleRequest,