mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +00:00
* feat(mercury): pending transactions, kind/counterpartyId metadata, and test coverage
- Transaction::PENDING_PROVIDERS: add "mercury" so the existing pending
reconciliation pipeline (pending→posted amount matching in
ProviderImportAdapter) activates for Mercury entries
- MercuryEntry::Processor: pass extra: to import_transaction with
extra["mercury"]["pending"] = true/false (status == "pending")
extra["mercury"]["kind"] = ACH / Wire / Card / etc.
extra["mercury"]["counterparty_id"] = Mercury counterparty UUID
Pending transactions are now imported with the flag set rather than
being silently ignored; failed transactions continue to be skipped
- Tests (new files, 30 cases):
- test/models/mercury_entry/processor_test.rb — sign convention,
date fallback, name priority, notes concat, pending flag, kind,
counterpartyId, failed skip, idempotency, merchant creation,
no-linked-account guard
- test/models/mercury_item/importer_test.rb — account discovery,
no-duplicate unlinked records, balance update on linked accounts,
transaction dedup (append-only new ids), sync window (90-day first
sync, last_synced_at-7d subsequent), 401 marks requires_update
- test/models/mercury_account/processor_test.rb — balance update,
CreditCard sign negation, cash_balance parity, no-linked-account
no-op, transaction processing delegation
* fix(mercury): address review findings — pending SQL, dedup upsert, N+1, nil assertion
- provider_import_adapter: add mercury to all three find_pending_transaction*
SQL predicates so Mercury pending entries are found and claimed when the
posted version arrives (exact, fuzzy, and low-confidence paths)
- mercury_item/importer: replace append-only dedup with an upsert-by-id
that replaces the stored raw payload when status changes from pending to
non-pending; prevents pending flag from persisting indefinitely when Mercury
reuses the same transaction ID for the posted version
- kraken_account/ledger_processor: preload all existing kraken ledger
external_ids into a Set before the loop; replaces per-entry exists? query
(N+1) with an in-memory Set#include? lookup
- test/models/mercury_account/processor_test: capture return value from
process and add assert_nil to enforce the nil contract stated in the test name
* fix(mercury): route transaction-count diagnostics through DebugLogEntry
157 lines
5.6 KiB
Ruby
157 lines
5.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
class MercuryItem::ImporterTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@item = MercuryItem.create!(family: @family, name: "Mercury", token: "tok")
|
|
@provider = mock
|
|
@provider.stubs(:get_accounts).returns({ accounts: [] })
|
|
@provider.stubs(:get_account_transactions).returns({ transactions: [] })
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# account discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test "creates unlinked mercury_account records for newly discovered accounts" do
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_001", "Business Checking") ] })
|
|
|
|
assert_difference "@item.mercury_accounts.count", 1 do
|
|
run_import
|
|
end
|
|
|
|
acct = @item.mercury_accounts.find_by(account_id: "acc_001")
|
|
assert acct
|
|
assert_equal "Business Checking", acct.name
|
|
assert_equal "USD", acct.currency
|
|
end
|
|
|
|
test "does not duplicate existing unlinked account records on re-import" do
|
|
@item.mercury_accounts.create!(name: "Existing", account_id: "acc_001", currency: "USD")
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_001", "Business Checking") ] })
|
|
|
|
assert_no_difference "@item.mercury_accounts.count" do
|
|
run_import
|
|
end
|
|
end
|
|
|
|
test "updates current_balance on linked accounts" do
|
|
account, mercury_account = create_linked_account("acc_balance")
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_balance", "Checking", balance: 5_000.0) ] })
|
|
@provider.stubs(:get_account_transactions).with("acc_balance", anything).returns({ transactions: [] })
|
|
|
|
run_import
|
|
|
|
assert_in_delta 5_000.0, mercury_account.reload.current_balance, 0.01
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# transaction deduplication
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test "appends new transactions and skips duplicate ids" do
|
|
_account, mercury_account = create_linked_account("acc_dedup",
|
|
raw_transactions: [ tx_payload("tx_old") ])
|
|
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_dedup", "Checking") ] })
|
|
@provider.stubs(:get_account_transactions).with("acc_dedup", anything).returns({
|
|
transactions: [ tx_payload("tx_old"), tx_payload("tx_new") ]
|
|
})
|
|
|
|
run_import
|
|
|
|
ids = mercury_account.reload.raw_transactions_payload.map { |tx| tx["id"] }
|
|
assert_includes ids, "tx_old"
|
|
assert_includes ids, "tx_new"
|
|
assert_equal 2, ids.uniq.size
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sync window
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test "uses 90-day window for account with no stored transactions" do
|
|
create_linked_account("acc_first")
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_first", "Checking") ] })
|
|
|
|
captured_start = nil
|
|
@provider.stubs(:get_account_transactions).with do |_id, opts|
|
|
captured_start = opts[:start_date]
|
|
true
|
|
end.returns({ transactions: [] })
|
|
|
|
run_import
|
|
|
|
assert_not_nil captured_start
|
|
assert captured_start >= 91.days.ago.to_date,
|
|
"first-sync start date must be within 90 days"
|
|
end
|
|
|
|
test "uses last_synced_at minus 7 days when account has existing transactions" do
|
|
ten_days_ago = 10.days.ago
|
|
_account, mercury_account = create_linked_account("acc_resync",
|
|
raw_transactions: [ tx_payload("existing_tx") ])
|
|
|
|
@item.stubs(:last_synced_at).returns(ten_days_ago)
|
|
@provider.stubs(:get_accounts).returns({ accounts: [ account_payload("acc_resync", "Checking") ] })
|
|
|
|
captured_start = nil
|
|
@provider.stubs(:get_account_transactions).with do |_id, opts|
|
|
captured_start = opts[:start_date]
|
|
true
|
|
end.returns({ transactions: [] })
|
|
|
|
run_import
|
|
|
|
expected = (ten_days_ago - 7.days).to_date
|
|
assert_equal expected, captured_start.to_date
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# auth error handling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
test "marks item requires_update on 401 from Mercury API" do
|
|
@provider.stubs(:get_accounts).raises(
|
|
Provider::Mercury::MercuryError.new("Unauthorized", :unauthorized)
|
|
)
|
|
|
|
run_import
|
|
|
|
assert @item.reload.requires_update?
|
|
end
|
|
|
|
private
|
|
|
|
def run_import
|
|
MercuryItem::Importer.new(@item, mercury_provider: @provider).import
|
|
end
|
|
|
|
def create_linked_account(account_id, raw_transactions: [])
|
|
account = @family.accounts.create!(
|
|
name: account_id, balance: 0, currency: "USD",
|
|
accountable: Depository.new(subtype: "checking")
|
|
)
|
|
mercury_account = @item.mercury_accounts.create!(
|
|
name: account_id, account_id: account_id, currency: "USD",
|
|
current_balance: 0, raw_transactions_payload: raw_transactions
|
|
)
|
|
AccountProvider.create!(provider: mercury_account, account: account)
|
|
[ account, mercury_account ]
|
|
end
|
|
|
|
def account_payload(id, name, balance: 1_000.0)
|
|
{ id: id, name: name, nickname: nil, legalBusinessName: nil,
|
|
currentBalance: balance, availableBalance: balance,
|
|
status: "active", type: "checking", kind: "checking" }
|
|
end
|
|
|
|
def tx_payload(id, amount: 50.0, status: "sent")
|
|
{ "id" => id, "amount" => amount, "status" => status,
|
|
"bankDescription" => "Test", "createdAt" => "2024-06-01T00:00:00Z",
|
|
"postedAt" => "2024-06-01T00:00:00Z" }
|
|
end
|
|
end
|