mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
feat(up): map Up category slugs to Sure categories on import (#2487)
* feat(up): map Up category slugs to Sure categories on import UpEntry::Processor captured Up's category slug into extra but never applied it, so Up transactions imported uncategorised even though the user had already tagged them in the Up app. Add UpAccount::Transactions::CategoryTaxonomy + CategoryMatcher, mirroring PlaidAccount::Transactions::CategoryMatcher: map Up's child category slugs onto the family's existing/default Sure categories by alias, and wire the matcher through UpAccount::Transactions::Processor into UpEntry::Processor. The category is applied via the adapter's enrich_attribute, so a category the user has set or locked is preserved on re-sync. High-confidence mappings only. Up-specific categories with no honest Sure default (Booze, Pets, Apps & Games, Life Admin, Technology, ...) intentionally stay uncategorised for the user's own rules / AI, since a wrong auto-category is worse than none. Adds a matcher unit test and processor wiring tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(up): match category slugs as strings in CategoryMatcher Up category ids are string slugs; compare them against the taxonomy keys as strings so the lookup does not depend on the keys being symbols. No behaviour change (the "slug": hash syntax already produces symbol keys that matched the symbolized input, covered by the matcher unit test), but it removes a subtle footgun and reads clearer. Flagged by the Codex review on the PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(up): make category import non-destructive; word-boundary the alias match Per review feedback: do not bootstrap Sure's default categories during a sync. family_categories now returns the family's existing categories without creating defaults, so a family that has none (deliberately cleared, or pre-onboarding) gets uncategorised transactions rather than having the full default set silently created. Matching resumes once the user sets up categories through the normal UI flow. Also word-boundary the "and" stripping in the matcher normalization so it strips only the standalone conjunction, not "and" inside a word (e.g. errand). Adds a processor test for the non-destructive guarantee. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Gavin Matthews <matthews.gav@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
Gavin Matthews
parent
4d1d33f91d
commit
4e010493c7
@@ -33,9 +33,14 @@ class UpEntry::Processor
|
||||
end
|
||||
|
||||
# Build a processor for a single raw Up transaction tied to +up_account+.
|
||||
def initialize(up_transaction, up_account:)
|
||||
#
|
||||
# category_matcher (optional) maps Up's category slug onto one of the family's
|
||||
# existing Sure categories. It is injected by UpAccount::Transactions::Processor so a
|
||||
# single matcher (built once per account) is reused across the account's transactions.
|
||||
def initialize(up_transaction, up_account:, category_matcher: nil)
|
||||
@up_transaction = up_transaction
|
||||
@up_account = up_account
|
||||
@category_matcher = category_matcher
|
||||
end
|
||||
|
||||
# Import the transaction into the linked Sure account via the import adapter.
|
||||
@@ -53,6 +58,7 @@ class UpEntry::Processor
|
||||
date: date,
|
||||
name: name,
|
||||
source: "up",
|
||||
category_id: matched_category_id,
|
||||
kind: kind,
|
||||
merchant: merchant,
|
||||
notes: notes,
|
||||
@@ -99,6 +105,17 @@ class UpEntry::Processor
|
||||
data[:description].presence || I18n.t("transactions.unknown_name")
|
||||
end
|
||||
|
||||
# The id of the Sure category that Up's category slug maps to, or nil when no
|
||||
# matcher was injected, the transaction has no Up category (transfers/income), or
|
||||
# the slug has no confident equivalent among the family's categories. The import
|
||||
# adapter applies this via enrich_attribute, so it never overwrites a category the
|
||||
# user has set or locked.
|
||||
def matched_category_id
|
||||
return nil unless @category_matcher
|
||||
|
||||
@category_matcher.match(data[:category_id])&.id
|
||||
end
|
||||
|
||||
# The id of the other account in an internal money movement, if any (see
|
||||
# Provider::Up#flatten_transaction). Present for transfers between the user's own
|
||||
# accounts and for round-ups swept into a Saver; nil for ordinary income/expense.
|
||||
|
||||
Reference in New Issue
Block a user