(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 (seq (:ids args)))]
    (if (seq (:ids args))
      (set specific-ids)
      (set 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))]
               (doto 
                [: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)))]
                 clojure.pprint/pprint)))
              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 memo 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 (and (not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total))
               (or
                (and (= approval_status :unapproved)
                     (> (count accounts) 0))
                (not= approval_status :unapproved)))  
      (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/memo memo
                                                   :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}
             :memo {:type 'String}
                          :is_locked {:type 'Boolean}
                          :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}
             :memo {:type 'String}
             :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)))
