Files
integreat/src/clj/auto_ap/graphql/transactions.clj

702 lines
37 KiB
Clojure

(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)))