First cut at bulk coding

This commit is contained in:
2022-03-07 09:33:46 -08:00
parent 0f61cfa6cc
commit 52f8c08569
4 changed files with 202 additions and 141 deletions

View File

@@ -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

View File

@@ -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))))))

View File

@@ -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}]]))

View File

@@ -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)})