- 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
75 lines
2.1 KiB
TypeScript
75 lines
2.1 KiB
TypeScript
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;
|
|
}
|
|
}
|