(ns auto-ap.datomic.transactions (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query observable-query pull-many]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [clj-time.coerce :as coerce] [clojure.string :as str] [datomic.api :as dc] [clj-time.core :as time])) (defn potential-duplicate-ids [db args] (when (and (:potential-duplicates args) (:bank-account-id args)) (->> (dc/q '[:find ?tx ?amount ?date :in $ ?ba :where [?tx :transaction/bank-account ?ba] [?tx :transaction/amount ?amount] [?tx :transaction/date ?date] (not [?tx :transaction/approval-status :transaction-approval-status/suppressed])] db (:bank-account-id args)) (group-by (fn [[_ amount date]] [amount date])) (filter (fn [[_ txes]] (> (count txes) 1))) (vals) (mapcat identity) (map first) set))) (defn raw-graphql-ids ([args] (raw-graphql-ids (dc/db conn) args)) ([db args] (let [valid-clients (extract-client-ids (:clients args) (:client-id args) (when (:client-code args) [:client/code (:client-code args)])) potential-duplicates (potential-duplicate-ids db args) query (if (:exact-match-id args) (cond-> {:query {:find '[?e] :in '[$ ?e [?c ...]] :where '[[?e :transaction/client ?c]]} :args [db (:exact-match-id args) valid-clients]}) (cond-> {:query {:find [] :in '[$ [?clients ?start ?end]] :where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]} :args [db [valid-clients (some-> (:start (:date-range args)) coerce/to-date) (some-> (:end (:date-range args)) coerce/to-date)]]} (:potential-duplicates args) (merge-query {:query {:in '[[?e ...]]} :args [potential-duplicates]}) (:bank-account-id args) (merge-query {:query {:in ['?bank-account-id] :where ['[?e :transaction/bank-account ?bank-account-id]]} :args [(:bank-account-id args)]}) (:import-batch-id args) (merge-query {:query {:in ['?import-batch-id] :where ['[?import-batch-id :import-batch/entry ?e]]} :args [(:import-batch-id args)]}) (:account-id args) (merge-query {:query {:in ['?account-id] :where ['[?e :transaction/accounts ?accounts] '[?accounts :transaction-account/account ?account-id]]} :args [(:account-id args)]}) (:vendor-id args) (merge-query {:query {:in ['?vendor-id] :where ['[?e :transaction/vendor ?vendor-id]]} :args [(:vendor-id args)]}) (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :transaction/amount ?a] '[(>= ?a ?amount-gte)]]} :args [(:amount-gte args)]}) (:amount-lte args) (merge-query {:query {:in ['?amount-lte] :where ['[?e :transaction/amount ?a] '[(<= ?a ?amount-lte)]]} :args [(:amount-lte args)]}) (:approval-status args) (merge-query {:query {:in ['?approval-status] :where ['[?e :transaction/approval-status ?approval-status]]} :args [(:approval-status args)]}) (= (:linked-to args) :payment) (merge-query {:query {:where ['[?e :transaction/payment]]}}) (= (:linked-to args) :expected-deposit) (merge-query {:query {:where ['[?e :transaction/expected-deposit]]}}) (= (:linked-to args) :invoice) (merge-query {:query {:where ['[?e :transaction/payment ?p] '[_ :invoice-payment/payment ?p]]}}) (= (:linked-to args) :none) (merge-query {:query {:where ['(not [?e :transaction/payment]) '(not [?e :transaction/expected-deposit])]}}) (:original-id args) (merge-query {:query {:in ['?original-id] :where ['[?e :transaction/client ?c] '[?c :client/original-id ?original-id]]} :args [(:original-id args)]}) (seq (:location args)) (merge-query {:query {:in ['?location] :where ['[?e :transaction/accounts ?tas] '[?tas :transaction-account/location ?location]]} :args [(:location args)]}) (:unresolved args) (merge-query {:query {:where ['[?e :transaction/date] '(or-join [?e] (not [?e :transaction/accounts]) (and [?e :transaction/accounts ?tas] (not [?tas :transaction-account/account])))]}}) (:description args) (merge-query {:query {:in ['?description] :where ['[?e :transaction/description-original ?do] '[(clojure.string/lower-case ?do) ?do2] '[(.contains ?do2 ?description)]]} :args [(clojure.string/lower-case (:description args))]}) (:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c] '[?c :client/name ?sort-client]] "account" ['[?e :transaction/date] '(or-join [?e ?sort-account] (and [?e :transaction/bank-account ?c] [?c :bank-account/name ?sort-account]) (and (not [?e :transaction/bank-account]) [(ground "") ?sort-account]))] "description-original" ['[?e :transaction/description-original ?sort-description-original]] "date" ['[?e :transaction/date ?sort-date]] "vendor" ['(or-join [?e ?sort-vendor] (and [(missing? $ ?e :transaction/vendor)] [?e :transaction/description-original ?sort-vendor]) (and [?e :transaction/vendor ?v] [?v :vendor/name ?sort-vendor]))] "amount" ['[?e :transaction/amount ?sort-amount]] "status" ['[?e :transaction/status ?sort-status]]} args) true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :transaction/id] '(not [?e :transaction/approval-status :transaction-approval-status/suppressed])]}})))] (cond->> (observable-query query) true (apply-sort-3 (assoc args :default-asc? false)) true (apply-pagination args))))) (defn is-locked? [transaction] (let [transaction-date (some-> transaction :transaction/date coerce/to-date-time) bank-account-start-date (some-> transaction :transaction/bank-account :bank-account/start-date coerce/to-date-time) client-locked-until (some-> transaction :transaction/client :client/locked-until coerce/to-date-time) locked-by-client? (cond (not transaction-date) false (not client-locked-until) false :else (time/before? transaction-date client-locked-until)) locked-by-bank-account? (cond (not transaction-date) false (not bank-account-start-date) false :else (time/before? transaction-date bank-account-start-date))] (or locked-by-bank-account? locked-by-client?))) (defn graphql-results [ids db _] (let [results (->> (pull-many db '[* {:transaction/client [:client/name :db/id :client/code :client/locked-until] :transaction/approval-status [:db/ident :db/id] :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance :bank-account/start-date] :transaction/forecast-match [:db/id :forecasted-transaction/identifier] :transaction/vendor [:db/id :vendor/name] :transaction/matched-rule [:db/id :transaction-rule/note] :transaction/payment [:db/id :payment/date] :transaction/expected-deposit [:db/id :expected-deposit/date] :transaction/accounts [:transaction-account/amount :db/id :transaction-account/location {:transaction-account/account [:account/name :db/id :account/location {:account/client-overrides [:account-client-override/name {:account-client-override/client [:db/id]}]}]}] :transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name] :transaction/plaid-merchant [:db/id :plaid-merchant/name]}] ids) (map #(assoc % :transaction/is-locked (is-locked? %))) (map #(update % :transaction/date coerce/from-date)) (map #(update % :transaction/post-date coerce/from-date)) (map (fn [t] (let [cn (:transaction/check-number t)] (if (and cn (nat-int? cn) (> cn Integer/MAX_VALUE)) (dissoc t :transaction/check-number) t)))) (map #(update % :transaction/accounts (fn [tas] (map (fn [ta] (update ta :transaction-account/account d-accounts/clientize (:db/id (:transaction/client %)))) tas)))) (map (fn [transaction] (cond-> transaction (:transaction/payment transaction) (update-in [:transaction/payment :payment/date] coerce/from-date) (:transaction/expected-deposit transaction) (update-in [:transaction/expected-deposit :expected-deposit/date] coerce/from-date)))) (map #(dissoc % :transaction/id)) (group-by :db/id))] (->> ids (map results) (map first)))) (defn get-graphql [args] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] [(->> (graphql-results ids-to-retrieve db args)) matching-count])) (defn filter-ids [ids] (if ids (->> (dc/q {:find ['?e] :in ['$ '[?e ...]] :where ['[?e :transaction/date]]} (dc/db conn) ids) (map first) vec) [])) (def default-read '[* {:transaction/client [:client/name :db/id :client/code :client/locations :client/groups] :transaction/approval-status [:db/ident :db/id] :transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance] :transaction/vendor [:db/id :vendor/name] :transaction/matched-rule [:db/id :transaction-rule/note] :transaction/forecast-match [:db/id :forecasted-transaction/identifier] :transaction/accounts [:transaction-account/amount :db/id :transaction-account/location {:transaction-account/account [:account/name :db/id :account/location {:account/client-overrides [:account-client-override/name {:account-client-override/client [:db/id]}]}]}] :transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name] :transaction/plaid-merchant [:db/id :plaid-merchant/name]}]) (defn get-by-id [id] (-> (dc/pull (dc/db conn) default-read id) (update :transaction/date coerce/from-date) (update :transaction/post-date coerce/from-date) (dissoc :transaction/id)))