(ns auto-ap.graphql.transactions (:require [auto-ap.datomic :refer [audit-transact audit-transact-batch conn remove-nils]] [auto-ap.datomic.accounts :as a] [auto-ap.datomic.checks :as d-checks] [auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.transaction-rules :as tr] [auto-ap.datomic.transactions :as d-transactions] [auto-ap.graphql.transaction-rules :as g-tr] [auto-ap.graphql.utils :refer [->graphql <-graphql assert-admin assert-can-see-client assert-not-locked assert-power-user enum->keyword ident->enum-f snake->kebab]] [auto-ap.import.transactions :as i-transactions] [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] [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [datomic.api :as d])) (def approval-status->graphql (ident->enum-f :transaction/approval-status)) (defn assert-filtered-enough [filters] (when (:potential_duplicates filters) (assert-admin (:id filters)) (when-not (:bank_account_id filters) (throw (ex-info "In order to select potential duplicates, you must choose a bank account." {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) (when-not (seq (->> filters (filter (fn [[_ v]] (not (nil? v)))) (filter (comp (complement #{:id :start :sort :client_id :bank_account_id :potential_duplicates :per_page}) first)) (filter (fn [[k v]] (if (= :date_range k) (and (some? (:start v)) (some? (:end v))) true))))) (throw (ex-info "In order to select potential duplicates, you must filter your view more." {:validation-error "In order to select potential duplicates, you must filter your view more."}))))) (defn get-transaction-page [context args _] (let [args (assoc (:filters args) :id (:id context)) _ (assert-filtered-enough args) [transactions transactions-count] (d-transactions/get-graphql (update (<-graphql args) :approval-status enum->keyword "transaction-approval-status")) transactions (map ->graphql (map approval-status->graphql transactions))] {:data transactions :total transactions-count :count (count transactions) :start (:start args 0) :end (+ (:start args 0) (count transactions))})) (defn get-ids-matching-filters [args] (let [ids (some-> (:filters args) (<-graphql) (update :approval-status enum->keyword "transaction-approval-status") (assoc :per-page Integer/MAX_VALUE) (d-transactions/raw-graphql-ids ) :ids) specific-ids (d-transactions/filter-ids (:ids args))] (into (set ids) specific-ids))) (defn all-ids-not-locked [all-ids] (->> all-ids (d/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)]] (d/db conn)))) (defn bulk-change-status [context args _] (let [_ (assert-admin (:id context)) args (assoc args :id (:id context)) all-ids (->> (get-ids-matching-filters args) all-ids-not-locked)] (log/info "Unapproving " (count all-ids) args) (audit-transact-batch (->> all-ids (mapv (fn [t] {:db/id t :transaction/approval-status (enum->keyword (:status args) "transaction-approval-status")}))) (:id context)) {:message (str "Succesfully changed " (count all-ids) " transactions to be " (name (:status args) ) ".")})) ;; TODO very similar to rule-matching (defn maybe-code-accounts [transaction account-rules valid-locations] (with-precision 2 (let [accounts (vec (mapcat (fn [ar] (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) (:transaction/amount transaction) 100))))] (if (= "Shared" (:location ar)) (do (log/info "here" valid-locations) (->> valid-locations (map (fn [cents location] {:transaction-account/account (:account_id ar) :transaction-account/amount (* 0.01 cents) :transaction-account/location location}) (rm/spread-cents cents-to-distribute (count valid-locations))))) [(cond-> {:transaction-account/account (:account_id ar) :transaction-account/amount (* 0.01 cents-to-distribute)} (:location ar) (assoc :transaction-account/location (:location ar)))]))) account-rules)) accounts (mapv (fn [a] (update a :transaction-account/amount #(with-precision 2 (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) accounts) leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction)) (Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts))))) *math-context*)) accounts (if (seq accounts) (update-in accounts [(dec (count accounts)) :transaction-account/amount] #(+ % (double leftover))) [])] [:reset (:db/id transaction) :transaction/accounts accounts]))) (defn bulk-code-transactions [context args _] (assert-admin (:id context)) (when-not (:client_id args) (throw (ex-info "Client is required" {:validation-error "client is required"}))) (let [args (assoc args :id (:id context)) locations (:client/locations (d/pull (d/db conn) [:client/locations] (:client_id args))) all-ids (all-ids-not-locked (get-ids-matching-filters args)) transactions (d/pull-many (d/db conn) '[:db/id :transaction/amount] (vec all-ids)) account-total (reduce + 0 (map (fn [x] (:percentage x)) (:accounts args)))] (log/info "client is" locations) (when (and (seq (:accounts args)) (not (dollars= 1.0 account-total))) (let [error (str "Account total (" account-total ") does not reach 100%")] (throw (ex-info error {:validation-error error})))) (doseq [a (:accounts args) :let [{:keys [:account/location :account/name]} (d/entity (d/db conn) (:account_id a))]] (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"} locations) (: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}) )))) (log/info "Bulk coding " (count all-ids) args) (audit-transact-batch (mapcat (fn [i] (cond-> [(cond-> i (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) (:vendor args) (assoc :transaction/vendor (:vendor args)))] (seq (:accounts args)) (conj (maybe-code-accounts i (:accounts args) locations)))) transactions) (:id context)) {:message (str "Successfully coded " (count all-ids) " transactions.")})) (defn delete-transactions [context args _] (let [_ (assert-admin (:id context)) args (assoc args :id (:id context)) all-ids (all-ids-not-locked (get-ids-matching-filters args)) db (d/db conn)] (log/info "Deleting " (count all-ids) args) (audit-transact-batch (mapcat (fn [i] (let [transaction (d/entity db i) payment-id (-> transaction :transaction/payment :db/id) expected-deposit-id (-> transaction :transaction/expected-deposit :db/id) transaction-tx (if (:suppress args) {:db/id i :transaction/approval-status :transaction-approval-status/suppressed} [:db/retractEntity i])] (cond->> [transaction-tx [:db/retractEntity [:journal-entry/original-entity i]]] payment-id (into [{:db/id payment-id :payment/status :payment-status/pending} [:db/retract (:db/id transaction) :transaction/payment payment-id]]) expected-deposit-id (into [{:db/id expected-deposit-id :expected-deposit/status :expected-deposit-status/pending} [:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]])))) all-ids) (:id context)) {:message (str "Succesfully deleted " (count all-ids) " transactions.")})) (defn get-potential-autopay-invoices-matches [context args _] (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) matches-set (i-transactions/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction) (:db/id (:transaction/client transaction)))] (->graphql (for [matches matches-set] (for [[_ invoice-id ] matches] (d-invoices/get-by-id invoice-id)))))) (defn get-potential-unpaid-invoices-matches [context args _] (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) matches-set (i-transactions/match-transaction-to-unpaid-invoices (:transaction/amount transaction) (:db/id (:transaction/client transaction)))] (->graphql (for [matches matches-set] (for [[_ invoice-id ] matches] (d-invoices/get-by-id invoice-id)))))) (defn unlink-transaction [context args _] (let [_ (assert-power-user (:id context)) args (assoc args :id (:id context)) transaction-id (:transaction_id args) transaction (d/pull (d/db conn) [:transaction/approval-status :transaction/status :transaction/date :transaction/location :transaction/vendor :transaction/accounts :transaction/client [:db/id] {:transaction/payment [:payment/date {:payment/status [:db/ident]} :db/id]} ] transaction-id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction)) _ (when (:transaction/payment transaction) (assert-not-locked (:db/id (:transaction/client transaction)) (-> transaction :transaction/payment :payment/date))) _ (log/info "Unlinking" transaction) payment (-> transaction :transaction/payment ) is-autopay-payment? (some->> (doto (d/query {:query {:find ['?sp] :in ['$ '?payment] :where ['[?ip :invoice-payment/payment ?payment] '[?ip :invoice-payment/invoice ?i] '[(get-else $ ?i :invoice/scheduled-payment "N/A") ?sp]]} :args [(d/db conn) (:db/id payment)]}) log/info) seq (map first) (every? #(instance? java.util.Date %))) ] (log/info (:db/id payment)) (log/info "Unlinking " transaction-id " from payment " payment " - Deleting the payment due to autopay? " is-autopay-payment?) (when (not= :payment-status/cleared (-> payment :payment/status :db/ident)) (throw (ex-info "Payment can't be undone because it isn't cleared." {:validation-error "Payment can't be undone because it isn't cleared."}))) (if is-autopay-payment? (audit-transact (->> [{:db/id (:db/id payment) :payment/status :payment-status/pending} {:db/id transaction-id :transaction/approval-status :transaction-approval-status/unapproved} [:db/retractEntity (:db/id payment) ] [:db/retract transaction-id :transaction/payment (:db/id payment)] [:db/retract transaction-id :transaction/vendor (:db/id (:transaction/vendor transaction))] [:db/retract transaction-id :transaction/location (:transaction/location transaction)]] (into (map (fn [a] [:db/retract transaction-id :transaction/accounts (:db/id a)]) (:transaction/accounts transaction))) (into (map (fn [[invoice-payment]] [:db/retractEntity invoice-payment]) (d/query {:query {:find ['?ip] :in ['$ '?p] :where ['[?ip :invoice-payment/payment ?p]]} :args [(d/db conn) (:db/id payment)]} )))) (:id context)) (audit-transact (into (cond-> [{:db/id (:db/id payment) :payment/status :payment-status/pending} {:db/id transaction-id :transaction/approval-status :transaction-approval-status/unapproved} [:db/retract transaction-id :transaction/payment (:db/id payment)] [:db/retract transaction-id :transaction/vendor (:db/id (:transaction/vendor transaction))] ] (:transaction/location transaction) (conj [:db/retract transaction-id :transaction/location (:transaction/location transaction)])) (map (fn [a] [:db/retract transaction-id :transaction/accounts (:db/id a)]) (:transaction/accounts transaction))) (:id context))) (-> (d-transactions/get-by-id transaction-id) approval-status->graphql ->graphql))) (defn transaction-account->entity [{:keys [id account_id amount location]}] (remove-nils #:transaction-account {:amount amount :db/id id :account account_id :location location})) (defn deleted-accounts [transaction accounts] (let [current-accounts (:transaction/accounts transaction) specified-ids (->> accounts (map :id) set) existing-ids (->> current-accounts (map :db/id) set)] (set/difference existing-ids specified-ids))) (defn assert-valid-expense-accounts [accounts] (doseq [trans-account accounts :let [account (d/entity (d/db conn) (:account_id trans-account))]] (when (empty? (:location trans-account)) (throw (ex-info "Account is missing location" {:validation-error "Account is missing location"}))) (when (and (seq (:account/location account)) (not= (:location trans-account) (:account/location account))) (let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")] (throw (ex-info err {:validation-error err})))) (when (and (empty? (:account/location account)) (= "A" (:location trans-account))) (let [err (str "Account uses location '" (:location trans-account) "', which is reserved for liabilities, equities, and assets.")] (throw (ex-info err {:validation-error err})))) (when (nil? (:account_id trans-account)) (throw (ex-info "Account is missing account" {:validation-error "Account is missing account"}))))) (defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match]} :transaction} _] (let [existing-transaction (d-transactions/get-by-id id) _ (assert-can-see-client (:id context) (:transaction/client existing-transaction) ) _ (assert-valid-expense-accounts accounts) _ (assert-not-locked (:db/id (:transaction/client existing-transaction)) (:transaction/date existing-transaction)) deleted (deleted-accounts existing-transaction accounts) account-total (reduce + 0 (map (fn [x] (:amount x)) accounts)) missing-locations (seq (set/difference (->> accounts (map :location) set) (-> (:transaction/client existing-transaction) :client/locations set (conj "A") (conj "HQ"))))] (when-not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total) (let [error (str "Expense account total (" account-total ") does not equal transaction total (" (Math/abs (:transaction/amount existing-transaction)) ")")] (throw (ex-info error {:validation-error error})))) (when missing-locations (throw (ex-info (str "Location '" (str/join ", " missing-locations) "' not found on client.") {})) ) (audit-transact (concat [(remove-nils {:db/id id :transaction/vendor vendor_id :transaction/approval-status (some->> approval_status name snake->kebab (keyword "transaction-approval-status")) :transaction/accounts (map transaction-account->entity accounts) }) ] (cond forecast_match [[:db/add id :transaction/forecast-match forecast_match]] (:db/id (:transaction/forecast-match existing-transaction)) [[:db/retract id :transaction/forecast-match (:db/id (:transaction/forecast-match existing-transaction))]] :else []) (map (fn [d] [:db/retract id :transaction/accounts d]) deleted)) (:id context)) (-> (d-transactions/get-by-id id) approval-status->graphql ->graphql))) (defn match-transaction [context {:keys [transaction_id payment_id]} _] (let [transaction (d-transactions/get-by-id transaction_id) payment (d-checks/get-by-id payment_id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) _ (assert-can-see-client (:id context) (:payment/client payment) ) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction))] (when (not= (:db/id (:transaction/client transaction)) (:db/id (:payment/client payment))) (throw (ex-info "Clients don't match" {:validation-error "Payment and client do not match."}))) (when-not (dollars= (- (:transaction/amount transaction)) (:payment/amount payment)) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) (audit-transact (into [{:db/id (:db/id payment) :payment/status :payment-status/cleared :payment/date (coerce/to-date (first (sort [(:payment/date payment) (:transaction/date transaction)])))} {:db/id (:db/id transaction) :transaction/payment (:db/id payment) :transaction/vendor (:db/id (:payment/vendor payment)) :transaction/location "A" :transaction/approval-status :transaction-approval-status/approved :transaction/accounts [{:transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) :transaction-account/location "A" :transaction-account/amount (Math/abs (:transaction/amount transaction))}]}] (map (fn [x] [:db/retractEntity (:db/id x)] ) (:transaction/accounts transaction))) (:id context))) (-> (d-transactions/get-by-id transaction_id) approval-status->graphql ->graphql)) (defn match-transaction-autopay-invoices [context {:keys [transaction_id autopay_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) db (d/db conn) invoice-clients (set (map (comp :db/id :invoice/client #(d/entity db %)) autopay_invoice_ids)) invoice-amount (reduce + 0.0 (map (comp :invoice/total #(d/entity db %)) autopay_invoice_ids)) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction))] (when (:transaction/payment transaction) (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"}))) (when (or (> (count invoice-clients) 1) (not= (:db/id (:transaction/client transaction)) (first invoice-clients))) (throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."}))) (when-not (dollars= (- (:transaction/amount transaction)) invoice-amount) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) #_(log/info [#_(select-keys (d/entity db transaction_id) #{:transaction/amount :db/id})] (->> autopay_invoice_ids (map (fn [id] (let [entity (d/entity db id)] [(-> entity :invoice/vendor :db/id) (-> entity :db/id) (-> entity :invoice/total)]))))) (let [payment-tx (i-transactions/add-new-payment [(select-keys (d/entity db transaction_id) #{:transaction/amount :transaction/date :db/id})] (map (fn [id] (let [entity (d/entity db id)] [(-> entity :invoice/vendor :db/id) (-> entity :db/id) (-> entity :invoice/total)])) autopay_invoice_ids) (:db/id (:transaction/bank-account transaction)) (:db/id (:transaction/client transaction)))] (log/info "Adding a new payment" payment-tx) @(d/transact conn payment-tx)) (-> (d-transactions/get-by-id transaction_id) approval-status->graphql ->graphql))) (defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) _ (assert-can-see-client (:id context) (:transaction/client transaction) ) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction)) db (d/db conn) invoice-clients (set (map (comp :db/id :invoice/client #(d/entity db %)) unpaid_invoice_ids)) invoice-amount (reduce + 0.0 (map (comp :invoice/outstanding-balance #(d/entity db %)) unpaid_invoice_ids))] (when (or (> (count invoice-clients) 1) (not= (:db/id (:transaction/client transaction)) (first invoice-clients))) (throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match." :invoice-clients (str invoice-clients)}))) (when-not (dollars= (- (:transaction/amount transaction)) invoice-amount) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) (when (:transaction/payment transaction) (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"}))) (let [payment-tx (i-transactions/add-new-payment [(select-keys (d/entity db transaction_id) #{:transaction/amount :transaction/date :db/id})] (map (fn [id] (let [entity (d/entity db id)] [(-> entity :invoice/vendor :db/id) (-> entity :db/id) (-> entity :invoice/total)])) unpaid_invoice_ids) (:db/id (:transaction/bank-account transaction)) (:db/id (:transaction/client transaction)))] (log/info "Adding a new payment" payment-tx) @(d/transact conn payment-tx)) (-> (d-transactions/get-by-id transaction_id) approval-status->graphql ->graphql))) (defn match-transaction-rules [context {:keys [transaction_ids transaction_rule_id all]} _] (let [_ (assert-admin (:id context)) transaction_ids (if all (->> (g-tr/run-transaction-rule context {:transaction_rule_id transaction_rule_id :count Integer/MAX_VALUE} nil) (filter #(not (:payment %))) (map :id )) transaction_ids) transaction_ids (all-ids-not-locked transaction_ids) transactions (transduce (comp (map d-transactions/get-by-id) (map #(update % :transaction/date coerce/to-date))) conj [] transaction_ids) transaction-rule (update (tr/get-by-id transaction_rule_id) :transaction-rule/description #(some-> % rm/->pattern))] (doseq [transaction transactions] (when (not (rm/rule-applies? transaction transaction-rule)) (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply" :transaction-rule transaction-rule :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 (transduce (map #(into [(remove-nils (rm/apply-rule {:db/id (:db/id %) :transaction/amount (:transaction/amount %)} transaction-rule (or (-> % :transaction/bank-account :bank-account/locations) (-> % :transaction/client :client/locations))))] (map (fn [x] [:db/retractEntity (:db/id x)] ) (:transaction/accounts %)))) into [] transactions) (:id context)) ) (transduce (comp (map d-transactions/get-by-id) (map approval-status->graphql) (map ->graphql)) conj [] transaction_ids )) (def objects {:transaction {:fields {:id {:type :id} :amount {:type 'String} :description_original {:type 'String} :description_simple {:type 'String} :location {:type 'String} :forecast_match {:type :forecast_match} :status {:type 'String} :yodlee_merchant {:type :yodlee_merchant} :client {:type :client} :accounts {:type '(list :invoices_expense_accounts)} :payment {:type :payment} :expected_deposit {:type :expected_deposit} :vendor {:type :vendor} :bank_account {:type :bank_account} :date {:type 'String} :post_date {:type 'String} :approval_status {:type :transaction_approval_status} :matched_rule {:type :transaction_rule}}} :transaction_page {:fields {:data {:type '(list :transaction)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}}}) (def queries {:potential_autopay_invoices_matches {:type '(list (list :invoice)) :args {:transaction_id {:type :id}} :resolve :get-potential-autopay-invoices-matches} :potential_unpaid_invoices_matches {:type '(list (list :invoice)) :args {:transaction_id {:type :id}} :resolve :get-potential-unpaid-invoices-matches} :transaction_page {:type :transaction_page :args {:filters {:type :transaction_filters}} :resolve :get-transaction-page}}) (def mutations {:bulk_change_transaction_status {:type :message :args {:filters {:type :transaction_filters} :status {:type :transaction_approval_status} :ids {:type '(list :id)}} :resolve :mutation/bulk-change-transaction-status} :bulk_code_transactions {:type :message :args {:filters {:type :transaction_filters} :client_id {:type :id} :vendor {:type :id} :approval_status {:type :transaction_approval_status} :accounts {:type '(list :edit_percentage_account)} :ids {:type '(list :id)}} :resolve :mutation/bulk-code-transactions} :delete_transactions {:type :message :args {:filters {:type :transaction_filters} :ids {:type '(list :id)} :suppress {:type 'Boolean}} :resolve :mutation/delete-transactions} :edit_transaction {:type :transaction :args {:transaction {:type :edit_transaction}} :resolve :mutation/edit-transaction} :match_transaction {:type :transaction :args {:transaction_id {:type :id} :payment_id {:type :id}} :resolve :mutation/match-transaction} :match_transaction_autopay_invoices {:type :transaction :args {:transaction_id {:type :id} :autopay_invoice_ids {:type '(list :id)}} :resolve :mutation/match-transaction-autopay-invoices} :match_transaction_unpaid_invoices {:type :transaction :args {:transaction_id {:type :id} :unpaid_invoice_ids {:type '(list :id)}} :resolve :mutation/match-transaction-unpaid-invoices} :unlink_transaction {:type :transaction :args {:transaction_id {:type :id}} :resolve :mutation/unlink-transaction} :match_transaction_rules {:type '(list :transaction) :args {:transaction_ids {:type '(list :id)} :all {:type 'Boolean} :transaction_rule_id {:type :id}} :resolve :mutation/match-transaction-rules}}) (def input-objects {:transaction_filters {:fields {:client_id {:type :id} :exact_match_id {:type :id} :import_batch_id {:type :id} :potential_duplicates {:type 'Boolean} :vendor_id {:type :id} :bank_account_id {:type :id} :account_id {:type :id} :date_range {:type :date_range} :location {:type 'String} :amount_lte {:type :money} :amount_gte {:type :money} :description {:type 'String} :start {:type 'Int} :per_page {:type 'Int} :sort {:type '(list :sort_item)} :approval_status {:type :transaction_approval_status} :unresolved {:type 'Boolean}}} :edit_transaction {:fields {:id {:type :id} :vendor_id {:type :id} :forecast_match {:type :id} :approval_status {:type :transaction_approval_status} :accounts {:type '(list :edit_expense_account)}}}}) (def enums {:transaction_approval_status {:values [{:enum-value :approved} {:enum-value :unapproved} {:enum-value :suppressed} {:enum-value :requires_feedback} {:enum-value :excluded}]}}) (def resolvers {:get-transaction-page get-transaction-page :get-potential-autopay-invoices-matches get-potential-autopay-invoices-matches :get-potential-unpaid-invoices-matches get-potential-unpaid-invoices-matches :mutation/edit-transaction edit-transaction :mutation/unlink-transaction unlink-transaction :mutation/bulk-change-transaction-status bulk-change-status :mutation/delete-transactions delete-transactions :mutation/bulk-code-transactions bulk-code-transactions :mutation/match-transaction match-transaction :mutation/match-transaction-autopay-invoices match-transaction-autopay-invoices :mutation/match-transaction-unpaid-invoices match-transaction-unpaid-invoices :mutation/match-transaction-rules match-transaction-rules}) (defn attach [schema] (-> (merge-with merge schema {:objects objects :queries queries :mutations mutations :input-objects input-objects :enums enums}) (attach-resolvers resolvers)))