(ns auto-ap.graphql.transaction-rules (:require [auto-ap.datomic :refer [audit-transact merge-query remove-nils replace-nils-with-retract uri conn]] [auto-ap.datomic.transaction-rules :as tr] [auto-ap.datomic.transactions :as d-transactions] [auto-ap.graphql.utils :refer [->graphql <-graphql assert-admin ident->enum-f limited-clients result->page snake->kebab]] [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] [datomic.api :as d] [clj-time.coerce :as c]) (:import java.time.temporal.ChronoField)) (defn get-transaction-rule-page [context args value] (let [args (assoc args :id (:id context)) [journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))] (result->page (->> journal-entries (map (ident->enum-f :transaction-rule/transaction-approval-status))) journal-entries-count :transaction_rules args))) (defn get-transaction-rule-matches [context args value] (if (= "admin" (:user/role (:id context))) (let [all-rules (tr/get-all) transaction (update (d-transactions/get-by-id (:transaction_id args)) :transaction/date coerce/to-date)] (map ->graphql (rm/get-matching-rules transaction all-rules))) nil)) (defn deleted-accounts [transaction accounts] (let [current-accounts (:transaction-rule/accounts transaction) specified-ids (->> accounts (map :id) set) existing-ids (->> current-accounts (map :db/id) set)] (set/difference existing-ids specified-ids))) (defn transaction-rule-account->entity [{:keys [id account_id percentage location]}] (remove-nils #:transaction-rule-account {:percentage percentage :db/id id :account account_id :location location})) (defn delete-transaction-rule [context {:keys [transaction_rule_id ]} value] (assert-admin (:id context)) (let [existing-transaction-rule (tr/get-by-id transaction_rule_id)] (when-not (:transaction-rule/description existing-transaction-rule) (throw (ex-info "Transaction rule not found" {:validation-error "Transaction Rule not found"}))) (audit-transact [[:db/retractEntity transaction_rule_id]] (:id context)) transaction_rule_id)) (defn upsert-transaction-rule [context {{:keys [id description yodlee_merchant_id note client_id bank_account_id amount_lte amount_gte vendor_id accounts transaction_approval_status dom_gte dom_lte]} :transaction_rule :as z} value] (assert-admin (:id context)) (let [existing-transaction (tr/get-by-id id) deleted (deleted-accounts existing-transaction accounts) account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts)) _ (when-not (dollars= 1.0 account-total) (let [error (str "Account total (" account-total ") does not reach 100%")] (throw (ex-info error {:validation-error error})))) _ (when (and (str/blank? description) (nil? yodlee_merchant_id)) (let [error (str "You must provide a description or a yodlee merchant")] (throw (ex-info error {:validation-error error})))) _ (doseq [a accounts :let [{:keys [:account/location :account/name] :as account} (d/entity (d/db conn) (:account_id a)) client (d/entity (d/db conn) client_id) ]] (when (and location (not= location (:location a))) (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] (throw (ex-info err {:validation-error err}) ))) (when (and (not location) (not (get (into #{"Shared"} (:client/locations client)) (:location a)))) (let [err (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")] (throw (ex-info err {:validation-error err}) )))) rule-id (if id id "transaction-rule") transaction (replace-nils-with-retract #:transaction-rule {:db/id rule-id :description description :note note :client client_id :bank-account bank_account_id :yodlee-merchant yodlee_merchant_id :dom-lte dom_lte :dom-gte dom_gte :amount-lte amount_lte :amount-gte amount_gte :vendor vendor_id :transaction-approval-status (some->> transaction_approval_status name snake->kebab (keyword "transaction-approval-status"))} existing-transaction) transaction (conj transaction [:reset rule-id :transaction-rule/accounts (map transaction-rule-account->entity accounts)]) transaction-result (audit-transact transaction (:id context))] (-> (tr/get-by-id (or (-> transaction-result :tempids (get "transaction-rule")) id)) ((ident->enum-f :transaction-rule/transaction-approval-status)) (->graphql)))) (defn tr [z x] (re-find (re-pattern z) x)) (defn -test-transaction-rule [id {:keys [:transaction-rule/description :transaction-rule/note :transaction-rule/client :transaction-rule/bank-account :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte :transaction-rule/yodlee-merchant]} include-coded? count] (->> (d/query (cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name] :transaction/bank-account [:bank-account/name] :transaction/payment [:db/id]} ])] :in ['$ ] :where []} :args [(d/db conn)]} (limited-clients id) (merge-query {:query {:in ['[?xx ...]] :where ['[?e :transaction/client ?xx]]} :args [(set (map :db/id (limited-clients id)))]}) bank-account (merge-query {:query {:in ['?bank-account-id] :where ['[?e :transaction/bank-account ?bank-account-id]]} :args [(:db/id bank-account)]}) description (merge-query {:query {:in ['?description-regex] :where ['[?e :transaction/description-original ?do] '[(re-find ?description-regex ?do)]]} :args [(rm/->pattern description)]}) yodlee-merchant (merge-query {:query {:in ['?yodlee-merchant-id] :where ['[?e :transaction/yodlee-merchant ?yodlee-merchant-id]]} :args [(:db/id yodlee-merchant)]}) 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]}) dom-lte (merge-query {:query {:in ['?dom-lte] :where ['[?e :transaction/date ?transaction-date] '[(.toInstant ^java.util.Date ?transaction-date ) ?transaction-instant] '[(.atZone ^java.time.Instant ?transaction-instant (java.time.ZoneId/of "US/Pacific")) ?transaction-local] '[(.get ?transaction-local java.time.temporal.ChronoField/DAY_OF_MONTH) ?dom] '[(<= ?dom ?dom-lte)]]} :args [dom-lte]}) dom-gte (merge-query {:query {:in ['?dom-gte] :where ['[?e :transaction/date ?transaction-date] '[(.toInstant ^java.util.Date ?transaction-date ) ?transaction-instant] '[(.atZone ^java.time.Instant ?transaction-instant (java.time.ZoneId/of "US/Pacific")) ?transaction-local] '[(.get ?transaction-local java.time.temporal.ChronoField/DAY_OF_MONTH) ?dom] '[(>= ?dom ?dom-gte)]]} :args [dom-gte]}) client (merge-query {:query {:in ['?client-id] :where ['[?e :transaction/client ?client-id]]} :args [(:db/id client)]}) (not include-coded?) (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]]}}))) (transduce (comp (take (or count 15)) (map first) (map #(dissoc % :transaction/id)) (map (fn [x] (update x :transaction/date c/from-date))) (map ->graphql)) conj []))) (defn test-transaction-rule [{:keys [id]} {{:keys [description note client_id bank_account_id amount_lte amount_gte dom_lte dom_gte yodlee_merchant_id]} :transaction_rule :as z} value] (assert-admin id) (-test-transaction-rule id #:transaction-rule {:description description :client (when client_id {:db/id client_id}) :bank-account (when bank_account_id {:db/id bank_account_id}) :amount-lte amount_lte :amount-gte amount_gte :dom-lte dom_lte :dom-gte dom_gte :yodlee-merchant (when yodlee_merchant_id {:db/id yodlee_merchant_id})} true 15)) (defn run-transaction-rule [{:keys [id]} {:keys [transaction_rule_id count]} value] (assert-admin id) (-test-transaction-rule id (tr/get-by-id transaction_rule_id) false count))