From 6df080cc3f388d2bd8dd7cc633eb9ad8c1ee0fff Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Wed, 29 Jul 2020 21:12:31 -0700 Subject: [PATCH] A bunch of bug fixes. --- config/dev.edn | 4 +- src/clj/auto_ap/ledger.clj | 23 +++++ src/clj/auto_ap/routes/yodlee.clj | 26 ++++++ src/clj/auto_ap/server.clj | 3 +- src/clj/auto_ap/yodlee/core.clj | 34 ++++++- src/clj/user.clj | 47 +++++++++- src/cljs/auto_ap/views/components/modal.cljs | 13 +++ .../auto_ap/views/pages/admin/yodlee.cljs | 90 +++++++++++++++++-- 8 files changed, 224 insertions(+), 16 deletions(-) diff --git a/config/dev.edn b/config/dev.edn index 4ec086ce..6c2b5573 100644 --- a/config/dev.edn +++ b/config/dev.edn @@ -11,8 +11,8 @@ :yodlee-cobrand-name "restserver" :yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a" :yodlee-cobrand-password "0a07ea32-1b5d-461b-ad0f-2752cdd77602" - :yodlee-user-login "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a2" - :yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a2#123" + :yodlee-user-login "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4" + :yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4#123" :yodlee-base-url "https://developer.api.yodlee.com/ysl" :yodlee-app "10003600" :yodlee-fastlink "https://node.developer.yodlee.com/authenticate/restserver/?channelAppName=restserver" diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index d42a447d..fa8989e0 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -164,3 +164,26 @@ #_(process-all) #_(reset! break true) + + + +(defn keep-up-to-date [] + (while (and (not @break) + (not (Thread/interrupted))) + (try + @(d/transact + (d/connect uri) + (mapv + #(auto-ap.ledger/entity-change->ledger (d/db (d/connect uri)) [:transaction %]) + (concat + (->> + (d/query {:query {:find ['?t ] + :in ['$] + :where ['[?t :transaction/date] + '(not [?t :transaction/approval-status :transaction-approval-status/excluded]) + '(not-join [?t] [?e :journal-entry/original-entity ?t])]} + :args [(d/db (d/connect uri))]}) + (map first))))) + (Thread/sleep 60000) + (catch Exception e + (println (.toString e)))))) diff --git a/src/clj/auto_ap/routes/yodlee.clj b/src/clj/auto_ap/routes/yodlee.clj index 6cf85c51..857c1ece 100644 --- a/src/clj/auto_ap/routes/yodlee.clj +++ b/src/clj/auto_ap/routes/yodlee.clj @@ -58,6 +58,32 @@ :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 diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index c51aa6a7..4317a9f7 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -1,7 +1,7 @@ (ns auto-ap.server (:require #_[auto-ap.background.mail :refer [always-process-sqs]] [auto-ap.handler :refer [app]] - [auto-ap.ledger :refer [process-all]] + [auto-ap.ledger :refer [process-all keep-up-to-date]] [auto-ap.yodlee.core :refer [load-in-memory-cache]] [nrepl.server :refer [start-server stop-server]] [config.core :refer [env]] @@ -17,5 +17,6 @@ (let [port (Integer/parseInt (or (env :port) "3000"))] (future (process-all)) (future (load-in-memory-cache)) + (future (keep-up-to-date)) #_(future (always-process-sqs)) (run-jetty app {:port port :join? false}))) diff --git a/src/clj/auto_ap/yodlee/core.clj b/src/clj/auto_ap/yodlee/core.clj index ea046646..7e313c0b 100644 --- a/src/clj/auto_ap/yodlee/core.clj +++ b/src/clj/auto_ap/yodlee/core.clj @@ -52,6 +52,15 @@ :body :account))) +(defn get-accounts-for-provider-account [provider-account-id] + (let [cob-session (login-cobrand) + user-session (login-user cob-session)] + (-> (str (:yodlee-base-url env) "/accounts?providerAccountId=" provider-account-id) + (client/get {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)}) + :as :json}) + :body + :account))) + (defn get-account [i] (let [cob-session (login-cobrand) user-session (login-user cob-session)] @@ -252,7 +261,7 @@ (future (loop [] (try - (reset! in-memory-cache (get-provider-accounts-with-accounts)) + (reset! in-memory-cache (get-provider-accounts-with-accounts)) (catch Exception e (println e))) (Thread/sleep (* 30 1000 5)) @@ -260,10 +269,29 @@ (recur))))) (defn refresh-provider-account [id] - (swap! in-memory-cache + (swap! in-memory-cache (fn [i] (-> (by :id i) - (update id merge (get-provider-account-detail id)) + (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-cobrand) + user-session (login-user cob-session) + batch-size 100] + + (-> (str (:yodlee-base-url env) "/providerAccounts/" id ) + + (client/delete {:headers (merge base-headers {"Authorization" (auth-header cob-session user-session)}) + :as :json}) + :body + :providerAccount + first)) + (swap! in-memory-cache + (fn [i] + (-> (by :id i) + (dissoc id) vals)))) (defn update-yodlee [id] diff --git a/src/clj/user.clj b/src/clj/user.clj index 3a19d8bc..17330052 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -287,7 +287,7 @@ '[?e :transaction/client ?c] '[?c :client/code ?client-code] ]} - :args [(d/db (d/connect uri)) client-code]}) + :args [(d/db (d/connect uri)) client-code]}) (mapcat (fn [[{:transaction/keys [accounts]}]] (mapv @@ -300,3 +300,48 @@ ) vec)) +(defn patch-missing-ledger-entries [] + @(d/transact + (d/connect uri) + (mapv + #(auto-ap.ledger/entity-change->ledger (d/db (d/connect uri)) [:transaction %]) + (concat + (->> + (d/query {:query {:find ['?t ] + :in ['$] + :where ['[?t :transaction/date] + '(not [?t :transaction/approval-status :transaction-approval-status/excluded]) + '(not-join [?t] [?e :journal-entry/original-entity ?t])]} + :args [(d/db (d/connect uri))]}) + (map first)))))) + + + +(defn check-for-out-of-date-ledger [?code] + [(d/query {:query {:find ['(count ?e)] + :in ['$ '?code] + :where ['[?e :transaction/accounts ?ta] + '[?e :transaction/matched-rule] + '[?e :transaction/approval-status :transaction-approval-status/approved] + '(not [?ta :transaction-account/location]) + '[?e :transaction/client ?c] + '[?c :client/code ?code] + ]} + :args [(d/db (d/connect uri)) ?code]}) + + (d/query {:query {:find ['?t ] + :in ['$] + :where ['[?t :transaction/date] + '(not [?t :transaction/approval-status :transaction-approval-status/excluded]) + '(not-join [?t] [?e :journal-entry/original-entity ?t])]} + :args [(d/db (d/connect uri))]}) + + (d/query {:query {:find ['?t ] + :in ['$] + :where ['[?t :transaction/date] + '(not [?t :transaction/approval-status :transaction-approval-status/excluded]) + '[?t :transaction/vendor ?v] + '[?j :journal-entry/original-entity ?t] + '(not [?j :journal-entry/vendor ?v]) + #_'(not-join [?t] [?e :journal-entry/original-entity ?t])]} + :args [(d/db (d/connect uri))]})]) diff --git a/src/cljs/auto_ap/views/components/modal.cljs b/src/cljs/auto_ap/views/components/modal.cljs index 8720d418..5b3dfab0 100644 --- a/src/cljs/auto_ap/views/components/modal.cljs +++ b/src/cljs/auto_ap/views/components/modal.cljs @@ -54,3 +54,16 @@ [:div.notification.is-info warning]]] (into (r/children (r/current-component))) (into [(when saving? [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])]))]))) + + +(defn simple-modal [{:keys [title foot class warning action-text id save-event can-submit? status-from] :or {can-submit? true}} & rest] + (let [{:keys [visible? saving? error-message]} @(re-frame/subscribe [::subs/modal-state id status-from])] + (when visible? + (-> [modal {:title [:span title] + :class class + :foot foot + :id id + :hide-event [::events/modal-status id {:visible? false :error-message nil}]}] + (into (r/children (r/current-component))) + (into [(when saving? [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])]))))) + diff --git a/src/cljs/auto_ap/views/pages/admin/yodlee.cljs b/src/cljs/auto_ap/views/pages/admin/yodlee.cljs index 01cd2f51..fcb40e5f 100644 --- a/src/cljs/auto_ap/views/pages/admin/yodlee.cljs +++ b/src/cljs/auto_ap/views/pages/admin/yodlee.cljs @@ -1,5 +1,6 @@ (ns auto-ap.views.pages.admin.yodlee - (:require-macros [cljs.core.async.macros :refer [go]]) + (:require-macros [cljs.core.async.macros :refer [go]] + ) (:require [re-frame.core :as re-frame] [auto-ap.forms :as forms] [reagent.core :as reagent] @@ -7,13 +8,13 @@ [cljs-time.format :as f] [cljs-time.core :as time] [auto-ap.subs :as subs] - [auto-ap.events.admin.clients :as events] + [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 :refer [action-modal]] + [auto-ap.views.components.modal :refer [simple-modal modal]] [cljs.reader :as edn] [auto-ap.routes :as routes] [bidi.bidi :as bidi])) @@ -224,20 +225,91 @@ :on-success [::authenticated] :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 [::forms/form-closing [::refresh-provider-account i]]})) + +(re-frame/reg-event-fx + ::delete-provider-account + [with-user ] + (fn [{:keys [user db]} [_ provider-account-id ]] + {:db (forms/loading db [::delete-provider-account provider-account-id]) + :dispatch (dispatch-event [::events/modal-status [::delete provider-account-id] {:visible? false}] ) + :http {:token user + :method :post + :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] ]}})) + + +(defn delete-button [account-id] + (let [confirmed? (reagent/atom false)] + (fn [] + (let [delete-form @(re-frame/subscribe [::forms/form [::delete-provider-account account-id]])] + [:div + (cond + (= :loading (:status delete-form)) [:button.button.is-disabled.is-loading [:i.fa.fa-times]] + (:error delete-form) [:button.button.is-disabled [:span.icon [:i.fa.fa-exclamation-triangle]]] + :else + [:button.button + {:on-click (dispatch-event [::events/modal-status [::delete account-id] {:visible? true}])} + [:span.icon [:i.fa.fa-times]]]) + [simple-modal {:id [::delete account-id] + :title "Confirmation" + :foot [:div + [:button.button.is-danger {:on-click (dispatch-event [::delete-provider-account account-id] )} "Delete provider account"] + [:button.button {:on-click (dispatch-event [::events/modal-status [::delete account-id] {:visible? false}] )}"Cancel"]]} + "Are you sure you want to delete provider account " account-id "?"]])))) + (defn yodlee-provider-accounts-table [] (if @(re-frame/subscribe [::provider-accounts-loading?]) [:div "Loading..."] [:div.columns - [:div.column.is-three-quarters + [:div.column.is-half (doall - (for [account @(re-frame/subscribe [::provider-accounts])] + (for [account @(re-frame/subscribe [::provider-accounts]) + :let [{:keys [error status] :as g} @(re-frame/subscribe [::forms/form [::refresh-provider-account (:id account)]])]] ^{:key (:id account)} [:div.card {:style {:margin-bottom "1em"}} [:div.card-header - [:div.card-header-title "Provider account " (:id account) - ]] + [:div.card-header-title "Provider account " (:id account)] + [: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) @@ -255,7 +327,7 @@ ", and last attempted " (yodlee-date->str (-> account :dataset first :lastUpdateAttempt)) "."]]] - [:div.level-right [:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Force refresh" ]]] + [:div.level-right [:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Sync yodlee with bank" ]]] ]) @@ -275,7 +347,7 @@ 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)] + :submit-event [::reauthenticate-mfa (:id account)] :id [::mfa-form (:id account)]} )] (form-inline {:title "Reauthenticate"} [:<>