plaid
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
:requests-queue-url "https://sqs.us-east-1.amazonaws.com/679918342773/integreat-background-request-prod"
|
||||
:invoice-email "invoices-staging@mail.app.integreatconsult.com"
|
||||
:data-bucket "data.staging.app.integreatconsult.com"
|
||||
:plaid {:base-url "https://sandbox.plaid.com"
|
||||
:client-id "61bfab05f7e762001b323f79"
|
||||
:secret-key "f902743e6f4cef86b7f3b51141aeea"}
|
||||
|
||||
:yodlee-cobrand-name "restserver"
|
||||
:yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a"
|
||||
|
||||
2394
package-lock.json
generated
2394
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"react": "^17.0.1",
|
||||
"react-datepicker": "^2.1.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-plaid-link": "^3.2.1",
|
||||
"react-prop-types": "^0.4.0",
|
||||
"react-signature-canvas": "^1.0.3",
|
||||
"react-signature-pad": "0.0.6",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(defn cleanse [e]
|
||||
(-> e
|
||||
(assoc :client/yodlee-provider-accounts (get e :yodlee-provider-account/_client))
|
||||
(assoc :client/plaid-items (get e :plaid-item/_client))
|
||||
(update :client/location-matches
|
||||
(fn [lms]
|
||||
(map #(assoc % :location-match/match (first (:location-match/matches %))) lms)))
|
||||
@@ -23,23 +24,26 @@
|
||||
{:client/address [*]}
|
||||
{:client/bank-accounts [* {:bank-account/type [*]
|
||||
:bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number]
|
||||
:bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]
|
||||
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]}
|
||||
]}
|
||||
{:yodlee-provider-account/_client [*]}])
|
||||
{:yodlee-provider-account/_client [*]}
|
||||
{:plaid-item/_client [*]}])
|
||||
:where [?e :client/name]]
|
||||
(d/db (d/connect uri)))
|
||||
(map first)
|
||||
(map cleanse)
|
||||
|
||||
))
|
||||
(map cleanse)))
|
||||
|
||||
(defn get-by-id [id]
|
||||
|
||||
(->>
|
||||
(d/pull (d/db conn )
|
||||
'[* {:client/bank-accounts [* {:bank-account/type [*]
|
||||
:bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number]
|
||||
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]}]}
|
||||
{:yodlee-provider-account/_client [*]}]
|
||||
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]
|
||||
:bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]}]}
|
||||
{:yodlee-provider-account/_client [*]}
|
||||
{:plaid-item/_client [*]}]
|
||||
id)
|
||||
(cleanse)))
|
||||
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
(ns auto-ap.datomic.migrate
|
||||
(:require [auto-ap.datomic :refer [uri conn]]
|
||||
[datomic.api :as d]
|
||||
[auto-ap.datomic.migrate.add-client-codes :refer [add-client-codes]]
|
||||
[auto-ap.datomic.migrate.add-bank-account-codes :refer [add-bank-account-codes]]
|
||||
[auto-ap.datomic.migrate.invoice-converter :refer [add-import-status-existing-invoices]]
|
||||
[auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger]
|
||||
[auto-ap.datomic.migrate.ledger :as ledger]
|
||||
[auto-ap.datomic.migrate.sales :as sales]
|
||||
[auto-ap.datomic.migrate.vendors :as vendors]
|
||||
[auto-ap.datomic.migrate.clients :as clients]
|
||||
[auto-ap.datomic.migrate.audit :as audit]
|
||||
[auto-ap.datomic.migrate.yodlee2 :as yodlee2]
|
||||
|
||||
[clojure.java.io :as io]
|
||||
[io.rkn.conformity :as c]
|
||||
[mount.core :as mount])
|
||||
(:import [datomic Util])
|
||||
(:gen-class))
|
||||
|
||||
(:gen-class)
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.datomic.migrate.add-bank-account-codes
|
||||
:refer [add-bank-account-codes]]
|
||||
[auto-ap.datomic.migrate.add-client-codes :refer [add-client-codes]]
|
||||
[auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger]
|
||||
[auto-ap.datomic.migrate.audit :as audit]
|
||||
[auto-ap.datomic.migrate.clients :as clients]
|
||||
[auto-ap.datomic.migrate.invoice-converter
|
||||
:refer [add-import-status-existing-invoices]]
|
||||
[auto-ap.datomic.migrate.ledger :as ledger]
|
||||
[auto-ap.datomic.migrate.plaid :as plaid]
|
||||
[auto-ap.datomic.migrate.sales :as sales]
|
||||
[auto-ap.datomic.migrate.vendors :as vendors]
|
||||
[auto-ap.datomic.migrate.yodlee2 :as yodlee2]
|
||||
[clojure.java.io :as io]
|
||||
[datomic.api :as d]
|
||||
[io.rkn.conformity :as c]
|
||||
[mount.core :as mount])
|
||||
(:import
|
||||
(datomic Util)))
|
||||
|
||||
(defn read-dtm
|
||||
"Reads a dtm file (i.e., an edn file with datomic tags in it) from the classpath
|
||||
@@ -434,6 +437,7 @@
|
||||
clients/norms-map
|
||||
ledger/norms-map
|
||||
yodlee2/norms-map
|
||||
plaid/norms-map
|
||||
audit/norms-map
|
||||
vendors/norms-map)]
|
||||
(println "Conforming database...")
|
||||
|
||||
55
src/clj/auto_ap/datomic/migrate/plaid.clj
Normal file
55
src/clj/auto_ap/datomic/migrate/plaid.clj
Normal file
@@ -0,0 +1,55 @@
|
||||
(ns auto-ap.datomic.migrate.plaid)
|
||||
|
||||
(def norms-map {::add-plaid
|
||||
{:txes [[{:db/ident :plaid-item/external-id
|
||||
:db/doc "Plaid Item ID"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/unique :db.unique/identity}
|
||||
{:db/ident :plaid-item/access-token
|
||||
:db/doc "Used to authenticate to plaid"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/ident :plaid-item/client
|
||||
:db/doc "Which client the plaid item is for"
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/ident :plaid-item/accounts
|
||||
:db/doc "Individual bank accounts"
|
||||
:db/isComponent true
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/many}
|
||||
{:db/ident :plaid-item/status
|
||||
:db/doc "Current status"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/ident :plaid-item/last-updated
|
||||
:db/doc "Last updated date"
|
||||
:db/valueType :db.type/instant
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/noHistory true}
|
||||
|
||||
{:db/ident :plaid-account/external-id
|
||||
:db/doc "plaid account id"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one
|
||||
:db/unique :db.unique/identity}
|
||||
{:db/ident :plaid-account/name
|
||||
:db/doc "account name"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/ident :plaid-account/number
|
||||
:db/doc "account number"
|
||||
:db/valueType :db.type/string
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :plaid-account/balance
|
||||
:db/doc "Available Balance"
|
||||
:db/valueType :db.type/double
|
||||
:db/cardinality :db.cardinality/one}
|
||||
|
||||
{:db/ident :bank-account/plaid-account
|
||||
:db/doc "Yodlee account for the bank account"
|
||||
:db/valueType :db.type/ref
|
||||
:db/cardinality :db.cardinality/one}
|
||||
{:db/ident :import-source/plaid}]]}})
|
||||
@@ -21,6 +21,7 @@
|
||||
[auto-ap.graphql.vendors :as gq-vendors]
|
||||
[auto-ap.graphql.yodlee-merchants :as ym]
|
||||
[auto-ap.graphql.yodlee2 :as gq-yodlee2]
|
||||
[auto-ap.graphql.plaid :as gq-plaid]
|
||||
[auto-ap.logging :refer [error-event info-event warn-event]]
|
||||
[auto-ap.time :as time]
|
||||
[clj-time.coerce :as coerce]
|
||||
@@ -107,7 +108,8 @@
|
||||
:matches {:type '(list String)}
|
||||
:bank_accounts {:type '(list :bank_account)}
|
||||
:forecasted_transactions {:type '(list :forecasted_transaction)}
|
||||
:yodlee_provider_accounts {:type '(list :yodlee_provider_account)}}}
|
||||
:yodlee_provider_accounts {:type '(list :yodlee_provider_account)}
|
||||
:plaid_items {:type '(list :plaid_item)}}}
|
||||
|
||||
:yodlee_provider_account
|
||||
{:fields {:id {:type 'Int}
|
||||
@@ -149,6 +151,7 @@
|
||||
:yodlee_balance_old {:type :money}
|
||||
:yodlee_account_id {:type 'Int}
|
||||
:yodlee_account {:type :yodlee_account}
|
||||
:plaid_account {:type :plaid_account}
|
||||
:intuit_bank_account {:type :intuit_bank_account}
|
||||
:locations {:type '(list String)}}}
|
||||
:forecasted_transaction {:fields {:identifier {:type 'String}
|
||||
@@ -827,6 +830,7 @@
|
||||
:locations {:type '(list String)}
|
||||
:yodlee_account_id {:type 'Int}
|
||||
:intuit_bank_account {:type :id}
|
||||
:plaid_account {:type :id}
|
||||
:yodlee_account {:type 'Int}}}
|
||||
:edit_user
|
||||
{:fields {:id {:type :id}
|
||||
@@ -966,6 +970,7 @@
|
||||
:import_batch_source {:values [{:enum-value :intuit}
|
||||
{:enum-value :yodlee}
|
||||
{:enum-value :yodlee2}
|
||||
{:enum-value :plaid}
|
||||
{:enum-value :manual}]}
|
||||
:import_batch_status {:values [{:enum-value :started}
|
||||
{:enum-value :completed}]}
|
||||
@@ -1415,6 +1420,7 @@
|
||||
:mutation/import-ledger gq-ledger/import-ledger
|
||||
:mutation/request-import gq-requests/request-import
|
||||
:get-vendor gq-vendors/get-graphql})
|
||||
gq-plaid/attach
|
||||
schema/compile))
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,11 @@
|
||||
(into (->> (:bank_accounts edit_client)
|
||||
(filter #(nil? (:intuit_bank_account %)))
|
||||
(filter #(:bank-account/intuit-bank-account (d/entity (d/db conn) (:id %))))
|
||||
(map (fn [ba] [:db/retract (:id ba) :bank-account/intuit-bank-account (:db/id (:bank-account/intuit-bank-account (d/entity (d/db conn) (:id ba))))])))))
|
||||
(map (fn [ba] [:db/retract (:id ba) :bank-account/intuit-bank-account (:db/id (:bank-account/intuit-bank-account (d/entity (d/db conn) (:id ba))))]))))
|
||||
(into (->> (:bank_accounts edit_client)
|
||||
(filter #(nil? (:plaid_account %)))
|
||||
(filter #(:bank-account/plaid-account (d/entity (d/db conn) (:id %))))
|
||||
(map (fn [ba] [:db/retract (:id ba) :bank-account/plaid-account (:db/id (:bank-account/plaid-account (d/entity (d/db conn) (:id ba))))])))))
|
||||
|
||||
|
||||
transactions (into [(remove-nils {:db/id id
|
||||
@@ -110,6 +114,7 @@
|
||||
:bank-account/yodlee-account-id (:yodlee_account_id %)
|
||||
:bank-account/type (keyword "bank-account-type" (name (:type %)))}
|
||||
(:yodlee_account %) (assoc :bank-account/yodlee-account [:yodlee-account/id (:yodlee_account %)])
|
||||
(:plaid_account %) (assoc :bank-account/plaid-account (:plaid_account %))
|
||||
(:intuit_bank_account %) (assoc :bank-account/intuit-bank-account (:intuit_bank_account %))))
|
||||
(:bank_accounts edit_client))
|
||||
|
||||
@@ -223,7 +228,9 @@
|
||||
(map (fn [c]
|
||||
(if (is-admin? (:id context))
|
||||
c
|
||||
(dissoc c :client/yodlee-provider-accounts))))
|
||||
(-> c
|
||||
(dissoc :client/yodlee-provider-accounts)
|
||||
(dissoc :client/plaid-items)))))
|
||||
(map (fn [c]
|
||||
(update c :client/bank-accounts
|
||||
(fn [bank-accounts]
|
||||
|
||||
159
src/clj/auto_ap/graphql/plaid.clj
Normal file
159
src/clj/auto_ap/graphql/plaid.clj
Normal file
@@ -0,0 +1,159 @@
|
||||
(ns auto-ap.graphql.plaid
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query]]
|
||||
[auto-ap.graphql.utils :refer [assert-admin assert-present]]
|
||||
[auto-ap.plaid.core :as p]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
||||
[datomic.api :as d]))
|
||||
|
||||
(defn plaid-link-token [context value args]
|
||||
(assert-admin (:id context))
|
||||
(let [client-code (:client/code (d/pull (d/db conn) [:client/code] (:client_id value)))]
|
||||
{:token (p/get-link-token client-code)}))
|
||||
|
||||
(defn link-plaid [context value args]
|
||||
(assert-admin (:id context))
|
||||
(when-not (:client_code value)
|
||||
(throw (ex-info "Client not provided" {:validation-error "Client not provided."})))
|
||||
(when-not (:public_token value)
|
||||
(throw (ex-info "Public token not provided" {:validation-error "public token not provided"})))
|
||||
|
||||
(let [access-token (:access_token (p/exchange-public-token (:public_token value) (:client_code value)))
|
||||
account-result (p/get-accounts access-token )
|
||||
item {:plaid-item/client [:client/code (:client_code value)]
|
||||
:plaid-item/external-id (-> account-result :item :item_id )
|
||||
:plaid-item/access-token access-token
|
||||
:plaid-item/status (or (some-> account-result :item :error)
|
||||
"SUCCESS")
|
||||
:plaid-item/last-updated (coerce/to-date (time/now))
|
||||
:db/id "plaid-item"}]
|
||||
|
||||
@(d/transact conn (->> (:accounts account-result)
|
||||
(map (fn [a]
|
||||
(let [balance (some-> a :balances :current (* 0.01))]
|
||||
(cond-> {:plaid-account/external-id (:account_id a)
|
||||
:plaid-account/number (:mask a)
|
||||
:plaid-account/name (str (:name a) " " (:mask a))
|
||||
:plaid-item/_accounts "plaid-item"}
|
||||
balance (assoc :plaid-account/balance balance)))))
|
||||
(into [item])))
|
||||
{:message (str "Plaid linked successfully. Access Token: " access-token)}))
|
||||
|
||||
|
||||
(def default-read '[:db/id
|
||||
:plaid-item/external-id
|
||||
:plaid-item/last-updated
|
||||
:plaid-item/status
|
||||
{:plaid-item/accounts [:db/id
|
||||
:plaid-account/external-id
|
||||
:plaid-account/number
|
||||
:plaid-account/balance
|
||||
:plaid-account/name]}])
|
||||
|
||||
(defn raw-graphql-ids [db args]
|
||||
(println args)
|
||||
(let [query (cond-> {:query {:find []
|
||||
:in ['$]
|
||||
:where []}
|
||||
:args [db]}
|
||||
|
||||
(:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]}
|
||||
args)
|
||||
|
||||
(:client-id args)
|
||||
(merge-query {:query {:in '[?client-id]
|
||||
:where ['[?e :plaid-item/client ?client-id]]}
|
||||
:args [(:client-id args)]})
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?e]
|
||||
:where ['[?e :plaid-item/external-id]]}}))]
|
||||
|
||||
(cond->> query
|
||||
true (d/query)
|
||||
true (apply-sort-3 args)
|
||||
true (apply-pagination args))))
|
||||
|
||||
(defn graphql-results [ids db args]
|
||||
(let [results (->> (d/pull-many db default-read ids)
|
||||
(group-by :db/id))]
|
||||
(->> ids
|
||||
(map results)
|
||||
(map first))))
|
||||
|
||||
(defn get-graphql [args]
|
||||
(let [db (d/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)]
|
||||
[(graphql-results ids-to-retrieve db args)
|
||||
matching-count]))
|
||||
|
||||
|
||||
|
||||
(defn get-plaid-item-page [context args value]
|
||||
(assert-admin (:id context))
|
||||
(let [args (assoc args :id (:id context))
|
||||
[plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))]
|
||||
{:plaid_items (->> plaid-items
|
||||
(map #(update % :plaid-item/last-updated coerce/from-date))
|
||||
(map ->graphql))
|
||||
:total cnt
|
||||
:count (count plaid-items)
|
||||
:start (:start args 0)
|
||||
:end (+ (:start args 0) (count plaid-items))}))
|
||||
|
||||
(defn delete-plaid-item [context args value]
|
||||
(assert-admin (:id context))
|
||||
(assert-present args :id)
|
||||
@(d/transact conn [[:db/retractEntity (:id args)]])
|
||||
{:message "Item deleted."})
|
||||
|
||||
(defn attach [schema]
|
||||
(->
|
||||
(merge-with merge schema
|
||||
{:objects {:plaid_link_result
|
||||
{:fields {:token {:type 'String}} }
|
||||
|
||||
:plaid_item
|
||||
{:fields {:external_id {:type 'String}
|
||||
:id {:type :id}
|
||||
:client {:type :client}
|
||||
:status {:type 'String}
|
||||
:last_updated {:type :iso_date}
|
||||
:accounts {:type '(list :plaid_account)}}}
|
||||
|
||||
:plaid_item_page {:fields {:plaid_items {:type '(list :plaid_item)}
|
||||
:count {:type 'Int}
|
||||
:total {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:end {:type 'Int}}}
|
||||
|
||||
:plaid_account
|
||||
{:fields {:external_id {:type 'String}
|
||||
:id {:type :id}
|
||||
:balance {:type :money}
|
||||
:name {:type 'String}
|
||||
:number {:type 'String}}}}
|
||||
:queries {:plaid_link_token {:type :plaid_link_result
|
||||
:args {:client_id {:type :id}}
|
||||
:resolve :plaid-link-token}
|
||||
:plaid_item_page {:type :plaid_item_page
|
||||
:args {:client_id {:type :id}
|
||||
:sort {:type '(list :sort_item)}
|
||||
:start {:type 'Int}
|
||||
:per_page {:type 'Int}}
|
||||
:resolve :get-plaid-item-page}}
|
||||
:mutations {:link_plaid {:type :message
|
||||
:args {:client_code {:type 'String}
|
||||
:public_token {:type 'String}}
|
||||
:resolve :mutation/link-plaid}
|
||||
:delete_plaid_item {:type :message
|
||||
:args {:id {:type :id}}
|
||||
:resolve :mutation/delete-plaid-item}}})
|
||||
(attach-resolvers {:plaid-link-token plaid-link-token
|
||||
:get-plaid-item-page get-plaid-item-page
|
||||
:mutation/link-plaid link-plaid
|
||||
:mutation/delete-plaid-item delete-plaid-item})))
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
(log/warn "user " id " not an admin!")
|
||||
(throw-unauthorized)))
|
||||
|
||||
(defn assert-present
|
||||
([args key]
|
||||
(assert-present args key (name key)))
|
||||
([args key name]
|
||||
(if (not (get args key))
|
||||
(throw (ex-info (str "Missing field '" name "'.")
|
||||
{:validation-error (str "Missing field '" name "'.")})))))
|
||||
|
||||
(defn assert-power-user [id]
|
||||
(when-not (#{"power-user" "admin"} (:user/role id))
|
||||
(log/warn "user " id " not an power-user!")
|
||||
|
||||
71
src/clj/auto_ap/plaid/core.clj
Normal file
71
src/clj/auto_ap/plaid/core.clj
Normal file
@@ -0,0 +1,71 @@
|
||||
(ns auto-ap.plaid.core
|
||||
(:require [auto-ap.datomic :refer [conn remove-nils]]
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[clj-http.client :as client]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clj-time.format :as f]
|
||||
[config.core :refer [env] :as cfg ]
|
||||
[clojure.string :as str]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.tools.logging :as log]
|
||||
[datomic.api :as d]
|
||||
[mount.core :as mount]
|
||||
[unilog.context :as lc]
|
||||
[yang.scheduler :as scheduler]
|
||||
[clojure.core.async :as async]
|
||||
[clojure.core.memoize :as m]))
|
||||
|
||||
(def base-url (-> env :plaid :base-url))
|
||||
|
||||
(def client-id (-> env :plaid :client-id))
|
||||
(def secret-key (-> env :plaid :secret-key))
|
||||
|
||||
|
||||
(defn get-link-token [client-code]
|
||||
(-> (client/post (str base-url "/link/token/create")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"client_name" "Integreat Consulting"
|
||||
"country_codes" ["US"]
|
||||
"language" "en"
|
||||
"user" {"client_user_id" client-code}
|
||||
"products" ["auth" "transactions"]})})
|
||||
:body
|
||||
:link_token))
|
||||
|
||||
(defn exchange-public-token [public-token client-code]
|
||||
(-> (client/post (str base-url "/item/public_token/exchange")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"public_token" public-token})})
|
||||
:body
|
||||
(doto println)))
|
||||
|
||||
(defn get-accounts [access-token ]
|
||||
(-> (client/post (str base-url "/accounts/get")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"access_token" access-token})})
|
||||
:body))
|
||||
|
||||
(defn get-transactions [access-token account-id start end]
|
||||
(-> (client/post (str base-url "/transactions/get")
|
||||
{:as :json
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {"client_id" client-id
|
||||
"secret" secret-key
|
||||
"access_token" access-token
|
||||
"start_date" (auto-ap.time/unparse start auto-ap.time/iso-date)
|
||||
"end_date" (auto-ap.time/unparse end auto-ap.time/iso-date)
|
||||
"options" {"account_ids" [account-id]}})})
|
||||
:body))
|
||||
|
||||
70
src/clj/auto_ap/plaid/import.clj
Normal file
70
src/clj/auto_ap/plaid/import.clj
Normal file
@@ -0,0 +1,70 @@
|
||||
(ns auto-ap.plaid.import
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.plaid.core :as p]
|
||||
[auto-ap.utils :refer [allow-once]]
|
||||
[auto-ap.yodlee.import :as y]
|
||||
[clj-time.core :as time]
|
||||
[clojure.tools.logging :as log]
|
||||
[datomic.api :as d]
|
||||
[mount.core :as mount]
|
||||
[unilog.context :as lc]
|
||||
[yang.scheduler :as scheduler]))
|
||||
|
||||
(defn get-plaid-accounts [db]
|
||||
(-> (d/q '[:find ?ba ?external-id ?t
|
||||
:in $
|
||||
:where
|
||||
[?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/plaid-account ?pa]
|
||||
[?pa :plaid-account/external-id ?external-id]
|
||||
[?pi :plaid-item/accounts ?pa]
|
||||
[?pi :plaid-item/access-token ?t]]
|
||||
db )))
|
||||
|
||||
(defn import-plaid
|
||||
[]
|
||||
(lc/with-context {:source "Importing plaid transactions"}
|
||||
(let [db (d/db conn)
|
||||
import-id (y/start-import :import-source/plaid "Automated Plaid User")]
|
||||
(try
|
||||
(let [result (->>
|
||||
(for [[bank-account external-id access-token] (get-plaid-accounts db)
|
||||
:let [end (auto-ap.time/local-now)
|
||||
start (time/plus end (time/days -30))
|
||||
_ (log/infof "importing from %s to %s for %s" start end external-id)
|
||||
transactions (p/get-transactions access-token external-id start end)
|
||||
_ (clojure.pprint/pprint transactions)
|
||||
transactions (->> transactions
|
||||
:transactions
|
||||
(filter (fn [t]
|
||||
(not (:pending t))))
|
||||
(mapv (fn [t]
|
||||
{:description {:original (:name t)
|
||||
:simple (:name t)}
|
||||
:id (:transaction_id t)
|
||||
:amount {:amount (:amount t)}
|
||||
:date (:date t)
|
||||
:status "POSTED"}))
|
||||
)]]
|
||||
|
||||
|
||||
(y/import-for-bank-account transactions bank-account import-id))
|
||||
y/aggregate-results)]
|
||||
(log/info "Plaid transactions imported" result)
|
||||
(y/finish-import (assoc result :db/id import-id)))
|
||||
(catch Exception e
|
||||
(log/error e)
|
||||
(y/finish-import {:db/id import-id}))))))
|
||||
|
||||
(defn do-import []
|
||||
(import-plaid))
|
||||
|
||||
(def do-import (allow-once do-import))
|
||||
|
||||
(mount/defstate import-worker
|
||||
:start (scheduler/every (* 1000 60 60 3) do-import)
|
||||
:stop (scheduler/stop import-worker))
|
||||
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@
|
||||
(defn import-yodlee2
|
||||
[transactions]
|
||||
(lc/with-context {:source "Import yodlee2 transactions"}
|
||||
(let [import-id (start-import :import-source/yodlee "Automated Yodlee User")]
|
||||
(let [import-id (start-import :import-source/yodlee2 "Automated Yodlee User")]
|
||||
(lc/with-context {:import-id import-id}
|
||||
(try
|
||||
(let [account-lookup (->> (d/q '[:find ?ya ?ba
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"vendors" :admin-vendors
|
||||
"excel-import" :admin-excel-import
|
||||
"yodlee" :admin-yodlee
|
||||
"yodlee2" :admin-yodlee2}
|
||||
"yodlee2" :admin-yodlee2
|
||||
"plaid" :admin-plaid}
|
||||
"invoices/" {"" :invoices
|
||||
"import" :import-invoices
|
||||
"unpaid" :unpaid-invoices
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
196
src/cljs/auto_ap/views/pages/admin/plaid.cljs
Normal file
196
src/cljs/auto_ap/views/pages/admin/plaid.cljs
Normal 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]}])}))
|
||||
|
||||
|
||||
109
src/cljs/auto_ap/views/pages/admin/plaid/table.cljs
Normal file
109
src/cljs/auto_ap/views/pages/admin/plaid/table.cljs
Normal 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"}]]]])]]]))
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user