can run transaction rules

This commit is contained in:
2023-10-28 21:03:59 -07:00
parent e8a419fb3c
commit f0a7c378f7
4 changed files with 164 additions and 66 deletions

View File

@@ -9,13 +9,17 @@
merge-query
pull-attr
pull-many
query2]]
query2
remove-nils]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.transactions :as d-transactions]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :as query-params]
[auto-ap.routes.admin.transaction-rules :as route]
[auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
[auto-ap.ssr.components :as com]
@@ -45,6 +49,8 @@
[auto-ap.time :as atime]
[auto-ap.utils :refer [dollars=]]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clojure.set :as set]
[clojure.string :as str]
[datomic.api :as dc]
[malli.core :as mc]))
@@ -306,73 +312,78 @@
:db/id
[:transaction/date :xform clj-time.coerce/from-date]])
(defn transaction-rule-test-table* [{{:transaction-rule/keys [description client bank-account amount-lte amount-gte dom-lte dom-gte yodlee-merchant]}
:entity
clients :clients
checkboxes? :checkboxes?
only-uncoded? :only-uncoded?}]
(defn transactions-matching-rule [{{:transaction-rule/keys [description client bank-account amount-lte amount-gte dom-lte dom-gte]}
:entity
clients :clients
only-uncoded? :only-uncoded?}]
(let [valid-clients (extract-client-ids clients
client)
query (cond-> {:query {:find ['(pull ?e read)]
:in ['$ 'read]
:where []}
:args [(dc/db conn) transaction-read]}
description
(merge-query {:query {:in ['?descr]
:where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]}
:args [description]})
query (cond-> {:query {:find ['(pull ?e read)]
:in ['$ 'read]
:where []}
:args [(dc/db conn) transaction-read]}
description
(merge-query {:query {:in ['?descr]
:where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]}
:args [description]})
valid-clients
(merge-query {:query {:in ['[?xx ...]]
:where ['[?e :transaction/client ?xx]]}
:args [(set valid-clients)]})
valid-clients
(merge-query {:query {:in ['[?xx ...]]
:where ['[?e :transaction/client ?xx]]}
:args [(set valid-clients)]})
bank-account
(merge-query {:query {:in ['?bank-account-id]
:where ['[?e :transaction/bank-account ?bank-account-id]]}
:args [bank-account]})
bank-account
(merge-query {:query {:in ['?bank-account-id]
:where ['[?e :transaction/bank-account ?bank-account-id]]}
:args [bank-account]})
description
(merge-query {:query {:where ['[?e :transaction/description-original ?do]
'[(re-find ?description-regex ?do)]]}})
description
(merge-query {:query {:where ['[?e :transaction/description-original ?do]
'[(re-find ?description-regex ?do)]]}})
amount-gte
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :transaction/amount ?ta]
'[(>= ?ta ?amount-gte)]]}
:args [amount-gte]})
amount-gte
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :transaction/amount ?ta]
'[(>= ?ta ?amount-gte)]]}
:args [amount-gte]})
amount-lte
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :transaction/amount ?ta]
'[(<= ?ta ?amount-lte)]]}
:args [amount-lte]})
amount-lte
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :transaction/amount ?ta]
'[(<= ?ta ?amount-lte)]]}
:args [amount-lte]})
dom-lte
(merge-query {:query {:in ['?dom-lte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(<= ?dom ?dom-lte)]]}
:args [dom-lte]})
dom-lte
(merge-query {:query {:in ['?dom-lte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(<= ?dom ?dom-lte)]]}
:args [dom-lte]})
dom-gte
(merge-query {:query {:in ['?dom-gte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(>= ?dom ?dom-gte)]]}
:args [dom-gte]})
dom-gte
(merge-query {:query {:in ['?dom-gte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(>= ?dom ?dom-gte)]]}
:args [dom-gte]})
only-uncoded?
(merge-query {:query {:where ['[or [?e :transaction/approval-status :transaction-approval-status/unapproved]
[(missing? $ ?e :transaction/approval-status)]]]}})
only-uncoded?
(merge-query {:query {:where ['[or [?e :transaction/approval-status :transaction-approval-status/unapproved]
[(missing? $ ?e :transaction/approval-status)]]]}})
true
(merge-query {:query {:where ['[?e :transaction/id]]}}))
true
(merge-query {:query {:where ['[?e :transaction/id]]}}))
results (->>
(query2 query)
(map first))]
results))
(defn transaction-rule-test-table* [{:keys [entity clients checkboxes? only-uncoded?]}]
(let [results (transactions-matching-rule
{:entity entity
:clients clients
:only-uncoded? only-uncoded?})]
[:div#transaction-test-results
[:h2.my-4.text-lg.flex {:x-data (hx/json {:resultCount (count results)})} "Matching transactions"
@@ -710,6 +721,71 @@
client-id
(some->> client-id (pull-attr (dc/db conn) :client/locations) client-id)))))
(defn all-ids-not-locked [all-ids]
(->> all-ids
(dc/q '[:find ?t
:in $ [?t ...]
:where
[?t :transaction/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?t :transaction/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first)))
(defn execute [{:keys [form-params clients entity identity]}]
(let [all-results (->> (transactions-matching-rule {:entity entity
:clients clients
:only-uncoded? true})
(map :db/id)
(into #{}))
ids (if (not-empty (:all form-params))
all-results
(set/intersection (into #{} (:transaction-id form-params))
all-results))
ids (all-ids-not-locked ids)
transactions (transduce
(comp
(map d-transactions/get-by-id)
(map #(update % :transaction/date coerce/to-date)))
conj
[]
ids)
entity (update entity :transaction-rule/description #(some-> % iol-ion.query/->pattern))
;; TODO
#_#_x (doseq [transaction transactions]
(when (not (rm/rule-applies? transaction entity))
(throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"
:transaction-rule entity
:transaction transaction})))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))))
]
(audit-transact (mapv (fn [t]
[:upsert-transaction
(remove-nils (rm/apply-rule {:db/id (:db/id t)
:transaction/amount (:transaction/amount t)}
entity
(or (-> t :transaction/bank-account :bank-account/locations)
(-> t :transaction/client :client/locations))))])
transactions)
identity)
(doseq [n transactions]
(solr/touch-with-ledger (:db/id n)))
(html-response [:div]
:headers {"hx-trigger" (hx/json {:modalclose ""
:notification (format "Successfully coded %d of %d transactions!"
(count ids)
(count all-results))})})))
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
@@ -771,16 +847,20 @@
0
{}
[:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]
[:div#my-form
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
:hx-trigger "change"
:hx-target "#transaction-test-results .gutter"
:hx-include "this"}
(transaction-rule-test-table* {:entity entity
:clients clients
:checkboxes? true
:only-uncoded? true})]
[:div.flex.justify-end (com/validated-save-button {:form "my-form"} "Code transactions")]))
[:form#my-form
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute
:db/id (:db/id entity))
:hx-indicator "#code"}
[:div
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
:hx-trigger "change"
:hx-target "#transaction-test-results .gutter"
:hx-include "this"}
(transaction-rule-test-table* {:entity entity
:clients clients
:checkboxes? true
:only-uncoded? true})]]
[:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")]))
:headers (-> {}
(assoc "hx-trigger-after-settle" "modalnext")
(assoc "hx-retarget" ".modal-stack")
@@ -821,6 +901,22 @@
(wrap-form-4xx-2 (-> edit-dialog
(wrap-entity [:form-params :db/id] default-read))))
::route/execute (-> execute
(wrap-entity [:route-params :db/id] default-read)
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-decode :route-schema [:map [:db/id entity-id]])
(wrap-schema-decode :form-schema
[:map
[:transaction-id {:optional true}
[:maybe [:vector {:decode/arbitrary (fn [x] ;; TODO make this easier
(if (sequential? x)
x
[x]))}
entity-id]]]
[:all {:optional true} [:maybe :string]]])
#_(wrap-form-4xx-2 (-> edit-dialog ;; TODO for example not having a single one checked
(wrap-entity [:form-params :db/id] default-read))))
::route/test (-> test
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)

View File

@@ -5,6 +5,7 @@
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.client-routes :as client-routes]
[auto-ap.ssr.hx :as hx]
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.ssr.hiccup-helper :as hh]))
(defn menu-button- [params & children]
@@ -214,7 +215,7 @@
[:li
(menu-button- {:icon svg/cog
:href (bidi/path-for ssr-routes/only-routes :admin-transaction-rules)}
:href (bidi/path-for ssr-routes/only-routes ::transaction-rules/page)}
"Rules")]
[:li

View File

@@ -8,7 +8,7 @@
(defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children]
[:div#app {"_" (hiccup/raw "
on notification put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder
on notification from body put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder
on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 30ms then remove .htmx-added from #error-holder"
)
:x-data (hx/json {:leftNavShow true})}

View File

@@ -11,6 +11,7 @@
"/test" ::test
"/new" {:get ::new-dialog}
[[#"\d+" :db/id] "/edit"] ::edit-dialog
[[#"\d+" :db/id] "/run"] ::execute-dialog
[[#"\d+" :db/id] "/run"] {:get ::execute-dialog
:post ::execute}
"/check-badges" ::check-badges
})