(ns auto-ap.graphql.invoices (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin 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]] [clj-time.coerce :as coerce] [clj-time.core :as time] [clojure.set :as set])) (defn get-invoice-page [context args value] (let [args (assoc args :id (:id context)) [invoices invoice-count outstanding] (d-invoices/get-graphql (update (<-graphql (assoc args :id (:id context))) :status enum->keyword "invoice-status"))] [{:invoices (map ->graphql invoices) :outstanding outstanding :total invoice-count :count (count invoices) :start (:start args 0) :end (+ (:start args 0) (count invoices))}])) (defn get-all-invoices [context args value] (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] (assert-admin (:id context)) (let [transactions (map (fn [i] [:db/retractEntity i ]) invoices) transaction-result @(d/transact (d/connect uri) transactions)] invoices)) (defn approve-invoices [context {:keys [invoices] :as in} value] (assert-admin (:id context)) (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices) transaction-result @(d/transact (d/connect uri) transactions)] invoices)) (defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}] (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 (Double/parseDouble amount) :db/id id :account account_id :location location})) (defn add-invoice-transaction [{:keys [total invoice_number location automatically_paid_when_due client_id vendor_id vendor_name date due expense_accounts] :as in}] (let [vendor (d-vendors/get-by-id vendor_id) account (:vendor/default-account vendor) _ (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)} (:vendor/terms vendor) (assoc :invoice/due (coerce/to-date (time/plus date (time/days (d-vendors/terms-for-client-id vendor client_id))))) due (assoc :invoice/due (coerce/to-date due)) (boolean? automatically_paid_when_due) (assoc :invoice/automatically-paid-when-due automatically_paid_when_due)))) (defn deleted-expense-accounts [invoice expense-accounts] (let [current-expense-accounts (:invoice/expense-accounts invoice) specified-ids (->> expense-accounts (map :id) set) existing-ids (->> current-expense-accounts (map :db/id) set)] (set/difference existing-ids specified-ids))) (defn add-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) (let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])] (-> (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."} )))) (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] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) (assert-bank-account-belongs client_id bank-account-id) (let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])] (-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"]) :amount total}] client_id bank-account-id type) ->graphql))) (defn edit-invoice [context {{:keys [id due invoice_number total vendor_id date client_id expense_accounts automatically_paid_when_due] :as in} :invoice} value] (let [invoice (d-invoices/get-by-id id) _ (when (seq (d-invoices/find-conflicting {:db/id id :invoice/invoice-number invoice_number :invoice/vendor (:db/id (:invoice/vendor invoice)) :invoice/client (:db/id (:invoice/client invoice))})) (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number}))) expense-account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) expense_accounts)) _ (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})))) 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) updated-invoice (cond-> {:db/id id :invoice/invoice-number invoice_number :invoice/date (coerce/to-date date) :invoice/total total :invoice/outstanding-balance (- total paid-amount) :invoice/expense-accounts (map expense-account->entity expense_accounts)} due (assoc :invoice/due (coerce/to-date due)) (boolean? automatically_paid_when_due) (assoc :invoice/automatically-paid-when-due automatically_paid_when_due))] @(d/transact (d/connect uri) (concat [updated-invoice] (map (fn [d] [:db/retract id :invoice/expense-accounts d]) deleted))) (-> (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))) updated-invoice (d-invoices/update {: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))})] (-> updated-invoice (->graphql)))) (defn unvoid-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))) conn (d/connect uri) history (d/history (d/db conn)) txs (d/query {:query {:find ['?tx '?e '?original-status '?original-outstanding '?total '?ea '?ea-amount] :where ['[?e :invoice/status :invoice-status/voided ?tx true] '[?e :invoice/status ?original-status ?tx false] '[?e :invoice/outstanding-balance ?original-outstanding ?tx false] '[?e :invoice/total ?total ?tx false] '[?ea :invoice-expense-account/amount ?ea-amount ?tx false]] :in ['$ '?e]} :args [history id]}) [last-transaction] (->> txs (sort-by first) (last))] @(d/transact conn [(->> txs (filter (fn [[tx]] (= tx last-transaction))) (reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]] (-> 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})) ) {}))]) (-> (d-invoices/get-by-id id) (->graphql)))) (defn edit-expense-accounts [context args value] (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) deleted (deleted-expense-accounts invoice (:expense_accounts args)) updated {:db/id invoice-id :invoice/expense-accounts (map expense-account->entity (:expense_accounts args))}] @(d/transact (d/connect uri) (concat [updated] (map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d])deleted))) (->graphql (d-invoices/get-by-id (:invoice_id args)))))