1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-08 23:15:24 +02:00

PlaidConnectable concern

This commit is contained in:
Zach Gollwitzer 2025-05-10 10:28:00 -04:00
parent a268c5a563
commit c8b1e1d059
6 changed files with 101 additions and 79 deletions

View file

@ -2,8 +2,8 @@ class PlaidItemsController < ApplicationController
before_action :set_plaid_item, only: %i[destroy sync] before_action :set_plaid_item, only: %i[destroy sync]
def create def create
Current.family.plaid_items.create_from_public_token( Current.family.create_plaid_item!(
plaid_item_params[:public_token], public_token: plaid_item_params[:public_token],
item_name: item_name, item_name: item_name,
region: plaid_item_params[:region] region: plaid_item_params[:region]
) )

View file

@ -1,5 +1,5 @@
class Family < ApplicationRecord class Family < ApplicationRecord
include Syncable, AutoTransferMatchable, Subscribeable include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable
DATE_FORMATS = [ DATE_FORMATS = [
[ "MM-DD-YYYY", "%m-%d-%Y" ], [ "MM-DD-YYYY", "%m-%d-%Y" ],
@ -15,7 +15,6 @@ class Family < ApplicationRecord
has_many :users, dependent: :destroy has_many :users, dependent: :destroy
has_many :accounts, dependent: :destroy has_many :accounts, dependent: :destroy
has_many :plaid_items, dependent: :destroy
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
has_many :imports, dependent: :destroy has_many :imports, dependent: :destroy
@ -65,69 +64,10 @@ class Family < ApplicationRecord
@income_statement ||= IncomeStatement.new(self) @income_statement ||= IncomeStatement.new(self)
end end
def sync_data(sync, start_date: nil)
# We don't rely on this value to guard the app, but keep it eventually consistent
sync_trial_status!
Rails.logger.info("Syncing accounts for family #{id}")
accounts.manual.each do |account|
account.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Syncing plaid items for family #{id}")
plaid_items.each do |plaid_item|
plaid_item.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Applying rules for family #{id}")
rules.each do |rule|
rule.apply_later
end
end
def remove_syncing_notice!
broadcast_remove target: "syncing-notice"
end
def post_sync(sync)
auto_match_transfers!
broadcast_refresh
end
# If family has any syncs pending/syncing within the last 10 minutes, we show a persistent "syncing" notice.
# Ignore syncs older than 10 minutes as they are considered "stale"
def syncing?
Sync.where(
"(syncable_type = 'Family' AND syncable_id = ?) OR
(syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR
(syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?))",
id, id, id
).where(status: [ "pending", "syncing" ], created_at: 10.minutes.ago..).exists?
end
def eu? def eu?
country != "US" && country != "CA" country != "US" && country != "CA"
end end
def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us, access_token: nil)
provider = if region.to_sym == :eu
Provider::Registry.get_provider(:plaid_eu)
else
Provider::Registry.get_provider(:plaid_us)
end
# early return when no provider
return nil unless provider
provider.get_link_token(
user_id: id,
webhooks_url: webhooks_url,
redirect_url: redirect_url,
accountable_type: accountable_type,
access_token: access_token
).link_token
end
def requires_data_provider? def requires_data_provider?
# If family has any trades, they need a provider for historical prices # If family has any trades, they need a provider for historical prices
return true if trades.any? return true if trades.any?

View file

@ -0,0 +1,51 @@
module Family::PlaidConnectable
extend ActiveSupport::Concern
included do
has_many :plaid_items, dependent: :destroy
end
def create_plaid_item!(public_token:, item_name:, region:)
provider = plaid_provider_for_region(region)
public_token_response = provider.exchange_public_token(public_token)
plaid_item = plaid_items.create!(
name: item_name,
plaid_id: public_token_response.item_id,
access_token: public_token_response.access_token,
plaid_region: region
)
plaid_item.sync_later
plaid_item
end
def get_link_token(webhooks_url:, redirect_url:, accountable_type: nil, region: :us, access_token: nil)
return nil unless plaid_us || plaid_eu
provider = plaid_provider_for_region(region)
provider.get_link_token(
user_id: self.id,
webhooks_url: webhooks_url,
redirect_url: redirect_url,
accountable_type: accountable_type,
access_token: access_token
).link_token
end
private
def plaid_us
@plaid ||= Provider::Registry.get_provider(:plaid_us)
end
def plaid_eu
@plaid_eu ||= Provider::Registry.get_provider(:plaid_eu)
end
def plaid_provider_for_region(region)
region.to_sym == :eu ? plaid_eu : plaid_us
end
end

View file

@ -0,0 +1,46 @@
module Family::Syncable
extend ActiveSupport::Concern
# Top-level, generic Syncable concern
include ::Syncable
def sync_data(sync, start_date: nil)
# We don't rely on this value to guard the app, but keep it eventually consistent
sync_trial_status!
Rails.logger.info("Syncing accounts for family #{id}")
accounts.manual.each do |account|
account.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Syncing plaid items for family #{id}")
plaid_items.each do |plaid_item|
plaid_item.sync_later(start_date: start_date, parent_sync: sync)
end
Rails.logger.info("Applying rules for family #{id}")
rules.each do |rule|
rule.apply_later
end
end
def remove_syncing_notice!
broadcast_remove target: "syncing-notice"
end
def post_sync(sync)
auto_match_transfers!
broadcast_refresh
end
# If family has any syncs pending/syncing within the last 10 minutes, we show a persistent "syncing" notice.
# Ignore syncs older than 10 minutes as they are considered "stale"
def syncing?
Sync.where(
"(syncable_type = 'Family' AND syncable_id = ?) OR
(syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR
(syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?))",
id, id, id
).where(status: [ "pending", "syncing" ], created_at: 10.minutes.ago..).exists?
end
end

View file

@ -22,21 +22,6 @@ class PlaidItem < ApplicationRecord
scope :ordered, -> { order(created_at: :desc) } scope :ordered, -> { order(created_at: :desc) }
scope :needs_update, -> { where(status: :requires_update) } scope :needs_update, -> { where(status: :requires_update) }
class << self
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,
plaid_id: response.item_id,
access_token: response.access_token,
plaid_region: region
)
new_plaid_item.sync_later
end
end
def sync_data(sync, start_date: nil) def sync_data(sync, start_date: nil)
begin begin
Rails.logger.info("Fetching and loading Plaid data") Rails.logger.info("Fetching and loading Plaid data")

View file

@ -8,7 +8,7 @@ class PlaidItemsControllerTest < ActionDispatch::IntegrationTest
test "create" do test "create" do
@plaid_provider = mock @plaid_provider = mock
PlaidItem.expects(:plaid_provider_for_region).with("us").returns(@plaid_provider) Provider::Registry.expects(:get_provider).with(:plaid_us).returns(@plaid_provider)
public_token = "public-sandbox-1234" public_token = "public-sandbox-1234"