mirror of
https://github.com/we-promise/sure.git
synced 2026-04-09 15:24:48 +00:00
* Add tests and enhance logic for SimpleFin account synchronization and reconciliation - Added retry logic with exponential backoff for network errors in `Provider::Simplefin`. - Introduced tests to verify retry functionality and error handling for rate-limit, server errors, and stale data. - Updated `SimplefinItem` to detect stale sync status and reconciliation issues. - Enhanced UI to display stale sync warnings and data integrity notices. - Improved SimpleFin account matching during updates with multi-tier strategy (ID, fingerprint, fuzzy match). - Added transaction reconciliation logic to detect data gaps, transaction count drops, and duplicate transaction IDs. * Introduce `SimplefinConnectionUpdateJob` for asynchronous SimpleFin connection updates - Moved SimpleFin connection update logic to `SimplefinConnectionUpdateJob` to improve response times by offloading network retries, data fetching, and reconciliation tasks. - Enhanced SimpleFin account matching with a multi-tier strategy (ID, fingerprint, fuzzy name match). - Added retry logic and bounded latency for token claim requests in `Provider::Simplefin`. - Updated tests to cover the new job flow and ensure correct account reconciliation during updates. * Remove unused SimpleFin account matching logic and improve error handling in `SimplefinConnectionUpdateJob` - Deleted the multi-tier account matching logic from `SimplefinItemsController` as it is no longer used. - Enhanced error handling in `SimplefinConnectionUpdateJob` to gracefully handle import failures, ensuring orphaned items can be manually resolved. - Updated job flow to conditionally set item status based on the success of import operations. * Fix SimpleFin sync: check both legacy FK and AccountProvider for linked accounts * Add crypto, checking, savings, and cash account detection; refine subtype selection and linking - Enhanced `Simplefin::AccountTypeMapper` to include detection for crypto, checking, savings, and standalone cash accounts. - Improved subtype selection UI with validation and warning indicators for missing selections. - Updated SimpleFin account linking to handle both legacy FK and `AccountProvider` associations consistently. - Refined job flow and importer logic for better handling of linked accounts and subtype inference. * Improve `SimplefinConnectionUpdateJob` and holdings processing logic - Fixed race condition in `SimplefinConnectionUpdateJob` by moving `destroy_later` calls outside of transactions. - Updated fuzzy name match logic to use Levenshtein distance for better accuracy. - Enhanced synthetic ticker generation in holdings processor with hash suffix for uniqueness. * Refine SimpleFin entry processing logic and ensure `extra` data persistence - Simplified pending flag determination to rely solely on provider-supplied values. - Fixed potential stale values in `extra` by ensuring deep merge overwrite with `entry.transaction.save!`. * Replace hardcoded fallback transaction description with localized string * Refine pending flag logic in SimpleFin processor tests - Adjust test to prevent falsely inferring pending status from missing posted dates. - Ensure provider explicitly sets pending flag for transactions. * Add `has_many :holdings` association to `AccountProvider` with `dependent: :nullify` --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
145 lines
5.1 KiB
Ruby
145 lines
5.1 KiB
Ruby
require "test_helper"
|
|
|
|
class SimplefinEntry::ProcessorTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@account = accounts(:depository)
|
|
@simplefin_item = SimplefinItem.create!(
|
|
family: @family,
|
|
name: "Test SimpleFin Bank",
|
|
access_url: "https://example.com/access_token"
|
|
)
|
|
@simplefin_account = SimplefinAccount.create!(
|
|
simplefin_item: @simplefin_item,
|
|
name: "SF Checking",
|
|
account_id: "sf_acc_1",
|
|
account_type: "checking",
|
|
currency: "USD",
|
|
current_balance: 1000,
|
|
available_balance: 1000,
|
|
account: @account
|
|
)
|
|
end
|
|
|
|
test "persists extra metadata (raw payee/memo/description and provider extra)" do
|
|
tx = {
|
|
id: "tx_1",
|
|
amount: "-12.34",
|
|
currency: "USD",
|
|
payee: "Pizza Hut",
|
|
description: "Order #1234",
|
|
memo: "Carryout",
|
|
posted: Date.today.to_s,
|
|
transacted_at: (Date.today - 1).to_s,
|
|
extra: { category: "restaurants", check_number: nil }
|
|
}
|
|
|
|
assert_difference "@account.entries.count", 1 do
|
|
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
|
|
end
|
|
|
|
entry = @account.entries.find_by!(external_id: "simplefin_tx_1", source: "simplefin")
|
|
extra = entry.transaction.extra
|
|
|
|
assert_equal "Pizza Hut - Order #1234", entry.name
|
|
assert_equal "USD", entry.currency
|
|
|
|
# Check extra payload structure
|
|
assert extra.is_a?(Hash), "extra should be a Hash"
|
|
assert extra["simplefin"].is_a?(Hash), "extra.simplefin should be a Hash"
|
|
sf = extra["simplefin"]
|
|
assert_equal "Pizza Hut", sf["payee"]
|
|
assert_equal "Carryout", sf["memo"]
|
|
assert_equal "Order #1234", sf["description"]
|
|
assert_equal({ "category" => "restaurants", "check_number" => nil }, sf["extra"])
|
|
end
|
|
test "does not flag pending when posted is nil but provider pending flag not set" do
|
|
# Previously we inferred pending from missing posted date, but this was too aggressive -
|
|
# some providers don't supply posted dates even for settled transactions
|
|
tx = {
|
|
id: "tx_pending_1",
|
|
amount: "-20.00",
|
|
currency: "USD",
|
|
payee: "Coffee Shop",
|
|
description: "Latte",
|
|
memo: "Morning run",
|
|
posted: nil,
|
|
transacted_at: (Date.today - 3).to_s
|
|
}
|
|
|
|
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
|
|
|
|
entry = @account.entries.find_by!(external_id: "simplefin_tx_pending_1", source: "simplefin")
|
|
sf = entry.transaction.extra.fetch("simplefin")
|
|
|
|
assert_equal false, sf["pending"], "expected pending flag to be false when provider doesn't explicitly set pending"
|
|
end
|
|
|
|
test "captures FX metadata when tx currency differs from account currency" do
|
|
# Account is USD from setup; use EUR for tx
|
|
t_date = (Date.today - 5)
|
|
p_date = Date.today
|
|
|
|
tx = {
|
|
id: "tx_fx_1",
|
|
amount: "-42.00",
|
|
currency: "EUR",
|
|
payee: "Boulangerie",
|
|
description: "Croissant",
|
|
posted: p_date.to_s,
|
|
transacted_at: t_date.to_s
|
|
}
|
|
|
|
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
|
|
|
|
entry = @account.entries.find_by!(external_id: "simplefin_tx_fx_1", source: "simplefin")
|
|
sf = entry.transaction.extra.fetch("simplefin")
|
|
|
|
assert_equal "EUR", sf["fx_from"]
|
|
assert_equal t_date.to_s, sf["fx_date"], "fx_date should prefer transacted_at"
|
|
end
|
|
test "flags pending when provider pending flag is true (even if posted provided)" do
|
|
tx = {
|
|
id: "tx_pending_flag_1",
|
|
amount: "-9.99",
|
|
currency: "USD",
|
|
payee: "Test Store",
|
|
description: "Auth",
|
|
memo: "",
|
|
posted: Date.today.to_s, # provider says pending=true should still flag
|
|
transacted_at: (Date.today - 1).to_s,
|
|
pending: true
|
|
}
|
|
|
|
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
|
|
|
|
entry = @account.entries.find_by!(external_id: "simplefin_tx_pending_flag_1", source: "simplefin")
|
|
sf = entry.transaction.extra.fetch("simplefin")
|
|
assert_equal true, sf["pending"], "expected pending flag to be true when provider sends pending=true"
|
|
end
|
|
|
|
test "posted==0 treated as missing, entry uses transacted_at date and flags pending" do
|
|
# Simulate provider sending epoch-like zeros for posted and an integer transacted_at
|
|
t_epoch = (Date.today - 2).to_time.to_i
|
|
tx = {
|
|
id: "tx_pending_zero_posted_1",
|
|
amount: "-6.48",
|
|
currency: "USD",
|
|
payee: "Dunkin'",
|
|
description: "DUNKIN #358863",
|
|
memo: "",
|
|
posted: 0,
|
|
transacted_at: t_epoch,
|
|
pending: true
|
|
}
|
|
|
|
SimplefinEntry::Processor.new(tx, simplefin_account: @simplefin_account).process
|
|
|
|
entry = @account.entries.find_by!(external_id: "simplefin_tx_pending_zero_posted_1", source: "simplefin")
|
|
# For depository accounts, processor prefers posted, then transacted; posted==0 should be treated as missing
|
|
assert_equal Time.at(t_epoch).to_date, entry.date, "expected entry.date to use transacted_at when posted==0"
|
|
sf = entry.transaction.extra.fetch("simplefin")
|
|
assert_equal true, sf["pending"], "expected pending flag to be true when posted==0 and/or pending=true"
|
|
end
|
|
end
|