diff --git a/src/clj/auto_ap/datomic/migrate/clients.clj b/src/clj/auto_ap/datomic/migrate/clients.clj index d4af9406..00544623 100644 --- a/src/clj/auto_ap/datomic/migrate/clients.clj +++ b/src/clj/auto_ap/datomic/migrate/clients.clj @@ -3,4 +3,11 @@ {:txes [[{:db/ident :bank-account/start-date :db/doc "Setting this date prevents older transactions from being imported" :db/valueType :db.type/instant - :db/cardinality :db.cardinality/one}]]}}) + :db/cardinality :db.cardinality/one}]]} + + ::add-bank-account-current-balance + {:txes [[{:db/ident :bank-account/current-balance + :db/doc "A precomputed balance for the account" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one + :db/noHistory true}]]}}) diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index cc57ed15..74e30d58 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -151,7 +151,7 @@ (defn graphql-results [ids db args] (let [results (->> (d/pull-many db '[* {:transaction/client [:client/name :db/id :client/code] :transaction/approval-status [:db/ident :db/id] - :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations] + :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance] :transaction/forecast-match [:db/id :forecasted-transaction/identifier] :transaction/vendor [:db/id :vendor/name] :transaction/matched-rule [:db/id :transaction-rule/note] @@ -198,7 +198,7 @@ (d/pull (d/db (d/connect uri)) '[* {:transaction/client [:client/name :db/id :client/code :client/locations] :transaction/approval-status [:db/ident :db/id] - :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations] + :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance] :transaction/vendor [:db/id :vendor/name] :transaction/matched-rule [:db/id :transaction-rule/note] :transaction/accounts [:transaction-account/amount diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 5f97f478..b1f6c579 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -139,6 +139,7 @@ :name {:type 'String} :bank_code {:type 'String} :bank_name {:type 'String} + :current_balance {:type :money} :yodlee_account_id {:type 'Int} :yodlee_account {:type :yodlee_account} :locations {:type '(list String)}}} diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index 77783f28..ac2a197b 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -5,10 +5,13 @@ [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]) + [amazonica.aws.s3 :as s3] + [yang.scheduler :as scheduler] + [mount.core :as mount]) (:import [org.apache.commons.codec.binary Base64] java.util.UUID)) @@ -122,6 +125,86 @@ lms))) ->graphql))) +(defn refresh-bank-account-current-balance [bank-account-id] + (let [all-transactions (d/query + {:query {:find ['?e '?debit '?credit] + :in ['$ '?bank-account-id] + :where '[[?e :journal-entry-line/account ?bank-account-id] + [(get-else $ ?e :journal-entry-line/debit 0.0) ?debit] + [(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]] + debit)) + (reduce + 0.0)) + credits (->> all-transactions + (map (fn [[e debit credit]] + credit)) + (reduce + 0.0)) + current-balance (if (= :bank-account-type/check (:bank-account/type (d/entity (d/db conn) bank-account-id))) + (- debits credits) + (- credits debits))] + @(d/transact conn [{:db/id bank-account-id + :bank-account/current-balance current-balance}]))) + +(defn bank-accounts-needing-refresh [] + (let [last-refreshed (->> (d/query + {:query {:find ['?ba '?tx] + :in ['$ ] + :where ['[?ba :bank-account/current-balance _ ?tx true]]} + :args [(d/history (d/db conn))]}) + (group-by first) + (map (fn [[ba balance-txes]] + [ba (->> balance-txes + (map second) + (sort-by #(- %)) + first)]))) + has-newer-transaction (->> (d/query + {:query {:find ['?ba] + :in '[$ [[?ba ?last-refreshed] ...] ] + :where ['[_ :journal-entry-line/account ?ba ?tx] + '[(>= ?tx ?last-refreshed)]]} + :args [(d/history (d/db conn)) + last-refreshed]}) + (map first) + (set)) + + no-current-balance (->> (d/query {:query {:find ['?ba] + :in '[$] + :where ['[?ba :bank-account/code] + '(not [?ba :bank-account/current-balance]) + ]} + :args [(d/db conn)]}) + (map first))] + (into has-newer-transaction no-current-balance))) + +(defn build-current-balance [bank-accounts] + (doseq [bank-account bank-accounts] + (log/info "Refreshing bank account" (-> (d/db conn) (d/entity bank-account) :bank-account/code)) + (try + (refresh-bank-account-current-balance bank-account) + (catch Exception e + (log/error "Can't refresh current balance" e))))) + +(defn refresh-all-current-balance [] + (lc/with-context {:source "current-balance-cache"} + (build-current-balance (->> (d/query {:query {:find ['?ba] + :in '[$] + :where ['[?ba :bank-account/code]]} + :args [(d/db conn)]}) + (map first))))) + +(defn refresh-current-balance [] + (lc/with-context {:source "current-balance-cache"} + (try + (log/info "Refreshing running balance cache") + (build-current-balance (bank-accounts-needing-refresh)) + (catch Exception e + (log/error e))))) + +(mount/defstate current-balance-worker + :start (scheduler/every (* 17 60 1000) refresh-current-balance) + :stop (scheduler/stop current-balance-worker)) (defn get-client [context args value] (->graphql diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index 0ea08efa..e9706f2c 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -1,5 +1,5 @@ (ns auto-ap.graphql.invoices - (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin enum->keyword]] + (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin assert-power-user enum->keyword]] [auto-ap.datomic.vendors :as d-vendors] [auto-ap.datomic.clients :as d-clients] @@ -38,13 +38,17 @@ :count Integer/MAX_VALUE))))) (defn reject-invoices [context {:keys [invoices] :as in} value] - (assert-admin (:id context)) + (assert-power-user (:id context)) + (doseq [i invoices] + (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i))))) (let [transactions (map (fn [i] [:db/retractEntity i ]) invoices) transaction-result (audit-transact transactions (:id context))] invoices)) (defn approve-invoices [context {:keys [invoices] :as in} value] - (assert-admin (:id context)) + (assert-power-user (:id context)) + (doseq [i invoices] + (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i))))) (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices) transaction-result (audit-transact transactions (:id context))] invoices)) diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index c8022ff7..cbd0b576 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -4,7 +4,7 @@ [auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.vendors :as d-vendors] - [auto-ap.graphql.utils :refer [assert-admin]] + [auto-ap.graphql.utils :refer [assert-admin assert-can-see-client]] [auto-ap.logging :refer [info-event]] [auto-ap.parse :as parse] [auto-ap.parse.util :as parse-u] @@ -214,7 +214,7 @@ (throw (ex-info (str "No vendor with the name " vendor-code " was found.") {:vendor-code vendor-code}))))) -(defn import-uploaded-invoice [client forced-location forced-vendor imports] +(defn import-uploaded-invoice [user client forced-location forced-vendor imports] (lc/with-context {:area "upload-invoice"} (log/info "Number of invoices to import is" (count imports) "sample: " (first imports)) (let [clients (d-clients/get-all) @@ -229,6 +229,7 @@ (first (filter (fn [c] (= (:db/id c) (Long/parseLong client))) clients)))) + _ (assert-can-see-client user (:db/id matching-client)) _ (when-not matching-client (throw (ex-info (str "Searched clients for '" customer-identifier "'. No client found in file. Select a client first.") {:invoice-number invoice-number @@ -485,7 +486,8 @@ location :location location-2 "location" vendor :vendor - vendor-2 "vendor"} :params :as params} + vendor-2 "vendor"} :params :as params + user :identity} (let [files (or files files-2) client (or client client-2) location (or location location-2) @@ -494,7 +496,7 @@ {:keys [filename tempfile]} files] (lc/with-context {:parsing-file filename} (try - (import-uploaded-invoice client location vendor (parse/parse-file (.getPath tempfile) filename)) + (import-uploaded-invoice user client location vendor (parse/parse-file (.getPath tempfile) filename)) {:status 200 :body (pr-str {}) :headers {"Content-Type" "application/edn"}} diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index dd968f3f..6df24f17 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -3,6 +3,7 @@ [auto-ap.ledger :as ledger] [auto-ap.yodlee.core] [auto-ap.yodlee.core2 :as yodlee2] + [auto-ap.graphql.clients :as gq-clients] [auto-ap.background.invoices] [auto-ap.square.core :as square] [auto-ap.datomic.migrate :as migrate] @@ -35,6 +36,7 @@ #'ledger/touch-broken-ledger-worker #'ledger/process-txes-worker #'ledger/ledger-reconciliation-worker + #'gq-clients/current-balance-worker #'yodlee/import-transaction-worker #'yodlee2/yodlee-sync-worker #'migrate/migrate-start]))] diff --git a/src/cljs/auto_ap/views/components/invoices/side_bar.cljs b/src/cljs/auto_ap/views/components/invoices/side_bar.cljs index 6a95a3fe..3784a4aa 100644 --- a/src/cljs/auto_ap/views/components/invoices/side_bar.cljs +++ b/src/cljs/auto_ap/views/components/invoices/side_bar.cljs @@ -46,7 +46,8 @@ [:span {:class "icon icon-bin-2" :style {:font-size "25px"}}] [:span {:class "name"} "Voided Invoices"]]] - (when (= "admin" (:user/role user)) + (when (or (= "admin" (:user/role user)) + (= "power-user" (:user/role user))) [:li.menu-item [:a.item {:href (bidi/path-for routes/routes :import-invoices) :class [(active-when ap = :import-invoices)]} diff --git a/src/cljs/auto_ap/views/pages/transactions/common.cljs b/src/cljs/auto_ap/views/pages/transactions/common.cljs index 66f70217..0f4cd1c8 100644 --- a/src/cljs/auto_ap/views/pages/transactions/common.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/common.cljs @@ -16,4 +16,4 @@ :description_original [:payment [:check_number :s3_url :id :date]] [:client [:name :id]] - [:bank-account [:name :yodlee-account-id]]]) + [:bank-account [:name :yodlee-account-id :current-balance]]]) diff --git a/src/cljs/auto_ap/views/pages/transactions/table.cljs b/src/cljs/auto_ap/views/pages/transactions/table.cljs index 36833cdc..ef8f53b9 100644 --- a/src/cljs/auto_ap/views/pages/transactions/table.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/table.cljs @@ -67,7 +67,9 @@ (defn table [{:keys [id data-page check-boxes?]}] (let [selected-client @(re-frame/subscribe [::subs/client]) {:keys [data status params]} @(re-frame/subscribe [::data-page/page data-page]) - states @(re-frame/subscribe [::status/multi ::edits])] + states @(re-frame/subscribe [::status/multi ::edits]) + is-power-user? @(re-frame/subscribe [::subs/is-power-user?]) + is-admin? @(re-frame/subscribe [::subs/is-admin?])] [grid/grid {:data-page data-page :column-count (if selected-client 6 7) :check-boxes? check-boxes?} @@ -92,7 +94,13 @@ [grid/cell {} (:name client)]) #_[:td description-original] - [grid/cell {} (:name bank-account )] + [grid/cell {} + (if (and (:current-balance bank-account) + (or is-power-user? + is-admin?)) + [:span.has-tooltip-arrow.has-tooltip-right {:data-tooltip (str "Current Balance: " (nf (:current-balance bank-account) ))} + (:name bank-account ) ] + (:name bank-account ))] [grid/cell {} (cond vendor (:name vendor) yodlee-merchant