diff --git a/app/controllers/concerns/accountable_resource.rb b/app/controllers/concerns/accountable_resource.rb index f149adc3..675c3d93 100644 --- a/app/controllers/concerns/accountable_resource.rb +++ b/app/controllers/concerns/accountable_resource.rb @@ -52,12 +52,21 @@ module AccountableResource private def set_link_token - @link_token = Current.family.get_link_token( + @us_link_token = Current.family.get_link_token( webhooks_url: webhooks_url, redirect_url: accounts_url, accountable_type: accountable_type.name, - region: Current.family.country.to_s.downcase == "us" ? :us : :eu + region: :us ) + + if Current.family.eu? + @eu_link_token = Current.family.get_link_token( + webhooks_url: webhooks_url, + redirect_url: accounts_url, + accountable_type: accountable_type.name, + region: :eu + ) + end end def webhooks_url diff --git a/app/javascript/controllers/plaid_controller.js b/app/javascript/controllers/plaid_controller.js index 5f4f1d29..cf1f71cb 100644 --- a/app/javascript/controllers/plaid_controller.js +++ b/app/javascript/controllers/plaid_controller.js @@ -4,7 +4,7 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static values = { linkToken: String, - region: { type: String, default: "us" } + region: { type: String, default: "us" }, }; open() { @@ -19,7 +19,7 @@ export default class extends Controller { handler.open(); } - handleSuccess(public_token, metadata) { + handleSuccess = (public_token, metadata) => { window.location.href = "/accounts"; fetch("/plaid_items", { @@ -32,7 +32,7 @@ export default class extends Controller { plaid_item: { public_token: public_token, metadata: metadata, - region: this.regionValue + region: this.regionValue, }, }), }).then((response) => { @@ -40,17 +40,17 @@ export default class extends Controller { window.location.href = response.url; } }); - } + }; - handleExit(err, metadata) { + handleExit = (err, metadata) => { // no-op - } + }; - handleEvent(eventName, metadata) { + handleEvent = (eventName, metadata) => { // no-op - } + }; - handleLoad() { + handleLoad = () => { // no-op - } + }; } diff --git a/app/models/account/balance_calculator.rb b/app/models/account/balance_calculator.rb index 55e094ed..d5b3b3d1 100644 --- a/app/models/account/balance_calculator.rb +++ b/app/models/account/balance_calculator.rb @@ -11,7 +11,7 @@ class Account::BalanceCalculator holdings_value = converted_holdings.select { |h| h.date == balance.date }.sum(&:amount) balance.balance = balance.balance + holdings_value balance - end + end.compact end private diff --git a/app/models/account/syncer.rb b/app/models/account/syncer.rb index d5a5ec84..37074aa7 100644 --- a/app/models/account/syncer.rb +++ b/app/models/account/syncer.rb @@ -76,29 +76,33 @@ class Account::Syncer exchange_rates = ExchangeRate.find_rates( from: from_currency, to: to_currency, - start_date: balances.first.date + start_date: balances.min_by(&:date).date ) converted_balances = balances.map do |balance| exchange_rate = exchange_rates.find { |er| er.date == balance.date } + next unless exchange_rate.present? + account.balances.build( date: balance.date, balance: exchange_rate.rate * balance.balance, currency: to_currency - ) if exchange_rate.present? - end + ) + end.compact converted_holdings = holdings.map do |holding| exchange_rate = exchange_rates.find { |er| er.date == holding.date } + next unless exchange_rate.present? + account.holdings.build( security: holding.security, date: holding.date, amount: exchange_rate.rate * holding.amount, currency: to_currency - ) if exchange_rate.present? - end + ) + end.compact Account.transaction do load_balances(converted_balances) diff --git a/app/models/concerns/plaidable.rb b/app/models/concerns/plaidable.rb index 838b4837..062eab8e 100644 --- a/app/models/concerns/plaidable.rb +++ b/app/models/concerns/plaidable.rb @@ -2,22 +2,25 @@ module Plaidable extend ActiveSupport::Concern class_methods do - def plaid_provider - Provider::Plaid.new if Rails.application.config.plaid + def plaid_us_provider + Provider::Plaid.new(Rails.application.config.plaid, :us) if Rails.application.config.plaid end def plaid_eu_provider - Provider::Plaid.new if Rails.application.config.plaid_eu + Provider::Plaid.new(Rails.application.config.plaid_eu, :eu) if Rails.application.config.plaid_eu end - def plaid_provider_for(plaid_item) - return nil unless plaid_item - plaid_item.eu? ? plaid_eu_provider : plaid_provider + def plaid_provider_for_region(region) + region.to_sym == :eu ? plaid_eu_provider : plaid_us_provider end end private - def plaid_provider_for(plaid_item) - self.class.plaid_provider_for(plaid_item) + def eu? + raise "eu? is not implemented for #{self.class.name}" + end + + def plaid_provider + eu? ? self.class.plaid_eu_provider : self.class.plaid_us_provider end end diff --git a/app/models/family.rb b/app/models/family.rb index 4b36fcd8..7ac41f25 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -1,4 +1,3 @@ -# rubocop:disable Layout/ElseAlignment, Layout/IndentationWidth class Family < ApplicationRecord include Plaidable, Syncable @@ -48,22 +47,22 @@ class Family < ApplicationRecord super || accounts.manual.any?(&:syncing?) || plaid_items.any?(&:syncing?) end - def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us) - provider = case region - when :eu - self.class.plaid_eu_provider - else - self.class.plaid_provider - end + def eu? + country != "US" && country != "CA" + end - return nil unless provider + def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us) + provider = if region.to_sym == :eu + self.class.plaid_eu_provider + else + self.class.plaid_us_provider + end provider.get_link_token( user_id: id, webhooks_url: webhooks_url, redirect_url: redirect_url, accountable_type: accountable_type, - eu: region == :eu ).link_token end @@ -238,4 +237,3 @@ class Family < ApplicationRecord ) end end -# rubocop:enable Layout/ElseAlignment, Layout/IndentationWidth diff --git a/app/models/plaid_item.rb b/app/models/plaid_item.rb index a41d96a9..9ac1e002 100644 --- a/app/models/plaid_item.rb +++ b/app/models/plaid_item.rb @@ -21,8 +21,8 @@ class PlaidItem < ApplicationRecord scope :ordered, -> { order(created_at: :desc) } class << self - def create_from_public_token(token, item_name:, region: "us") - response = plaid_provider.exchange_public_token(token) + def create_from_public_token(token, item_name:, region:) + response = plaid_provider_for_region(region).exchange_public_token(token) new_plaid_item = create!( name: item_name, @@ -59,11 +59,10 @@ class PlaidItem < ApplicationRecord private def fetch_and_load_plaid_data data = {} - provider = plaid_provider_for(self) - item = provider.get_item(access_token).item + item = plaid_provider.get_item(access_token).item update!(available_products: item.available_products, billed_products: item.billed_products) - fetched_accounts = provider.get_item_accounts(self).accounts + fetched_accounts = plaid_provider.get_item_accounts(self).accounts data[:accounts] = fetched_accounts || [] internal_plaid_accounts = fetched_accounts.map do |account| diff --git a/app/models/provider/plaid.rb b/app/models/provider/plaid.rb index 813102a0..877a955f 100644 --- a/app/models/provider/plaid.rb +++ b/app/models/provider/plaid.rb @@ -1,5 +1,5 @@ class Provider::Plaid - attr_reader :client + attr_reader :client, :region MAYBE_SUPPORTED_PLAID_PRODUCTS = %w[transactions investments liabilities].freeze MAX_HISTORY_DAYS = Rails.env.development? ? 90 : 730 @@ -54,27 +54,22 @@ class Provider::Plaid actual_hash = Digest::SHA256.hexdigest(raw_body) raise JWT::VerificationError, "Invalid webhook body hash" unless ActiveSupport::SecurityUtils.secure_compare(expected_hash, actual_hash) end - - def client - api_client = Plaid::ApiClient.new( - Rails.application.config.plaid - ) - - Plaid::PlaidApi.new(api_client) - end end - def initialize - @client = self.class.client + def initialize(config, region) + @client = Plaid::PlaidApi.new( + Plaid::ApiClient.new(config) + ) + @region = region end - def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil, eu: false) + def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil) request = Plaid::LinkTokenCreateRequest.new({ user: { client_user_id: user_id }, client_name: "Maybe Finance", products: [ get_primary_product(accountable_type) ], additional_consented_products: get_additional_consented_products(accountable_type), - country_codes: get_country_codes(eu), + country_codes: country_codes, language: "en", webhook: webhooks_url, redirect_uri: redirect_url, @@ -199,8 +194,8 @@ class Provider::Plaid MAYBE_SUPPORTED_PLAID_PRODUCTS - [ get_primary_product(accountable_type) ] end - def get_country_codes(eu) - if eu + def country_codes + if region.to_sym == :eu [ "ES", "NL", "FR", "IE", "DE", "IT", "PL", "DK", "NO", "SE", "EE", "LT", "LV", "PT", "BE" ] # EU supported countries else [ "US", "CA" ] # US + CA only diff --git a/app/views/accounts/new/_method_selector.html.erb b/app/views/accounts/new/_method_selector.html.erb index 0ec22cfd..13834f09 100644 --- a/app/views/accounts/new/_method_selector.html.erb +++ b/app/views/accounts/new/_method_selector.html.erb @@ -1,4 +1,4 @@ -<%# locals: (path:, link_token: nil) %> +<%# locals: (path:, us_link_token: nil, eu_link_token: nil) %> <%= render layout: "accounts/new/container", locals: { title: t(".title"), back_path: new_account_path } do %>