(ns auto-ap.ssr.admin.transaction-rules (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 audit-transact conn merge-query pull-attr pull-many query2]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :as query-params] [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.company :refer [bank-account-typeahead*]] [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers entity-id field-validation-error form-validation-error html-response many-entity money path->name2 percentage ref->enum-schema ref->radio-options regex temp-id wrap-form-4xx-2 wrap-schema-decode]] [auto-ap.utils :refer [dollars=]] [bidi.bidi :as bidi] [cheshire.core :as cheshire] [clojure.string :as str] [datomic.api :as dc] [hiccup2.core :as hiccup] [iol-ion.query :refer [ident]] [malli.core :as mc] [auto-ap.cursor :as cursor] [auto-ap.ssr.hiccup-helper :as hh])) ;; TODO with dependencies, I really don't like that you have to be ultra specific in what ;; you want to include, and generating the routes and interconnection is weird too. ;; I'm tempted to say to include a full snapshot of the form, and the indicator ;; as to which one to generate. (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-table) "hx-target" "#transaction-rule-table" "hx-indicator" "#transaction-rule-table"} [:fieldset.space-y-6 (com/field {:label "Vendor"} (com/typeahead-2 {:name "vendor" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :vendor-search) :id (str "vendor-search") :value [(:db/id (:vendor (:parsed-query-params request))) (:vendor/name (:vendor (:parsed-query-params request)))]})) (com/field {:label "Note"} (com/text-input {:name "note" :id "note" :class "hot-filter" :value (:note (:parsed-query-params request)) :placeholder "HOME DEPOT lte 250.0" :size :small})) (com/field {:label "Description"} (com/text-input {:name "description" :id "description" :class "hot-filter" :value (:description (:parsed-query-params request)) :placeholder "LOWES" :size :small}))]]) (def default-read '[:db/id :transaction-rule/description :transaction-rule/note :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte {:transaction-rule/client [:client/name :db/id :client/code :client/locations]} {:transaction-rule/bank-account [:db/id :bank-account/name]} {:transaction-rule/yodlee-merchant [:db/id :yodlee-merchant/name :yodlee-merchant/yodlee-id]} {[:transaction-rule/transaction-approval-status :xform iol-ion.query/ident] [:db/id :db/ident]} {:transaction-rule/vendor [:vendor/name :db/id :vendor/default-account]} {:transaction-rule/accounts [:transaction-rule-account/percentage :transaction-rule-account/location {:transaction-rule-account/account [:account/name :db/id :account/numeric-code :account/location {:account/client-overrides [:db/id :account-client-override/name {:account-client-override/client [:db/id :client/name]}]}]} :db/id]}]) (defn fetch-ids [db request] (let [query-params (:parsed-query-params request) valid-clients (extract-client-ids (:clients request) (:client request) (:client-id query-params) (when (:client-code query-params) [:client/code (:client-code query-params)])) query (cond-> {:query {:find [] :in ['$] :where []} :args [db]} (:sort query-params) (add-sorter-fields {"client" ['[?e :transaction-rule/client ?c] '[?c :client/name ?sort-client]] "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?ym] '[?ym :yodlee-merchant/name ?sort-yodlee-merchant]] "bank-account" ['[?e :transaction-rule/bank-account ?ba] '[?ba :bank-account/name ?sort-bank-account]] "description" ['[?e :transaction-rule/description ?sort-description]] "note" ['[?e :transaction-rule/note ?sort-note]] "amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]] "amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]} query-params) (seq valid-clients) (merge-query {:query {:in ['[?xx ...]] :where ['(or-join [?e] (and [?e :transaction-rule/client ?xx]) (and (not [?e :transaction-rule/client]) [?e :transaction-rule/note]))]} :args [valid-clients]}) (-> query-params :vendor :db/id) (merge-query {:query {:in ['?vendor-id] :where ['[?e :transaction-rule/vendor ?vendor-id]]} :args [(-> query-params :vendor :db/id)]}) (not (str/blank? (:note query-params))) (merge-query {:query {:in ['?note-pattern] :where ['[?e :transaction-rule/note ?n] '[(re-find ?note-pattern ?n)]]} :args [(re-pattern (str "(?i)" (:note query-params)))]}) (not (str/blank? (:description query-params))) (merge-query {:query {:in ['?description] :where ['[?e :transaction-rule/description ?d] '[(clojure.string/lower-case ?d) ?d2] '[(clojure.string/includes? ?d2 ?description)]]} :args [(clojure.string/lower-case (:description query-params))]}) true (merge-query {:query {:find ['?e] :where ['[?e :transaction-rule/transaction-approval-status]]}}))] (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) (defn hydrate-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) refunds (->> ids (map results) (map first))] refunds)) (defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) (def grid-page (helper/build {:id "transaction-rule-table" :nav (com/admin-aside-nav) :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp (query-params/parse-key :vendor #(dc/pull (dc/db conn) '[:vendor/name :db/id] (Long/parseLong %))) (helper/default-parse-query-params grid-page)) :action-buttons (fn [request] [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-new-dialog)) :hx-target "#modal-content" :hx-swap "outerHTML" :color :primary} "New Transaction Rule")]) :row-buttons (fn [request entity] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-dialog :db/id (:db/id entity))) :hx-target "#modal-content" :hx-swap "outerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} "Admin"] [:a {:href (bidi/path-for ssr-routes/only-routes :admin-transaction-rules)} "Transaction Rules"]] :title "Rules" :entity-name "Rule" :route :admin-transaction-rule-table :headers [{:key "client" :name "Client" :sort-key "client" :render #(-> % :transaction-rule/client :client/name)} {:key "bank-account" :name "Bank account" :sort-key "bank-account" :render #(-> % :transaction-rule/bank-account :bank-account/name) :show-starting "lg"} {:key "description" :name "Description" :sort-key "description" :render :transaction-rule/description} {:key "amount" :name "Amount" :sort-key "amount" :render (fn [{:transaction-rule/keys [amount-gte amount-lte]}] [:div.flex.gap-2 (when amount-gte (com/pill {:color :red} (format "more than $%.2f" amount-gte))) (when amount-lte (com/pill {:color :primary} (format "less than $%.2f" amount-lte)))]) :show-starting "md"} {:key "note" :name "Note" :sort-key "note" :render :transaction-rule/note} ]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (defn entity->note [{:transaction-rule/keys [amount-lte amount-gte description client dom-lte dom-gte]}] (str/join " - " (filter (complement str/blank?) [(when client (pull-attr (dc/db conn) :client/code client)) description (when (or amount-lte amount-gte) (str (when amount-gte (str amount-gte "<")) "amt" (when amount-lte (str "<" amount-lte)))) (when (or dom-lte dom-gte) (str (when dom-gte (str dom-gte "<")) "dom" (when dom-lte (str "<" dom-lte))))]))) (defn bank-account-belongs-to-client? [bank-account-id client-id] (get (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id]}] client-id) :client/bank-accounts (map :db/id) (set)) bank-account-id)) (defn transaction-rule-save [{:keys [form-params request-method identity] :as request}] (let [entity (cond-> form-params (= :post request-method) (assoc :db/id "new") true (assoc :transaction-rule/note (entity->note form-params))) _ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range)) :let [account-location (pull-attr (dc/db conn) :account/location account)] :when (and account-location (not= account-location location))] (field-validation-error (str "must be " account-location) [:transaction-rule/accounts i :transaction-rule-account/location] :form form-params)) total (reduce + 0.0 (map :transaction-rule-account/percentage (:transaction-rule/accounts entity))) _ (when-not (dollars= 1.0 total) (form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total))) :form form-params)) _ (when (and (:transaction-rule/bank-account entity) (not (bank-account-belongs-to-client? (:transaction-rule/bank-account entity) (:transaction-rule/client entity)))) (field-validation-error "does not belong to client" [:transaction-rule/bank-account] :form form-params)) {:keys [tempids]} (audit-transact [[:upsert-entity entity]] (:identity request)) updated-account (dc/pull (dc/db conn) default-read (or (get tempids (:db/id entity)) (:db/id entity)))] (html-response (row* identity updated-account {:flash? true}) :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#transaction-rule-table tr[data-id=\"%d\"]" (:db/id updated-account))}))) (defn- location-select* [{:keys [ name account-location client-locations value]}] (com/select {:options (into [["" ""]] (cond account-location [[account-location account-location]] (seq client-locations) (into [["Shared" "Shared"]] (for [cl client-locations] [cl cl])) :else [["Shared" "Shared"]])) :name name :value value :class "w-full"})) (defn- account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col (com/typeahead-2 {:name name :placeholder "Search..." :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) :id name :x-model x-model :value value :value-fn (some-fn :db/id identity) :content-fn (fn [value] (:account/name (d-accounts/clientize (cond->> value (nat-int? value) (dc/pull (dc/db conn) d-accounts/default-read)) client-id)))})]) (defn- transaction-rule-account-row* [transaction-rule account] (com/data-grid-row {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) (fc/field-value (:transaction-rule-account/account account)))})} (let [account-name (fc/field-name (:transaction-rule-account/account account))] (list (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :transaction-rule-account/account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} [:div {:hx-trigger "changed" :hx-target "next div" :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" account-name) :hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead)) :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] (account-typeahead* {:value (fc/field-value) :client-id (:db/id (:transaction-rule/client transaction-rule)) :name (fc/field-name) :x-model "accountId" })))) (fc/with-field :transaction-rule-account/location (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors) :x-data (hx/json {:location (fc/field-value)})} [:div {:hx-trigger "changed" :hx-target "next *" :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || ''}" (fc/field-name) ) :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select) :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data))"}] (location-select* {:name (fc/field-name) :account-location (:account/location (cond->> (:transaction-rule-account/account @account) (nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn) '[:account/location]))) :client-locations (:client/locations (:transaction-rule/client transaction-rule)) :hx-model "location" :value (fc/field-value)})))) (fc/with-field :transaction-rule-account/percentage (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} (com/money-input {:name (fc/field-name) :class "w-16" :value (some-> (fc/field-value) (* 100 ) (long ))})))))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest 's opacity to 0 then remove closest ") :href "#"} svg/x)))) ;; TODO dialog is no longer closeable (defn dialog* [& {:keys [entity form-params form-errors]}] (com/modal {:modal-class "max-w-2xl"} [:form#edit-form (merge {:hx-ext "response-targets" :hx-swap "outerHTML swap:300ms" :hx-target "#modal-holder" :hx-target-400 "#form-errors .error-content" :x-trap "true" :class "group/form"} form-params) (com/modal-card {} [:div.flex [:div.p-2 "Transaction Rule"]] [:fieldset {:class "hx-disable" :hx-disinherit "hx-target" :x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client entity)) (:transaction-rule/client entity))})} (fc/start-form entity form-errors [:div.space-y-1 (when-let [id (:db/id entity)] (com/hidden {:name "db/id" :value id})) (fc/with-field :transaction-rule/description (com/validated-field {:label "Description" :errors (fc/field-errors)} (com/text-input {:name (fc/field-name) :error? (fc/error?) :x-init "$el.focus()" :placeholder "HOME DEPOT" :class "w-96" :value (fc/field-value)}))) [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} [:div.flex.gap-2.mb-2 (com/a-button {"@click" "clientFilter=true" "x-show" "!clientFilter"} "Filter client") (com/a-button {"@click" "bankAccountFilter=true" "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") (com/a-button {"@click" "amountFilter=true" "x-show" "!amountFilter"} "Filter amount") (com/a-button {"@click" "domFilter=true" "x-show" "!domFilter"} "Filter day of month")] (fc/with-field :transaction-rule/client (com/validated-field (-> {:label "Client" :errors (fc/field-errors) :x-show "clientFilter"} (hx/alpine-appear)) [:div.w-96 (com/typeahead-2 {:name (fc/field-name) :error? (fc/error?) :class "w-96" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :company-search) :x-model "clientId" :value (fc/field-value) :value-fn (some-fn :db/id identity) :content-fn (fn [c] (cond->> c (nat-int? c) (dc/pull (dc/db conn) '[:client/name]) true :client/name))})])) (fc/with-field :transaction-rule/bank-account (com/validated-field (-> {:label "Bank Account" :errors (fc/field-errors) :x-show "bankAccountFilter"} hx/alpine-appear) [:div.w-96 [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) :hx-trigger "changed" :hx-target "next *" :hx-include "#bank-account-changer" :hx-swap "innerHTML" :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] (bank-account-typeahead* {:client-id ((some-fn :db/id identity) (:transaction-rule/client entity)) :name (fc/field-name) :value (fc/field-value)})])) (com/field (-> {:label "Amount" :x-show "amountFilter"} hx/alpine-appear) [:div.flex.gap-2 (fc/with-field :transaction-rule/amount-gte [:div.flex.flex-col (com/money-input {:name (fc/field-name) :placeholder ">=" :class "w-24" :value (fc/field-value)}) (com/errors {:errors (fc/field-errors)})]) (fc/with-field :transaction-rule/amount-lte [:div.flex.flex-col (com/money-input {:name (fc/field-name) :placeholder "<=" :class "w-24" :value (fc/field-value)}) (com/errors {:errors (fc/field-errors)})])]) (com/field (-> {:label "Day of month" :x-show "domFilter"} hx/alpine-appear) [:div.flex.gap-2 (fc/with-field :transaction-rule/dom-gte (com/validated-field {:errors (fc/field-errors)} (com/int-input {:name (fc/field-name) :placeholder ">=" :class "w-24" :value (fc/field-value)}))) (fc/with-field :transaction-rule/dom-lte (com/validated-field {:errors (fc/field-errors)} (com/int-input {:name (fc/field-name) :placeholder ">=" :class "w-24" :value (fc/field-value)})))])] [:h2.text-lg "Outcomes"] (fc/with-field :transaction-rule/vendor (com/validated-field {:label "Assign Vendor" :errors (fc/field-errors)} [:div.w-96 (com/typeahead-2 {:name (fc/field-name) :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :vendor-search) :id (str "form-vendor-search") :class "w-96" :value (fc/field-value) :value-fn (some-fn :db/id identity) :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])) (fc/with-field :transaction-rule/accounts (list (com/data-grid {:headers [(com/data-grid-header {} "Account") (com/data-grid-header {:class "w-32"} "Location") (com/data-grid-header {:class "w-16"} "%") (com/data-grid-header {:class "w-16"})] :id "transaction-rule-account-table"} (when @fc/*current* (doall (for [tra fc/*current*] (fc/with-cursor tra (transaction-rule-account-row* entity tra))))) (com/data-grid-row {:class "new-row"} (com/data-grid-cell {:colspan 4 :class "bg-gray-100"} [:div.flex.justify-center (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-new-account) :color :secondary :hx-include "#edit-form" :hx-ext "rename-params" :hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id" "index" "index"}) :hx-vals (hiccup/raw "js:{index: countRows(\"#transaction-rule-account-table\")}") :hx-target "#edit-form .new-row" :hx-swap "beforebegin"} "New account")]))) (com/errors {:errors (fc/field-errors)}))) (fc/with-field :transaction-rule/transaction-approval-status (com/validated-field {:label "Approval status" :errors (fc/field-errors)} (com/radio {:options (ref->radio-options "transaction-approval-status") :value (fc/field-value) :name (fc/field-name) :size :small :orientation :horizontal}))) [:div#form-errors (when (:errors fc/*form-errors*) [:span.error-content (com/errors {:errors (:errors fc/*form-errors*)})])]])] [:div (com/button {:color :primary :form "edit-form" :type "submit" :class (cond-> "w-32" (seq form-errors) (-> (hh/add-class "animate-shake")))} "Save")])])) ;; TODO Should forms have some kind of helper to render an individual field? saving ;; could generate the entire form and swap if there are errors ;; but also when you tab out it could call the same function, and just ;; pull out the single field to swap (defn new-account [{{:keys [client-id index]} :query-params}] (let [index (or index 0) ;; TODO schema decode is not working transaction-rule {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id] client-id) :transaction-rule/accounts (conj (into [] (repeat index {} )) {:db/id (str (java.util.UUID/randomUUID)) :transaction-rule-account/location "Shared"})}] (html-response (fc/start-form transaction-rule [] (fc/with-cursor (get-in fc/*current* [:transaction-rule/accounts index]) (transaction-rule-account-row* ;; TODO store a pointer to the "head " cursor for errors instead of nesting them ;; makes it so you don't have to do this transaction-rule fc/*current*)))))) ;; TODO check to see if it should be called "Shared" or "shared" for the value ;; TODO hydrate nested types more easily. make it easy to hydrate so you don't do weird sometimes pulls ;; TODO is it possible to make it easy to get a virtual cursor in the case of adding a new row? setting up ;; fake data doesn't feel right - maybe have a "prelude" that's dynamic (defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}] (html-response (location-select* {:name name :value value :account-location (some->> account-id (pull-attr (dc/db conn) :account/location)) :client-locations (some->> client-id (pull-attr (dc/db conn) :client/locations))}))) (defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}] (let [account (some->> value (dc/pull (dc/db conn) [:account/name :db/id {:account/client-overrides [:db/id :account-client-override/name {:account-client-override/client [:db/id :client/name]}]}])) client-id client-id] (html-response (account-typeahead* {:name name :value account :client-id client-id})))) (defn transaction-rule-edit-dialog [request] (let [entity (or (some-> request :last-form) (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %))))] (html-response (dialog* :entity entity :form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-save))}) :headers {"hx-trigger-after-settle" "modalopen"}))) (defn transaction-rule-error [request] (let [entity (some-> request :last-form)] (html-response (dialog* :entity entity :form-errors (:form-errors request) :form-params (if (:db/id entity) {:hx-put (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-save))} {:hx-post (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-save))})) :headers {"hx-retarget" "#edit-form fieldset" "hx-reselect" "#edit-form fieldset"}))) (defn transaction-rule-new-dialog [_] (html-response (dialog* :entity {} :form-errors {} :form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-save))}) :headers {"hx-trigger-after-settle" "modalopen"})) (def transaction-rule-schema (mc/schema [:map [:db/id {:optional true} [:maybe entity-id]] [:transaction-rule/client {:optional true} [:maybe entity-id]] [:transaction-rule/description [:and regex [:string {:min 3}]]] [:transaction-rule/bank-account [:maybe entity-id]] [:transaction-rule/amount-gte {:optional true} [:maybe money]] [:transaction-rule/amount-lte {:optional true} [:maybe money]] [:transaction-rule/dom-gte {:optional true} [:maybe :int]] [:transaction-rule/dom-lte {:optional true} [:maybe :int]] [:transaction-rule/vendor {:optional true} [:maybe entity-id]] [:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")] [:transaction-rule/accounts (many-entity {:min 1} [:db/id [:or entity-id temp-id]] [:transaction-rule-account/account entity-id] [:transaction-rule-account/location [:string {:min 1 :error/message "required"}]] [:transaction-rule-account/percentage percentage])]])) (def key->handler (apply-middleware-to-all-handlers (->> {:admin-transaction-rules (helper/page-route grid-page) :admin-transaction-rule-table (helper/table-route grid-page) :admin-transaction-rule-new-account (-> new-account (wrap-schema-decode :query-schema [:map [:client-id {:optional true} [:maybe entity-id]] [:index {:optional true :default 0} [nat-int? {:default 0}]]]) wrap-admin wrap-client-redirect-unauthenticated) :admin-transaction-rule-location-select (-> location-select (wrap-schema-decode :query-schema [:map [:name :string] [:client-id {:optional true} [:maybe entity-id]] [:account-id {:optional true} [:maybe entity-id]]])) :admin-transaction-rule-account-typeahead (-> account-typeahead (wrap-schema-decode :query-schema [:map [:name :string] [:client-id {:optional true} [:maybe entity-id]] [:value {:optional true} [:maybe entity-id]]])) :admin-transaction-rule-save (-> transaction-rule-save (wrap-schema-decode :form-schema transaction-rule-schema) (wrap-nested-form-params) (wrap-form-4xx-2 transaction-rule-error)) :admin-transaction-rule-edit-dialog (-> transaction-rule-edit-dialog (wrap-schema-decode :route-schema [:map [:db/id entity-id]])) :admin-transaction-rule-new-dialog transaction-rule-new-dialog}) (fn [h] (-> h (wrap-admin) (wrap-client-redirect-unauthenticated)))))