From b4ff65deb8b18a11437f719ffb41ccc6978822e7 Mon Sep 17 00:00:00 2001 From: Munkherdene Date: Sun, 23 Aug 2026 01:27:40 +0800 Subject: [PATCH] feat(web): exclude bank fees from spending views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consume the new `fee` flag from the API: bank service/maintenance fees are dropped from the transaction list, expense totals, the categorize review queue, and the recurring estimate — the user treats them as noise, like transfers. --- src/api/schemas/transaction.ts | 3 ++- src/features/accounting/CategorizeReview.tsx | 1 + src/features/accounting/TransactionList.tsx | 4 +++- src/features/home/recurring.ts | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/api/schemas/transaction.ts b/src/api/schemas/transaction.ts index b9124af..00e2010 100644 --- a/src/api/schemas/transaction.ts +++ b/src/api/schemas/transaction.ts @@ -1,5 +1,6 @@ import { z } from "zod"; export const TxnSchema = z.object({ date: z.string(), amount: z.string(), direction: z.string(), category: z.string(), title: z.string(), balanceAfter: z.string().nullish(), accountId: z.number(), transfer: z.boolean().nullish(), - txnId: z.number().nullish(), note: z.string().nullish(), matchKey: z.string().nullish(), salary: z.boolean().nullish() }); + txnId: z.number().nullish(), note: z.string().nullish(), matchKey: z.string().nullish(), salary: z.boolean().nullish(), + fee: z.boolean().nullish() }); export type Txn = z.infer; diff --git a/src/features/accounting/CategorizeReview.tsx b/src/features/accounting/CategorizeReview.tsx index 695685c..282ab4b 100644 --- a/src/features/accounting/CategorizeReview.tsx +++ b/src/features/accounting/CategorizeReview.tsx @@ -36,6 +36,7 @@ function isReviewableSpend(t: Txn): boolean { if (t.salary === true) return false; if (t.direction === "income") return false; if (t.transfer === true) return false; + if (t.fee === true) return false; // bank's own fees are excluded, not categorized return Boolean(t.matchKey || t.title); } diff --git a/src/features/accounting/TransactionList.tsx b/src/features/accounting/TransactionList.tsx index 9729d81..94af981 100644 --- a/src/features/accounting/TransactionList.tsx +++ b/src/features/accounting/TransactionList.tsx @@ -154,6 +154,7 @@ export function TransactionList() { let expense = 0; for (const txn of all) { if (txn.transfer === true) continue; + if (txn.fee === true) continue; // bank's own fees are excluded from totals if (txn.direction === "income") income += amountOf(txn); else expense += amountOf(txn); } @@ -169,6 +170,7 @@ export function TransactionList() { !txn.category && txn.salary !== true && txn.transfer !== true && + txn.fee !== true && txn.direction !== "income" && (txn.matchKey || txn.title), ), @@ -178,7 +180,7 @@ export function TransactionList() { const visible = useMemo( () => all - .filter((txn) => txn.salary !== true) + .filter((txn) => txn.salary !== true && txn.fee !== true) .filter((txn) => selectedCategory === null || txn.category === selectedCategory) .slice() .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()), diff --git a/src/features/home/recurring.ts b/src/features/home/recurring.ts index 5f3d15c..f334a9f 100644 --- a/src/features/home/recurring.ts +++ b/src/features/home/recurring.ts @@ -26,7 +26,7 @@ export function detectRecurringMerchants( ): DetectedRecurring[] { const groups = new Map(); for (const t of txns) { - if (t.transfer === true || t.direction === "income") continue; + if (t.transfer === true || t.fee === true || t.direction === "income") continue; const key = t.matchKey || t.title; if (!key || knownMatchKeys.has(key)) continue; const amount = dec(t.amount);