Skip to content
This repository was archived by the owner on Jul 27, 2025. It is now read-only.

Commit cbba2ba

Browse files
authored
Basic Plaid Integration (#1433)
* Basic plaid data model and linking * Remove institutions, add plaid items * Improve schema and Plaid provider * Add webhook verification sketch * Webhook verification * Item accounts and balances sync setup * Provide test encryption keys * Fix test * Only provide encryption keys in prod * Try defining keys in test env * Consolidate account sync logic * Add back plaid account initialization * Plaid transaction sync * Sync UI overhaul for Plaid * Add liability and investment syncing * Handle investment webhooks and process current day holdings * Remove logs * Remove "all" period select for performance * fix amount calc * Remove todo comment * Coming soon for investment historical data * Document Plaid configuration * Listen for holding updates
1 parent 3bc9da4 commit cbba2ba

File tree

127 files changed

+1537
-841
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+1537
-841
lines changed

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,12 @@ GITHUB_REPO_BRANCH=main
110110
#
111111
STRIPE_PUBLISHABLE_KEY=
112112
STRIPE_SECRET_KEY=
113-
STRIPE_WEBHOOK_SECRET=
113+
STRIPE_WEBHOOK_SECRET=
114+
115+
# ======================================================================================================
116+
# Plaid Configuration
117+
# ======================================================================================================
118+
#
119+
PLAID_CLIENT_ID=
120+
PLAID_SECRET=
121+
PLAID_ENV=

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ gem "image_processing", ">= 1.2"
3737

3838
# Other
3939
gem "bcrypt", "~> 3.1"
40+
gem "jwt"
4041
gem "faraday"
4142
gem "faraday-retry"
4243
gem "faraday-multipart"
@@ -50,6 +51,7 @@ gem "redcarpet"
5051
gem "stripe"
5152
gem "intercom-rails"
5253
gem "holidays"
54+
gem "plaid"
5355

5456
group :development, :test do
5557
gem "debug", platforms: %i[mri windows]

Gemfile.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ GEM
224224
reline (>= 0.4.2)
225225
jmespath (1.6.2)
226226
json (2.7.2)
227+
jwt (2.9.3)
228+
base64
227229
language_server-protocol (3.17.0.3)
228230
launchy (3.0.1)
229231
addressable (~> 2.8)
@@ -284,6 +286,9 @@ GEM
284286
ast (~> 2.4.1)
285287
racc
286288
pg (1.5.9)
289+
plaid (33.0.0)
290+
faraday (>= 1.0.1, < 3.0)
291+
faraday-multipart (>= 1.0.1, < 2.0)
287292
prism (1.2.0)
288293
propshaft (1.1.0)
289294
actionpack (>= 7.0.0)
@@ -495,12 +500,14 @@ DEPENDENCIES
495500
importmap-rails
496501
inline_svg
497502
intercom-rails
503+
jwt
498504
letter_opener
499505
lucide-rails!
500506
mocha
501507
octokit
502508
pagy
503509
pg (~> 1.5)
510+
plaid
504511
propshaft
505512
puma (>= 5.0)
506513
rails (~> 7.2.2)
Lines changed: 10 additions & 0 deletions
Loading

app/assets/stylesheets/application.tailwind.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
}
102102

103103
.btn {
104-
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer focus:outline-gray-500;
104+
@apply px-3 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:cursor-not-allowed focus:outline-gray-500;
105105
}
106106

107107
.btn--primary {
@@ -113,7 +113,7 @@
113113
}
114114

115115
.btn--outline {
116-
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50;
116+
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50 disabled:bg-gray-50 disabled:hover:bg-gray-50 disabled:text-gray-400;
117117
}
118118

119119
.btn--ghost {

app/controllers/accounts_controller.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ class AccountsController < ApplicationController
44
before_action :set_account, only: %i[sync]
55

66
def index
7-
@institutions = Current.family.institutions
8-
@accounts = Current.family.accounts.ungrouped.alphabetically
7+
@manual_accounts = Current.family.accounts.manual.active.alphabetically
8+
@plaid_items = Current.family.plaid_items.active.ordered
99
end
1010

1111
def summary
@@ -27,11 +27,16 @@ def sync
2727
unless @account.syncing?
2828
@account.sync_later
2929
end
30+
31+
redirect_to account_path(@account)
3032
end
3133

3234
def sync_all
33-
Current.family.accounts.active.sync
34-
redirect_back_or_to accounts_path, notice: t(".success")
35+
unless Current.family.syncing?
36+
Current.family.sync_later
37+
end
38+
39+
redirect_to accounts_path
3540
end
3641

3742
private

app/controllers/concerns/accountable_resource.rb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module AccountableResource
44
included do
55
layout :with_sidebar
66
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
7+
before_action :set_link_token, only: :new
78
end
89

910
class_methods do
@@ -16,8 +17,7 @@ def permitted_accountable_attributes(*attrs)
1617
def new
1718
@account = Current.family.accounts.build(
1819
currency: Current.family.currency,
19-
accountable: accountable_type.new,
20-
institution_id: params[:institution_id]
20+
accountable: accountable_type.new
2121
)
2222
end
2323

@@ -29,20 +29,35 @@ def edit
2929

3030
def create
3131
@account = Current.family.accounts.create_and_sync(account_params.except(:return_to))
32-
redirect_to account_params[:return_to].presence || @account, notice: t(".success")
32+
redirect_to account_params[:return_to].presence || @account, notice: t("accounts.create.success", type: accountable_type.name.underscore.humanize)
3333
end
3434

3535
def update
3636
@account.update_with_sync!(account_params.except(:return_to))
37-
redirect_back_or_to @account, notice: t(".success")
37+
redirect_back_or_to @account, notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize)
3838
end
3939

4040
def destroy
41-
@account.destroy!
42-
redirect_to accounts_path, notice: t(".success")
41+
@account.destroy_later
42+
redirect_to accounts_path, notice: t("accounts.destroy.success", type: accountable_type.name.underscore.humanize)
4343
end
4444

4545
private
46+
def set_link_token
47+
@link_token = Current.family.get_link_token(
48+
webhooks_url: webhooks_url,
49+
redirect_url: accounts_url,
50+
accountable_type: accountable_type.name
51+
)
52+
end
53+
54+
def webhooks_url
55+
return webhooks_plaid_url if Rails.env.production?
56+
57+
base_url = ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/"))
58+
base_url + "/webhooks/plaid"
59+
end
60+
4661
def accountable_type
4762
controller_name.classify.constantize
4863
end
@@ -53,7 +68,7 @@ def set_account
5368

5469
def account_params
5570
params.require(:account).permit(
56-
:name, :is_active, :balance, :subtype, :currency, :institution_id, :accountable_type, :return_to,
71+
:name, :is_active, :balance, :subtype, :currency, :accountable_type, :return_to,
5772
accountable_attributes: self.class.permitted_accountable_attributes
5873
)
5974
end

app/controllers/concerns/auto_sync.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ module AutoSync
22
extend ActiveSupport::Concern
33

44
included do
5-
before_action :sync_family, if: -> { Current.family.present? && Current.family.needs_sync? }
5+
before_action :sync_family, if: :family_needs_auto_sync?
66
end
77

88
private
9-
109
def sync_family
11-
Current.family.sync
10+
Current.family.update!(last_synced_at: Time.current)
11+
Current.family.sync_later
12+
end
13+
14+
def family_needs_auto_sync?
15+
return false unless Current.family.present?
16+
return false unless Current.family.accounts.any?
17+
18+
Current.family.last_synced_at.blank? ||
19+
Current.family.last_synced_at.to_date < Date.current
1220
end
1321
end

app/controllers/institutions_controller.rb

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class PlaidItemsController < ApplicationController
2+
before_action :set_plaid_item, only: %i[destroy sync]
3+
4+
def create
5+
Current.family.plaid_items.create_from_public_token(
6+
plaid_item_params[:public_token],
7+
item_name: item_name,
8+
)
9+
10+
redirect_to accounts_path, notice: t(".success")
11+
end
12+
13+
def destroy
14+
@plaid_item.destroy_later
15+
redirect_to accounts_path, notice: t(".success")
16+
end
17+
18+
def sync
19+
unless @plaid_item.syncing?
20+
@plaid_item.sync_later
21+
end
22+
23+
redirect_to accounts_path
24+
end
25+
26+
private
27+
def set_plaid_item
28+
@plaid_item = Current.family.plaid_items.find(params[:id])
29+
end
30+
31+
def plaid_item_params
32+
params.require(:plaid_item).permit(:public_token, metadata: {})
33+
end
34+
35+
def item_name
36+
plaid_item_params.dig(:metadata, :institution, :name)
37+
end
38+
end

0 commit comments

Comments
 (0)