diff --git a/config/dev.edn b/config/dev.edn
index b9d97a76..5a8de086 100644
--- a/config/dev.edn
+++ b/config/dev.edn
@@ -9,19 +9,22 @@
:invoice-email "invoices-staging@mail.app.integreatconsult.com"
:data-bucket "data.staging.app.integreatconsult.com"
- :yodlee-cobrand-name "50ec7e57-297d-4970-941e-1cb07b8dcb4e_ADMIN"
- :yodlee-cobrand-login "G7T9kiwaG8rMiykdV4pckmQnfj4OM2pf"
+ :yodlee-cobrand-name "restserver"
+ :yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a"
:yodlee-cobrand-password "0a07ea32-1b5d-461b-ad0f-2752cdd77602"
-
- :yodlee-client-user-new "e02b38f9-9865-4264-8e4f-6a5ac2c500b0_ADMIN"
- :yodlee-client-id "l6sUyK2NEq3mwopISHlFGWUcJ1U8OUQd"
- :yodlee-client-secret "wZQHoGEkv5AGG2ZH"
-
- :yodlee-user-login "8I0mmq1wmAWSSpr9"
+ :yodlee-user-login "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4"
:yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4#123"
- :yodlee-base-url "https://development.api.yodlee.com/ysl"
+ :yodlee-base-url "https://developer.api.yodlee.com/ysl"
:yodlee-app "10003600"
:yodlee-fastlink "https://node.developer.yodlee.com/authenticate/restserver/?channelAppName=restserver"
:run-web? true
:run-background? true
+
+
+ :yodlee2-admin-user "e02b38f9-9865-4264-8e4f-6a5ac2c500b0_ADMIN"
+ :yodlee2-integreat-user "integreat-main"
+ :yodlee2-client-id "l6sUyK2NEq3mwopISHlFGWUcJ1U8OUQd"
+ :yodlee2-client-secret "wZQHoGEkv5AGG2ZH"
+ :yodlee2-base-url "https://development.api.yodlee.com/ysl"
+ :yodlee2-fastlink "https://fl4.preprod.yodlee.com/authenticate/USDevexPreProd2-195/fastlink/?channelAppName=usdevexpreprod2"
}
diff --git a/dev.cljs.edn b/dev.cljs.edn
index a4d776cd..dc8d5bb7 100644
--- a/dev.cljs.edn
+++ b/dev.cljs.edn
@@ -2,7 +2,7 @@
:target :bundle
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/"
- :source-map "resources/public/js/compiled/app.js.map"
+ :source-map true
#_#_:pretty-print true
#_#_:pseudo-names true
diff --git a/resources/public/index.html b/resources/public/index.html
index a0708f7f..d2e074b0 100644
--- a/resources/public/index.html
+++ b/resources/public/index.html
@@ -15,6 +15,7 @@
+
diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj
index b2ceb3f8..28c71535 100644
--- a/src/clj/auto_ap/handler.clj
+++ b/src/clj/auto_ap/handler.clj
@@ -6,6 +6,7 @@
[auto-ap.routes.graphql :as graphql]
[auto-ap.routes.invoices :as invoices]
[auto-ap.routes.yodlee :as yodlee]
+ [auto-ap.routes.yodlee2 :as yodlee2]
[buddy.auth.backends.token :refer [jws-backend]]
[buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]
[clojure.tools.logging :as log]
@@ -54,6 +55,7 @@
(context "/api" []
exports/routes
yodlee/routes
+ yodlee2/routes
invoices/routes
graphql/routes
auth/routes
diff --git a/src/clj/auto_ap/routes/yodlee2.clj b/src/clj/auto_ap/routes/yodlee2.clj
new file mode 100644
index 00000000..3a355967
--- /dev/null
+++ b/src/clj/auto_ap/routes/yodlee2.clj
@@ -0,0 +1,92 @@
+(ns auto-ap.routes.yodlee2
+ (:require
+ [auto-ap.graphql :as graphql]
+ [clj-http.client :as http]
+
+ [auto-ap.yodlee.core2 :as yodlee]
+ [auto-ap.graphql.utils :refer [->graphql assert-admin]]
+ [auto-ap.routes.utils :refer [wrap-secure]]
+ [clj-time.coerce :refer [to-date]]
+ [ring.middleware.json :refer [wrap-json-response]]
+ [compojure.core :refer [GET POST context defroutes wrap-routes]]
+ [clojure.string :as str]
+ [config.core :refer [env]]
+
+ [clojure.tools.logging :as log]))
+
+(defroutes routes
+ (wrap-routes
+ (context "/yodlee2" []
+ (GET "/fastlink" {:keys [query-params identity] :as request}
+ (assert-admin identity)
+ (let [token (yodlee/get-access-token)]
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str {:token token
+ :url (:yodlee2-fastlink env)}) }))
+ (GET "/accounts" {:keys [query-params identity] :as request}
+ (assert-admin identity)
+ (let [[session token] (yodlee/get-access-token)]
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str (yodlee/get-accounts)) }))
+
+ (GET "/provider-accounts" {:keys [query-params identity] :as request}
+ (assert-admin identity)
+ (log/info "working on provider accounts...")
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str @yodlee/in-memory-cache) })
+ (POST "/reauthenticate/:id" {:keys [query-params identity] {:keys [id]} :route-params
+ data :edn-params
+ :as request}
+ (assert-admin identity)
+ (try
+ (let [[session token] (yodlee/get-access-token)]
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str (yodlee/reauthenticate (Long/parseLong id) data)) })
+ (catch Exception e
+ (log/error e)
+ {:status 500
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str {:message (.getMessage e)
+ :error (.toString e)})})))
+ (POST "/provider-accounts/refresh/:id" {:keys [query-params identity] {:keys [id]} :route-params :as request}
+ (assert-admin identity)
+ (try
+ (let [[session token] (yodlee/get-access-token)]
+ (yodlee/refresh-provider-account (Long/parseLong id))
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str @yodlee/in-memory-cache) })
+ (catch Exception e
+ {:status 400
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str {:message (.getMessage e)
+ :error (.toString e)})})))
+ (POST "/provider-accounts/delete/:id" {:keys [query-params identity] {:keys [id]} :route-params :as request}
+ (assert-admin identity)
+ (try
+ (let [[session token] (yodlee/get-access-token)]
+ (yodlee/delete-provider-account (Long/parseLong id))
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str @yodlee/in-memory-cache) })
+ (catch Exception e
+ {:status 400
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str {:message (.getMessage e)
+ :error (.toString e)})})))
+ (POST "/provider-accounts/:id" {:keys [query-params identity] {:keys [id]} :route-params :as request}
+ (assert-admin identity)
+ (try
+ (let [[session token] (yodlee/get-access-token)]
+ {:status 200
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str (yodlee/update-yodlee (Long/parseLong id))) })
+ (catch Exception e
+ {:status 400
+ :headers {"Content-Type" "application/edn"}
+ :body (pr-str e)}))))
+ wrap-secure))
diff --git a/src/clj/auto_ap/yodlee/core.clj b/src/clj/auto_ap/yodlee/core.clj
index 0422ea2e..e2d7afa7 100644
--- a/src/clj/auto_ap/yodlee/core.clj
+++ b/src/clj/auto_ap/yodlee/core.clj
@@ -26,18 +26,22 @@
false)}))
(def base-headers {"Api-Version" "1.1"
- "loginName" (:yodlee-client-user-new env)
+ "Cobrand-Name" (:yodlee-cobrand-name env)
"Content-Type" "application/json"})
(defn login-cobrand []
- (-> (str (:yodlee-base-url env) "/auth/token")
- (client/post (merge {:headers (assoc base-headers
- "Content-Type" "application/x-www-form-urlencoded")
- :body (str "clientId=" (:yodlee-client-id env) " &secret=" (:yodlee-client-secret env))
+ (-> (str (:yodlee-base-url env) "/cobrand/login")
+ (client/post (merge {:headers base-headers
+ :body
+ (json/write-str {:cobrand {:cobrandLogin (:yodlee-cobrand-login env)
+ :cobrandPassword (:yodlee-cobrand-password env)
+ :locale "en_US"}})
:as :json}
other-config)
)
- :body))
+ :body
+ :session
+ :cobSession))
(defn login-user
diff --git a/src/clj/auto_ap/yodlee/core2.clj b/src/clj/auto_ap/yodlee/core2.clj
new file mode 100644
index 00000000..56379ac1
--- /dev/null
+++ b/src/clj/auto_ap/yodlee/core2.clj
@@ -0,0 +1,301 @@
+(ns auto-ap.yodlee.core2
+ (:require [clj-http.client :as client]
+ [auto-ap.utils :refer [by]]
+ [cemerick.url :as u]
+ [unilog.context :as lc]
+ [clojure.tools.logging :as log]
+ [clojure.data.json :as json]
+ [clojure.core.async :as async]
+ [config.core :refer [env]]
+ [mount.core :as mount]
+ [yang.scheduler :as scheduler]))
+
+(defn auth-header
+ ([cob-session] (str "Bearer " cob-session)))
+
+(def other-config
+ (if (:yodlee2-proxy-host env)
+ {:proxy-host (:yodlee2-proxy-host env)
+ :proxy-port (:yodlee2-proxy-port env)
+ :retry-handler (fn [ex try-count http-context]
+ (log/error "yodlee Error." ex)
+ false)}
+ {:retry-handler (fn [ex try-count http-context]
+ (log/error "yodlee Error." ex)
+ false)}))
+
+(def base-headers {"Api-Version" "1.1"
+ "Content-Type" "application/json"})
+
+(defn login-cobrand []
+ (-> (str (:yodlee2-base-url env) "/auth/token")
+ (client/post (merge {:headers (assoc base-headers
+ "loginName" (:yodlee2-admin-user env)
+ "Content-Type" "application/x-www-form-urlencoded")
+ :body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
+ :as :json}
+ other-config)
+ )
+ :body
+ :token
+ :accessToken))
+
+(defn login-user []
+ (-> (str (:yodlee2-base-url env) "/auth/token")
+ (client/post (merge {:headers (assoc base-headers
+ "loginName" (:yodlee2-integreat-user env)
+ "Content-Type" "application/x-www-form-urlencoded")
+ :body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
+ :as :json}
+ other-config)
+ )
+ :body
+ :token
+ :accessToken))
+
+
+(defn get-accounts []
+ (let [cob-session (login-user)]
+ (-> (str (:yodlee2-base-url env) "/accounts")
+ (client/get (merge {:headers (merge base-headers {"Authorization" (str "Bearer " cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :account)))
+
+(defn get-accounts-for-provider-account [provider-account-id]
+ (try
+ (let [cob-session (login-user)]
+ (-> (str (:yodlee2-base-url env) "/accounts?providerAccountId=" provider-account-id)
+ (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :account))
+ (catch Exception e
+ (log/error (str "Couldn't get accounts for provider account '" provider-account-id "'")
+ e)
+ [])))
+
+(defn get-provider-accounts []
+ (let [cob-session (login-user)]
+ (-> (str (:yodlee2-base-url env) "/providerAccounts")
+ (-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )})
+ :as :json}
+ other-config))
+ :body
+ :providerAccount))))
+
+
+
+(defn get-transactions []
+ (let [cob-session (login-user)
+ batch-size 100
+ get-transaction-batch (fn [skip]
+ (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip)
+
+
+ (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :transaction
+ ))]
+
+ (loop [transactions []
+ skip 0]
+ (let [transaction-batch (get-transaction-batch skip)]
+ (if (seq transaction-batch)
+ (recur (concat transactions transaction-batch) (+ batch-size skip))
+ transactions)))))
+
+
+
+(defn get-provider-account [id]
+ (let [cob-session (login-user)
+ batch-size 100]
+
+ (-> (str (:yodlee2-base-url env) "/providerAccounts/" id)
+
+ (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :providerAccount)))
+
+(defn get-provider-account-detail [id]
+ (let [cob-session (login-user)]
+
+ (-> (str (:yodlee2-base-url env) "/providerAccounts/" id )
+
+ (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :query-params {"include" "credentials,preferences"}
+ :as :json}
+ other-config))
+ :body
+ :providerAccount
+ first)))
+
+(defn update-provider-account [pa]
+ (let [cob-session (login-user)]
+
+ (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa)
+
+ (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :body "{\"dataSetName\": [\"BASIC_AGG_DATA\"]}"
+ :as :json}
+ other-config)))))
+
+
+
+
+
+
+(defn get-specific-transactions [account]
+ (let [cob-session (login-user)
+ batch-size 100
+ get-transaction-batch (fn [skip]
+ (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip "&accountId=" account)
+
+ (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :transaction
+ ))]
+
+ (loop [transactions []
+ skip 0]
+ (let [transaction-batch (get-transaction-batch skip)]
+ (if (seq transaction-batch)
+ (recur (concat transactions transaction-batch) (+ batch-size skip))
+ transactions)))))
+
+
+(defn get-access-token []
+ (try
+ (let [cob-session (login-user)]
+ cob-session)
+ (catch Exception e
+ (log/error e)
+ (throw e))))
+
+(defn create-user []
+ (let [cob-session (login-cobrand)]
+ (-> (str (:yodlee2-base-url env) "/user/register")
+ (client/post (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :body (json/write-str {"user" {
+ "loginName" "integreat-main"
+ "email" "bryce@integreatconsult.com"
+ "name" {"first" "Bryce"
+ "last" "Covert"}
+ "address" {"address1" "200 Lincoln Ave"
+ "state" "CA"
+ "city" "Salinas"
+ "zip" "93901"
+ "country" "USA"}
+ "preferences" {"currency" "USD"
+ "timeZone" "GMT"
+ "dateFormat" "YYYY-MMM-DD"
+ "locale" "en_US"}}})
+ :as :json}
+ other-config))
+ :body)))
+
+
+
+(defn get-provider-accounts-with-details []
+ (let [provider-accounts (get-provider-accounts)]
+ (let [concurrent 20
+ output-chan (async/chan)]
+ (async/pipeline-blocking concurrent
+ output-chan
+ (map (fn [provider-account]
+ (lc/with-context {:provider-account-id (:id provider-account)}
+ (get-provider-account-detail (:id provider-account)))))
+ (async/to-chan provider-accounts))
+ (async/> accounts
+ (reduce
+ (fn [provider-accounts [which accounts]]
+ (assoc-in provider-accounts [which :accounts] accounts))
+ provider-accounts)
+ vals)))
+
+(mount/defstate in-memory-cache
+ :start (atom []))
+
+(defn refresh-in-memory-cache []
+ (lc/with-context {:source "refreshing-in-memory-cache"}
+ (try
+ (log/info "Refreshing Yodlee in memory cache")
+ (reset! in-memory-cache (get-provider-accounts-with-accounts))
+
+ (catch Exception e
+ (log/error e)))))
+
+(mount/defstate in-memory-cache-worker
+ :start (scheduler/every (* 5 60 1000) refresh-in-memory-cache)
+ :stop (scheduler/stop in-memory-cache-worker))
+
+
+(defn refresh-provider-account [id]
+ (swap! in-memory-cache
+ (fn [i]
+ (-> (by :id i)
+ (assoc id (assoc (get-provider-account-detail id)
+ :accounts (get-accounts-for-provider-account id)))
+ vals))))
+
+(defn delete-provider-account [id]
+ (let [cob-session (login-user)]
+
+ (-> (str (:yodlee2-base-url env) "/providerAccounts/" id )
+
+ (client/delete (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :as :json}
+ other-config))
+ :body
+ :providerAccount
+ first))
+ (swap! in-memory-cache
+ (fn [i]
+ (-> (by :id i)
+ (dissoc id)
+ vals))))
+
+(defn update-yodlee [id]
+ (update-provider-account id)
+ (refresh-provider-account id))
+
+(defn reauthenticate [pa data]
+ (let [cob-session (login-cobrand)]
+
+ (try
+
+ (doto (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa)
+
+ (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)})
+ :body (json/write-str data)
+ :as :json}
+ other-config)))
+ log/info)
+ (refresh-provider-account pa)
+ (catch Exception e
+ (log/error e)))))
diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc
index d5bbb1fc..5c196cd6 100644
--- a/src/cljc/auto_ap/client_routes.cljc
+++ b/src/cljc/auto_ap/client_routes.cljc
@@ -14,7 +14,8 @@
"reminders" :admin-reminders
"vendors" :admin-vendors
"excel-import" :admin-excel-import
- "yodlee" :admin-yodlee}
+ "yodlee" :admin-yodlee
+ "yodlee2" :admin-yodlee2}
"invoices/" {"" :invoices
"import" :import-invoices
"unpaid" :unpaid-invoices
diff --git a/src/cljs/auto_ap/views/components/admin/side_bar.cljs b/src/cljs/auto_ap/views/components/admin/side_bar.cljs
index 2c5e6ad4..099eae4a 100644
--- a/src/cljs/auto_ap/views/components/admin/side_bar.cljs
+++ b/src/cljs/auto_ap/views/components/admin/side_bar.cljs
@@ -55,6 +55,11 @@
[:a {:href (bidi/path-for routes/routes :admin-yodlee), :class (str "item" (active-when ap = :admin-yodlee))}
[:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Yodlee Link"]]]
+
+ [:li.menu-item
+ [:a {:href (bidi/path-for routes/routes :admin-yodlee2), :class (str "item" (active-when ap = :admin-yodlee2))}
+ [:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}]
+ [:span {:class "name"} "Yodlee 2 Link"]]]
[:ul ]]
[:p.menu-label "History"]
diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs
index 02cd320e..2b53c14f 100644
--- a/src/cljs/auto_ap/views/main.cljs
+++ b/src/cljs/auto_ap/views/main.cljs
@@ -29,6 +29,7 @@
[auto-ap.views.pages.admin.excel-import :refer [admin-excel-import-page]]
[auto-ap.views.pages.admin.users :refer [admin-users-page]]
[auto-ap.views.pages.admin.yodlee :refer [admin-yodlee-page]]
+ [auto-ap.views.pages.admin.yodlee2 :as yodlee2]
[auto-ap.entities.clients :as clients]))
(defmulti page (fn [active-page] active-page))
@@ -103,6 +104,9 @@
(defmethod page :admin-yodlee [_]
(admin-yodlee-page))
+(defmethod page :admin-yodlee2 [_]
+ (yodlee2/admin-yodlee-page))
+
(defmethod page :admin-accounts [_]
(admin-accounts-page))
diff --git a/src/cljs/auto_ap/views/pages/admin/yodlee2.cljs b/src/cljs/auto_ap/views/pages/admin/yodlee2.cljs
new file mode 100644
index 00000000..a34b794a
--- /dev/null
+++ b/src/cljs/auto_ap/views/pages/admin/yodlee2.cljs
@@ -0,0 +1,412 @@
+(ns auto-ap.views.pages.admin.yodlee2
+ (:require [re-frame.core :as re-frame]
+ [auto-ap.forms :as forms]
+ [reagent.core :as reagent]
+ [clojure.string :as str]
+ [cljs-time.format :as f]
+ [cljs-time.core :as time]
+ [auto-ap.subs :as subs]
+ [auto-ap.events :as events]
+ [auto-ap.entities.clients :as entity]
+ [auto-ap.views.components.layouts :refer [side-bar-layout]]
+ [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
+ [auto-ap.views.components.address :refer [address-field]]
+ [auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field str->date date->str with-user]]
+ [auto-ap.views.components.modal :as modal]
+ [auto-ap.status :as status]
+ [cljs.reader :as edn]
+ [auto-ap.routes :as routes]
+ [bidi.bidi :as bidi]))
+
+
+
+(re-frame/reg-sub
+ ::authentication
+ (fn [db]
+ (-> db ::yodlee :authentication)))
+
+(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
+ ::authenticate-with-yodlee
+ (fn [{:keys [db]} _]
+ {:db (assoc-in db [::yodlee :loading?] true)
+ :http {:token (:user db)
+ :method :get
+ :headers {"Content-Type" "application/edn"}
+ :uri (str "/api/yodlee2/fastlink")
+ :on-success [::authenticated]
+ :on-error [::save-error]}}))
+
+(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
+ (fn [{:keys [db]} [_ authentication]]
+ {:db (-> db
+ (assoc-in [::yodlee :authentication] authentication)
+ (assoc-in [::yodlee :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-link-button []
+ [:div
+ (let [authentication @(re-frame/subscribe [::authentication])
+ loading? @(re-frame/subscribe [::loading?])]
+
+ (if authentication
+ [:div
+ "Authentication successful!"
+ [:div#fa-spot]
+ [:button.button.is-primary {:on-click (fn []
+ #_(println #js {"fastLinkUrl" (:url authentication)
+ "accessToken" (:token authentication)
+ "params" #js { "configName" "Aggregation"}})
+ (.open (.-fastlink js/window)
+ (doto #js {"fastLinkURL" (:url authentication)
+ "accessToken" (:token authentication)
+ "params" #js { "configName" "Aggregation"}}
+ println)
+ "fa-spot")
+ )}[:span [:span.icon [:i.fa.fa-external-link]] " Go to yodlee"]]]
+
+ [:button.button.is-primary {:class (if loading? "is-loading" "") :on-click (dispatch-event [::authenticate-with-yodlee])} "Authenticate with Yodlee"]))])
+
+(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-event-fx
+ ::provider-account-refreshed
+ (fn [{:keys [db]} [_ i result]]
+
+ {:db (assoc-in db [::provider-accounts] result)
+ :dispatch [::forms/form-closing [::refresh-provider-account i]]}))
+
+(re-frame/reg-event-fx
+ ::refresh-provider-account
+ [with-user ]
+ (fn [{:keys [user db]} [_ provider-account-id ]]
+ {:db (forms/loading db [::refresh-provider-account provider-account-id])
+ :http {:token user
+ :method :post
+ :headers {"Content-Type" "application/edn"}
+ :uri (str "/api/yodlee2/provider-accounts/refresh/" provider-account-id )
+ :body {}
+ :on-success [::provider-account-refreshed provider-account-id]
+ :on-error [::forms/save-error [::refresh-provider-account provider-account-id] ]}}))
+
+(re-frame/reg-event-fx
+ ::provider-account-deleted
+ (fn [{:keys [db]} [_ i result]]
+ {:db (assoc-in db [::provider-accounts] result)
+ :dispatch-n [[::forms/form-closing [::refresh-provider-account i]]
+ [::modal/modal-closed ]]}))
+
+(re-frame/reg-event-fx
+ ::delete-provider-account
+ [with-user ]
+ (fn [{:keys [user db]} [_ provider-account-id ]]
+ {:http {:token user
+ :method :post
+ :owns-state {:single ::delete-provider-account}
+ :headers {"Content-Type" "application/edn"}
+ :uri (str "/api/yodlee2/provider-accounts/delete/" provider-account-id )
+ :body {}
+ :on-success [::provider-account-deleted provider-account-id]
+ :on-error [::forms/save-error [::delete-provider-account provider-account-id] ]}}))
+
+
+
+(re-frame/reg-event-fx
+ ::delete-requested
+ [with-user]
+ (fn [{:keys [user db]} [_ account-id]]
+ {:dispatch
+ [::modal/modal-requested {:title "Delete Provider account "
+ :body [:div "Are you sure you want to delete provider account " account-id "?"]
+ :confirm {:value "Delete provider account"
+ :status-from [::status/single ::delete-provider-account]
+ :class "is-danger"
+ :on-click (dispatch-event [::delete-provider-account account-id])
+ :close-event [::status/completed ::delete-provider-account]}
+ :cancel? true}]}))
+
+
+(defn delete-button [account-id]
+ [:button.button
+ {:on-click (dispatch-event [::delete-requested account-id])}
+ [:span.icon [:i.fa.fa-times]]])
+
+(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]}])