diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index a273a8f5..241b99e7 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -1,32 +1,31 @@ (ns auto-ap.graphql.transactions - (:require [auto-ap.datomic - :refer - [audit-transact audit-transact-batch conn remove-nils]] - [auto-ap.datomic.accounts :as a] - [auto-ap.datomic.checks :as d-checks] - [auto-ap.datomic.invoices :as d-invoices] - [auto-ap.datomic.transaction-rules :as tr] - [auto-ap.datomic.transactions :as d-transactions] - [auto-ap.graphql.transaction-rules :as g-tr] - [auto-ap.graphql.utils - :refer - [->graphql - <-graphql - assert-admin - assert-can-see-client - assert-power-user - enum->keyword - ident->enum-f - snake->kebab]] - [auto-ap.import.transactions :as i-transactions] - [auto-ap.rule-matching :as rm] - [auto-ap.utils :refer [dollars=]] - [clj-time.coerce :as coerce] - [clojure.set :as set] - [clojure.string :as str] - [clojure.tools.logging :as log] - [com.walmartlabs.lacinia.util :refer [attach-resolvers]] - [datomic.api :as d])) + (:require + [auto-ap.datomic + :refer [audit-transact audit-transact-batch conn remove-nils]] + [auto-ap.datomic.accounts :as a] + [auto-ap.datomic.checks :as d-checks] + [auto-ap.datomic.invoices :as d-invoices] + [auto-ap.datomic.transaction-rules :as tr] + [auto-ap.datomic.transactions :as d-transactions] + [auto-ap.graphql.transaction-rules :as g-tr] + [auto-ap.graphql.utils + :refer [->graphql + <-graphql + assert-admin + assert-can-see-client + assert-power-user + enum->keyword + ident->enum-f + snake->kebab]] + [auto-ap.import.transactions :as i-transactions] + [auto-ap.rule-matching :as rm] + [auto-ap.utils :refer [dollars=]] + [clj-time.coerce :as coerce] + [clojure.set :as set] + [clojure.string :as str] + [clojure.tools.logging :as log] + [com.walmartlabs.lacinia.util :refer [attach-resolvers]] + [datomic.api :as d])) (def approval-status->graphql (ident->enum-f :transaction/approval-status)) @@ -37,7 +36,7 @@ (throw (ex-info "In order to select potential duplicates, you must choose a bank account." {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) (when-not (seq (->> filters - (filter (fn [[k v]] + (filter (fn [[_ v]] (not (nil? v)))) (filter (comp (complement #{:id :start :sort :client_id :bank_account_id :potential_duplicates :per_page}) first)) @@ -49,7 +48,7 @@ (throw (ex-info "In order to select potential duplicates, you must filter your view more." {:validation-error "In order to select potential duplicates, you must filter your view more."}))))) -(defn get-transaction-page [context args value] +(defn get-transaction-page [context args _] (let [args (assoc (:filters args) :id (:id context)) _ (assert-filtered-enough args) [transactions transactions-count] (d-transactions/get-graphql (update (<-graphql args) :approval-status enum->keyword "transaction-approval-status")) @@ -60,18 +59,20 @@ :start (:start args 0) :end (+ (:start args 0) (count transactions))})) -(defn bulk-change-status [context args value] +(defn get-ids-matching-filters [args] + (let [ids (some-> (:filters args) + (<-graphql) + (update :approval-status enum->keyword "transaction-approval-status") + (assoc :per-page Integer/MAX_VALUE) + (d-transactions/raw-graphql-ids ) + :ids) + specific-ids (d-transactions/filter-ids (:ids args))] + (into (set ids) specific-ids))) + +(defn bulk-change-status [context args _] (let [_ (assert-admin (:id context)) args (assoc args :id (:id context)) - ids (some-> (:filters args) - (assoc :id (:id context)) - (<-graphql) - (update :approval-status enum->keyword "transaction-approval-status") - (assoc :per-page Integer/MAX_VALUE) - (d-transactions/raw-graphql-ids ) - :ids) - specific-ids (d-transactions/filter-ids (:ids args)) - all-ids (into (set ids) specific-ids)] + all-ids (get-ids-matching-filters args)] (log/info "Unapproving " (count all-ids) args) @@ -83,18 +84,68 @@ (:id context)) {:message (str "Succesfully changed " (count all-ids) " transactions to be " (name (:status args) ) ".")})) +;; TODO very similar to rule-matching +(defn maybe-code-accounts [transaction account-rules valid-locations] + (with-precision 2 + (let [accounts (vec (mapcat + (fn [ar] + (println ar) + (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) + (:transaction/amount transaction) + 100))))] + (if (= "Shared" (:location ar)) + (do + (log/info "here" valid-locations) + (->> valid-locations + (map + (fn [cents location] + {:transaction-account/account (:account_id ar) + :transaction-account/amount (* 0.01 cents) + :transaction-account/location location}) + (rm/spread-cents cents-to-distribute (count valid-locations))))) + [(cond-> {:transaction-account/account (:account_id ar) + :transaction-account/amount (* 0.01 cents-to-distribute)} + (:location ar) (assoc :transaction-account/location (:location ar)))]))) + account-rules)) + accounts (mapv + (fn [a] + (update a :transaction-account/amount + #(with-precision 2 + (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) + accounts) + leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction)) + (Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts))))) + *math-context*)) + accounts (if (seq accounts) + (update-in accounts [(dec (count accounts)) :transaction-account/amount] #(+ % (double leftover))) + [])] + [:reset (:db/id transaction) :transaction/accounts accounts]))) -(defn delete-transactions [context args value] +(defn bulk-code-transactions [context args _] + (assert-admin (:id context)) + (let [args (assoc args :id (:id context)) + locations (:client/locations (d/pull (d/db conn) + [:client/locations] + (:client_id (:filters args)))) + all-ids (get-ids-matching-filters args) + transactions (d/pull-many (d/db conn) '[:db/id :transaction/amount] (vec all-ids))] + (log/info "Bulk coding " (count all-ids) args) + (audit-transact-batch + (mapcat (fn [i] + (cond-> [(cond-> i + (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) + (:vendor args) (assoc :transaction/vendor (:vendor args)))] + + (seq (:accounts args)) (conj (maybe-code-accounts i (:accounts args) locations)))) + transactions) + (:id context)) + {:message (str "Successfully coded " (count all-ids) " transactions.")})) + + +(defn delete-transactions [context args _] (let [_ (assert-admin (:id context)) args (assoc args :id (:id context)) - ids (some-> (:filters args) - (<-graphql) - (update :approval-status enum->keyword "transaction-approval-status") - (assoc :per-page Integer/MAX_VALUE) - (d-transactions/raw-graphql-ids ) - :ids) - specific-ids (d-transactions/filter-ids (:ids args)) - all-ids (into (set ids) specific-ids) + all-ids (get-ids-matching-filters args) db (d/db conn)] (log/info "Deleting " (count all-ids) args) @@ -119,32 +170,29 @@ (:id context)) {:message (str "Succesfully deleted " (count all-ids) " transactions.")})) -(defn get-potential-autopay-invoices-matches [context args value] +(defn get-potential-autopay-invoices-matches [context args _] (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) - _ (assert-can-see-client (:id context) (:transaction/client transaction) )] - - (let [matches-set (i-transactions/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction) - (:db/id (:transaction/client transaction)))] - (->graphql (for [matches matches-set] - (for [[_ invoice-id ] matches] - (d-invoices/get-by-id invoice-id))))))) + _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + matches-set (i-transactions/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction) + (:db/id (:transaction/client transaction)))] + (->graphql (for [matches matches-set] + (for [[_ invoice-id ] matches] + (d-invoices/get-by-id invoice-id)))))) -(defn get-potential-unpaid-invoices-matches [context args value] +(defn get-potential-unpaid-invoices-matches [context args _] (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) - _ (assert-can-see-client (:id context) (:transaction/client transaction) )] - - (let [matches-set (i-transactions/match-transaction-to-unpaid-invoices (:transaction/amount transaction) - (:db/id (:transaction/client transaction)))] - (->graphql (for [matches matches-set] - (for [[_ invoice-id ] matches] - (d-invoices/get-by-id invoice-id))))))) + _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + matches-set (i-transactions/match-transaction-to-unpaid-invoices (:transaction/amount transaction) + (:db/id (:transaction/client transaction)))] + (->graphql (for [matches matches-set] + (for [[_ invoice-id ] matches] + (d-invoices/get-by-id invoice-id)))))) -(defn unlink-transaction [context args value] +(defn unlink-transaction [context args _] (let [_ (assert-power-user (:id context)) - args (assoc args :id (:id context)) transaction-id (:transaction_id args) transaction (d/pull (d/db conn) @@ -154,7 +202,6 @@ :transaction/vendor :transaction/accounts :transaction/client [:db/id] - {:transaction/payment [{:payment/status [:db/ident]} :db/id]} ] transaction-id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) @@ -241,7 +288,7 @@ (when (empty? (:location trans-account)) (throw (ex-info "Account is missing location" {:validation-error "Account is missing location"}))) - (when (and (not (empty? (:account/location account))) + (when (and (seq (:account/location account)) (not= (:location trans-account) (:account/location account))) (let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")] @@ -258,7 +305,7 @@ (when (nil? (:account_id trans-account)) (throw (ex-info "Account is missing account" {:validation-error "Account is missing account"}))))) -(defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match] :as transaction} :transaction} value] +(defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match]} :transaction} _] (let [existing-transaction (d-transactions/get-by-id id) _ (assert-can-see-client (:id context) (:transaction/client existing-transaction) ) _ (assert-valid-expense-accounts accounts) @@ -306,7 +353,7 @@ approval-status->graphql ->graphql))) -(defn match-transaction [context {:keys [transaction_id payment_id]} value] +(defn match-transaction [context {:keys [transaction_id payment_id]} _] (let [transaction (d-transactions/get-by-id transaction_id) payment (d-checks/get-by-id payment_id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) @@ -339,7 +386,7 @@ approval-status->graphql ->graphql)) -(defn match-transaction-autopay-invoices [context {:keys [transaction_id autopay_invoice_ids]} value] +(defn match-transaction-autopay-invoices [context {:keys [transaction_id autopay_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) @@ -379,7 +426,7 @@ approval-status->graphql ->graphql))) -(defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} value] +(defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) @@ -415,7 +462,7 @@ approval-status->graphql ->graphql))) -(defn match-transaction-rules [context {:keys [transaction_ids transaction_rule_id all]} value] +(defn match-transaction-rules [context {:keys [transaction_ids transaction_rule_id all]} _] (let [_ (assert-admin (:id context)) transaction_ids (if all (->> (g-tr/run-transaction-rule context {:transaction_rule_id transaction_rule_id @@ -509,8 +556,15 @@ :status {:type :transaction_approval_status} :ids {:type '(list :id)}} :resolve :mutation/bulk-change-transaction-status} - :delete_transactions {:type :message - :args {:filters {:type :transaction_filters} + :bulk_code_transactions {:type :message + :args {:filters {:type :transaction_filters} + :vendor {:type :id} + :approval_status {:type :transaction_approval_status} + :accounts {:type '(list :edit_percentage_account)} + :ids {:type '(list :id)}} + :resolve :mutation/bulk-code-transactions} + :delete_transactions {:type :message + :args {:filters {:type :transaction_filters} :ids {:type '(list :id)} :suppress {:type 'Boolean}} :resolve :mutation/delete-transactions} @@ -583,6 +637,7 @@ :mutation/unlink-transaction unlink-transaction :mutation/bulk-change-transaction-status bulk-change-status :mutation/delete-transactions delete-transactions + :mutation/bulk-code-transactions bulk-code-transactions :mutation/match-transaction match-transaction :mutation/match-transaction-autopay-invoices match-transaction-autopay-invoices :mutation/match-transaction-unpaid-invoices match-transaction-unpaid-invoices diff --git a/src/clj/auto_ap/rule_matching.clj b/src/clj/auto_ap/rule_matching.clj index 6fe251fc..a548ad1f 100644 --- a/src/clj/auto_ap/rule_matching.clj +++ b/src/clj/auto_ap/rule_matching.clj @@ -1,17 +1,4 @@ -(ns auto-ap.rule-matching - (:require [auto-ap.yodlee.core :as client] - [auto-ap.utils :refer [by]] - [datomic.api :as d] - [auto-ap.datomic :refer [uri remove-nils]] - [auto-ap.datomic.accounts :as a] - [clj-time.coerce :as coerce] - [digest :refer [sha-256]] - [auto-ap.datomic.checks :as d-checks] - [auto-ap.datomic.transactions :as d-transactions] - [auto-ap.datomic.clients :as d-clients] - [auto-ap.time :as time] - [auto-ap.datomic.transaction-rules :as tr] - [clojure.tools.logging :as log])) +(ns auto-ap.rule-matching) (defn ->pattern [x] (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) @@ -101,7 +88,7 @@ (filter #(rule-applies? transaction %)))) (defn spread-cents [cents n] - (let [default-spread (for [x (range n)] + (let [default-spread (for [_ (range n)] (int (* cents (/ 1.0 n)))) short-by (- cents (reduce + 0 default-spread)) ;; amount that was lost in the differenc adjusted-spread (map @@ -157,5 +144,4 @@ (let [matching-rules (get-matching-rules-by-priority rules-by-priority transaction )] (if-let [top-match (and (= (count matching-rules) 1) (first matching-rules))] (apply-rule transaction top-match valid-locations) - transaction)))))) diff --git a/src/cljs/auto_ap/views/pages/transactions.cljs b/src/cljs/auto_ap/views/pages/transactions.cljs index 44a84143..ac3b3403 100644 --- a/src/cljs/auto_ap/views/pages/transactions.cljs +++ b/src/cljs/auto_ap/views/pages/transactions.cljs @@ -2,18 +2,18 @@ (:require [auto-ap.effects.forward :as forward] [auto-ap.forms :as forms] [auto-ap.subs :as subs] - [auto-ap.utils :refer [replace-by]] + [auto-ap.views.components.modal :as modal] [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.pages.transactions.common :refer [transaction-read]] + [auto-ap.views.pages.transactions.common :refer [transaction-read data-params->query-params]] [auto-ap.views.pages.transactions.form :as edit] [auto-ap.views.pages.transactions.manual :as manual] + [auto-ap.views.pages.transactions.bulk-updates :as bulk] [auto-ap.views.pages.transactions.side-bar :as side-bar] [auto-ap.views.pages.transactions.table :as table] [auto-ap.views.utils :refer [dispatch-event with-user]] - [clojure.set :as set] [re-frame.core :as re-frame] [reagent.core :as reagent] [vimsical.re-frame.fx.track :as track] @@ -22,34 +22,10 @@ -(defn data-params->query-params [params] - {:start (:start params 0) - :per-page (:per-page params) - :sort (:sort params) - :client-id (:id @(re-frame/subscribe [::subs/client])) - :vendor-id (:id (:vendor params)) - :date-range (:date-range params) - :account-id (:id (:account params)) - :bank-account-id (:id (:bank-account params)) - :amount-gte (:amount-gte (:amount-range params)) - :exact-match-id (some-> (:exact-match-id params) str) - :unresolved (:unresolved params) - :potential-duplicates (:potential-duplicates params) - :location (:location params) - :import-batch-id (some-> (:import-batch-id params) str) - :amount-lte (:amount-lte (:amount-range params)) - :description (:description params) - :approval-status (condp = @(re-frame/subscribe [::subs/active-page]) - :transactions nil - :unapproved-transactions :unapproved - :requires-feedback-transactions :requires-feedback - :excluded-transactions :excluded - :approved-transactions :approved)}) - (re-frame/reg-event-fx ::params-change [with-user] - (fn [{:keys [user db ]} [_ params]] + (fn [{:keys [user]} [_ params]] (try {:graphql {:token user :owns-state {:single [::data-page/page ::page]} @@ -61,8 +37,6 @@ :end]] :query/alias :result}]} :on-success (fn [result] - - [::data-page/received ::page (:result result)])}} (catch js/Error e ;; this catches an error where you choose a parameter, change to invoices page, then change to voided invoices @@ -88,7 +62,7 @@ :status status :ids specific-transactions} [:message]]}]} - :on-success (fn [result] + :on-success (fn [_] [::params-change params])}}))) (re-frame/reg-event-fx @@ -110,13 +84,32 @@ :ids specific-transactions :suppress suppress} [:message]]}]} - :on-success (fn [result] + :on-success (fn [_] [::params-change params])} - :dispatch [::data-page/reset-checked ::page]}))) + :dispatch-n [[::data-page/reset-checked ::page] + [::modal/modal-closed]]}))) + +(re-frame/reg-event-fx + ::delete-selected-requested + (fn [_ [_ params suppress]] + (let [checked @(re-frame/subscribe [::data-page/checked ::page]) + to-delete (if (get checked "header") + "all visible transactions" + (str (count checked) " transactions"))] + {:dispatch [::modal/modal-requested {:title "Confirmation" + :body [:div (str "Are you sure you want to delete " to-delete "?")] + :cancel? true + :confirm {:value "Delete" + :class "is-danger" + :status-from [::status/single ::delete-selected] + :on-click (dispatch-event [::delete-selected params suppress] )} + :close-event [::status/completed ::delete-selected]}]}))) + + (re-frame/reg-event-fx ::unmounted - (fn [{:keys [db]} _] + (fn [{:keys []} _] {:dispatch-n [[::data-page/dispose ::page] [::status/dispose-single ::manual-import]] ::track/dispose {:id ::params} @@ -125,7 +118,7 @@ (re-frame/reg-event-fx ::mounted - (fn [{:keys [db]} _] + (fn [{:keys []} _] {::track/register {:id ::params :subscription [::data-page/params ::page] :event-fn (fn [params] @@ -136,7 +129,7 @@ [::data-page/updated-entity ::page edited-transaction])} {:id ::manual-import :events #{::manual/import-completed} - :event-fn (fn [[_ {:keys [imported errors] :as result}]] + :event-fn (fn [[_ result]] (println result) [::status/info ::manual-import (str "Successfully " @@ -182,28 +175,28 @@ :class (status/class-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) (not (seq checked)))} - "Unapprove selected"] + "Unapprove"] [:button.button.is-warning {:on-click (dispatch-event [::bulk-change-transaction-status :excluded params]) :class (status/class-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) (not (seq checked)))} - "Exclude selected"] + "Exclude"] [:button.button.is-warning {:on-click (dispatch-event [::bulk-change-transaction-status :requires-feedback params]) :class (status/class-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::bulk-change-transaction-status])) (not (seq checked)))} "Client Review"] - [:button.button.is-danger {:on-click (dispatch-event [::delete-selected params false]) - :class (status/class-for @(re-frame/subscribe [::status/single ::delete-selected])) - :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::delete-selected])) - (not (seq checked)))} - "Delete selected"] - [:button.button.is-danger {:on-click (dispatch-event [::delete-selected params true]) - :class (status/class-for @(re-frame/subscribe [::status/single ::delete-selected])) - :disabled (or (status/disabled-for @(re-frame/subscribe [::status/single ::delete-selected])) - (not (seq checked)))} - "Suppress selected"]]]) + (when (:client-id params) + [:button.button.is-warning {:on-click (dispatch-event [::bulk/code-requested checked params]) + :disabled (not (seq checked))} + "Code"]) + [:button.button.is-danger {:on-click (dispatch-event [::delete-selected-requested params false]) + :disabled (not (seq checked))} + "Delete"] + [:button.button.is-danger {:on-click (dispatch-event [::delete-selected-requested params true]) + :disabled (not (seq checked))} + "Suppress"]]]) [table/table {:id :transactions :check-boxes? is-admin? :data-page ::page}]])) diff --git a/src/cljs/auto_ap/views/pages/transactions/common.cljs b/src/cljs/auto_ap/views/pages/transactions/common.cljs index 69d142bb..6a86e7da 100644 --- a/src/cljs/auto_ap/views/pages/transactions/common.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/common.cljs @@ -1,4 +1,7 @@ -(ns auto-ap.views.pages.transactions.common) +(ns auto-ap.views.pages.transactions.common + (:require + [auto-ap.subs :as subs] + [re-frame.core :as re-frame])) (def transaction-read [:id @@ -18,3 +21,27 @@ [:payment [:check_number :s3_url :id :date]] [:client [:name :id]] [:bank-account [:name :yodlee-account-id :current-balance]]]) + +(defn data-params->query-params [params] + {:start (:start params 0) + :per-page (:per-page params) + :sort (:sort params) + :client-id (:id @(re-frame/subscribe [::subs/client])) + :vendor-id (:id (:vendor params)) + :date-range (:date-range params) + :account-id (:id (:account params)) + :bank-account-id (:id (:bank-account params)) + :amount-gte (:amount-gte (:amount-range params)) + :exact-match-id (some-> (:exact-match-id params) str) + :unresolved (:unresolved params) + :potential-duplicates (:potential-duplicates params) + :location (:location params) + :import-batch-id (some-> (:import-batch-id params) str) + :amount-lte (:amount-lte (:amount-range params)) + :description (:description params) + :approval-status (condp = @(re-frame/subscribe [::subs/active-page]) + :transactions nil + :unapproved-transactions :unapproved + :requires-feedback-transactions :requires-feedback + :excluded-transactions :excluded + :approved-transactions :approved)})