mirror of
https://github.com/we-promise/sure.git
synced 2026-04-16 02:24:08 +00:00
Add global sync summary component for all providers (#588)
* Add shared sync statistics collection and provider sync summary UI - Introduced `SyncStats::Collector` concern to centralize sync statistics logic, including account, transaction, holdings, and health stats collection. - Added collapsible `ProviderSyncSummary` component for displaying sync summaries across providers. - Updated syncers (e.g., `LunchflowItem::Syncer`) to use the shared collector methods for consistent stats calculation. - Added rake tasks under `dev:sync_stats` for testing and development purposes, including fake stats generation with optional issues. - Enhanced provider-specific views to include sync summaries using the new shared component. * Refactor `ProviderSyncSummary` to improve maintainability - Extracted `severity_color_class` to simplify severity-to-CSS mapping. - Replaced `holdings_label` with `holdings_label_key` for streamlined localization. - Updated locale file to separate `found` and `processed` translations for clarity. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
315
lib/tasks/dev_sync_stats.rake
Normal file
315
lib/tasks/dev_sync_stats.rake
Normal file
@@ -0,0 +1,315 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Helper module for sync stats rake tasks
|
||||
module DevSyncStatsHelpers
|
||||
extend self
|
||||
|
||||
def generate_fake_stats_for_items(item_class, provider_name, include_issues: false)
|
||||
items = item_class.all
|
||||
if items.empty?
|
||||
puts " No #{item_class.name} items found, skipping..."
|
||||
return
|
||||
end
|
||||
|
||||
items.each do |item|
|
||||
# Create or find a sync record
|
||||
sync = item.syncs.ordered.first
|
||||
if sync.nil?
|
||||
sync = item.syncs.create!(status: :completed, completed_at: Time.current)
|
||||
end
|
||||
|
||||
stats = generate_fake_stats(provider_name, include_issues: include_issues)
|
||||
sync.update!(sync_stats: stats, status: :completed, completed_at: Time.current)
|
||||
|
||||
item_name = item.respond_to?(:name) ? item.name : item.try(:institution_name) || item.id
|
||||
puts " Generated stats for #{item_class.name} ##{item.id} (#{item_name})"
|
||||
end
|
||||
end
|
||||
|
||||
def generate_fake_stats(provider_name, include_issues: false)
|
||||
# Base stats that all providers have
|
||||
stats = {
|
||||
"total_accounts" => rand(3..15),
|
||||
"linked_accounts" => rand(2..10),
|
||||
"unlinked_accounts" => rand(0..3),
|
||||
"import_started" => true,
|
||||
"window_start" => 1.hour.ago.iso8601,
|
||||
"window_end" => Time.current.iso8601
|
||||
}
|
||||
|
||||
# Ensure linked + unlinked <= total
|
||||
stats["linked_accounts"] = [ stats["linked_accounts"], stats["total_accounts"] ].min
|
||||
stats["unlinked_accounts"] = stats["total_accounts"] - stats["linked_accounts"]
|
||||
|
||||
# Add transaction stats for most providers
|
||||
unless provider_name == "coinstats"
|
||||
stats.merge!(
|
||||
"tx_seen" => rand(50..500),
|
||||
"tx_imported" => rand(10..100),
|
||||
"tx_updated" => rand(0..50),
|
||||
"tx_skipped" => rand(0..5)
|
||||
)
|
||||
# Ensure seen = imported + updated
|
||||
stats["tx_seen"] = stats["tx_imported"] + stats["tx_updated"]
|
||||
end
|
||||
|
||||
# Add holdings stats for investment-capable providers
|
||||
if %w[simplefin plaid coinstats].include?(provider_name)
|
||||
stats["holdings_found"] = rand(5..50)
|
||||
end
|
||||
|
||||
# Add issues if requested
|
||||
if include_issues
|
||||
# Random chance of rate limiting
|
||||
if rand < 0.3
|
||||
stats["rate_limited"] = true
|
||||
stats["rate_limited_at"] = rand(1..24).hours.ago.iso8601
|
||||
end
|
||||
|
||||
# Random errors
|
||||
if rand < 0.4
|
||||
error_count = rand(1..3)
|
||||
stats["errors"] = error_count.times.map do
|
||||
{
|
||||
"message" => [
|
||||
"Connection timeout",
|
||||
"Invalid credentials",
|
||||
"Rate limit exceeded",
|
||||
"Temporary API error"
|
||||
].sample,
|
||||
"category" => %w[api_error connection_error auth_error].sample
|
||||
}
|
||||
end
|
||||
stats["total_errors"] = error_count
|
||||
else
|
||||
stats["total_errors"] = 0
|
||||
end
|
||||
|
||||
# Data quality warnings
|
||||
if rand < 0.5
|
||||
stats["data_warnings"] = rand(1..8)
|
||||
stats["notices"] = rand(0..3)
|
||||
stats["data_quality_details"] = stats["data_warnings"].times.map do |i|
|
||||
start_date = rand(30..180).days.ago.to_date
|
||||
end_date = start_date + rand(14..60).days
|
||||
gap_days = (end_date - start_date).to_i
|
||||
|
||||
{
|
||||
"message" => "No transactions between #{start_date} and #{end_date} (#{gap_days} days)",
|
||||
"severity" => gap_days > 30 ? "warning" : "info"
|
||||
}
|
||||
end
|
||||
end
|
||||
else
|
||||
stats["total_errors"] = 0
|
||||
end
|
||||
|
||||
stats
|
||||
end
|
||||
end
|
||||
|
||||
namespace :dev do
|
||||
namespace :sync_stats do
|
||||
desc "Generate fake sync stats for testing the sync summary UI"
|
||||
task generate: :environment do
|
||||
unless Rails.env.development?
|
||||
puts "This task is only available in development mode"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Generating fake sync stats for testing..."
|
||||
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(PlaidItem, "plaid")
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(SimplefinItem, "simplefin")
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(LunchflowItem, "lunchflow")
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(EnableBankingItem, "enable_banking")
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(CoinstatsItem, "coinstats")
|
||||
|
||||
puts "Done! Refresh your browser to see the sync summaries."
|
||||
end
|
||||
|
||||
desc "Clear all sync stats from syncs"
|
||||
task clear: :environment do
|
||||
unless Rails.env.development?
|
||||
puts "This task is only available in development mode"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Clearing all sync stats..."
|
||||
Sync.where.not(sync_stats: nil).update_all(sync_stats: nil)
|
||||
puts "Done!"
|
||||
end
|
||||
|
||||
desc "Generate fake sync stats with errors and warnings for testing"
|
||||
task generate_with_issues: :environment do
|
||||
unless Rails.env.development?
|
||||
puts "This task is only available in development mode"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Generating fake sync stats with errors and warnings..."
|
||||
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(PlaidItem, "plaid", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(SimplefinItem, "simplefin", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(LunchflowItem, "lunchflow", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(EnableBankingItem, "enable_banking", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(CoinstatsItem, "coinstats", include_issues: true)
|
||||
|
||||
puts "Done! Refresh your browser to see the sync summaries with issues."
|
||||
end
|
||||
|
||||
desc "Create fake provider items with sync stats for testing (use when you have no provider connections)"
|
||||
task create_test_providers: :environment do
|
||||
unless Rails.env.development?
|
||||
puts "This task is only available in development mode"
|
||||
exit 1
|
||||
end
|
||||
|
||||
family = Family.first
|
||||
unless family
|
||||
puts "No family found. Please create a user account first."
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Creating fake provider items for family: #{family.name || family.id}..."
|
||||
|
||||
# Create a fake SimpleFIN item
|
||||
simplefin_item = family.simplefin_items.create!(
|
||||
name: "Test SimpleFIN Connection",
|
||||
access_url: "https://test.simplefin.org/fake"
|
||||
)
|
||||
puts " Created SimplefinItem: #{simplefin_item.name}"
|
||||
|
||||
# Create fake SimpleFIN accounts
|
||||
3.times do |i|
|
||||
simplefin_item.simplefin_accounts.create!(
|
||||
name: "Test Account #{i + 1}",
|
||||
account_id: "test-account-#{SecureRandom.hex(8)}",
|
||||
currency: "USD",
|
||||
current_balance: rand(1000..50000),
|
||||
account_type: %w[checking savings credit_card].sample
|
||||
)
|
||||
end
|
||||
puts " Created 3 SimplefinAccounts"
|
||||
|
||||
# Create a fake Plaid item (requires access_token)
|
||||
plaid_item = family.plaid_items.create!(
|
||||
name: "Test Plaid Connection",
|
||||
access_token: "test-access-token-#{SecureRandom.hex(16)}",
|
||||
plaid_id: "test-plaid-id-#{SecureRandom.hex(8)}"
|
||||
)
|
||||
puts " Created PlaidItem: #{plaid_item.name}"
|
||||
|
||||
# Create fake Plaid accounts
|
||||
2.times do |i|
|
||||
plaid_item.plaid_accounts.create!(
|
||||
name: "Test Plaid Account #{i + 1}",
|
||||
plaid_id: "test-plaid-account-#{SecureRandom.hex(8)}",
|
||||
currency: "USD",
|
||||
current_balance: rand(1000..50000),
|
||||
plaid_type: %w[depository credit investment].sample,
|
||||
plaid_subtype: "checking"
|
||||
)
|
||||
end
|
||||
puts " Created 2 PlaidAccounts"
|
||||
|
||||
# Create a fake Lunchflow item
|
||||
lunchflow_item = family.lunchflow_items.create!(
|
||||
name: "Test Lunchflow Connection",
|
||||
api_key: "test-api-key-#{SecureRandom.hex(16)}"
|
||||
)
|
||||
puts " Created LunchflowItem: #{lunchflow_item.name}"
|
||||
|
||||
# Create fake Lunchflow accounts
|
||||
2.times do |i|
|
||||
lunchflow_item.lunchflow_accounts.create!(
|
||||
name: "Test Lunchflow Account #{i + 1}",
|
||||
account_id: "test-lunchflow-#{SecureRandom.hex(8)}",
|
||||
currency: "USD",
|
||||
current_balance: rand(1000..50000)
|
||||
)
|
||||
end
|
||||
puts " Created 2 LunchflowAccounts"
|
||||
|
||||
# Create a fake CoinStats item
|
||||
coinstats_item = family.coinstats_items.create!(
|
||||
name: "Test CoinStats Connection",
|
||||
api_key: "test-coinstats-key-#{SecureRandom.hex(16)}",
|
||||
institution_name: "CoinStats"
|
||||
)
|
||||
puts " Created CoinstatsItem: #{coinstats_item.name}"
|
||||
|
||||
# Create fake CoinStats accounts (wallets)
|
||||
3.times do |i|
|
||||
coinstats_item.coinstats_accounts.create!(
|
||||
name: "Test Wallet #{i + 1}",
|
||||
account_id: "test-wallet-#{SecureRandom.hex(8)}",
|
||||
currency: "USD",
|
||||
current_balance: rand(100..10000),
|
||||
account_type: %w[wallet exchange defi].sample
|
||||
)
|
||||
end
|
||||
puts " Created 3 CoinstatsAccounts"
|
||||
|
||||
# Create a fake EnableBanking item
|
||||
begin
|
||||
enable_banking_item = family.enable_banking_items.create!(
|
||||
name: "Test EnableBanking Connection",
|
||||
institution_name: "Test Bank EU",
|
||||
institution_id: "test-bank-#{SecureRandom.hex(8)}",
|
||||
country_code: "DE",
|
||||
aspsp_name: "Test Bank",
|
||||
aspsp_id: "test-aspsp-#{SecureRandom.hex(8)}",
|
||||
application_id: "test-app-#{SecureRandom.hex(8)}",
|
||||
client_certificate: "-----BEGIN CERTIFICATE-----\nTEST_CERTIFICATE\n-----END CERTIFICATE-----"
|
||||
)
|
||||
puts " Created EnableBankingItem: #{enable_banking_item.institution_name}"
|
||||
|
||||
# Create fake EnableBanking accounts
|
||||
2.times do |i|
|
||||
uid = "test-eb-uid-#{SecureRandom.hex(8)}"
|
||||
enable_banking_item.enable_banking_accounts.create!(
|
||||
name: "Test EU Account #{i + 1}",
|
||||
uid: uid,
|
||||
account_id: "test-eb-account-#{SecureRandom.hex(8)}",
|
||||
currency: "EUR",
|
||||
current_balance: rand(1000..50000),
|
||||
iban: "DE#{rand(10..99)}#{SecureRandom.hex(10).upcase[0..17]}"
|
||||
)
|
||||
end
|
||||
puts " Created 2 EnableBankingAccounts"
|
||||
rescue => e
|
||||
puts " Failed to create EnableBankingItem: #{e.message}"
|
||||
end
|
||||
|
||||
puts "\nNow generating sync stats for the test providers..."
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(SimplefinItem, "simplefin", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(PlaidItem, "plaid", include_issues: false)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(LunchflowItem, "lunchflow", include_issues: false)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(CoinstatsItem, "coinstats", include_issues: true)
|
||||
DevSyncStatsHelpers.generate_fake_stats_for_items(EnableBankingItem, "enable_banking", include_issues: false)
|
||||
|
||||
puts "\nDone! Visit /accounts to see the sync summaries."
|
||||
end
|
||||
|
||||
desc "Remove all test provider items created by create_test_providers"
|
||||
task remove_test_providers: :environment do
|
||||
unless Rails.env.development?
|
||||
puts "This task is only available in development mode"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts "Removing test provider items..."
|
||||
|
||||
# Remove items that start with "Test "
|
||||
count = 0
|
||||
count += SimplefinItem.where("name LIKE ?", "Test %").destroy_all.count
|
||||
count += PlaidItem.where("name LIKE ?", "Test %").destroy_all.count
|
||||
count += LunchflowItem.where("name LIKE ?", "Test %").destroy_all.count
|
||||
count += CoinstatsItem.where("name LIKE ?", "Test %").destroy_all.count
|
||||
count += EnableBankingItem.where("name LIKE ? OR institution_name LIKE ?", "Test %", "Test %").destroy_all.count
|
||||
|
||||
puts "Removed #{count} test provider items. Done!"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user