(ns auto-ap.graphql.transactions (:require [auto-ap.datomic :refer [conn pull-attr pull-many pull-ref remove-nils audit-transact audit-transact-batch]] [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 attach-tracing-resolvers 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] [datomic.api :as dc] [iol-ion.tx :refer [random-tempid]] [com.brunobonacci.mulog :as mu] [auto-ap.solr :as solr] [auto-ap.logging :as alog])) (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 :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) :clients (:clients context) :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] (alog/info ::getting-ids-matching-filters :args args) (let [ids (some-> (:filters args) (assoc :clients (:clients args)) (assoc :id (:id 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 (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 bulk-change-status [context args _] (let [_ (assert-admin (:id context)) args (assoc args :clients (:clients context) :id (:id context)) all-ids (->> (get-ids-matching-filters args) all-ids-not-locked)] (alog/info ::bulk-change-status :count (count all-ids) :sample (take 3 all-ids) :status (:status args) ) (audit-transact-batch (->> all-ids (mapv (fn [t] [:upsert-transaction {: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)) (->> valid-locations (map (fn [cents location] {:db/id (random-tempid) :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-> {:db/id (random-tempid) :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))) [])] accounts))) (defn bulk-code-transactions [context args _] (assert-admin (:id context)) (when-not (seq (:clients context)) (throw (ex-info "Client is required" {:validation-error "Client is required"}))) (let [args (assoc args :clients (:clients context) :id (:id context)) client->locations (->> (:clients context) (map :db/id ) (dc/q '[:find (pull ?e [:db/id :client/locations]) :in $ [?e ...]] (dc/db conn)) (map (fn [[client]] [(:db/id client) (:client/locations client)])) (into {})) all-ids (all-ids-not-locked (get-ids-matching-filters args)) transactions (pull-many (dc/db conn) [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids)) account-total (reduce + 0 (map (fn [x] (:percentage x)) (:accounts args)))] (alog/info ::bulk-coding-transactions :count (count transactions) :sample (take 3 transactions)) (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]} (dc/pull (dc/db conn) [:account/location :account/name] (: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}) ))) (doseq [[_ locations] client->locations] (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}) ))))) (audit-transact-batch (map (fn [t] (let [locations (client->locations (-> t :transaction/client :db/id))] [:upsert-transaction (cond-> t (: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)) (assoc :transaction/accounts (maybe-code-accounts t (: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 :clients (:clients context)) all-ids (all-ids-not-locked (get-ids-matching-filters args)) db (dc/db conn)] (alog/info ::bulk-delete-transactions :count (count all-ids) :sample (take 3 all-ids)) (audit-transact-batch (mapcat (fn [i] (let [transaction (dc/pull db [:transaction/payment :transaction/expected-deposit :db/id] i) payment-id (-> transaction :transaction/payment :db/id) expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] (cond->> [[: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)) (audit-transact-batch (mapcat (fn [i] (let [transaction-tx (if (:suppress args) {:db/id i :transaction/approval-status :transaction-approval-status/suppressed} [:db/retractEntity i])] [transaction-tx [:db/retractEntity [:journal-entry/original-entity i]]])) 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 (dc/pull (dc/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))) payment (-> transaction :transaction/payment ) is-autopay-payment? (some->> (dc/q {:find ['?sp] :in ['$ '?payment] :where ['[?ip :invoice-payment/payment ?payment] '[?ip :invoice-payment/invoice ?i] '[(get-else $ ?i :invoice/scheduled-payment "N/A") ?sp]]} (dc/db conn) (:db/id payment)) seq (map first) (every? #(instance? java.util.Date %))) ] (alog/info ::unlinking :transaction (pr-str transaction) :autopay is-autopay-payment? :payment (pr-str 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} [:upsert-transaction {:db/id transaction-id :transaction/approval-status :transaction-approval-status/unapproved :transaction/payment nil :transaction/vendor nil :transaction/location nil :transaction/accounts nil}] [:db/retractEntity (:db/id payment) ]] (into (map (fn [[invoice-payment]] [:db/retractEntity invoice-payment]) (dc/q {:find ['?ip] :in ['$ '?p] :where ['[?ip :invoice-payment/payment ?p]]} (dc/db conn) (:db/id payment) )))) (:id context)) (audit-transact [{:db/id (:db/id payment) :payment/status :payment-status/pending} [:upsert-transaction {:db/id transaction-id :transaction/approval-status :transaction-approval-status/unapproved :transaction/payment nil :transaction/vendor nil :transaction/location nil :transaction/accounts nil}]] (:id context))) (-> (d-transactions/get-by-id transaction-id) approval-status->graphql ->graphql))) (defn transaction-account->entity [{:keys [id account_id amount location]}] #:transaction-account {:amount amount :db/id (or id (random-tempid)) :account account_id :location location}) (defn assert-valid-expense-accounts [accounts] (doseq [trans-account accounts :let [account (dc/pull (dc/db conn) [:account/location] (: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)) 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 (cond-> [[:upsert-transaction {: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) :transaction/forecast-match forecast_match}]] (and (:transaction/plaid-merchant existing-transaction) (not (pull-attr (dc/db conn) :vendor/plaid-merchant vendor_id)) (pull-attr (dc/db conn) :vendor/name vendor_id) vendor_id) (conj {:db/id vendor_id :vendor/plaid-merchant (-> existing-transaction :transaction/plaid-merchant :db/id)})) (:id context)) (solr/touch-with-ledger id) (-> (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)])))} [:upsert-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 [{:db/id (random-tempid) :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))}]}]]) (:id context))) (solr/touch-with-ledger transaction_id) (-> (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 (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) autopay_invoice_ids)) invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/total %) 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"}))) (let [payment-tx (i-transactions/add-new-payment (dc/pull db [:transaction/amount :transaction/date :db/id] transaction_id) (map (fn [id] (let [entity (dc/pull db [:invoice/vendor :db/id :invoice/total] id)] [(or (-> entity :invoice/vendor :db/id) (-> entity :invoice/vendor)) (-> entity :db/id) (-> entity :invoice/total)])) autopay_invoice_ids) (:db/id (:transaction/bank-account transaction)) (:db/id (:transaction/client transaction)))] (alog/info ::adding-payment-from-autopay-invoice :payment (pr-str payment-tx)) (audit-transact payment-tx (:id context))) (solr/touch-with-ledger transaction_id) (-> (d-transactions/get-by-id transaction_id) approval-status->graphql ->graphql))) ;; TODO autopay and unpaid are basically the exact same. Migrate to one call (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 (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) unpaid_invoice_ids)) invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/outstanding-balance %) 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 (dc/pull db [:transaction/amount :transaction/date :db/id] transaction_id) (map (fn [id] (let [entity (dc/pull db [:invoice/vendor :db/id :invoice/total] id)] [(or (-> entity :invoice/vendor :db/id) (-> entity :invoice/vendor)) (-> entity :db/id) (-> entity :invoice/total)])) unpaid_invoice_ids) (:db/id (:transaction/bank-account transaction)) (:db/id (:transaction/client transaction)))] (audit-transact payment-tx (:id context))) (solr/touch-with-ledger transaction_id) (-> (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) _ (mu/log ::here :txids 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-> % iol-ion.query/->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 (mapv (fn [t] [:upsert-transaction (remove-nils (rm/apply-rule {:db/id (:db/id t) :transaction/amount (:transaction/amount t)} transaction-rule (or (-> t :transaction/bank-account :bank-account/locations) (-> t :transaction/client :client/locations))))]) transactions) (:id context)) (doseq [n transactions] (solr/touch-with-ledger (:db/id n))) ) (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} :plaid_merchant {:type :plaid_merchant} :check_number {:type 'Int} :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} :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 {: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} :linked_to {:type :transaction_link_type} :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}]} :transaction_link_type {:values [{:enum-value :none} {:enum-value :expected_deposit} {:enum-value :payment} {:enum-value :invoice}]}}) (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-tracing-resolvers resolvers)))