From f9b5f967ef2a8efc5fcec6b45df699476407d04b Mon Sep 17 00:00:00 2001 From: Itzik Ezra <68964966+ItzikEzra@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:22:24 +0300 Subject: [PATCH] fix: honor user's manual category override (trackingCategory) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user manually reclassifies a transaction in the RiseUp app (e.g., moves "גן הפקאן" from "פנאי" to "אוכל בחוץ"), the override is stored on `Transaction.trackingCategory.name`. The auto-classification remains on `Transaction.expense`. Before this change the CLI read only `tx.expense`, so manual overrides were invisible. `riseup spending`, `riseup transactions`, `riseup trends`, and `riseup income` all showed numbers that disagreed with the app. This adds a shared `getEffectiveCategory(tx)` helper that returns `tx.trackingCategory.name` when set, falling back to `tx.expense`. All category-reading call sites now route through the helper. `manage.ts rename` is left alone — it forwards the auto category to the rename API on purpose; only the display side is updated. --- src/client/categories.ts | 32 ++++++++++++++++++++++++++++++++ src/commands/income.ts | 7 ++++--- src/commands/manage.ts | 3 ++- src/commands/spending.ts | 5 +++-- src/commands/transactions.ts | 7 ++++--- src/commands/trends.ts | 3 ++- 6 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 src/client/categories.ts diff --git a/src/client/categories.ts b/src/client/categories.ts new file mode 100644 index 0000000..c0e972e --- /dev/null +++ b/src/client/categories.ts @@ -0,0 +1,32 @@ +import type { Transaction } from "./types.js"; + +/** + * RiseUp uses `trackingCategory` for two distinct purposes: + * + * 1. A user-chosen category override (e.g. "Eating Out" instead of the + * auto "Leisure"). This is what we want to surface. + * 2. Internal tracking flags like "blacklist" — used to exclude certain + * transactions (e.g. recurring BIT transfers) from the budget total. + * These are not real categories and should not be shown as such. + * + * Known internal flag names that are NOT user-chosen categories. + */ +const INTERNAL_TRACKING_FLAGS = new Set(["blacklist"]); + +/** + * Returns the effective category for a transaction. + * + * When the user manually reclassifies a transaction in the RiseUp app + * (via the "Change category" action), the override is stored on + * `Transaction.trackingCategory`. The auto-classification remains on + * `Transaction.expense`. + * + * The CLI prefers the user's override when it's a real category. + * Falls back to `expense` when no override is set or when the + * override is an internal RiseUp flag (e.g. "blacklist"). + */ +export function getEffectiveCategory(tx: Transaction): string { + const name = tx.trackingCategory?.name; + if (name && !INTERNAL_TRACKING_FLAGS.has(name)) return name; + return tx.expense ?? ""; +} diff --git a/src/commands/income.ts b/src/commands/income.ts index 951edd0..69a4684 100644 --- a/src/commands/income.ts +++ b/src/commands/income.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import type { Command } from "commander"; +import { getEffectiveCategory } from "../client/categories.js"; import { parseMonth } from "../utils/dates.js"; import { formatNIS } from "../formatters/currency.js"; import { createTable, printTable } from "../formatters/table.js"; @@ -29,7 +30,7 @@ export async function incomeAction( // Apply --salary-only filter. const filtered = salaryOnly - ? allTransactions.filter((tx) => tx.expense === "\u05DE\u05E9\u05DB\u05D5\u05E8\u05EA") + ? allTransactions.filter((tx) => getEffectiveCategory(tx) === "\u05DE\u05E9\u05DB\u05D5\u05E8\u05EA") : allTransactions; // Sort by date. @@ -45,7 +46,7 @@ export async function incomeAction( date: tx.transactionDate, amount: tx.incomeAmount, businessName: tx.businessName, - category: tx.expense, + category: getEffectiveCategory(tx), })), ); return; @@ -61,7 +62,7 @@ export async function incomeAction( tx.transactionDate, formatNIS(tx.incomeAmount ?? 0), tx.businessName, - tx.expense, + getEffectiveCategory(tx), ]); } diff --git a/src/commands/manage.ts b/src/commands/manage.ts index 2426be9..d55274e 100644 --- a/src/commands/manage.ts +++ b/src/commands/manage.ts @@ -2,6 +2,7 @@ import chalk from "chalk"; import type { Command } from "commander"; import type { Transaction } from "../client/types.js"; import type { RiseUpClient } from "../client/RiseUpClient.js"; +import { getEffectiveCategory } from "../client/categories.js"; import { parseMonth } from "../utils/dates.js"; import { formatNIS } from "../formatters/currency.js"; import { createTable, printTable } from "../formatters/table.js"; @@ -239,7 +240,7 @@ export async function unclassifiedAction( date: tx.transactionDate, amount: Math.abs(tx.billingAmount ?? 0), businessName: tx.businessName, - category: tx.expense, + category: getEffectiveCategory(tx), source: tx.source, })), ); diff --git a/src/commands/spending.ts b/src/commands/spending.ts index 8292e7d..b954397 100644 --- a/src/commands/spending.ts +++ b/src/commands/spending.ts @@ -1,5 +1,6 @@ import chalk from "chalk"; import type { Command } from "commander"; +import { getEffectiveCategory } from "../client/categories.js"; import { parseMonth } from "../utils/dates.js"; import { formatNIS } from "../formatters/currency.js"; import { createTable, printTable } from "../formatters/table.js"; @@ -40,7 +41,7 @@ export async function spendingAction( // Apply --category filter. const filtered = category ? expenses.filter( - (tx) => tx.expense.toLowerCase() === category.toLowerCase(), + (tx) => getEffectiveCategory(tx).toLowerCase() === category.toLowerCase(), ) : expenses; @@ -57,7 +58,7 @@ export async function spendingAction( break; case "category": default: - key = tx.expense || "(uncategorized)"; + key = getEffectiveCategory(tx) || "(uncategorized)"; break; } diff --git a/src/commands/transactions.ts b/src/commands/transactions.ts index 7ba9b5a..43b394a 100644 --- a/src/commands/transactions.ts +++ b/src/commands/transactions.ts @@ -1,6 +1,7 @@ import chalk from "chalk"; import type { Command } from "commander"; import type { Transaction } from "../client/types.js"; +import { getEffectiveCategory } from "../client/categories.js"; import { parseMonth } from "../utils/dates.js"; import { formatNIS } from "../formatters/currency.js"; import { createTable, printTable } from "../formatters/table.js"; @@ -49,7 +50,7 @@ export async function transactionsAction( if (category) { const lowerCategory = category.toLowerCase(); transactions = transactions.filter( - (tx) => tx.expense.toLowerCase() === lowerCategory, + (tx) => getEffectiveCategory(tx).toLowerCase() === lowerCategory, ); } if (min != null) { @@ -86,7 +87,7 @@ export async function transactionsAction( date: tx.transactionDate, amount: getDisplayAmount(tx), businessName: tx.businessName, - category: tx.expense, + category: getEffectiveCategory(tx), source: tx.source, isIncome: tx.isIncome, ...(tx.customerComment ? { comment: tx.customerComment } : {}), @@ -107,7 +108,7 @@ export async function transactionsAction( tx.transactionDate, `${prefix}${formatNIS(amount)}`, tx.businessName, - tx.expense, + getEffectiveCategory(tx), tx.source, ]); } diff --git a/src/commands/trends.ts b/src/commands/trends.ts index 7ddb092..145e722 100644 --- a/src/commands/trends.ts +++ b/src/commands/trends.ts @@ -1,6 +1,7 @@ import chalk from "chalk"; import type { Command } from "commander"; import type { Budget, CashflowTrends } from "../client/types.js"; +import { getEffectiveCategory } from "../client/categories.js"; import { formatNIS } from "../formatters/currency.js"; import { createTable, printTable } from "../formatters/table.js"; import { printJson } from "../formatters/json.js"; @@ -131,7 +132,7 @@ function showByCategory( const categories = new Map(); for (const tx of expenses) { - const cat = tx.expense || "(uncategorized)"; + const cat = getEffectiveCategory(tx) || "(uncategorized)"; const amount = Math.abs(tx.billingAmount ?? 0); categories.set(cat, (categories.get(cat) ?? 0) + amount); globalCategoryTotals.set(cat, (globalCategoryTotals.get(cat) ?? 0) + amount);