Skip to content

Bulk update YNAB payees/merchants#34

Merged
wesm merged 5 commits intowesm:mainfrom
tlmaloney:bulk-update-ynab-payees
Nov 8, 2025
Merged

Bulk update YNAB payees/merchants#34
wesm merged 5 commits intowesm:mainfrom
tlmaloney:bulk-update-ynab-payees

Conversation

@tlmaloney
Copy link
Contributor

For #31, implements a performance improvement for bulk merchant renames by updating YNAB payees once instead of updating each transaction individually. When renaming "Amazon.com/abc" to "Amazon" across 100 transactions, this reduces API calls from 100 to 1.

Changes:

  • Add YNABClient.update_payee() and batch_update_merchant() methods
  • Enhance DataManager.commit_pending_edits() to detect and use batch updates
  • Falls back to individual transaction updates if batch fails
  • Works alongside other edit types (category, hide_from_reports)
  • Add comprehensive test coverage for batch optimization

Notes:
YNAB doesn't enforce name uniqueness for Payees, two different Payees with distinct ids can share the same name.

🤖 Generated with Claude Code

Tom Maloney and others added 2 commits November 8, 2025 10:10
Implements a 100x performance improvement for bulk merchant renames by updating YNAB payees once instead of updating each transaction individually. When renaming "Amazon.com/abc" to "Amazon" across 100 transactions, this reduces API calls from 100 to 1.

Changes:
- Add YNABClient.update_payee() and batch_update_merchant() methods
- Enhance DataManager.commit_pending_edits() to detect and use batch updates
- Falls back to individual transaction updates if batch fails
- Works alongside other edit types (category, hide_from_reports)
- Add comprehensive test coverage for batch optimization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
YNAB allows multiple payees with the same name, which can cause the batch
merchant update optimization to update the wrong payee. This adds validation
to detect duplicate payees and safely abort batch updates when found.

Changes:
- Enhanced _find_or_create_payee() to detect and warn about duplicates
- batch_update_merchant() now aborts with clear error when duplicates exist
- update_transaction() handles new duplicate detection format
- Added comprehensive tests for duplicate detection scenarios

Safety: Prevents accidentally updating wrong payee during bulk renames when
duplicate payee names exist. Users are guided to merge duplicates in YNAB.

Test coverage: YNAB backend 93% (up from 62%), all 24 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
success_count += 1
try:
# Call synchronous batch_update_merchant method
result = self.mm.batch_update_merchant(old_name, new_name) # type: ignore[attr-defined]
Copy link

Choose a reason for hiding this comment

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

⚠️ Synchronous API call in async function without protection (medium severity)

Line 977 calls self.mm.batch_update_merchant() synchronously within an async function (commit_pending_edits). Since batch_update_merchant is synchronous and makes network API calls, this blocks the event loop. Should use 'await asyncio.to_thread(self.mm.batch_update_merchant, old_name, new_name)' to prevent blocking and maintain async safety.


Automated security review by Claude 4.5 Sonnet - Human review still required

duplicates_found = len(matching_payees) > 1
duplicate_ids = [p.id for p in matching_payees] if duplicates_found else []

if duplicates_found:
Copy link

Choose a reason for hiding this comment

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

ℹ️ Payee lookup vulnerable to timing-based enumeration (low severity)

The _find_or_create_payee method iterates through all payees linearly and returns on first match. While not directly exploitable, in a very large payee list, response times could theoretically leak information about payee name existence. Consider using a set/dict lookup if performance becomes an issue with very large payee lists.


Automated security review by Claude 4.5 Sonnet - Human review still required

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

🔒 Security Review: 2 Issues Found

Claude's automated security review identified potential security concerns. Please review the inline comments.

Note: This is an automated review. False positives are possible. Please review each issue carefully and use your judgment.


Powered by Claude 4.5 Sonnet

wesm added a commit that referenced this pull request Nov 8, 2025
Fixed medium severity issue:
- Use asyncio.to_thread() for batch_update_merchant to prevent blocking event loop
- Ensures network API calls don't block async operations

Addressed low severity finding:
- Added comment explaining linear search is intentional for duplicate detection
- Dict lookup would miss duplicates, current approach is correct

All 1001 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixed medium severity issue:
- Use asyncio.to_thread() for batch_update_merchant to prevent blocking event loop
- Ensures network API calls don't block async operations

Addressed low severity finding:
- Added comment explaining linear search is intentional for duplicate detection
- Dict lookup would miss duplicates, current approach is correct

All 1001 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
save_payee = ynab.SavePayee(name=new_name)
wrapper = ynab.PatchPayeeWrapper(payee=save_payee)

# Call the PATCH /payees/{payee_id} endpoint
Copy link

Choose a reason for hiding this comment

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

⚠️ Insufficient validation on payee name input (medium severity)

The update_payee method only validates name length (1-500 chars) but doesn't sanitize content. While YNAB API may handle this, the method should validate against potentially dangerous characters or patterns before sending to the API. Consider adding pattern validation (e.g., rejecting control characters, excessive special characters) to prevent injection attempts or API exploitation.


Automated security review by Claude 4.5 Sonnet - Human review still required

Dictionary with:
- payee: The payee object (first match) or None
- duplicates_found: True if multiple payees with same name exist
- duplicate_ids: List of all payee IDs with matching names
Copy link

Choose a reason for hiding this comment

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

ℹ️ Linear search on all payees could cause performance DoS (low severity)

The _find_or_create_payee method performs a linear search through all payees for each call. For users with thousands of payees, repeated calls during batch operations could cause significant performance degradation or resource exhaustion. While not a direct security vulnerability, this could be exploited for denial-of-service. Consider caching payee lookups or using a more efficient search method.


Automated security review by Claude 4.5 Sonnet - Human review still required

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

🔒 Security Review: 4 Issues Found

Claude's automated security review identified potential security concerns. Please review the inline comments.

Note: This is an automated review. False positives are possible. Please review each issue carefully and use your judgment.


Powered by Claude 4.5 Sonnet

…king

Added assertions to ensure strict separation between successfully batched
and individually processed edits:
- Track all batch attempts to prevent double-processing
- Assert no overlap between successful and failed batch lists
- Prevents incorrect commit statistics from race conditions

Addresses medium severity security concern about batch update tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
results = await asyncio.gather(*tasks, return_exceptions=True)

# Count successes and failures, and log errors
success_count = 0
Copy link

Choose a reason for hiding this comment

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

⚠️ Unvalidated external input in batch merchant update (medium severity)

The old_merchant_name and new_merchant_name from user edits are passed directly to batch_update_merchant without validation. While YNAB client does some validation (empty/length checks), SQL injection-style attacks could occur if the backend uses these names in database queries. Add input validation here to sanitize merchant names (max length, allowed characters, no SQL/NoSQL injection patterns) before passing to backend.


Automated security review by Claude 4.5 Sonnet - Human review still required

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

In moneyflow/ynab_client.py:

ℹ️ Linear search performance degrades with many payees (low severity)

The _find_or_create_payee method uses a list comprehension to find all matching payees. With thousands of payees, this O(n) search on every batch update could cause performance issues. While the comment justifies this for duplicate detection, consider building a name->payee dict once and reusing it for multiple lookups within the same operation, or caching the payee list with invalidation.


Automated security review by Claude 4.5 Sonnet - Human review still required

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

🔒 Security Review: 3 Issues Found

Claude's automated security review identified potential security concerns. Please review the inline comments.

Note: This is an automated review. False positives are possible. Please review each issue carefully and use your judgment.


Powered by Claude 4.5 Sonnet

Fixed race condition where same transaction could have multiple merchant
edits (e.g., A→B then B→C) that would be grouped into different batches.

Solution:
- Track processed transaction IDs across all batch groups
- Filter out already-processed edits before batching
- Send skipped edits to individual processing with latest values
- Prevents double-counting and ensures correct final state

This properly handles the edge case tested in test_concurrent_edits_to_same_transaction.

Addresses high severity security concern about race condition in batch tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
# Create the update payload
save_payee = ynab.SavePayee(name=new_name)
wrapper = ynab.PatchPayeeWrapper(payee=save_payee)

Copy link

Choose a reason for hiding this comment

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

ℹ️ Invalidate cache could mask update failures (low severity)

The _invalidate_cache() call on line 303 happens after a successful payee update but within the try block. If cache invalidation fails with an exception, the method returns False even though the update succeeded. This could cause incorrect retry logic. Move cache invalidation outside the try/except or handle its exceptions separately to ensure update success is accurately reported.


Automated security review by Claude 4.5 Sonnet - Human review still required

@github-actions
Copy link

github-actions bot commented Nov 8, 2025

🔒 Security Review: 2 Issues Found

Claude's automated security review identified potential security concerns. Please review the inline comments.

Note: This is an automated review. False positives are possible. Please review each issue carefully and use your judgment.


Powered by Claude 4.5 Sonnet

@wesm wesm merged commit 334301b into wesm:main Nov 8, 2025
1 check passed
@wesm
Copy link
Owner

wesm commented Nov 8, 2025

thank you!

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.

2 participants