test: add unit and integration test suites

- Extract evaluateCondition + rule types into src/lib/rules.ts for testability
- 48 unit tests for evaluateCondition (all fields/operators) and formatCategory
- 21 integration tests for getTransactions filters and getParticipantBalances
- Vitest configs for unit (vitest.config.ts) and integration (vitest.integration.config.ts)
- setup-test-db.sh creates personal_test DB from production schema via pg_dump
- Use vi.doMock + dynamic import pattern to isolate test DB from Prisma singleton
This commit is contained in:
2026-04-01 19:59:29 +11:00
parent 7491e70a15
commit 1296555f17
12 changed files with 2036 additions and 74 deletions
+74
View File
@@ -0,0 +1,74 @@
export interface Condition {
field:
| "merchant_normalized"
| "description"
| "category"
| "bank_name"
| "amount"
| "transaction_type"
| "tag";
operator: "contains" | "equals" | "starts_with" | "gt" | "lt" | "not_equals";
value: string;
}
export interface SplitEntry {
participant_id: number;
share_percent: number;
}
export interface Actions {
set_category?: string;
add_tag_ids?: number[];
set_merchant?: string;
apply_split?: SplitEntry[];
}
export interface TxFields {
effective_category: string;
effective_merchant: string;
description: string;
bank_name: string;
amount: number;
transaction_type: string;
tags: { id: number }[];
}
export function evaluateCondition(cond: Condition, tx: TxFields): boolean {
if (cond.field === "amount") {
const numVal = Number(tx.amount);
const numCond = Number(cond.value);
switch (cond.operator) {
case "equals": return numVal === numCond;
case "not_equals": return numVal !== numCond;
case "gt": return numVal > numCond;
case "lt": return numVal < numCond;
default: return false;
}
}
if (cond.field === "tag") {
const tagId = Number(cond.value);
const hasTag = tx.tags.some((t) => t.id === tagId);
return cond.operator === "not_equals" ? !hasTag : hasTag;
}
let fieldVal: string;
switch (cond.field) {
case "merchant_normalized": fieldVal = tx.effective_merchant || ""; break;
case "description": fieldVal = tx.description || ""; break;
case "category": fieldVal = tx.effective_category || ""; break;
case "bank_name": fieldVal = tx.bank_name || ""; break;
case "transaction_type": fieldVal = tx.transaction_type || ""; break;
default: return false;
}
const strVal = fieldVal.toLowerCase();
const strCond = cond.value.toLowerCase();
switch (cond.operator) {
case "contains": return strVal.includes(strCond);
case "equals": return strVal === strCond;
case "starts_with": return strVal.startsWith(strCond);
case "not_equals": return strVal !== strCond;
default: return false;
}
}