Summary
The filteredViewTotals and chartAggregations computed properties in spending_report.js both call categorizeAmount() using merchant.tags (the aggregate union of all tags for a merchant entry) rather than txn.tags (each transaction's own tags). This causes the Filtered View KPI card to show different income/spending/transfer totals than the Cash Flow KPI card (which is computed correctly by the Python analyzer using per-transaction tags).
The PR correctly fixes the Python-side bug — changing by_merchant to use a composite (merchant, category, subcategory) key so same-named merchants with different categories appear as separate rows. This significantly reduces the practical impact of the JavaScript-side bug described here.
However, even after the PR fix, merchant.tags in the report data is still the union of all transaction tags within that merchant entry. When that union includes tags that not every transaction individually carries, filteredViewTotals misclassifies those transactions.
Root Cause
In spending_report.js, both affected computed properties hoist the tags lookup outside the transaction loop:
// filteredViewTotals
const tags = merchant.tags || []; // ← merchant-level aggregate
const txns = merchant.filteredTxns || merchant.transactions || [];
for (const txn of txns) {
const c = categorizeAmount(txn.amount || 0, tags); // ← all txns get same tags
// chartAggregations
const tags = merchant.tags || []; // ← same problem
for (const txn of merchant.filteredTxns || []) {
const c = categorizeAmount(txn.amount, tags);
Each individual transaction already carries its own tags array in the report JSON (e.g., {"id": "...", "date": "06/22", "amount": 33.51, "tags": ["monthly-bill"]}), so the data is available.
Fix
Change both loops to use per-transaction tags:
filteredViewTotals
// Before
const tags = merchant.tags || [];
const txns = merchant.filteredTxns || merchant.transactions || [];
for (const txn of txns) {
const c = categorizeAmount(txn.amount || 0, tags);
// After
const txns = merchant.filteredTxns || merchant.transactions || [];
for (const txn of txns) {
const c = categorizeAmount(txn.amount || 0, txn.tags || []);
chartAggregations
// Before
const tags = merchant.tags || [];
for (const txn of merchant.filteredTxns || []) {
const c = categorizeAmount(txn.amount, tags);
// After
for (const txn of merchant.filteredTxns || []) {
const c = categorizeAmount(txn.amount, txn.tags || []);
Observable Symptom
At page load with no filters active, the Filtered View KPI card and the Cash Flow KPI card show different income, spending, and net cash-flow figures. Since Cash Flow is precomputed by Python (which uses per-transaction tags), the Filtered View card is the one showing incorrect values.
Specific example: a merchant like Josh Aney that appears in both an income rule and a transfer rule has merchant.tags = ["income", "transfer", "monthly-bill"]. Before the PR fix, all of Josh Aney's transactions (income deposits AND transfer payments) would be classified by this aggregate, incorrectly inflating both income and transfer totals. After the PR fix, the merchant is split by category so the impact is reduced — but any merchant entry where individual transactions have a strict subset of merchant.tags can still produce a mismatch.
Files Affected
src/tally/spending_report.js — two locations (search for const tags = merchant.tags || [] inside the filteredViewTotals and chartAggregations computed properties)
Summary
The
filteredViewTotalsandchartAggregationscomputed properties inspending_report.jsboth callcategorizeAmount()usingmerchant.tags(the aggregate union of all tags for a merchant entry) rather thantxn.tags(each transaction's own tags). This causes the Filtered View KPI card to show different income/spending/transfer totals than the Cash Flow KPI card (which is computed correctly by the Python analyzer using per-transaction tags).Relationship to PR davidfowl#91 (issue davidfowl#88 fix)
The PR correctly fixes the Python-side bug — changing
by_merchantto use a composite(merchant, category, subcategory)key so same-named merchants with different categories appear as separate rows. This significantly reduces the practical impact of the JavaScript-side bug described here.However, even after the PR fix,
merchant.tagsin the report data is still the union of all transaction tags within that merchant entry. When that union includes tags that not every transaction individually carries,filteredViewTotalsmisclassifies those transactions.Root Cause
In
spending_report.js, both affected computed properties hoist the tags lookup outside the transaction loop:Each individual transaction already carries its own
tagsarray in the report JSON (e.g.,{"id": "...", "date": "06/22", "amount": 33.51, "tags": ["monthly-bill"]}), so the data is available.Fix
Change both loops to use per-transaction tags:
filteredViewTotalschartAggregationsObservable Symptom
At page load with no filters active, the Filtered View KPI card and the Cash Flow KPI card show different income, spending, and net cash-flow figures. Since Cash Flow is precomputed by Python (which uses per-transaction tags), the Filtered View card is the one showing incorrect values.
Specific example: a merchant like
Josh Aneythat appears in both an income rule and a transfer rule hasmerchant.tags = ["income", "transfer", "monthly-bill"]. Before the PR fix, all of Josh Aney's transactions (income deposits AND transfer payments) would be classified by this aggregate, incorrectly inflating both income and transfer totals. After the PR fix, the merchant is split by category so the impact is reduced — but any merchant entry where individual transactions have a strict subset ofmerchant.tagscan still produce a mismatch.Files Affected
src/tally/spending_report.js— two locations (search forconst tags = merchant.tags || []inside thefilteredViewTotalsandchartAggregationscomputed properties)