Skip to content

filteredViewTotals and chartAggregations use merchant-level aggregate tags instead of per-transaction tags #12

Description

@terryaney

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).

Relationship to PR davidfowl#91 (issue davidfowl#88 fix)

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)

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions