Skip to content

Fix incorrect category display in report for same merchant names#91

Open
terryaney wants to merge 3 commits into
davidfowl:mainfrom
terryaney:issue/88-merchant-category-display
Open

Fix incorrect category display in report for same merchant names#91
terryaney wants to merge 3 commits into
davidfowl:mainfrom
terryaney:issue/88-merchant-category-display

Conversation

@terryaney

Copy link
Copy Markdown
Contributor

Fixes #88 allowing merchants to different categorization rules with same name.

Instead of requiring:

[Costco Grocery]
match: contains("COSTCO") and amount <= 200
category: Food
subcategory: Grocery

[Costco Bulk]
match: contains("COSTCO") and amount > 200
category: Shopping
subcategory: Wholesale

Can instead be:

[Costco]
match: contains("COSTCO") and amount <= 200
category: Food
subcategory: Grocery

[Costco]
match: contains("COSTCO") and amount > 200
category: Shopping
subcategory: Wholesale

Allows easier auditing of spending to a merchant. Of course, if you prefer unique merchant names, that is still possible.

@github-actions

github-actions Bot commented Apr 26, 2026

Copy link
Copy Markdown

PR Build Available

Version: 0.0.91-e1adce4

Install from this PR

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/davidfowl/tally/main/docs/install-pr.sh | bash -s -- 91

Windows PowerShell:

iex "& { $(irm https://raw.githubusercontent.com/davidfowl/tally/main/docs/install-pr.ps1) } 91"

Manual download: View workflow run and download artifacts.

Requirements
  • GitHub CLI (gh) must be installed and authenticated
  • Run gh auth login if not already authenticated

@terryaney

Copy link
Copy Markdown
Contributor Author

The first commit on 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 that 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. The second commit on my PR addresses this.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates merchant aggregation/reporting so identical merchant display names can appear as separate rows when categorized differently, while keeping merchant-name filtering logical across those category variants.

Changes:

  • Uses composite merchant identity in analysis/report data for same-name merchants with different category/subcategory values.
  • Updates HTML filtering/autocomplete behavior to filter by visible merchant name across duplicate category variants.
  • Adds regression coverage for duplicate merchant names and updates tests for composite row IDs and “Uncategorized” fallback text.
Show a summary per file
File Description
src/tally/analyzer.py Changes merchant aggregation keys to include category/subcategory and propagates merchant names from data.
src/tally/report.py Builds composite merchant IDs and category-view rows for duplicate merchant names.
src/tally/spending_report.js Adjusts merchant filtering/autocomplete and transaction-level categorization in filtered totals/charts.
src/tally/commands/explain.py Updates explain lookup/filter paths for duplicate merchant-name entries.
tests/test_report_html.py Adds duplicate-name regression tests and updates row locators/fallback expectations.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 5

Comment thread src/tally/report.py Outdated
category = data.get('category', '')
subcategory = data.get('subcategory', '')
# Use a composite ID so same-named merchants with different categories are distinct
merchant_id = make_merchant_id(f"{merchant_name}_{category}_{subcategory}")
Comment thread src/tally/commands/explain.py Outdated
Comment on lines +132 to +133
for entry in name_to_entries[merchant_query]:
_print_merchant_explanation(merchant_query, entry, args.format, verbose, stats['num_months'], views_config)
Comment thread src/tally/commands/explain.py Outdated
Comment on lines +139 to +140
for entry in name_to_entries[matches[0]]:
_print_merchant_explanation(matches[0], entry, args.format, verbose, stats['num_months'], views_config)
Comment thread src/tally/commands/explain.py Outdated
Comment on lines +148 to +151
print(f"Merchants matching '{merchant_query}':\n")
for m in sorted(partial_matches):
_print_merchant_explanation(m, all_merchants[m], args.format, verbose, stats['num_months'], views_config)
for n in sorted(partial_matches):
for entry in name_to_entries[n]:
_print_merchant_explanation(n, entry, args.format, verbose, stats['num_months'], views_config)
Comment thread src/tally/analyzer.py
Comment on lines +93 to +96
# Track by merchant - use composite key (merchant, category, subcategory)
# so same-named merchants with different categories appear as separate rows
merchant_key = (txn['merchant'], txn['category'], txn['subcategory'])
by_merchant[merchant_key]['name'] = txn['merchant']
@davidfowl

Copy link
Copy Markdown
Owner

There are some pr comments from copilot

@terryaney

Copy link
Copy Markdown
Contributor Author

There are some pr comments from copilot

Working on them and testing. Curious, I used Copilot to help code up this branch and 4-5 comments above are troubling to me in that I feel Copilot should have found those (all the json format issues). Given that, I'm curious what kind of prompt is running the copilot review? Something custom you would share? Or just a standard review? And my plan/implementation via Copilot missed thinking about these somehow?

terryaney added a commit to terryaney/OpenSource.tally that referenced this pull request May 20, 2026
terryaney and others added 3 commits July 9, 2026 09:30
…ags (#13)

filteredViewTotals and chartAggregations to use per-transaction tags

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.
@terryaney terryaney force-pushed the issue/88-merchant-category-display branch from 05694d9 to e1adce4 Compare July 9, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Same merchant name with different categories causes incorrect category display in report

4 participants