mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Remove plaid initialiser (#317)
* Remove plaid initialiser The initializer can be safely removed because: - Config is lazily loaded via Provider::Registry - reload_configuration is called after settings updates - All calling code handles nil configs gracefully - Initial nil state is fine - config loads on first use * Fix for missing config * Actually don't pollute application.rb * Add currency loading for balances * Fix race condition on lazy load * Allow loans to be imported in lunch flow also * Fix currency processor
This commit is contained in:
50
app/models/concerns/currency_normalizable.rb
Normal file
50
app/models/concerns/currency_normalizable.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
# Provides currency normalization and validation for provider data imports
|
||||
#
|
||||
# This concern provides a shared method to parse and normalize currency codes
|
||||
# from external providers (Plaid, SimpleFIN, LunchFlow), ensuring:
|
||||
# - Consistent uppercase formatting (e.g., "eur" -> "EUR")
|
||||
# - Validation of 3-letter ISO currency codes
|
||||
# - Proper handling of nil, empty, and invalid values
|
||||
#
|
||||
# Usage:
|
||||
# include CurrencyNormalizable
|
||||
# currency = parse_currency(api_data[:currency])
|
||||
module CurrencyNormalizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
# Parse and normalize a currency code from provider data
|
||||
#
|
||||
# @param currency_value [String, nil] Raw currency value from provider API
|
||||
# @return [String, nil] Normalized uppercase 3-letter currency code, or nil if invalid
|
||||
#
|
||||
# @example
|
||||
# parse_currency("usd") # => "USD"
|
||||
# parse_currency("EUR") # => "EUR"
|
||||
# parse_currency(" gbp ") # => "GBP"
|
||||
# parse_currency("invalid") # => nil (logs warning)
|
||||
# parse_currency(nil) # => nil
|
||||
# parse_currency("") # => nil
|
||||
def parse_currency(currency_value)
|
||||
# Handle nil, empty string, or whitespace-only strings
|
||||
return nil if currency_value.blank?
|
||||
|
||||
# Normalize to uppercase 3-letter code
|
||||
normalized = currency_value.to_s.strip.upcase
|
||||
|
||||
# Validate it's a reasonable currency code (3 letters)
|
||||
if normalized.match?(/\A[A-Z]{3}\z/)
|
||||
normalized
|
||||
else
|
||||
log_invalid_currency(currency_value)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Log warning for invalid currency codes
|
||||
# Override this method in including classes to provide context-specific logging
|
||||
def log_invalid_currency(currency_value)
|
||||
Rails.logger.warn("Invalid currency code '#{currency_value}', defaulting to fallback")
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
class LunchflowAccount < ApplicationRecord
|
||||
include CurrencyNormalizable
|
||||
|
||||
belongs_to :lunchflow_item
|
||||
|
||||
# New association through account_providers
|
||||
@@ -21,7 +23,7 @@ class LunchflowAccount < ApplicationRecord
|
||||
# Lunchflow API returns: { id, name, institution_name, institution_logo, provider, currency, status }
|
||||
update!(
|
||||
current_balance: nil, # Balance not provided by accounts endpoint
|
||||
currency: snapshot[:currency] || "USD",
|
||||
currency: parse_currency(snapshot[:currency]) || "USD",
|
||||
name: snapshot[:name],
|
||||
account_id: snapshot[:id].to_s,
|
||||
account_status: snapshot[:status],
|
||||
@@ -41,4 +43,10 @@ class LunchflowAccount < ApplicationRecord
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_invalid_currency(currency_value)
|
||||
Rails.logger.warn("Invalid currency code '#{currency_value}' for LunchFlow account #{id}, defaulting to USD")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class LunchflowAccount::Processor
|
||||
include CurrencyNormalizable
|
||||
|
||||
attr_reader :lunchflow_account
|
||||
|
||||
def initialize(lunchflow_account)
|
||||
@@ -37,14 +39,20 @@ class LunchflowAccount::Processor
|
||||
account = lunchflow_account.current_account
|
||||
balance = lunchflow_account.current_balance || 0
|
||||
|
||||
# For credit cards and loans, ensure positive balances
|
||||
# For liability accounts (credit cards and loans), ensure positive balances
|
||||
# LunchFlow may return negative values for liabilities, but Sure expects positive
|
||||
if account.accountable_type == "CreditCard" || account.accountable_type == "Loan"
|
||||
balance = balance.abs
|
||||
end
|
||||
|
||||
# Normalize currency with fallback chain: parsed lunchflow currency -> existing account currency -> USD
|
||||
currency = parse_currency(lunchflow_account.currency) || account.currency || "USD"
|
||||
|
||||
# Update account balance
|
||||
account.update!(
|
||||
balance: balance,
|
||||
cash_balance: balance
|
||||
cash_balance: balance,
|
||||
currency: currency
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require "digest/md5"
|
||||
|
||||
class LunchflowEntry::Processor
|
||||
include CurrencyNormalizable
|
||||
# lunchflow_transaction is the raw hash fetched from Lunchflow API and converted to JSONB
|
||||
# Transaction structure: { id, accountId, amount, currency, date, merchant, description }
|
||||
def initialize(lunchflow_transaction, lunchflow_account:)
|
||||
@@ -122,7 +123,11 @@ class LunchflowEntry::Processor
|
||||
end
|
||||
|
||||
def currency
|
||||
data[:currency].presence || account&.currency || "USD"
|
||||
parse_currency(data[:currency]) || account&.currency || "USD"
|
||||
end
|
||||
|
||||
def log_invalid_currency(currency_value)
|
||||
Rails.logger.warn("Invalid currency code '#{currency_value}' in LunchFlow transaction #{external_id}, falling back to account currency")
|
||||
end
|
||||
|
||||
def date
|
||||
|
||||
@@ -17,6 +17,10 @@ class Provider::PlaidAdapter < Provider::Base
|
||||
# Register this adapter with the factory for ALL PlaidAccount instances
|
||||
Provider::Factory.register("PlaidAccount", self)
|
||||
|
||||
# Mutex for thread-safe configuration loading
|
||||
# Initialized at class load time to avoid race conditions on mutex creation
|
||||
@config_mutex = Mutex.new
|
||||
|
||||
# Configuration for Plaid US
|
||||
configure do
|
||||
description <<~DESC
|
||||
@@ -51,6 +55,21 @@ class Provider::PlaidAdapter < Provider::Base
|
||||
"plaid"
|
||||
end
|
||||
|
||||
# Thread-safe lazy loading of Plaid US configuration
|
||||
# Ensures configuration is loaded exactly once even under concurrent access
|
||||
def self.ensure_configuration_loaded
|
||||
# Fast path: return immediately if already loaded (no lock needed)
|
||||
return if Rails.application.config.plaid.present?
|
||||
|
||||
# Slow path: acquire lock and reload if still needed
|
||||
@config_mutex.synchronize do
|
||||
# Double-check after acquiring lock (another thread may have loaded it)
|
||||
return if Rails.application.config.plaid.present?
|
||||
|
||||
reload_configuration
|
||||
end
|
||||
end
|
||||
|
||||
# Reload Plaid US configuration when settings are updated
|
||||
def self.reload_configuration
|
||||
client_id = config_value(:client_id).presence || ENV["PLAID_CLIENT_ID"]
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
class Provider::PlaidEuAdapter
|
||||
include Provider::Configurable
|
||||
|
||||
# Mutex for thread-safe configuration loading
|
||||
# Initialized at class load time to avoid race conditions on mutex creation
|
||||
@config_mutex = Mutex.new
|
||||
|
||||
# Configuration for Plaid EU
|
||||
configure do
|
||||
description <<~DESC
|
||||
@@ -43,6 +47,21 @@ class Provider::PlaidEuAdapter
|
||||
description: "Plaid environment: sandbox, development, or production"
|
||||
end
|
||||
|
||||
# Thread-safe lazy loading of Plaid EU configuration
|
||||
# Ensures configuration is loaded exactly once even under concurrent access
|
||||
def self.ensure_configuration_loaded
|
||||
# Fast path: return immediately if already loaded (no lock needed)
|
||||
return if Rails.application.config.plaid_eu.present?
|
||||
|
||||
# Slow path: acquire lock and reload if still needed
|
||||
@config_mutex.synchronize do
|
||||
# Double-check after acquiring lock (another thread may have loaded it)
|
||||
return if Rails.application.config.plaid_eu.present?
|
||||
|
||||
reload_configuration
|
||||
end
|
||||
end
|
||||
|
||||
# Reload Plaid EU configuration when settings are updated
|
||||
def self.reload_configuration
|
||||
client_id = config_value(:client_id).presence || ENV["PLAID_EU_CLIENT_ID"]
|
||||
|
||||
@@ -40,6 +40,8 @@ class Provider::PlaidSandbox < Provider::Plaid
|
||||
def create_client
|
||||
raise "Plaid sandbox is not supported in production" if Rails.env.production?
|
||||
|
||||
Provider::PlaidAdapter.ensure_configuration_loaded
|
||||
|
||||
api_client = Plaid::ApiClient.new(
|
||||
Rails.application.config.plaid
|
||||
)
|
||||
|
||||
@@ -41,6 +41,7 @@ class Provider::Registry
|
||||
end
|
||||
|
||||
def plaid_us
|
||||
Provider::PlaidAdapter.ensure_configuration_loaded
|
||||
config = Rails.application.config.plaid
|
||||
|
||||
return nil unless config.present?
|
||||
@@ -49,6 +50,7 @@ class Provider::Registry
|
||||
end
|
||||
|
||||
def plaid_eu
|
||||
Provider::PlaidEuAdapter.ensure_configuration_loaded
|
||||
config = Rails.application.config.plaid_eu
|
||||
|
||||
return nil unless config.present?
|
||||
|
||||
@@ -49,7 +49,8 @@ class SimplefinAccount::Processor
|
||||
|
||||
account.update!(
|
||||
balance: balance,
|
||||
cash_balance: cash_balance
|
||||
cash_balance: cash_balance,
|
||||
currency: simplefin_account.currency
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require "digest/md5"
|
||||
|
||||
class SimplefinEntry::Processor
|
||||
include CurrencyNormalizable
|
||||
# simplefin_transaction is the raw hash fetched from SimpleFin API and converted to JSONB
|
||||
def initialize(simplefin_transaction, simplefin_account:)
|
||||
@simplefin_transaction = simplefin_transaction
|
||||
@@ -77,7 +78,11 @@ class SimplefinEntry::Processor
|
||||
end
|
||||
|
||||
def currency
|
||||
data[:currency] || account.currency
|
||||
parse_currency(data[:currency]) || account.currency
|
||||
end
|
||||
|
||||
def log_invalid_currency(currency_value)
|
||||
Rails.logger.warn("Invalid currency code '#{currency_value}' in SimpleFIN transaction #{external_id}, falling back to account currency")
|
||||
end
|
||||
|
||||
def date
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
path: new_loan_path(return_to: params[:return_to]),
|
||||
show_us_link: @show_us_link,
|
||||
show_eu_link: @show_eu_link,
|
||||
show_lunchflow_link: @show_lunchflow_link,
|
||||
accountable_type: "Loan" %>
|
||||
<% else %>
|
||||
<%= render DS::Dialog.new do |dialog| %>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
Rails.application.configure do
|
||||
# Initialize Plaid configuration to nil
|
||||
config.plaid = nil
|
||||
config.plaid_eu = nil
|
||||
end
|
||||
|
||||
# Load Plaid configuration from adapters after initialization
|
||||
Rails.application.config.after_initialize do
|
||||
# Skip if database is not ready (e.g., during db:create)
|
||||
next unless ActiveRecord::Base.connection.table_exists?("settings")
|
||||
|
||||
# Ensure provider adapters are loaded
|
||||
Provider::Factory.ensure_adapters_loaded
|
||||
|
||||
# Reload configurations from settings/ENV
|
||||
Provider::PlaidAdapter.reload_configuration # US region
|
||||
Provider::PlaidEuAdapter.reload_configuration # EU region
|
||||
rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
|
||||
# Database doesn't exist yet, skip initialization
|
||||
nil
|
||||
end
|
||||
7
config/initializers/plaid_config.rb
Normal file
7
config/initializers/plaid_config.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# Plaid configuration attributes
|
||||
# These are initialized to nil and loaded lazily on first access by Provider::Registry
|
||||
# Configuration is loaded from database settings or ENV variables via the adapter's reload_configuration method
|
||||
Rails.application.configure do
|
||||
config.plaid = nil
|
||||
config.plaid_eu = nil
|
||||
end
|
||||
@@ -3,6 +3,7 @@ namespace :data_migration do
|
||||
# 2025-02-07: EU Plaid items need to be moved over to a new webhook URL so that we can
|
||||
# instantiate the correct Plaid client for verification based on which Plaid instance it comes from
|
||||
task eu_plaid_webhooks: :environment do
|
||||
Provider::PlaidEuAdapter.ensure_configuration_loaded
|
||||
provider = Provider::Plaid.new(Rails.application.config.plaid_eu, region: :eu)
|
||||
|
||||
eu_items = PlaidItem.where(plaid_region: "eu")
|
||||
|
||||
Reference in New Issue
Block a user