mirror of
https://github.com/we-promise/sure.git
synced 2026-05-08 05:04:59 +00:00
* SimpleFIN: setup UX + same-provider relink + card-replacement detection Fixes three bugs and adds auto-detection for credit-card fraud replacement. Bugs: - Importer: per-institution auth errors no longer flip the whole item to requires_update. Partial errors stay on sync_stats so other institutions keep syncing. - Setup page: new activity badges (recent / dormant / empty / likely-closed) via SimplefinAccount::ActivitySummary. Likely-closed (dormant + near-zero balance + prior history) defaults to "skip" in the type picker. - Relink: link_existing_account allows SimpleFIN to SimpleFIN swaps by atomically detaching the old AccountProvider inside a transaction. Adds "Change SimpleFIN account" menu item on linked-account dropdowns. Feature (credit-card scope only): - SimplefinItem::ReplacementDetector runs post-sync. Pairs a linked dormant zero-balance sfa with an unlinked active sfa at the same institution and account type. Persists suggestions on Sync#sync_stats. - Inline banner on the SimpleFIN item card prompts relink via CustomConfirm. Per-pair dismiss button scoped to the current sync (resurfaces on next sync if still applicable). Auto-suppresses once the relink has landed. Dev tooling: - bin/rails simplefin:seed_fraud_scenario[email] creates a realistic broken pair for manual QA; cleanup_fraud_scenario reverses it. * Address review feedback on #1493 - ReplacementDetector: symmetric one-to-one matching. Two dormant cards pointing at the same active card are now both skipped — previously the detector could emit two suggestions that would clobber each other if the user accepted both. - ReplacementDetector: require non-blank institution names on both sides before matching. Blank-vs-blank was accidentally treated as equal, risking cross-provider false matches when SimpleFIN omitted org_data. - ActivitySummary: fall back to "posted" when "transacted_at" is 0 (SimpleFIN's "unknown" sentinel). Integer 0 is truthy in Ruby, so the previous `|| fallback` short-circuited and ignored posted. - Controller: dismiss key is now the (dormant, active) pair so dismissing one candidate for a dormant card doesn't suppress others. - Helper test: freeze time around "6.hours.ago" and "5.days.ago" assertions so they don't flake when the suite runs before 06:00. * Address second review pass on #1493 - ReplacementDetector: canonicalize account_type in one place so filtering (supported_type?) and matching (type_matches?) agree on "credit card" vs "credit_card" variants. - ReplacementDetector: skip candidates with nil current_balance. nil is "unknown," not "zero" — previously fell back to 0 and passed the near- zero gate, allowing suggestions without balance evidence.
63 lines
2.2 KiB
Ruby
63 lines
2.2 KiB
Ruby
require "test_helper"
|
|
|
|
class SimplefinItemsHelperTest < ActionView::TestCase
|
|
test "#activity_when returns nil for blank time" do
|
|
assert_nil activity_when(nil)
|
|
assert_nil activity_when("")
|
|
end
|
|
|
|
test "#activity_when returns 'today' for current time" do
|
|
assert_equal "today", activity_when(Time.current)
|
|
end
|
|
|
|
test "#activity_when returns 'today' for earlier today" do
|
|
# Freeze at mid-day so 6.hours.ago is guaranteed to fall on the same
|
|
# calendar day regardless of when the suite runs.
|
|
travel_to(Time.zone.parse("2026-04-17 15:00:00")) do
|
|
assert_equal "today", activity_when(6.hours.ago)
|
|
end
|
|
end
|
|
|
|
test "#activity_when returns 'yesterday' one day back" do
|
|
assert_equal "yesterday", activity_when(1.day.ago)
|
|
end
|
|
|
|
test "#activity_when returns 'N days ago' for older dates" do
|
|
# Freeze time so relative "N days ago" stays stable regardless of the
|
|
# hour-of-day the suite runs.
|
|
travel_to(Time.zone.parse("2026-04-17 15:00:00")) do
|
|
assert_equal "5 days ago", activity_when(5.days.ago)
|
|
# 2 days ago is the first value that hits the plural "N days ago" branch
|
|
# (0 -> today, 1 -> yesterday, >=2 -> N days ago).
|
|
assert_equal "2 days ago", activity_when(2.days.ago)
|
|
end
|
|
end
|
|
|
|
test "#activity_when respects injected now: for deterministic formatting" do
|
|
now = Time.zone.parse("2026-04-17 12:00:00")
|
|
assert_equal "7 days ago", activity_when(now - 7.days, now: now)
|
|
end
|
|
|
|
# ---- simplefin_error_tooltip (pre-existing) ----
|
|
test "#simplefin_error_tooltip returns nil for blank stats" do
|
|
assert_nil simplefin_error_tooltip(nil)
|
|
assert_nil simplefin_error_tooltip({})
|
|
assert_nil simplefin_error_tooltip({ "total_errors" => 0 })
|
|
end
|
|
|
|
test "#simplefin_error_tooltip builds a sample with bucket counts" do
|
|
stats = {
|
|
"total_errors" => 3,
|
|
"errors" => [
|
|
{ "name" => "Chase", "message" => "Timeout" },
|
|
{ "name" => "Citi", "message" => "Auth" }
|
|
],
|
|
"error_buckets" => { "auth" => 1, "network" => 2 }
|
|
}
|
|
tooltip = simplefin_error_tooltip(stats)
|
|
assert_includes tooltip, "Errors:"
|
|
assert_includes tooltip, "3"
|
|
assert_includes tooltip, "auth: 1"
|
|
end
|
|
end
|