From 45cf97a480bc14176a375b139ef017c4329c4044 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Thu, 7 Apr 2022 08:22:50 -0700 Subject: [PATCH] User can set up sales queries on their own now. --- src/clj/auto_ap/datomic/migrate.clj | 4 +- src/clj/auto_ap/graphql/clients.clj | 153 +++++++++++++++++- src/clj/auto_ap/routes/auth.clj | 3 +- src/clj/auto_ap/routes/queries.clj | 32 ++-- src/clj/auto_ap/yodlee/core2.clj | 95 ++++++----- src/clj/user.clj | 135 +--------------- .../views/pages/admin/clients/table.cljs | 90 +++++++---- 7 files changed, 288 insertions(+), 224 deletions(-) diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 16702105..e09c3eba 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -12,6 +12,7 @@ [auto-ap.datomic.migrate.invoice-converter :refer [add-import-status-existing-invoices]] [auto-ap.datomic.migrate.ledger :as ledger] + [auto-ap.datomic.migrate.queries :as queries] [auto-ap.datomic.migrate.plaid :as plaid] [auto-ap.datomic.migrate.sales :as sales] [auto-ap.datomic.migrate.vendors :as vendors] @@ -501,7 +502,8 @@ reports/norms-map plaid/norms-map audit/norms-map - vendors/norms-map)] + vendors/norms-map + queries/norms-map)] (println "Conforming database...") (c/ensure-conforms conn norms-map) #_(when (not (seq args)) diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index f7d89511..5f58cfbe 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -4,16 +4,17 @@ [auto-ap.graphql.utils :refer [->graphql assert-admin can-see-client? is-admin?]] [auto-ap.utils :refer [by]] [auto-ap.yodlee.core :refer [in-memory-cache]] + [auto-ap.routes.queries :as q] [auto-ap.square.core :as square] [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [clj-time.coerce :as coerce] - [config.core :refer [env]] [clojure.string :as str] [unilog.context :as lc] [clojure.tools.logging :as log] [datomic.api :as d] [clojure.java.io :as io] [amazonica.aws.s3 :as s3] + #_{:clj-kondo/ignore [:unused-namespace]} [yang.scheduler :as scheduler] [mount.core :as mount]) (:import [org.apache.commons.codec.binary Base64] @@ -51,7 +52,7 @@ [?ba :bank-account/plaid-account ?src] [?ba :bank-account/yodlee-account-id ?src])] new-db [:client/code client-code]) - (filter (fn [[src cnt]] + (filter (fn [[_ cnt]] (> cnt 1))))) (throw (ex-info "Cannot reuse yodlee/plaid/intuit account" {:validation-error (str "Cannot reuse yodlee/plaid/intuit account")}))))) @@ -183,11 +184,11 @@ [(get-else $ ?e :journal-entry-line/credit 0.0) ?credit]]} :args [(d/db conn) bank-account-id]}) debits (->> all-transactions - (map (fn [[e debit credit]] + (map (fn [[_ debit _]] debit)) (reduce + 0.0)) credits (->> all-transactions - (map (fn [[e debit credit]] + (map (fn [[_ _ credit]] credit)) (reduce + 0.0)) current-balance (if (= :bank-account-type/check (:bank-account/type (d/entity (d/db conn) bank-account-id))) @@ -255,7 +256,7 @@ :start (scheduler/every (* 17 60 1000) refresh-current-balance) :stop (scheduler/stop current-balance-worker)) -(defn get-client [context args value] +(defn get-client [context _ _] (->graphql (->> (d-clients/get-all) (filter #(can-see-client? (:id context) %)) @@ -276,6 +277,140 @@ +(def sales-summary-query + "[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns) + :with ?s + :in $ + :where + [?s :sales-order/client [:client/code \"%s\"]] +[?s :sales-order/date ?d] +[?s :sales-order/total ?total] +[?s :sales-order/tax ?tax] +[?s :sales-order/tip ?tip] +[?s :sales-order/service-charge ?service-charge] +[?s :sales-order/returns ?returns] +[?s :sales-order/discount ?discount] +[(clj-time.coerce/to-date-time ?d) ?d2] +[(auto-ap.time/localize ?d2) ?d3] +[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4] +]") + +(def sales-category-query + "[:find ?d4 ?n ?n2 (sum ?total) (sum ?tax) (sum ?discount) + :with ?s ?li + :in $ + :where + [?s :sales-order/client [:client/code \"%s\"]] + [?s :sales-order/date ?d] + [?s :sales-order/line-items ?li] + [?li :order-line-item/category ?n] + [(get-else $ ?li :order-line-item/item-name \"\") ?n2] +[?li :order-line-item/total ?total] +[?li :order-line-item/tax ?tax] +[?li :order-line-item/discount ?discount] +[(clj-time.coerce/to-date-time ?d) ?d2] +[(auto-ap.time/localize ?d2) ?d3] +[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4] +]") + +(def expected-deposits-query + "[:find ?d4 ?t ?f +:in $ +:where + [?c :client/code \"%s\"] +[?s :expected-deposit/client ?c] +[?s :expected-deposit/total ?t] +[?s :expected-deposit/fee ?f] +[?s :expected-deposit/sales-date ?date] +[(clj-time.coerce/to-date-time ?date) ?d2] +[(auto-ap.time/localize ?d2) ?d3] +[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4] +]") + +(def tenders-query + "[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip) +:with ?charge +:in $ +:where + [?c :client/code \"%s\"] +[?s :sales-order/client ?c] +[?s :sales-order/charges ?charge] +[?charge :charge/type-name ?type] +[?charge :charge/total ?total] +[?charge :charge/tip ?tip] +[(get-else $ ?charge :charge/processor :na) ?ccp] +[(get-else $ ?ccp :db/ident :na) ?p] +[(name ?p) ?p2] +[?s :sales-order/date ?date] +[(clj-time.coerce/to-date-time ?date) ?d2] +[(auto-ap.time/localize ?d2) ?d3] +[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4] +]") + +(def refunds-query + "[:find ?d4 ?t (sum ?total) (sum ?fee) +:with ?r +:in $ +:where + [?r :sales-refund/client [:client/code \"%s\"]] +[?r :sales-refund/total ?total] +[?r :sales-refund/fee ?fee] +[?r :sales-refund/date ?date] +[?r :sales-refund/type ?t] +[(clj-time.coerce/to-date-time ?date) ?d2] +[(auto-ap.time/localize ?d2) ?d3] +[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]]") + + + +(defn setup-sales-queries [context args _] + (assert-admin (:id context)) + (let [{client-code :client/code} (d/pull (d/db conn) '[:client/code] (:client_id args))] + (q/put-query (str (UUID/randomUUID)) + (format sales-summary-query client-code) + (str "sales query for " client-code) + (str client-code "-sales-summary") + [:client/code client-code] + ) + (q/put-query (str (UUID/randomUUID)) + (format sales-category-query client-code) + (str "sales category query for " client-code) + (str client-code "-sales-category") + [:client/code client-code] + ) + (q/put-query (str (UUID/randomUUID)) + (format expected-deposits-query client-code) + (str "expected deposit query for " client-code) + (str client-code "-expected-deposit") + [:client/code client-code] + ) + (q/put-query (str (UUID/randomUUID)) + (format tenders-query client-code) + (str "tender query for " client-code) + (str client-code "-tender") + [:client/code client-code] + ) + + (q/put-query (str (UUID/randomUUID)) + (format refunds-query client-code) + (str "refunds query for " client-code) + (str client-code "-refund") + [:client/code client-code]) + + (let [sales-summary-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-summary")])) + sales-category-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-category")])) + expected-deposit-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-expected-deposit")])) + tender-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-tender")])) + refund-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-refund")]))] + {:message (str/join "\n" + [ + (str "For " client-code ":") + (str "Sales: " "https://app.integreatconsult.com/api/queries/" sales-summary-id "/results/json") + (str "Sales Category: " "https://app.integreatconsult.com/api/queries/" sales-category-id "/results/json") + (str "Expected Deposits: " "https://app.integreatconsult.com/api/queries/" expected-deposit-id "/results/json") + (str "Tenders: " "https://app.integreatconsult.com/api/queries/" tender-id "/results/json") + (str "Refund: " "https://app.integreatconsult.com/api/queries/" refund-id "/results/json")])}))) + (def objects {:location_match @@ -353,7 +488,10 @@ (def mutations {:edit_client {:type :client :args {:edit_client {:type :edit_client}} - :resolve :mutation/edit-client}}) + :resolve :mutation/edit-client} + :setup_sales_queries {:type :message + :args {:client_id {:type :id}} + :resolve :mutation/setup-sales-queries}}) (def input-objects {:edit_location_match {:fields {:location {:type 'String} @@ -418,7 +556,8 @@ (def resolvers {:get-client get-client - :mutation/edit-client edit-client}) + :mutation/edit-client edit-client + :mutation/setup-sales-queries setup-sales-queries}) (defn attach [schema] diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index 83470192..e8e3dee7 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -40,13 +40,12 @@ :user/name (:name profile)}) ] (log/info "authenticated as user" user) - ;; TODO - these namespaces are not being transmitted/deserialized properly (if (and token user) (let [jwt (jwt/sign {:user (:name profile) :exp (time/plus (time/now) (time/days 30)) :user/clients (map (fn [c] - (dissoc c :client/bank-accounts :client/location-matches :client/forecasted-transactions :client/matches :client/week-a-debits :client/week-a-credits :client/week-b-debits :client/week-b-credits :client/signature-file :client/address :client/emails :client/square-auth-token :client/square-locations)) + (select-keys c [:client/code :db/id :client/name :client/locations])) (:user/clients user)) :user/role (name (:user/role user)) :user/name (:name profile)} diff --git a/src/clj/auto_ap/routes/queries.clj b/src/clj/auto_ap/routes/queries.clj index 41315fc0..3732309a 100644 --- a/src/clj/auto_ap/routes/queries.clj +++ b/src/clj/auto_ap/routes/queries.clj @@ -41,17 +41,27 @@ (into [(d/db conn)] (clojure.edn/read-string (get query-params "args" "[]"))))))))) -(defn put-query [id body note] - (s3/put-object :bucket-name (:data-bucket env) - :key (str "queries/" id) - :input-stream (io/make-input-stream (.getBytes body) {}) - :metadata {:content-type "application/text" - :user-metadata {:note note}}) - {:body {:query body - :id id - :results-url (str "/api/queries/" id "/results") - :csv-results-url (str "/api/queries/" id "/results/csv") - :json-results-url (str "/api/queries/" id "/results/json")}}) +(defn put-query [guid body note & [lookup-key client]] + (let [guid (if lookup-key + (or (:saved-query/guid (d/pull (d/db conn) [:saved-query/guid] [:saved-query/lookup-key lookup-key])) + guid) + guid)] + @(d/transact conn [(cond-> + {:saved-query/guid guid + :saved-query/description note + :saved-query/key (str "queries/" guid)} + client (assoc :saved-query/client client) + lookup-key (assoc :saved-query/lookup-key lookup-key))]) + (s3/put-object :bucket-name (:data-bucket env) + :key (str "queries/" guid) + :input-stream (io/make-input-stream (.getBytes body) {}) + :metadata {:content-type "application/text" + :user-metadata {:note note}}) + {:body {:query body + :id guid + :results-url (str "/api/queries/" guid "/results") + :csv-results-url (str "/api/queries/" guid "/results/csv") + :json-results-url (str "/api/queries/" guid "/results/json")}})) (def json-routes (context "/queries" [] diff --git a/src/clj/auto_ap/yodlee/core2.clj b/src/clj/auto_ap/yodlee/core2.clj index 56dcb907..9efa11c0 100644 --- a/src/clj/auto_ap/yodlee/core2.clj +++ b/src/clj/auto_ap/yodlee/core2.clj @@ -24,50 +24,68 @@ (defn auth-header ([cob-session] (str "Bearer " cob-session))) - +(defn retry-thrice + ([x] (retry-thrice x 0)) + ([x i] + (try + (x) + (catch Exception e + (if (>= i 3) + (throw e) + (do + (Thread/sleep 5000) + (retry-thrice x (inc i)))))))) (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] + :socket-timeout 60000 + :connection-timeout 60000 + :retry-handler (fn [ex _ _] (log/error "yodlee Error." ex) false)} - {:retry-handler (fn [ex try-count http-context] + {:retry-handler (fn [ex _ _] (log/error "yodlee Error." ex) - false)})) + false) + :socket-timeout 60000 + :connection-timeout 60000})) (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)) + (retry-thrice + (fn [] + (-> (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 [client-code] - (log/info "logging in as " client-code) - (-> (str (:yodlee2-base-url env) "/auth/token") - (client/post (merge {:headers (assoc base-headers - "loginName" (if (<= (count client-code) 3) - (str client-code client-code) - client-code) - "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)) + (retry-thrice + (fn [] + (log/info "logging in as " client-code) + (-> (str (:yodlee2-base-url env) "/auth/token") + (client/post (merge {:headers (assoc base-headers + "loginName" (if (<= (count client-code) 3) + (str client-code client-code) + client-code) + "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 [client-code ] (let [cob-session (login-user (client-code->login client-code))] @@ -93,14 +111,16 @@ []))) (defn get-provider-accounts [client-code ] - (log/info "logging in user " client-code) - (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts") - (-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )}) - :as :json} - other-config)) - :body - :providerAccount)))) + (retry-thrice + (fn [] + (log/info "logging in user " client-code) + (let [cob-session (login-user (client-code->login client-code))] + (-> (str (:yodlee2-base-url env) "/providerAccounts") + (-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )}) + :as :json} + other-config)) + :body + :providerAccount)))))) @@ -323,7 +343,6 @@ (defn reauthenticate [client-code pa data] - (println client-code) (try (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa) diff --git a/src/clj/user.clj b/src/clj/user.clj index 14410985..682a2354 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -15,7 +15,7 @@ [clj-time.core :as t] [clojure.java.io :as io] [clojure.string :as str] - [auto-ap.routes.queries :as q] + [amazonica.aws.s3 :as s3]) (:import [org.apache.commons.io.input BOMInputStream] java.util.UUID)) @@ -477,139 +477,6 @@ (println "failed " e))) (async/str]] + [auto-ap.views.utils :refer [action-cell-width date->str with-user]] [auto-ap.views.components.grid :as grid] - [auto-ap.views.components.buttons :as buttons])) + [auto-ap.views.components.modal :as modal] + [auto-ap.views.components.buttons :as buttons] + [auto-ap.status :as status])) (re-frame/reg-sub ::specific-params @@ -17,6 +19,28 @@ (fn [{:keys [db]} [_ p]] {:db (assoc db ::params p)})) +(re-frame/reg-event-fx + ::sales-queries-setup + (fn [_ [_ results]] + {:dispatch [::modal/modal-requested {:title "Sales Queries" + :body [:div [:pre (:message (:setup-sales-queries results))]]}]})) + +(re-frame/reg-event-fx + ::setup-sales-queries + [with-user] + (fn [{:keys [user]} [_ client-id]] + {:graphql + {:token user + :owns-state {:multi ::setup-sales-queries + :which client-id} + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "SetupSalesQueries"} + :venia/queries [{:query/data [:setup-sales-queries + {:client-id client-id} + [:message]]}]} + :on-success [::sales-queries-setup]}} + )) + (re-frame/reg-sub ::params :<- [::specific-params] @@ -26,33 +50,37 @@ (defn clients-table [{:keys [page status]}] - [grid/grid {:on-params-change (fn [p] - (re-frame/dispatch [::params-changed p])) - :status status - :params @(re-frame/subscribe [::params]) - :column-count 5} - [grid/controls page] - [grid/table {:fullwidth true} - [grid/header - [grid/row {} - [grid/header-cell {} "Name"] - [grid/header-cell {:style {:width "20em"}} "Code"] - [grid/header-cell {} "Locations"] - [grid/header-cell {} "Locked Until"] - [grid/header-cell {} "Email"] - [grid/header-cell {:style {:width (action-cell-width 1)}}]] - ] - [grid/body - (for [{:keys [id name email locked-until code locations] :as c} (:data page)] - ^{:key (str name "-" id )} - [grid/row {:id id} - [grid/cell {} name] - [grid/cell {} code] - [grid/cell {} (str/join ", " locations)] - [grid/cell {} [:div.tag (or (some-> locked-until date->str) - "Not locked" + (let [states @(re-frame/subscribe [::status/multi ::setup-sales-queries])] + [grid/grid {:on-params-change (fn [p] + (re-frame/dispatch [::params-changed p])) + :status status + :params @(re-frame/subscribe [::params]) + :column-count 5} + [grid/controls page] + [grid/table {:fullwidth true} + [grid/header + [grid/row {} + [grid/header-cell {} "Name"] + [grid/header-cell {:style {:width "20em"}} "Code"] + [grid/header-cell {} "Locations"] + [grid/header-cell {} "Locked Until"] + [grid/header-cell {} "Email"] + [grid/header-cell {:style {:width (action-cell-width 2)}}]] + ] + [grid/body + (for [{:keys [id name email locked-until code locations]} (:data page)] + ^{:key (str name "-" id )} + [grid/row {:id id} + [grid/cell {} name] + [grid/cell {} code] + [grid/cell {} (str/join ", " locations)] + [grid/cell {} [:div.tag (or (some-> locked-until date->str) + "Not locked" - )]] - [grid/cell {} email] - [grid/cell {} [buttons/fa-icon {:event [::form/editing id] - :icon :fa-pencil}]]])]]]) + )]] + [grid/cell {} email] + [grid/cell {} [:div.buttons [buttons/fa-icon {:event [::setup-sales-queries id] + :class (status/class-for (get states id)) + :icon :fa-dollar}] + [buttons/fa-icon {:event [::form/editing id] + :icon :fa-pencil}]]]])]]]))