(ns auto-ap.views.pages.admin.yodlee (: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/yodlee/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/yodlee/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/yodlee/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!" [:form {:action (:url authentication) :method "POST"} [:input {:type "hidden" :name "rsession" :value (:session authentication)}] [:input {:type "hidden" :name "token" :value (:token authentication)}] [:input {:type "hidden" :name "app" :value (:app authentication)}] [:input {:type "hidden" :name "redirectReq" :value "true"}] [:button.button.is-primary [: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/yodlee/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/yodlee/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/yodlee/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]}])