Less error prone
This commit is contained in:
@@ -1,28 +1,33 @@
|
||||
(ns auto-ap.graphql.invoices
|
||||
(:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin assert-power-user enum->keyword]]
|
||||
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.graphql.checks :as gq-checks]
|
||||
[auto-ap.time :refer [parse iso-date]]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[datomic.api :as d]
|
||||
[auto-ap.datomic :refer [uri remove-nils audit-transact conn]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.set :as set]
|
||||
[clojure.tools.logging :as log]))
|
||||
(:require
|
||||
[auto-ap.datomic :refer [audit-transact conn remove-nils uri]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
[auto-ap.graphql.checks :as gq-checks]
|
||||
[auto-ap.graphql.utils
|
||||
:refer [->graphql
|
||||
<-graphql
|
||||
assert-admin
|
||||
assert-can-see-client
|
||||
assert-power-user
|
||||
enum->keyword]]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.set :as set]
|
||||
[clojure.tools.logging :as log]
|
||||
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
||||
[datomic.api :as d]))
|
||||
|
||||
(defn get-invoice-page [context args _]
|
||||
|
||||
(defn get-invoice-page [context args value]
|
||||
|
||||
(let [args (assoc args :id (:id context))
|
||||
[invoices invoice-count outstanding] (-> args
|
||||
(assoc :id (:id context))
|
||||
(<-graphql )
|
||||
(<-graphql)
|
||||
(update :status enum->keyword "invoice-status")
|
||||
(d-invoices/get-graphql ))]
|
||||
(d-invoices/get-graphql))]
|
||||
[{:invoices (map ->graphql invoices)
|
||||
:outstanding outstanding
|
||||
:total invoice-count
|
||||
@@ -30,44 +35,42 @@
|
||||
:start (:start args 0)
|
||||
:end (+ (:start args 0) (count invoices))}]))
|
||||
|
||||
(defn get-all-invoices [context args value]
|
||||
(defn get-all-invoices [context args _]
|
||||
(assert-admin (:id context))
|
||||
(map
|
||||
->graphql
|
||||
(first (d-invoices/get-graphql (assoc (<-graphql args)
|
||||
:count Integer/MAX_VALUE)))))
|
||||
|
||||
(defn reject-invoices [context {:keys [invoices] :as in} value]
|
||||
(defn reject-invoices [context {:keys [invoices]} _]
|
||||
(assert-power-user (:id context))
|
||||
(doseq [i invoices]
|
||||
(assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i)))))
|
||||
(let [transactions (map (fn [i] [:db/retractEntity i ]) invoices)
|
||||
transaction-result (audit-transact transactions (:id context))]
|
||||
(let [transactions (map (fn [i] [:db/retractEntity i]) invoices)]
|
||||
(audit-transact transactions (:id context))
|
||||
invoices))
|
||||
|
||||
(defn approve-invoices [context {:keys [invoices] :as in} value]
|
||||
(defn approve-invoices [context {:keys [invoices]} _]
|
||||
(assert-power-user (:id context))
|
||||
(doseq [i invoices]
|
||||
(assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i)))))
|
||||
(let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices)
|
||||
transaction-result (audit-transact transactions (:id context))]
|
||||
(let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices)]
|
||||
(audit-transact transactions (:id context))
|
||||
invoices))
|
||||
|
||||
(defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}]
|
||||
(defn assert-no-conflicting [{:keys [invoice_number client_id vendor_id]}]
|
||||
(when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice_number
|
||||
:invoice/vendor vendor_id
|
||||
:invoice/client client_id}))
|
||||
(throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")}))))
|
||||
|
||||
(defn expense-account->entity [{:keys [id account_id amount location]}]
|
||||
|
||||
|
||||
(remove-nils #:invoice-expense-account {:amount amount
|
||||
:db/id id
|
||||
:db/id id
|
||||
:account account_id
|
||||
:location location}))
|
||||
|
||||
(defn add-invoice-transaction [{:keys [total invoice_number location scheduled_payment client_id vendor_id vendor_name date due expense_accounts] :as in}]
|
||||
(defn add-invoice-transaction [{:keys [total invoice_number scheduled_payment client_id vendor_id date due expense_accounts]}]
|
||||
(let [vendor (d-vendors/get-by-id vendor_id)
|
||||
account (:vendor/default-account vendor)
|
||||
due (or (and (:vendor/terms vendor)
|
||||
@@ -78,24 +81,22 @@
|
||||
(and (d-vendors/automatically-paid-for-client-id? vendor client_id)
|
||||
due))
|
||||
_ (when-not (:db/id account)
|
||||
(throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id} )))]
|
||||
(cond->
|
||||
{:db/id "invoice"
|
||||
:invoice/invoice-number invoice_number
|
||||
:invoice/client client_id
|
||||
:invoice/vendor vendor_id
|
||||
:invoice/import-status :import-status/imported
|
||||
:invoice/total total
|
||||
:invoice/outstanding-balance total
|
||||
:invoice/status :invoice-status/unpaid
|
||||
:invoice/date (coerce/to-date date)
|
||||
:invoice/expense-accounts (map expense-account->entity
|
||||
expense_accounts)}
|
||||
(throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id})))]
|
||||
(cond->
|
||||
{:db/id "invoice"
|
||||
:invoice/invoice-number invoice_number
|
||||
:invoice/client client_id
|
||||
:invoice/vendor vendor_id
|
||||
:invoice/import-status :import-status/imported
|
||||
:invoice/total total
|
||||
:invoice/outstanding-balance total
|
||||
:invoice/status :invoice-status/unpaid
|
||||
:invoice/date (coerce/to-date date)
|
||||
:invoice/expense-accounts (map expense-account->entity
|
||||
expense_accounts)}
|
||||
due (assoc :invoice/due (coerce/to-date due))
|
||||
|
||||
scheduled_payment (assoc :invoice/scheduled-payment (coerce/to-date scheduled_payment)))))
|
||||
|
||||
|
||||
(defn deleted-expense-accounts [invoice expense-accounts]
|
||||
(let [current-expense-accounts (:invoice/expense-accounts invoice)
|
||||
specified-ids (->> expense-accounts
|
||||
@@ -109,16 +110,16 @@
|
||||
(defn assert-valid-expense-accounts [expense_accounts]
|
||||
(doseq [expense-account expense_accounts
|
||||
:let [account (d/entity (d/db conn) (:account_id expense-account))]]
|
||||
(log/info "ACCOUNT" (:account/location account) )
|
||||
(log/info "ACCOUNT" (:account/location account))
|
||||
(when (empty? (:location expense-account))
|
||||
(throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"})))
|
||||
|
||||
(when (and (not (empty? (:account/location account)))
|
||||
(when (and (seq (:account/location account))
|
||||
(not= (:location expense-account)
|
||||
(:account/location account)))
|
||||
(let [err (str "Account uses location '" (:location expense-account) "' but expects '" (:account/location account) "'")]
|
||||
(throw (ex-info err
|
||||
{:validation-error err}))))
|
||||
{:validation-error err}))))
|
||||
|
||||
(when (and (empty? (:account/location account))
|
||||
(= "A" (:location expense-account)))
|
||||
@@ -126,36 +127,33 @@
|
||||
(throw (ex-info err
|
||||
{:validation-error err}))))
|
||||
|
||||
|
||||
(when (nil? (:account_id expense-account))
|
||||
(throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"})))
|
||||
|
||||
(when (nil? (:amount expense-account))
|
||||
(throw (ex-info "Expense account does not have an amount specified." {:validation-error "Expense account does not have an amount specified."})))))
|
||||
|
||||
|
||||
(defn assert-invoice-amounts-add-up [{:keys [expense_accounts total]}]
|
||||
(let [expense-account-total (reduce + 0 (map (fn [x] (:amount x)) expense_accounts))]
|
||||
(when-not (dollars= total expense-account-total)
|
||||
(when-not (dollars= total expense-account-total)
|
||||
(let [error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")]
|
||||
(throw (ex-info error {:validation-error error}))))))
|
||||
|
||||
|
||||
(defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value]
|
||||
(defn add-invoice [context {{:keys [expense_accounts client_id] :as in} :invoice} _]
|
||||
(assert-no-conflicting in)
|
||||
(assert-can-see-client (:id context) client_id)
|
||||
(assert-valid-expense-accounts expense_accounts)
|
||||
(assert-invoice-amounts-add-up in)
|
||||
|
||||
|
||||
(let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))]
|
||||
(-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"]))
|
||||
(->graphql))))
|
||||
|
||||
(defn assert-bank-account-belongs [client-id bank-account-id]
|
||||
(when-not ((set (map :db/id (:client/bank-accounts (d-clients/get-by-id client-id)))) bank-account-id)
|
||||
(throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."} ))))
|
||||
(throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."}))))
|
||||
|
||||
(defn add-and-print-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice bank-account-id :bank_account_id type :type} value]
|
||||
(defn add-and-print-invoice [context {{:keys [total client_id] :as in} :invoice bank-account-id :bank_account_id type :type} _]
|
||||
(assert-no-conflicting in)
|
||||
(assert-can-see-client (:id context) client_id)
|
||||
(assert-bank-account-belongs client_id bank-account-id)
|
||||
@@ -170,8 +168,7 @@
|
||||
(:id context))
|
||||
->graphql)))
|
||||
|
||||
|
||||
(defn edit-invoice [context {{:keys [id due invoice_number total vendor_id date client_id expense_accounts scheduled_payment] :as in} :invoice} value]
|
||||
(defn edit-invoice [context {{:keys [id due invoice_number total date expense_accounts scheduled_payment] :as in} :invoice} _]
|
||||
(let [invoice (d-invoices/get-by-id id)
|
||||
_ (when (seq (d-invoices/find-conflicting {:db/id id
|
||||
:invoice/invoice-number invoice_number
|
||||
@@ -179,20 +176,17 @@
|
||||
:invoice/client (:db/id (:invoice/client invoice))}))
|
||||
(throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number})))
|
||||
|
||||
|
||||
|
||||
paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice))
|
||||
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
|
||||
deleted (deleted-expense-accounts invoice expense_accounts)
|
||||
_ (assert-valid-expense-accounts expense_accounts)
|
||||
_ (assert-invoice-amounts-add-up in)
|
||||
|
||||
|
||||
|
||||
updated-invoice (cond-> {:db/id id
|
||||
:invoice/invoice-number invoice_number
|
||||
:invoice/date (coerce/to-date date)
|
||||
|
||||
:invoice/total total
|
||||
|
||||
:invoice/total total
|
||||
:invoice/outstanding-balance (- total paid-amount)
|
||||
:invoice/expense-accounts (map expense-account->entity
|
||||
expense_accounts)}
|
||||
@@ -204,22 +198,21 @@
|
||||
(-> (d-invoices/get-by-id id)
|
||||
(->graphql))))
|
||||
|
||||
(defn void-invoice [context {id :invoice_id} value]
|
||||
(let [invoice (d-invoices/get-by-id id)
|
||||
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))]
|
||||
(audit-transact [{:db/id id
|
||||
:invoice/total 0.0
|
||||
:invoice/outstanding-balance 0.0
|
||||
:invoice/status :invoice-status/voided
|
||||
:invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea)
|
||||
:invoice-expense-account/amount 0.0})
|
||||
(:invoice/expense-accounts invoice))}]
|
||||
(:id context))
|
||||
(defn void-invoice [context {id :invoice_id} _]
|
||||
(let [invoice (d-invoices/get-by-id id)
|
||||
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))]
|
||||
(audit-transact [{:db/id id
|
||||
:invoice/total 0.0
|
||||
:invoice/outstanding-balance 0.0
|
||||
:invoice/status :invoice-status/voided
|
||||
:invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea)
|
||||
:invoice-expense-account/amount 0.0})
|
||||
(:invoice/expense-accounts invoice))}]
|
||||
(:id context))
|
||||
|
||||
(-> (d-invoices/get-by-id id) (->graphql))))
|
||||
(-> (d-invoices/get-by-id id) (->graphql))))
|
||||
|
||||
|
||||
(defn unvoid-invoice [context {id :invoice_id} value]
|
||||
(defn unvoid-invoice [context {id :invoice_id} _]
|
||||
(let [invoice (d-invoices/get-by-id id)
|
||||
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
|
||||
conn (d/connect uri)
|
||||
@@ -236,20 +229,19 @@
|
||||
(audit-transact [(->> txs
|
||||
(filter (fn [[tx]] (= tx last-transaction)))
|
||||
(reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]]
|
||||
(-> new-transaction
|
||||
(-> new-transaction
|
||||
(assoc :db/id entity
|
||||
:invoice/total total
|
||||
:invoice/status original-status
|
||||
:invoice/outstanding-balance original-outstanding)
|
||||
|
||||
(update :invoice/expense-accounts conj {:db/id expense-account :invoice-expense-account/amount expense-account-amount}))
|
||||
) {}))]
|
||||
(update :invoice/expense-accounts conj {:db/id expense-account :invoice-expense-account/amount expense-account-amount}))) {}))]
|
||||
(:id context))
|
||||
|
||||
(-> (d-invoices/get-by-id id)
|
||||
(->graphql))))
|
||||
|
||||
(defn unautopay-invoice [context {id :invoice_id} value]
|
||||
(defn unautopay-invoice [context {id :invoice_id} _]
|
||||
(let [invoice (d/entity (d/db conn) id)
|
||||
_ (assert (:invoice/client invoice))
|
||||
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))]
|
||||
@@ -261,7 +253,7 @@
|
||||
(-> (d-invoices/get-by-id id)
|
||||
(->graphql))))
|
||||
|
||||
(defn edit-expense-accounts [context args value]
|
||||
(defn edit-expense-accounts [context args _]
|
||||
(assert-can-see-client (:id context) (:db/id (:invoice/client (d-invoices/get-by-id (:invoice_id args)))))
|
||||
(let [invoice-id (:invoice_id args)
|
||||
invoice (d-invoices/get-by-id invoice-id)
|
||||
@@ -273,8 +265,157 @@
|
||||
(:expense_accounts args))}]
|
||||
|
||||
(audit-transact (concat [updated]
|
||||
(map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d])deleted))
|
||||
(map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d]) deleted))
|
||||
(:id context))
|
||||
(->graphql
|
||||
(d-invoices/get-by-id (:invoice_id args)))))
|
||||
|
||||
(def objects
|
||||
{:invoice
|
||||
{:fields {:id {:type :id}
|
||||
:original_id {:type 'Int}
|
||||
:client_identifier {:type 'String}
|
||||
:total {:type 'String}
|
||||
:source_url {:type 'String}
|
||||
:outstanding_balance {:type 'String}
|
||||
:invoice_number {:type 'String}
|
||||
:status {:type 'String}
|
||||
:expense_accounts {:type '(list :invoices_expense_accounts)}
|
||||
:similarity {:type 'Float}
|
||||
:date {:type :iso_date}
|
||||
:due {:type :iso_date}
|
||||
:client_id {:type 'Int}
|
||||
:payments {:type '(list :invoice_payment)}
|
||||
:vendor {:type :vendor}
|
||||
:client {:type :client}
|
||||
:scheduled_payment {:type :iso_date}}}
|
||||
|
||||
:invoices_expense_accounts
|
||||
{:fields {:id {:type :id}
|
||||
:invoice_id {:type 'String}
|
||||
:account {:type :account}
|
||||
:location {:type 'String}
|
||||
:amount {:type 'String}}}
|
||||
|
||||
:invoice_page {:fields {:invoices {:type '(list :invoice)}
|
||||
:outstanding {:type :money}
|
||||
:count {:type 'Int}
|
||||
:total {:type 'Int}
|
||||
:start {:type 'Int}
|
||||
:end {:type 'Int}}}})
|
||||
|
||||
(def queries
|
||||
{:invoice_page {:type '(list :invoice_page)
|
||||
:args {:import_status {:type 'String}
|
||||
:exact_match_id {:type :id}
|
||||
:date_range {:type :date_range}
|
||||
:due_range {:type :date_range}
|
||||
:status {:type :invoice_status}
|
||||
:unresolved {:type 'Boolean}
|
||||
:scheduled_payments {:type 'Boolean}
|
||||
:client_id {:type :id}
|
||||
:vendor_id {:type :id}
|
||||
:amount_lte {:type :money}
|
||||
:amount_gte {:type :money}
|
||||
:invoice_number_like {:type 'String}
|
||||
:location {:type 'String}
|
||||
:start {:type 'Int}
|
||||
:per_page {:type 'Int}
|
||||
:sort {:type '(list :sort_item)}}
|
||||
|
||||
:resolve :get-invoice-page}
|
||||
:all_invoices {:type '(list :invoice)
|
||||
:args {:client_id {:type :id}
|
||||
:client_code {:type 'String}
|
||||
:original_id {:type 'Int}
|
||||
:statuses {:type '(list String)}}
|
||||
:resolve :get-all-invoices}})
|
||||
|
||||
(def mutations
|
||||
{:edit_invoice {:type :invoice
|
||||
:args {:invoice {:type :edit_invoice}}
|
||||
:resolve :mutation/edit-invoice}
|
||||
:add_invoice {:type :invoice
|
||||
:args {:invoice {:type :add_invoice}}
|
||||
:resolve :mutation/add-invoice}
|
||||
:void_invoice {:type :invoice
|
||||
:args {:invoice_id {:type :id}}
|
||||
:resolve :mutation/void-invoice}
|
||||
:unvoid_invoice {:type :invoice
|
||||
:args {:invoice_id {:type :id}}
|
||||
:resolve :mutation/unvoid-invoice}
|
||||
:unautopay_invoice {:type :invoice
|
||||
:args {:invoice_id {:type :id}}
|
||||
:resolve :mutation/unautopay-invoice}
|
||||
:reject_invoices {:type '(list :id)
|
||||
:args {:invoices {:type '(list :id)}}
|
||||
:resolve :mutation/reject-invoices}
|
||||
|
||||
:approve_invoices {:type '(list :id)
|
||||
:args {:invoices {:type '(list :id)}}
|
||||
:resolve :mutation/approve-invoices}
|
||||
:add_and_print_invoice {:type :check_result
|
||||
:args {:invoice {:type :add_invoice}
|
||||
:bank_account_id {:type :id}
|
||||
:type {:type :payment_type}}
|
||||
:resolve :mutation/add-and-print-invoice}
|
||||
:edit_expense_accounts {:type :invoice
|
||||
:args {:invoice_id {:type :id}
|
||||
:expense_accounts {:type '(list :edit_expense_account)}}
|
||||
:resolve :mutation/edit-expense-accounts}})
|
||||
|
||||
(def input-objects
|
||||
{:add_invoice
|
||||
{:fields {:id {:type :id}
|
||||
:invoice_number {:type 'String}
|
||||
:expense_accounts {:type '(list :edit_expense_account)}
|
||||
:location {:type 'String}
|
||||
:scheduled_payment {:type :iso_date}
|
||||
:date {:type :iso_date}
|
||||
:due {:type :iso_date}
|
||||
:client_id {:type :id}
|
||||
:vendor_id {:type :id}
|
||||
:vendor_name {:type 'String}
|
||||
:total {:type :money}}}
|
||||
:edit_invoice
|
||||
{:fields {:id {:type :id}
|
||||
:invoice_number {:type 'String}
|
||||
:expense_accounts {:type '(list :edit_expense_account)}
|
||||
:date {:type :iso_date}
|
||||
:scheduled_payment {:type :iso_date}
|
||||
:due {:type :iso_date}
|
||||
:total {:type :money}}}
|
||||
|
||||
:edit_expense_account
|
||||
{:fields {:id {:type :id}
|
||||
:account_id {:type :id}
|
||||
:location {:type 'String}
|
||||
:amount {:type :money}}}})
|
||||
|
||||
(def enums
|
||||
{:invoice_status {:values [{:enum-value :paid}
|
||||
{:enum-value :unpaid}
|
||||
{:enum-value :voided}]}})
|
||||
|
||||
(def resolvers
|
||||
{:get-invoice-page get-invoice-page
|
||||
:get-all-invoices get-all-invoices
|
||||
:mutation/reject-invoices reject-invoices
|
||||
:mutation/approve-invoices approve-invoices
|
||||
:mutation/add-invoice add-invoice
|
||||
:mutation/add-and-print-invoice add-and-print-invoice
|
||||
:mutation/edit-invoice edit-invoice
|
||||
:mutation/void-invoice void-invoice
|
||||
:mutation/unvoid-invoice unvoid-invoice
|
||||
:mutation/unautopay-invoice unautopay-invoice
|
||||
:mutation/edit-expense-accounts edit-expense-accounts})
|
||||
|
||||
(defn attach [schema]
|
||||
(->
|
||||
(merge-with merge schema
|
||||
{:objects objects
|
||||
:queries queries
|
||||
:mutations mutations
|
||||
:input-objects input-objects
|
||||
:enums enums})
|
||||
(attach-resolvers resolvers)))
|
||||
|
||||
Reference in New Issue
Block a user