mirror of
https://github.com/we-promise/sure.git
synced 2026-09-04 22:31:07 +00:00
* 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>
69 lines
2.6 KiB
Ruby
69 lines
2.6 KiB
Ruby
require "test_helper"
|
|
|
|
class UpAccount::Transactions::ProcessorTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:empty)
|
|
@up_item = UpItem.create!(family: @family, name: "Up", access_token: "tok")
|
|
@up_account = UpAccount.create!(
|
|
up_item: @up_item, name: "Spending", account_id: "acc_1", currency: "AUD"
|
|
)
|
|
@account = Account.create!(
|
|
family: @family, name: "Spending",
|
|
accountable: Depository.new(subtype: "checking"), balance: 100, currency: "AUD"
|
|
)
|
|
AccountProvider.create!(account: @account, provider: @up_account)
|
|
end
|
|
|
|
# Importing must not create categories as a side effect: a family that has none
|
|
# (deliberately cleared, or pre-onboarding) keeps none, and transactions stay
|
|
# uncategorised until the user sets up categories themselves.
|
|
test "does not bootstrap default categories during import" do
|
|
assert_equal 0, @family.categories.count, "family starts with no categories"
|
|
|
|
@up_account.update!(raw_transactions_payload: [
|
|
{
|
|
"id" => "tx_1",
|
|
"status" => "SETTLED",
|
|
"description" => "Woolworths",
|
|
"amount" => { "currencyCode" => "AUD", "value" => "-40.00", "valueInBaseUnits" => -4000 },
|
|
"settledAt" => "2026-01-15T00:00:00+11:00",
|
|
"createdAt" => "2026-01-15T00:00:00+11:00",
|
|
"account_id" => "acc_1",
|
|
"category_id" => "groceries"
|
|
}
|
|
])
|
|
|
|
result = UpAccount::Transactions::Processor.new(@up_account).process
|
|
|
|
assert result[:success]
|
|
assert_equal 0, @family.categories.reload.count, "import must not create categories"
|
|
|
|
entry = @account.entries.find_by(external_id: "up_tx_1")
|
|
assert_not_nil entry, "the transaction was still imported"
|
|
assert_nil entry.transaction.category_id, "stays uncategorised when the family has no categories"
|
|
end
|
|
|
|
# With the default categories present, the same Up category resolves and is applied.
|
|
test "applies matched categories when the family already has them" do
|
|
@family.categories.bootstrap!
|
|
|
|
@up_account.update!(raw_transactions_payload: [
|
|
{
|
|
"id" => "tx_2",
|
|
"status" => "SETTLED",
|
|
"description" => "Woolworths",
|
|
"amount" => { "currencyCode" => "AUD", "value" => "-40.00", "valueInBaseUnits" => -4000 },
|
|
"settledAt" => "2026-01-15T00:00:00+11:00",
|
|
"createdAt" => "2026-01-15T00:00:00+11:00",
|
|
"account_id" => "acc_1",
|
|
"category_id" => "groceries"
|
|
}
|
|
])
|
|
|
|
UpAccount::Transactions::Processor.new(@up_account).process
|
|
|
|
entry = @account.entries.find_by(external_id: "up_tx_2")
|
|
assert_equal "Groceries", entry.transaction.category&.name
|
|
end
|
|
end
|