feat(web): exclude bank fees from spending views

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.
This commit is contained in:
Munkherdene 2026-08-23 01:27:40 +08:00
parent 9e5d094ca1
commit b4ff65deb8
4 changed files with 7 additions and 3 deletions

View file

@ -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<typeof TxnSchema>;

View file

@ -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);
}

View file

@ -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()),

View file

@ -26,7 +26,7 @@ export function detectRecurringMerchants(
): DetectedRecurring[] {
const groups = new Map<string, { label: string; count: number; total: number }>();
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);