Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Plaid Integration #1433

Merged
merged 26 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3796d7e
Basic plaid data model and linking
zachgoll Oct 29, 2024
5a52bc7
Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/p…
zachgoll Nov 6, 2024
775cd90
Remove institutions, add plaid items
zachgoll Nov 6, 2024
f8b9168
Improve schema and Plaid provider
zachgoll Nov 7, 2024
cc3da86
Add webhook verification sketch
zachgoll Nov 7, 2024
4a5a410
Webhook verification
zachgoll Nov 7, 2024
babad55
Item accounts and balances sync setup
zachgoll Nov 8, 2024
b9c2589
Provide test encryption keys
zachgoll Nov 8, 2024
2806573
Fix test
zachgoll Nov 8, 2024
e46694e
Only provide encryption keys in prod
zachgoll Nov 8, 2024
4067892
Try defining keys in test env
zachgoll Nov 8, 2024
e11e96d
Consolidate account sync logic
zachgoll Nov 8, 2024
e1acd23
Add back plaid account initialization
zachgoll Nov 8, 2024
a8ad667
Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/p…
zachgoll Nov 8, 2024
04fb03e
Plaid transaction sync
zachgoll Nov 13, 2024
d3b163b
Sync UI overhaul for Plaid
zachgoll Nov 13, 2024
5407c53
Add liability and investment syncing
zachgoll Nov 15, 2024
7c87d38
Merge branch 'main' of github.com:maybe-finance/maybe into zachgoll/p…
zachgoll Nov 15, 2024
a96e3d1
Handle investment webhooks and process current day holdings
zachgoll Nov 15, 2024
676e11a
Remove logs
zachgoll Nov 15, 2024
fcd6ca4
Remove "all" period select for performance
zachgoll Nov 15, 2024
64d8fe4
fix amount calc
zachgoll Nov 15, 2024
38717f2
Remove todo comment
zachgoll Nov 15, 2024
1190490
Coming soon for investment historical data
zachgoll Nov 15, 2024
297ee90
Document Plaid configuration
zachgoll Nov 15, 2024
7f2a085
Listen for holding updates
zachgoll Nov 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,12 @@ GITHUB_REPO_BRANCH=main
#
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET=

# ======================================================================================================
# Plaid Configuration
# ======================================================================================================
#
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_ENV=
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gem "image_processing", ">= 1.2"

# Other
gem "bcrypt", "~> 3.1"
gem "jwt"
gem "faraday"
gem "faraday-retry"
gem "faraday-multipart"
Expand All @@ -50,6 +51,7 @@ gem "redcarpet"
gem "stripe"
gem "intercom-rails"
gem "holidays"
gem "plaid"

group :development, :test do
gem "debug", platforms: %i[mri windows]
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ GEM
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.2)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
launchy (3.0.1)
addressable (~> 2.8)
Expand Down Expand Up @@ -284,6 +286,9 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
plaid (33.0.0)
faraday (>= 1.0.1, < 3.0)
faraday-multipart (>= 1.0.1, < 2.0)
prism (1.2.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
Expand Down Expand Up @@ -495,12 +500,14 @@ DEPENDENCIES
importmap-rails
inline_svg
intercom-rails
jwt
letter_opener
lucide-rails!
mocha
octokit
pagy
pg (~> 1.5)
plaid
propshaft
puma (>= 5.0)
rails (~> 7.2.2)
Expand Down
10 changes: 10 additions & 0 deletions app/assets/images/placeholder-graph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions app/assets/stylesheets/application.tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
}

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

.btn--primary {
Expand All @@ -113,7 +113,7 @@
}

.btn--outline {
@apply border border-alpha-black-200 text-gray-900 hover:bg-gray-50;
@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;
}

.btn--ghost {
Expand Down
13 changes: 9 additions & 4 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ class AccountsController < ApplicationController
before_action :set_account, only: %i[sync]

def index
@institutions = Current.family.institutions
@accounts = Current.family.accounts.ungrouped.alphabetically
@manual_accounts = Current.family.accounts.manual.active.alphabetically
@plaid_items = Current.family.plaid_items.active.ordered
end

def summary
Expand All @@ -27,11 +27,16 @@ def sync
unless @account.syncing?
@account.sync_later
end

redirect_to account_path(@account)
end

def sync_all
Current.family.accounts.active.sync
redirect_back_or_to accounts_path, notice: t(".success")
unless Current.family.syncing?
Current.family.sync_later
end

redirect_to accounts_path
end

private
Expand Down
29 changes: 22 additions & 7 deletions app/controllers/concerns/accountable_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module AccountableResource
included do
layout :with_sidebar
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
before_action :set_link_token, only: :new
end

class_methods do
Expand All @@ -16,8 +17,7 @@ def permitted_accountable_attributes(*attrs)
def new
@account = Current.family.accounts.build(
currency: Current.family.currency,
accountable: accountable_type.new,
institution_id: params[:institution_id]
accountable: accountable_type.new
)
end

Expand All @@ -29,20 +29,35 @@ def edit

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

def update
@account.update_with_sync!(account_params.except(:return_to))
redirect_back_or_to @account, notice: t(".success")
redirect_back_or_to @account, notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize)
end

def destroy
@account.destroy!
redirect_to accounts_path, notice: t(".success")
@account.destroy_later
redirect_to accounts_path, notice: t("accounts.destroy.success", type: accountable_type.name.underscore.humanize)
end

private
def set_link_token
@link_token = Current.family.get_link_token(
webhooks_url: webhooks_url,
redirect_url: accounts_url,
accountable_type: accountable_type.name
)
end

def webhooks_url
return webhooks_plaid_url if Rails.env.production?

base_url = ENV.fetch("DEV_WEBHOOKS_URL", root_url.chomp("/"))
base_url + "/webhooks/plaid"
end

def accountable_type
controller_name.classify.constantize
end
Expand All @@ -53,7 +68,7 @@ def set_account

def account_params
params.require(:account).permit(
:name, :is_active, :balance, :subtype, :currency, :institution_id, :accountable_type, :return_to,
:name, :is_active, :balance, :subtype, :currency, :accountable_type, :return_to,
accountable_attributes: self.class.permitted_accountable_attributes
)
end
Expand Down
14 changes: 11 additions & 3 deletions app/controllers/concerns/auto_sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ module AutoSync
extend ActiveSupport::Concern

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

private

def sync_family
Current.family.sync
Current.family.update!(last_synced_at: Time.current)
Current.family.sync_later
end

def family_needs_auto_sync?
return false unless Current.family.present?
return false unless Current.family.accounts.any?

Current.family.last_synced_at.blank? ||
Current.family.last_synced_at.to_date < Date.current
end
end
40 changes: 0 additions & 40 deletions app/controllers/institutions_controller.rb

This file was deleted.

38 changes: 38 additions & 0 deletions app/controllers/plaid_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
class PlaidItemsController < ApplicationController
before_action :set_plaid_item, only: %i[destroy sync]

def create
Current.family.plaid_items.create_from_public_token(
plaid_item_params[:public_token],
item_name: item_name,
)

redirect_to accounts_path, notice: t(".success")
end

def destroy
@plaid_item.destroy_later
redirect_to accounts_path, notice: t(".success")
end

def sync
unless @plaid_item.syncing?
@plaid_item.sync_later
end

redirect_to accounts_path
end

private
def set_plaid_item
@plaid_item = Current.family.plaid_items.find(params[:id])
end

def plaid_item_params
params.require(:plaid_item).permit(:public_token, metadata: {})
end

def item_name
plaid_item_params.dig(:metadata, :institution, :name)
end
end
3 changes: 1 addition & 2 deletions app/controllers/properties_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ def new
currency: Current.family.currency,
accountable: Property.new(
address: Address.new
),
institution_id: params[:institution_id]
)
)
end

Expand Down
14 changes: 13 additions & 1 deletion app/controllers/webhooks_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token, only: [ :stripe ]
skip_before_action :verify_authenticity_token
skip_authentication

def plaid
webhook_body = request.body.read
plaid_verification_header = request.headers["Plaid-Verification"]

Provider::Plaid.validate_webhook!(plaid_verification_header, webhook_body)
Provider::Plaid.process_webhook(webhook_body)

render json: { received: true }, status: :ok
rescue => error
render json: { error: "Invalid webhook: #{error.message}" }, status: :bad_request
end

def stripe
webhook_body = request.body.read
sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def radio_tab_tag(form:, name:, value:, label:, icon:, checked: false, disabled:
end

def period_select(form:, selected:, classes: "border border-alpha-black-100 shadow-xs rounded-lg text-sm pr-7 cursor-pointer text-gray-900 focus:outline-none focus:ring-0")
periods_for_select = [ [ "7D", "last_7_days" ], [ "1M", "last_30_days" ], [ "1Y", "last_365_days" ], [ "All", "all" ] ]
periods_for_select = [ [ "7D", "last_7_days" ], [ "1M", "last_30_days" ], [ "1Y", "last_365_days" ] ]
form.select(:period, periods_for_select, { selected: selected }, class: classes, data: { "auto-submit-form-target": "auto" })
end

Expand Down
5 changes: 0 additions & 5 deletions app/helpers/institutions_helper.rb

This file was deleted.

52 changes: 52 additions & 0 deletions app/javascript/controllers/plaid_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Controller } from "@hotwired/stimulus";

// Connects to data-controller="plaid"
export default class extends Controller {
static values = {
linkToken: String,
};

open() {
const handler = Plaid.create({
token: this.linkTokenValue,
onSuccess: this.handleSuccess,
onLoad: this.handleLoad,
onExit: this.handleExit,
onEvent: this.handleEvent,
});

handler.open();
}

handleSuccess(public_token, metadata) {
fetch("/plaid_items", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": document.querySelector('[name="csrf-token"]').content,
},
body: JSON.stringify({
plaid_item: {
public_token: public_token,
metadata: metadata,
},
}),
}).then((response) => {
if (response.redirected) {
window.location.href = response.url;
}
});
}

handleExit(err, metadata) {
// no-op
}

handleEvent(eventName, metadata) {
// no-op
}

handleLoad() {
// no-op
}
}
Loading