mirror of
https://github.com/we-promise/sure.git
synced 2026-07-25 11:12:13 +00:00
* Add per-account toggle to disable automatic transaction categorization Adds an `enable_category_matcher` boolean (default: true) to accounts so users can opt out of Plaid's automatic category suggestions on a per-account basis. When disabled, newly synced transactions arrive uncategorized so rules or manual assignment take precedence. - New migration adds `enable_category_matcher` column (default true, null: false) - `PlaidEntry::Processor#matched_category` gates the CategoryMatcher call on the account flag - Toggle rendered in the account edit modal for linked accounts (saves via main form submit) - `AccountableResource#account_params` permits the new field - `AccountsController#toggle_category_matcher` action added for potential API use - i18n strings added for label and hint text - Unit test covers the disabled-matcher path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Only show category matcher toggle for Plaid-linked accounts Only PlaidEntry::Processor honors enable_category_matcher, but the toggle rendered for every linked account, silently doing nothing for other providers. Adds Account::Linkable#supports_category_matcher? (covering both the legacy plaid_account_id link and AccountProvider rows) and gates the form toggle on it. The SimpleFIN TODO now also points at this helper so the toggle appears once SimpleFIN matching lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add controller tests for category matcher toggle persistence and rendering Covers the two paths flagged in review as untested: the flag persisting through the shared AccountableResource#update action via account_params (both disable and re-enable), and the edit form rendering the toggle only for accounts where supports_category_matcher? is true. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Mike Lloyd <49411532+mike-lloyd03@users.noreply.github.com> Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
86 lines
2.3 KiB
Ruby
86 lines
2.3 KiB
Ruby
class PlaidEntry::Processor
|
|
# plaid_transaction is the raw hash fetched from Plaid API and converted to JSONB
|
|
def initialize(plaid_transaction, plaid_account:, category_matcher:)
|
|
@plaid_transaction = plaid_transaction
|
|
@plaid_account = plaid_account
|
|
@category_matcher = category_matcher
|
|
end
|
|
|
|
def process
|
|
import_adapter.import_transaction(
|
|
external_id: external_id,
|
|
amount: amount,
|
|
currency: currency,
|
|
date: date,
|
|
name: name,
|
|
source: "plaid",
|
|
category_id: matched_category&.id,
|
|
merchant: merchant,
|
|
pending_transaction_id: pending_transaction_id, # Plaid's linking ID for pending→posted
|
|
extra: {
|
|
plaid: {
|
|
pending: plaid_transaction["pending"],
|
|
pending_transaction_id: pending_transaction_id # Also store for reference
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
private
|
|
attr_reader :plaid_transaction, :plaid_account, :category_matcher
|
|
|
|
def import_adapter
|
|
@import_adapter ||= Account::ProviderImportAdapter.new(account)
|
|
end
|
|
|
|
def account
|
|
plaid_account.current_account
|
|
end
|
|
|
|
def external_id
|
|
plaid_transaction["transaction_id"]
|
|
end
|
|
|
|
def name
|
|
plaid_transaction["merchant_name"] || plaid_transaction["original_description"]
|
|
end
|
|
|
|
def amount
|
|
plaid_transaction["amount"]
|
|
end
|
|
|
|
def currency
|
|
plaid_transaction["iso_currency_code"]
|
|
end
|
|
|
|
def date
|
|
plaid_transaction["date"]
|
|
end
|
|
|
|
# Plaid provides this linking ID when a posted transaction matches a pending one
|
|
# This is the most reliable way to reconcile pending→posted
|
|
def pending_transaction_id
|
|
plaid_transaction["pending_transaction_id"]
|
|
end
|
|
|
|
def detailed_category
|
|
plaid_transaction.dig("personal_finance_category", "detailed")
|
|
end
|
|
|
|
def matched_category
|
|
return nil unless detailed_category
|
|
return nil unless account&.enable_category_matcher?
|
|
@matched_category ||= category_matcher.match(detailed_category)
|
|
end
|
|
|
|
def merchant
|
|
@merchant ||= import_adapter.find_or_create_merchant(
|
|
provider_merchant_id: plaid_transaction["merchant_entity_id"],
|
|
name: plaid_transaction["merchant_name"],
|
|
source: "plaid",
|
|
website_url: plaid_transaction["website"],
|
|
logo_url: plaid_transaction["logo_url"]
|
|
)
|
|
end
|
|
end
|