This commit is contained in:
2021-12-20 20:20:40 -08:00
parent 13c689221c
commit 563e1c51ff
20 changed files with 3144 additions and 337 deletions

View File

@@ -35,11 +35,13 @@
[:location-matches [:id :location :match]]
[:bank-accounts [:id :start-date :numeric-code :code :number :bank-name :bank-code :check-number :name :routing :type :sort-order :visible :yodlee-account-id
[:yodlee-account [:name :id :number]]
[:plaid-account [:name :id :number]]
[:intuit-bank-account [:name :id :external-id]]
:locations :include-in-reports :current-balance :yodlee-balance-old] ]
[:address [:street1 :street2 :city :state :zip]]
[:forecasted-transactions [:id :amount :identifier :day-of-month]]]
(= "admin" (or (get (jwt->data token) "role") (get (jwt->data token) "user/role")) ) (conj [:yodlee-provider-accounts [:id [:accounts [:id :name :number :available-balance]]]])))
(= "admin" (or (get (jwt->data token) "role") (get (jwt->data token) "user/role")) ) (into [[:yodlee-provider-accounts [:id [:accounts [:id :name :number :available-balance]]]]
[:plaid-items [:id [:accounts [:id :name :number :balance]]]]])))
(re-frame/reg-event-fx
::initialize-db

View File

@@ -32,6 +32,7 @@
[auto-ap.views.pages.admin.import-batches :refer [import-batches-page]]
[auto-ap.views.pages.admin.yodlee :refer [admin-yodlee-page]]
[auto-ap.views.pages.admin.yodlee2 :as yodlee2]
[auto-ap.views.pages.admin.plaid :as plaid]
[auto-ap.entities.clients :as clients]))
(defmulti page (fn [active-page] active-page))
@@ -114,6 +115,9 @@
(defmethod page :admin-yodlee2 [_]
(yodlee2/admin-yodle-provider-accounts-page))
(defmethod page :admin-plaid [_]
(plaid/admin-plaid-page))
(defmethod page :admin-accounts [_]
(admin-accounts-page))

View File

@@ -127,7 +127,7 @@
:identifier identifier
:amount amount})
(:forecasted-transactions new-client-data))
:bank-accounts (map (fn [{:keys [number name check-number intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? sort-order visible yodlee-account-id locations yodlee-account]}]
:bank-accounts (map (fn [{:keys [number name check-number plaid-account intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? sort-order visible yodlee-account-id locations yodlee-account]}]
(println intuit-bank-account)
{:number number
:name name
@@ -153,6 +153,7 @@
:yodlee-account-id (when-not (str/blank? yodlee-account-id)
(js/parseInt yodlee-account-id))
:yodlee-account (:id yodlee-account)
:plaid-account (:id plaid-account)
:intuit-bank-account (:id intuit-bank-account)
:code (if new?
(str (:code new-client-data) "-" code)
@@ -277,6 +278,15 @@
(mapcat :accounts (:yodlee-provider-accounts (get clients id) ))
[])))
(re-frame/reg-sub
::plaid-accounts
:<- [::subs/clients-by-id]
(fn [clients [_ id]]
(if id
(mapcat :accounts (:plaid-items (get clients id) ))
[])))
(defn bank-account-card [new-client {:keys [active? new? type visible code name number check-number id sort-order] :as bank-account} first? last?]
(let [{:keys [form field raw-field error-notification submit-button ]} client-form]
@@ -397,7 +407,12 @@
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m)))
:type "typeahead-v3"
:field [:bank-accounts sort-order :intuit-bank-account]}]]])
:field [:bank-accounts sort-order :intuit-bank-account]}]]
[field "Plaid Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m)))
:type "typeahead-v3"
:field [:bank-accounts sort-order :plaid-account]}]]])
(when (#{:credit ":credit"} type )
[:div
@@ -432,7 +447,13 @@
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m)))
:type "typeahead-v3"
:field [:bank-accounts sort-order :intuit-bank-account]}]]])
:field [:bank-accounts sort-order :intuit-bank-account]}]]
[field "Plaid Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m)))
:type "typeahead-v3"
:field [:bank-accounts sort-order :plaid-account]}]]
])
[:div.field
[:label.label "Locations"]
[:div.control

View File

@@ -0,0 +1,196 @@
(ns auto-ap.views.pages.admin.plaid
(:require
[auto-ap.effects.forward :as forward]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.pages.admin.plaid.table :as table]
[auto-ap.views.utils :refer [dispatch-event with-user]]
[re-frame.core :as re-frame]
[react-plaid-link :refer [usePlaidLink]]
[reagent.core :as reagent]
[auto-ap.views.pages.data-page :as data-page]
[clojure.set :as set]
[vimsical.re-frame.fx.track :as track]))
(re-frame/reg-sub
::link-token
(fn [db]
(-> db ::link-token)))
(re-frame/reg-sub
::message
(fn [db]
(-> db ::message)))
(re-frame/reg-sub
::params
:<- [::table/params]
(fn [table-params]
table-params))
(re-frame/reg-sub
::plaid-items
(fn [db]
(::plaid-items db)))
(re-frame/reg-event-fx
::params-change
(fn [_ [_ params]]
{:set-uri-params params}))
(re-frame/reg-event-fx
::data-requested
(fn [{:keys [db]} [_ params]]
(println "PRAAMS" params)
{:graphql {:token (:user db)
:owns-state {:single ::page}
:query-obj {:venia/queries [{:query/data [:plaid-item-page {:client-id (:id @(re-frame/subscribe [::subs/client]))
:sort (:sort params)
:start (:start params 0)}
[[:plaid-items [:id :last-updated :status
[:client [:id]]
[:accounts [:id :name :number :balance]]]]
:count
:start
:end
:total]]
:query/alias :result}]
}
:on-success (fn [result]
[::data-page/received ::page
(set/rename-keys (:result result)
{:plaid-items :data})])}}))
(re-frame/reg-event-fx
::mounted
(fn [{:keys [db]} _]
{:dispatch [::data-requested]
::forward/register {:id ::plaid-item-deleted
:events #{::table/plaid-item-deleted}
:event-fn (fn [[_ query-result]]
[::data-requested])}
::track/register {:id ::params
:subscription [::data-page/params ::page]
:event-fn (fn [params]
[::data-requested params])}
:db (dissoc db
::link-token
::message)}))
(re-frame/reg-event-fx
::unmounted
(fn [{:keys [db]} _]
{::forward/dispose {:id ::plaid-item-deleted}}))
(re-frame/reg-event-fx
::get-link-token
[with-user]
(fn [{:keys [db]} [_ client]]
{:graphql {:token (:user db)
:owns-state {:single ::get-link-token}
:query-obj {:venia/queries [[:plaid-link-token {:client-id (:id @(re-frame/subscribe [::subs/client]))}
[:token]]]}
:on-success [::authenticated]}}))
(re-frame/reg-event-fx
::plaid-linked
(fn [{:keys [db]} [_ m]]
{:db (assoc db ::message (:message (:link-plaid m)))
:dispatch [::data-requested]}))
(re-frame/reg-event-fx
::exchange-token
[with-user]
(fn [{:keys [db]} [_ client public-token]]
{:graphql {:token (:user db)
:owns-state {:single ::get-link-token}
:query-obj
{:venia/operation {:operation/type :mutation
:operation/name "LinkPlaid"}
:venia/queries [{:query/data
[:link-plaid
{:client-code client
:public-token public-token}
[:message]]}]}
:on-success [::plaid-linked]}}))
(re-frame/reg-event-db
::authenticated
(fn [db [_ link-token]]
(-> db
(assoc-in [::link-token] (:token (:plaid-link-token link-token))))))
(re-frame/reg-event-db
::received
(fn [db [_ d]]
(assoc-in db [::plaid-items] (:plaid-item-page d))))
(defn plaid-item-table []
[table/table {:data-page ::page
:status @(re-frame/subscribe [::status/single ::page])}])
(defn link-button [{:keys [link-token client-code]}]
(let [plaid (usePlaidLink #js {:token link-token
:onSuccess (fn [x]
(re-frame/dispatch [::exchange-token client-code x]))})]
[:div
[:button.button.is-primary {:on-click (.-open plaid)}
[:span [:span.icon [:i.fa.fa-external-link]] " Go to plaid"]]]))
(defn plaid-link-token-button []
(let [status @(re-frame/subscribe [::status/single ::get-link-token])
client-code (:code @(re-frame/subscribe [::subs/client]))]
[:button.button.is-primary {:disabled (status/disabled-for status)
:class (status/class-for status)
:on-click (dispatch-event [::get-link-token client-code])}
"Authenticate with Plaid (" client-code ")"]))
(defn link-flow []
[:div
(let [link-token @(re-frame/subscribe [::link-token])
client-code (:code @(re-frame/subscribe [::subs/client]))]
(cond
(and link-token client-code)
[:div
"Authentication successful!"
[:f> link-button {:link-token link-token
:client-code client-code}]]
client-code
[plaid-link-token-button]
:else
nil))])
(defn admin-plaid-item-content []
(let [message @(re-frame/subscribe [::message])]
[:div
[:h1.title "Plaid Accounts"]
(when message
[:div.notification.is-info.is-light
message])
[plaid-item-table]
[link-flow]
]))
(defn admin-plaid-page []
(reagent/create-class
{:component-will-unmount #(re-frame/dispatch [::unmounted])
:component-did-mount #(re-frame/dispatch [::mounted])
:reagent-render (fn []
[side-bar-layout {:side-bar [admin-side-bar {}]
:main [admin-plaid-item-content]}])}))

View File

@@ -0,0 +1,109 @@
(ns auto-ap.views.pages.admin.plaid.table
(:require [auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.admin.users.form :as form]
[auto-ap.views.utils :refer [->$ action-cell-width date->str with-user dispatch-event]]
[re-frame.core :as re-frame]
[auto-ap.forms :as forms]
[auto-ap.views.pages.data-page :as data-page]))
#_(re-frame/reg-event-fx
::refreshed
[with-user ]
(fn [{:keys [user db]} [_ provider-account ]]
;; this is tracked in yodlee main, for refreshing
{}))
#_(re-frame/reg-event-fx
::request-refresh
[with-user ]
(fn [{:keys [user db]} [_ provider-account client-id ]]
{:http {:token user
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee2/provider-accounts/refresh/")
:owns-state {:multi ::refresh
:which provider-account}
:body {:client-id client-id
:provider-account-id provider-account}
:on-success [::refreshed provider-account]}}))
(re-frame/reg-event-fx
::plaid-item-deleted
(fn [{:keys [db]} [_ i result]]
{:dispatch [::modal/modal-closed ]}))
(re-frame/reg-event-fx
::delete-plaid-item
[with-user ]
(fn [{:keys [user db]} [_ id ]]
{:graphql {:token user
:owns-state {:single ::delete-plaid-item}
:query-obj
{:venia/operation {:operation/type :mutation
:operation/name "DeletePlaidItem"}
:venia/queries [{:query/data
[:delete-plaid-item
{:id id}
[:message]]}]}
:on-success [::plaid-item-deleted]}}))
(re-frame/reg-event-fx
::delete-requested
[with-user]
(fn [{:keys [user db]} [_ id]]
{:dispatch
[::modal/modal-requested {:title "Delete Provider account "
:body [:div "Are you sure you want to delete " id "?"]
:confirm {:value "Delete plaid accounts"
:status-from [::status/single ::delete-plaid-item]
:class "is-danger"
:on-click (dispatch-event [::delete-plaid-item id])
:close-event [::status/completed ::delete-plaid-item]}
:cancel? true}]}))
(re-frame/reg-event-fx
::params-changed
(fn [{:keys [db]} [_ p]]
{:db (assoc db ::params p)}))
(re-frame/reg-sub
::params
(fn [db]
(-> db ::params)))
(defn table [{:keys [status data-page]}]
(let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])
params @(re-frame/subscribe [::params])
statuses @(re-frame/subscribe [::status/multi ::refresh])]
[grid/grid {:data-page data-page
:column-count 5}
[grid/controls data]
[grid/table {:fullwidth true}
[grid/header
[grid/row {}
[grid/header-cell {:style {:width "18em"}} "Provider Account"]
[grid/header-cell {:style {:width "12em"}} "Status"]
[grid/header-cell {:style {:width "12em"}} "Last Updated"]
[grid/header-cell {} "Accounts"]
[grid/header-cell {:style {:width (action-cell-width 1)}} ]]]
[grid/body
(for [{:keys [id name accounts status last-updated clients] :as c} (:data data)]
^{:key (str name "-" id )}
[grid/row {:class (:class c) :id id}
[grid/cell {} id]
[grid/cell {} status]
[grid/cell {} (date->str last-updated)]
[grid/cell {}
[:ul
(for [a accounts]
^{:key (:id a)}
[:li (:name a) [:div.tag (->$ (:balance a))]])]]
[grid/cell {}
[:div.buttons
[buttons/fa-icon {:event [::delete-requested (:id c)]
:icon "fa-times"}]]]])]]]))

View File

@@ -22,304 +22,6 @@
[auto-ap.views.components.grid :as grid]
[auto-ap.effects.forward :as forward]))
(comment
(re-frame/reg-sub
::can-submit
(fn [db]
true))
(re-frame/reg-sub
::loading?
(fn [db]
(-> db ::yodlee :loading?)))
(re-frame/reg-sub
::accounts
(fn [db]
(-> db ::yodlee :accounts)))
(re-frame/reg-sub
::accounts-loading?
(fn [db]
(-> db ::yodlee :accounts-loading?)))
(re-frame/reg-sub
::provider-accounts-loading?
(fn [db]
(-> db ::provider-accounts-loading?)))
(re-frame/reg-sub
::provider-accounts
(fn [db]
(-> db ::provider-accounts)))
(re-frame/reg-event-fx
::mounted
(fn [{:keys [db]} _]
{:db (-> db
(assoc ::yodlee {:provider-accounts-loading? true})
(assoc ::save-error nil)
(assoc ::provider-accounts [])
(assoc ::provider-accounts-loading? true))
:http {:token (:user db)
:method :get
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee2/provider-accounts")
:on-success [::got-provider-accounts]
:on-error [::save-error]}}))
(re-frame/reg-event-fx
::kicked
(fn [{:keys [db]} [_ id state]]
{:dispatch [::mounted]}))
(re-frame/reg-event-fx
::kicked
(fn [{:keys [db]} [_ id state]]
{:dispatch [::mounted]}))
(re-frame/reg-event-fx
::kick
(fn [{:keys [db]} [_ id]]
{:http {:token (:user db)
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee2/provider-accounts/" id)
:on-success [::kicked id :kicked]
:on-error [::kicked id :errored]}}))
(re-frame/reg-event-fx
::got-accounts
(fn [{:keys [db]} [_ accounts]]
{:db (-> db
(assoc-in [::yodlee :accounts] accounts)
(assoc-in [::yodlee :accounts-loading?] false))}))
(re-frame/reg-event-fx
::got-provider-accounts
(fn [{:keys [db]} [_ accounts]]
{:db (-> db
(assoc-in [::provider-accounts] accounts)
(assoc-in [::provider-accounts-loading?] false))}))
(re-frame/reg-event-fx
::authenticated-mfa
(fn [{:keys [db]} [_ provider-account-id authentication]]
{:db (-> db
(assoc-in [::yodlee :authentication] authentication)
(assoc-in [::yodlee :loading?] false)
(forms/stop-form [::mfa-form provider-account-id]))}))
(re-frame/reg-event-fx
::save-error
(fn [{:keys [db]} [_ authentication]]
{:db (assoc :db ::load-error "error")}))
(defn yodlee-date->date [d]
(try
(some-> d
(str->date (:date-time-no-ms f/formatters))
)
(catch js/Error e
nil)))
(defn yodlee-date->str [d]
(try
(or (some-> d
(str->date (:date-time-no-ms f/formatters))
date->str)
"N/A")
(catch js/Error e
"N/A")))
(defn yodlee-accounts-table [accounts]
(let [bank-accounts @(re-frame/subscribe [::bank-accounts-by-yodlee-account-id])]
[:div
[:table.table
[:thead
[:tr
[:th "Account Name"]
[:th "Account Number"]
[:th "Yodlee Account Number"]
[:th "Balance"]
[:th "Yodlee Status"]
[:th "Usage"]]]
[:tbody
(for [account accounts]
^{:key (:id account)} [:tr
[:td (:accountName account)]
[:td (:accountNumber account)]
[:td (:id account)]
[:td.has-text-right (:amount (:balance account))]
[:td (str/join ", " (map :additionalStatus (:dataset account)))]
[:td
(when-let [bank-accounts (get bank-accounts (:id account))]
[:div.tags
(for [bank-account bank-accounts]
^{:key (:id bank-account)}
[:div.tag (:name bank-account) " (" (:code bank-account) ")"])])]
])]]]))
(re-frame/reg-event-fx
::reauthenticate-mfa
[with-user ]
(fn [{:keys [user db]} [_ provider-account-id ]]
{:db (forms/loading db [::mfa-form provider-account-id])
:http {:token user
:method :post
:headers {"Content-Type" "application/edn"}
:uri (str "/api/yodlee2/reauthenticate/" provider-account-id )
:body {"loginForm"
{"row"
(->> (get-in db [::forms/forms [::mfa-form provider-account-id]])
:data
:login
(sort-by (fn [[k v]] k))
(map second)
(map (fn [row]
{"field"
(mapv (fn [[k v]]
{"id" k
"value" v})
row)})))}
"field"
(mapv (fn [[k v]]
{"id" k
"value" v})
(:mfa (:data (get-in db [::forms/forms [::mfa-form provider-account-id]]))))}
:on-success [::authenticated-mfa provider-account-id]
:on-error [::forms/save-error [::mfa-form provider-account-id] ]}}))
(re-frame/reg-sub
::bank-accounts-by-yodlee-account-id
:<- [::subs/bank-accounts]
(fn [bank-accounts]
(group-by :yodlee-account-id bank-accounts)))
(defn yodlee-provider-accounts-table []
(let [bank-accounts @(re-frame/subscribe [::bank-accounts-by-yodlee-account-id])]
(if @(re-frame/subscribe [::provider-accounts-loading?])
[:div "Loading..."]
[:div.columns
[:div.column.is-half
(doall
(for [account @(re-frame/subscribe [::provider-accounts])
:let [{:keys [error status] :as g} @(re-frame/subscribe [::forms/form [::refresh-provider-account (:id account)]])
total-usages (mapcat (comp bank-accounts :id) (:accounts account))]]
^{:key (:id account)}
[:div.card {:style {:margin-bottom "1em"}}
[:div.card-header
[:div.card-header-title "Provider account " (:id account)]
[:div.card-header-icon
(when (seq total-usages)
[:div.tags
[:div.tag.is-primary (count total-usages) " usages"]])]
[:div.card-header-icon
[delete-button (:id account)]]
[:div.card-header-icon
(cond
(= :loading status) [:button.button.is-disabled.is-loading [:i.fa.fa-refresh]]
error [:button.button.is-disabled [:span.icon [:i.fa.fa-exclamation-triangle]]]
:else
[:button.button
{:on-click (dispatch-event [::refresh-provider-account (:id account)])}
[:span.icon [:i.fa.fa-refresh]]])]]
[:div.card-content
(if (> (some-> (-> account :dataset first :lastUpdated)
(yodlee-date->date )
(time/interval (time/now))
(time/in-days ))
1)
[:div.notification.is-info.is-light
[:div.level
[:div.level-left
[:div.level-item
[:p
"This account was last updated on "
(yodlee-date->str (-> account :dataset first :lastUpdated))
", and last attempted "
(yodlee-date->str (-> account :dataset first :lastUpdateAttempt))
"."]]]
[:div.level-right [:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Sync yodlee with bank" ]]]
])
[yodlee-accounts-table (:accounts account)]
(if (not= (-> account :dataset first :additionalStatus)
"AVAILABLE_DATA_RETRIEVED")
[:div
[:div.notification.is-info.is-warning
[:div.level
[:div.level-left
[:div.level-item
"This provider account's status is '"
(-> account :dataset first :additionalStatus)
"'. If this is in error, it might help to try reauthenticating by filling out the form below."]]]]
(let [{error :error account-data :data } @(re-frame/subscribe [::forms/form [::mfa-form (:id account)]])
change-event [::forms/change [::mfa-form (:id account)]]
{:keys [form-inline field field-holder raw-field error-notification submit-button]} (forms/vertical-form {:can-submit [::can-submit]
:change-event change-event
:submit-event [::reauthenticate-mfa (:id account)]
:id [::mfa-form (:id account)]} )]
(form-inline {:title "Reauthenticate"}
[:<>
(error-notification)
(doall
(for [[row i] (map vector (-> account :loginForm last :row) (range))
f (:field row)
:let [options (map :optionValue (:option f))]]
^{:key (:id f)}
[:div
(field (:label row)
[:input.input {:type "text" :field [:login i (:id f)]}])
(if (seq options)
[:ul
(for [o options]
^{:key o}
[:li [:pre o]])])]))
(doall
(for [f (-> account :field)]
^{:key (:id f)}
(field (:label f)
[:input.input {:type "text" :mfa [:form (:id f)] :value (-> f :field first :value)}])))
(submit-button "Reauthenticate")]))])]]))]])))
(defn admin-yodlee-content []
[(with-meta
(fn []
[:div
[:h1.title "Yodlee provider accounts"]
[yodlee-provider-accounts-table]
[yodlee-link-button]])
{:component-did-mount (fn []
(re-frame/dispatch [::mounted]))})])
(defn admin-yodlee-page []
[side-bar-layout {:side-bar [admin-side-bar {}]
:main [admin-yodlee-content]}]))
(re-frame/reg-sub
::authentication
(fn [db]