From de78d5604e5acf0f8053646e9bdfd30c584521d1 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Wed, 18 Dec 2024 11:04:18 -0600 Subject: [PATCH 1/4] Initial pass at Plaid EU --- .env.example | 5 ++- .../concerns/accountable_resource.rb | 3 +- app/models/concerns/plaidable.rb | 8 ++++ app/models/family.rb | 16 ++++++-- app/models/provider/plaid.rb | 12 +++++- .../accounts/new/_method_selector.html.erb | 33 +++++++++++---- config/initializers/plaid.rb | 8 ++++ config/locales/views/accounts/en.yml | 2 + db/schema.rb | 41 +++++++++++++++++++ 9 files changed, 113 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index ef90f4a374d..77df0bb68d5 100644 --- a/.env.example +++ b/.env.example @@ -118,4 +118,7 @@ STRIPE_WEBHOOK_SECRET= # PLAID_CLIENT_ID= PLAID_SECRET= -PLAID_ENV= \ No newline at end of file +PLAID_ENV= +PLAID_EU_CLIENT_ID= +PLAID_EU_SECRET= +PLAID_EU_ENV= diff --git a/app/controllers/concerns/accountable_resource.rb b/app/controllers/concerns/accountable_resource.rb index 8f6a3244abd..5a160917225 100644 --- a/app/controllers/concerns/accountable_resource.rb +++ b/app/controllers/concerns/accountable_resource.rb @@ -47,7 +47,8 @@ def set_link_token @link_token = Current.family.get_link_token( webhooks_url: webhooks_url, redirect_url: accounts_url, - accountable_type: accountable_type.name + accountable_type: accountable_type.name, + region: Current.family.country.to_s.downcase == "us" ? :us : :eu ) end diff --git a/app/models/concerns/plaidable.rb b/app/models/concerns/plaidable.rb index ddecd89325b..2913006b803 100644 --- a/app/models/concerns/plaidable.rb +++ b/app/models/concerns/plaidable.rb @@ -5,10 +5,18 @@ module Plaidable def plaid_provider Provider::Plaid.new if Rails.application.config.plaid end + + def plaid_eu_provider + Provider::Plaid.new if Rails.application.config.plaid_eu + end end private def plaid_provider self.class.plaid_provider end + + def plaid_eu_provider + self.class.plaid_eu_provider + end end diff --git a/app/models/family.rb b/app/models/family.rb index 69ac5eb7ca4..1c2ff25c1f8 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -45,14 +45,22 @@ def syncing? super || accounts.manual.any?(&:syncing?) || plaid_items.any?(&:syncing?) end - def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil) - return nil unless plaid_provider + 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 - plaid_provider.get_link_token( + return nil unless provider + + provider.get_link_token( user_id: id, webhooks_url: webhooks_url, redirect_url: redirect_url, - accountable_type: accountable_type + accountable_type: accountable_type, + eu: region == :eu ).link_token end diff --git a/app/models/provider/plaid.rb b/app/models/provider/plaid.rb index e41a0a46dab..924e10c9459 100644 --- a/app/models/provider/plaid.rb +++ b/app/models/provider/plaid.rb @@ -68,13 +68,13 @@ def initialize @client = self.class.client end - def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil) + def get_link_token(user_id:, webhooks_url:, redirect_url:, accountable_type: nil, eu: false) 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: [ "US" ], + country_codes: get_country_codes(eu), language: "en", webhook: webhooks_url, redirect_uri: redirect_url, @@ -198,4 +198,12 @@ def get_primary_product(accountable_type) def get_additional_consented_products(accountable_type) MAYBE_SUPPORTED_PLAID_PRODUCTS - [ get_primary_product(accountable_type) ] end + + def get_country_codes(eu) + if eu + [ "ES", "NL", "FR", "IE", "DE", "IT", "PL", "DK", "NO", "SE", "EE", "LT", "LV", "PT", "BE" ] # EU supported countries + else + [ "US" ] # US only + end + end end diff --git a/app/views/accounts/new/_method_selector.html.erb b/app/views/accounts/new/_method_selector.html.erb index 4c32ede4af3..1e3d9f66e8b 100644 --- a/app/views/accounts/new/_method_selector.html.erb +++ b/app/views/accounts/new/_method_selector.html.erb @@ -6,16 +6,35 @@ <%= lucide_icon("keyboard", class: "text-gray-500 w-5 h-5") %> - <%= t("accounts.new.method_selector.manual_entry") %> + <%= t(".manual_entry") %> <% end %> <% if link_token.present? %> - + <% if Current.family.country.to_s.downcase != "us" %> + <%# US Link %> + + + <%# EU Link %> + + <% else %> + <%# Default US-only Link %> + + <% end %> <% end %> <% end %> diff --git a/config/initializers/plaid.rb b/config/initializers/plaid.rb index b0631110fd9..5a7a99510d1 100644 --- a/config/initializers/plaid.rb +++ b/config/initializers/plaid.rb @@ -1,5 +1,6 @@ Rails.application.configure do config.plaid = nil + config.plaid_eu = nil if ENV["PLAID_CLIENT_ID"].present? && ENV["PLAID_SECRET"].present? config.plaid = Plaid::Configuration.new @@ -7,4 +8,11 @@ config.plaid.api_key["PLAID-CLIENT-ID"] = ENV["PLAID_CLIENT_ID"] config.plaid.api_key["PLAID-SECRET"] = ENV["PLAID_SECRET"] end + + if ENV["PLAID_EU_CLIENT_ID"].present? && ENV["PLAID_EU_SECRET"].present? + config.plaid_eu = Plaid::Configuration.new + config.plaid_eu.server_index = Plaid::Configuration::Environment[ENV["PLAID_EU_ENV"] || "sandbox"] + config.plaid_eu.api_key["PLAID-CLIENT-ID"] = ENV["PLAID_EU_CLIENT_ID"] + config.plaid_eu.api_key["PLAID-SECRET"] = ENV["PLAID_EU_SECRET"] + end end diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index 3180b37ec15..d986cedee25 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -30,6 +30,8 @@ en: import_accounts: Import accounts method_selector: connected_entry: Link account + connected_entry_us: Link US account + connected_entry_eu: Link EU account manual_entry: Enter account balance title: How would you like to add it? title: What would you like to add? diff --git a/db/schema.rb b/db/schema.rb index 5ae96e5274b..3ee8e4483d9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -177,6 +177,15 @@ t.index ["family_id"], name: "index_categories_on_family_id" end + create_table "chats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "user_id", null: false + t.string "title" + t.text "summary" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_chats_on_user_id" + end + create_table "credit_cards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -459,6 +468,33 @@ t.index ["family_id"], name: "index_merchants_on_family_id" end + create_table "messages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "chat_id", null: false + t.uuid "user_id" + t.text "content" + t.text "log" + t.string "role" + t.string "status", default: "pending" + t.boolean "hidden", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["chat_id"], name: "index_messages_on_chat_id" + t.index ["user_id"], name: "index_messages_on_user_id" + end + + create_table "metrics", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "family_id", null: false + t.uuid "account_id" + t.string "kind", null: false + t.string "subkind" + t.date "date", null: false + t.decimal "value", precision: 10, scale: 2, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_metrics_on_account_id" + t.index ["family_id"], name: "index_metrics_on_family_id" + end + create_table "other_assets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -646,6 +682,7 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "categories", "families" + add_foreign_key "chats", "users" add_foreign_key "impersonation_session_logs", "impersonation_sessions" add_foreign_key "impersonation_sessions", "users", column: "impersonated_id" add_foreign_key "impersonation_sessions", "users", column: "impersonator_id" @@ -654,6 +691,10 @@ add_foreign_key "invitations", "families" add_foreign_key "invitations", "users", column: "inviter_id" add_foreign_key "merchants", "families" + add_foreign_key "messages", "chats" + add_foreign_key "messages", "users" + add_foreign_key "metrics", "accounts" + add_foreign_key "metrics", "families" add_foreign_key "plaid_accounts", "plaid_items" add_foreign_key "plaid_items", "families" add_foreign_key "security_prices", "securities" From 0c9cb333d5bbae0748379aef2532399a0eb1506a Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Thu, 19 Dec 2024 09:18:30 -0600 Subject: [PATCH 2/4] Add EU support to Plaid Items --- .env.example | 1 - app/controllers/plaid_items_controller.rb | 3 +- .../controllers/plaid_controller.js | 2 ++ app/models/concerns/plaidable.rb | 13 +++++---- app/models/plaid_item.rb | 8 ++++-- .../accounts/new/_method_selector.html.erb | 28 +++++++------------ config/initializers/plaid.rb | 2 +- ...20241219151540_add_region_to_plaid_item.rb | 5 ++++ db/schema.rb | 3 +- 9 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 db/migrate/20241219151540_add_region_to_plaid_item.rb diff --git a/.env.example b/.env.example index 77df0bb68d5..5253039cd5c 100644 --- a/.env.example +++ b/.env.example @@ -121,4 +121,3 @@ PLAID_SECRET= PLAID_ENV= PLAID_EU_CLIENT_ID= PLAID_EU_SECRET= -PLAID_EU_ENV= diff --git a/app/controllers/plaid_items_controller.rb b/app/controllers/plaid_items_controller.rb index c0ac89ad744..64537896a0c 100644 --- a/app/controllers/plaid_items_controller.rb +++ b/app/controllers/plaid_items_controller.rb @@ -5,6 +5,7 @@ def create Current.family.plaid_items.create_from_public_token( plaid_item_params[:public_token], item_name: item_name, + region: plaid_item_params[:region] ) redirect_to accounts_path, notice: t(".success") @@ -29,7 +30,7 @@ def set_plaid_item end def plaid_item_params - params.require(:plaid_item).permit(:public_token, metadata: {}) + params.require(:plaid_item).permit(:public_token, :region, metadata: {}) end def item_name diff --git a/app/javascript/controllers/plaid_controller.js b/app/javascript/controllers/plaid_controller.js index 14b0c182ea1..5f4f1d29248 100644 --- a/app/javascript/controllers/plaid_controller.js +++ b/app/javascript/controllers/plaid_controller.js @@ -4,6 +4,7 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static values = { linkToken: String, + region: { type: String, default: "us" } }; open() { @@ -31,6 +32,7 @@ export default class extends Controller { plaid_item: { public_token: public_token, metadata: metadata, + region: this.regionValue }, }), }).then((response) => { diff --git a/app/models/concerns/plaidable.rb b/app/models/concerns/plaidable.rb index 2913006b803..838b4837e4b 100644 --- a/app/models/concerns/plaidable.rb +++ b/app/models/concerns/plaidable.rb @@ -9,14 +9,15 @@ def plaid_provider def plaid_eu_provider Provider::Plaid.new if Rails.application.config.plaid_eu end - end - private - def plaid_provider - self.class.plaid_provider + def plaid_provider_for(plaid_item) + return nil unless plaid_item + plaid_item.eu? ? plaid_eu_provider : plaid_provider end + end - def plaid_eu_provider - self.class.plaid_eu_provider + private + def plaid_provider_for(plaid_item) + self.class.plaid_provider_for(plaid_item) end end diff --git a/app/models/plaid_item.rb b/app/models/plaid_item.rb index 79087900c3c..1230613b713 100644 --- a/app/models/plaid_item.rb +++ b/app/models/plaid_item.rb @@ -1,6 +1,9 @@ class PlaidItem < ApplicationRecord include Plaidable, Syncable + enum :plaid_region, { us: "us", eu: "eu" } + validates :plaid_region, inclusion: { in: plaid_regions.keys } + if Rails.application.credentials.active_record_encryption.present? encrypts :access_token, deterministic: true end @@ -56,10 +59,11 @@ def destroy_later private def fetch_and_load_plaid_data data = {} - item = plaid_provider.get_item(access_token).item + provider = plaid_provider_for(self) + item = provider.get_item(access_token).item update!(available_products: item.available_products, billed_products: item.billed_products) - fetched_accounts = plaid_provider.get_item_accounts(self).accounts + fetched_accounts = provider.get_item_accounts(self).accounts data[:accounts] = fetched_accounts || [] internal_plaid_accounts = fetched_accounts.map do |account| diff --git a/app/views/accounts/new/_method_selector.html.erb b/app/views/accounts/new/_method_selector.html.erb index 1e3d9f66e8b..a4e2afa98a1 100644 --- a/app/views/accounts/new/_method_selector.html.erb +++ b/app/views/accounts/new/_method_selector.html.erb @@ -10,31 +10,23 @@ <% end %> <% if link_token.present? %> - <% if Current.family.country.to_s.downcase != "us" %> - <%# US Link %> - + <%# Default US-only Link %> + - <%# EU Link %> + <%# EU Link %> + <% unless Current.family.country == "US" %> - <% else %> - <%# Default US-only Link %> - - <% end %> + <% end %> <% end %> <% end %> diff --git a/config/initializers/plaid.rb b/config/initializers/plaid.rb index 5a7a99510d1..1925158a01f 100644 --- a/config/initializers/plaid.rb +++ b/config/initializers/plaid.rb @@ -11,7 +11,7 @@ if ENV["PLAID_EU_CLIENT_ID"].present? && ENV["PLAID_EU_SECRET"].present? config.plaid_eu = Plaid::Configuration.new - config.plaid_eu.server_index = Plaid::Configuration::Environment[ENV["PLAID_EU_ENV"] || "sandbox"] + config.plaid_eu.server_index = Plaid::Configuration::Environment[ENV["PLAID_ENV"] || "sandbox"] config.plaid_eu.api_key["PLAID-CLIENT-ID"] = ENV["PLAID_EU_CLIENT_ID"] config.plaid_eu.api_key["PLAID-SECRET"] = ENV["PLAID_EU_SECRET"] end diff --git a/db/migrate/20241219151540_add_region_to_plaid_item.rb b/db/migrate/20241219151540_add_region_to_plaid_item.rb new file mode 100644 index 00000000000..55e9607505f --- /dev/null +++ b/db/migrate/20241219151540_add_region_to_plaid_item.rb @@ -0,0 +1,5 @@ +class AddRegionToPlaidItem < ActiveRecord::Migration[7.2] + def change + add_column :plaid_items, :plaid_region, :string, null: false, default: "us" + end +end diff --git a/db/schema.rb b/db/schema.rb index 3ee8e4483d9..1616c5bd78d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_12_17_141716) do +ActiveRecord::Schema[7.2].define(version: 2024_12_19_151540) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -532,6 +532,7 @@ t.string "available_products", default: [], array: true t.string "billed_products", default: [], array: true t.datetime "last_synced_at" + t.string "plaid_region", default: "us", null: false t.index ["family_id"], name: "index_plaid_items_on_family_id" end From de3e2e11ff75ddfe18c4e3af36b1bcbc0c0283eb Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Thu, 19 Dec 2024 09:22:08 -0600 Subject: [PATCH 3/4] Lint --- app/models/family.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/family.rb b/app/models/family.rb index 1c2ff25c1f8..d6f57b6e31b 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -47,11 +47,11 @@ def syncing? def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us) provider = case region - when :eu + when :eu self.class.plaid_eu_provider else self.class.plaid_provider - end + end return nil unless provider From 37ca549e86e9624befee0c9335df8a7fc747dd49 Mon Sep 17 00:00:00 2001 From: Josh Pigford Date: Thu, 19 Dec 2024 09:33:09 -0600 Subject: [PATCH 4/4] Temp fix for rubocop isseus --- app/models/family.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/family.rb b/app/models/family.rb index d6f57b6e31b..75e8d1c515d 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -1,3 +1,4 @@ +# rubocop:disable Layout/ElseAlignment, Layout/IndentationWidth class Family < ApplicationRecord include Plaidable, Syncable @@ -182,3 +183,4 @@ def primary_user users.order(:created_at).first end end +# rubocop:enable Layout/ElseAlignment, Layout/IndentationWidth