From f0a7c378f737c3f8d260f73e667453caa4445393 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sat, 28 Oct 2023 21:03:59 -0700 Subject: [PATCH] can run transaction rules --- .../auto_ap/ssr/admin/transaction_rules.clj | 222 +++++++++++++----- src/clj/auto_ap/ssr/components/aside.clj | 3 +- src/clj/auto_ap/ssr/components/page.clj | 2 +- .../routes/admin/transaction_rules.cljc | 3 +- 4 files changed, 164 insertions(+), 66 deletions(-) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index bb006350..dafd1477 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -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) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 075e7163..ac0a0173 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -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 diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index 0fbcdaef..a83fed42 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -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})} diff --git a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc index ec3d35f5..49b017c7 100644 --- a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc +++ b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc @@ -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 })