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]}])