779 lines
40 KiB
Clojure
779 lines
40 KiB
Clojure
(ns auto-ap.graphql.checks
|
|
(:require
|
|
[amazonica.aws.s3 :as s3]
|
|
[auto-ap.datomic :refer [conn remove-nils pull-many audit-transact]]
|
|
[auto-ap.datomic.accounts :as a]
|
|
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
|
|
[auto-ap.datomic.checks :as d-checks]
|
|
[auto-ap.datomic.clients :as d-clients]
|
|
[auto-ap.datomic.invoices :as d-invoices]
|
|
[auto-ap.datomic.transactions :as d-transactions]
|
|
[auto-ap.datomic.vendors :as d-vendors]
|
|
[auto-ap.graphql.utils
|
|
:refer [->graphql
|
|
<-graphql
|
|
attach-tracing-resolvers
|
|
assert-admin
|
|
assert-can-see-client
|
|
assert-failure
|
|
assert-not-locked
|
|
enum->keyword]]
|
|
[auto-ap.numeric :refer [num->words]]
|
|
[auto-ap.time :refer [iso-date local-now parse]]
|
|
[auto-ap.utils :refer [by dollars-0?]]
|
|
[clj-pdf.core :as pdf]
|
|
[clj-time.coerce :as c]
|
|
[clj-time.core :as time]
|
|
[clj-time.format :as f]
|
|
[clojure.edn :as edn]
|
|
[clojure.java.io :as io]
|
|
[clojure.set :as set]
|
|
[clojure.string :as str]
|
|
[clojure.tools.logging :as log]
|
|
[com.brunobonacci.mulog :as mu]
|
|
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
|
[config.core :refer [env]]
|
|
[clj-time.coerce :as coerce]
|
|
[datomic.api :as dc]
|
|
[digest]
|
|
[auto-ap.solr :as solr])
|
|
(:import
|
|
(java.io ByteArrayOutputStream)
|
|
(java.text DecimalFormat)
|
|
(java.util UUID)))
|
|
|
|
(def parser (f/formatter "MM/dd/YYYY"))
|
|
(defn date->str [t]
|
|
(f/unparse parser t))
|
|
|
|
(defn distribute [nums]
|
|
(let [sum (reduce + 0 nums)]
|
|
(map #(* 100 (/ % sum)) nums)))
|
|
|
|
(defn split-memo [memo]
|
|
(str/join "\n"
|
|
(reverse
|
|
(take 6
|
|
(concat
|
|
(reduce
|
|
(fn [[line & rest ] word]
|
|
(let [line (or line "")]
|
|
(if (> (+ (count line) (count word)) 50)
|
|
(concat [word line] rest)
|
|
(concat [(str line " " word)]
|
|
rest))))
|
|
[]
|
|
(str/split memo #" "))
|
|
(repeat ""))))))
|
|
|
|
(defn make-check-pdf [check]
|
|
(let [output-stream (ByteArrayOutputStream.)]
|
|
(pdf/pdf
|
|
[{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter}
|
|
(let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check
|
|
df (DecimalFormat. "#,###.00")
|
|
word-amount (num->words amount)
|
|
amount (str "--" (.format df amount) "--")]
|
|
[:table {:num-cols 12 :border false :leading 11 :widths (distribute [2 3 3 3 3 3 3 3 3 2 2 2])}
|
|
[(let [{:keys [:client/name] {:keys [:address/street1 :address/city :address/state :address/zip]} :client/address} client]
|
|
[:cell {:colspan 5 } [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)] ])
|
|
(let [{:keys [:bank-account/bank-name :bank-account/bank-code] } bank-account]
|
|
[:cell {:colspan 5 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]])
|
|
[:cell {:colspan 2 :size 13}
|
|
check]]
|
|
|
|
[[:cell {:colspan 9}]
|
|
[:cell {:colspan 3 :leading -10} date]]
|
|
[[:cell {:colspan 12 :size 14}]
|
|
]
|
|
|
|
[[:cell {:size 13 :leading 13} "PAY"]
|
|
[:cell {:size 8 :leading 8 } "TO THE ORDER OF"]
|
|
[:cell {:colspan 7} (if (seq print-as)
|
|
print-as
|
|
vendor-name)]
|
|
[:cell {:colspan 3} amount]]
|
|
|
|
[[:cell {}]
|
|
[:cell {:colspan 8 :size 7} (str " -- " word-amount " " (str/join "" (take (max
|
|
2
|
|
(- 95
|
|
(count word-amount)))
|
|
(repeat "-"))))
|
|
[:line {:line-width 0.15 :color [50 50 50]}]]
|
|
[:cell {:colspan 3}]]
|
|
|
|
|
|
[[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"]
|
|
[:cell {:colspan 5 :leading 11.5} (split-memo memo)
|
|
[:line {:line-width 0.15 :color [50 50 50]}]]
|
|
[:cell {:colspan 6 } (if (:client/signature-file client)
|
|
[:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center}
|
|
|
|
(:client/signature-file client)]
|
|
[:spacer])]]
|
|
|
|
#_[
|
|
#_[:cell {:colspan 5} #_memo ]
|
|
#_[:cell {:colspan 6}]]
|
|
|
|
[[:cell {:colspan 2}]
|
|
[:cell {:colspan 10 :leading 30}
|
|
[:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]]
|
|
[[:cell {:colspan 12 :leading 18} [:spacer]]]
|
|
[[:cell]
|
|
(into
|
|
[:cell {:colspan 9}]
|
|
(let [{:keys [:client/name]
|
|
{:keys [:address/street1 :address/street2 :address/city :address/state :address/zip ]} :client/address} client]
|
|
(filter identity
|
|
(list
|
|
[:paragraph " " name]
|
|
[:paragraph " " street1]
|
|
(when (not (str/blank? street2))
|
|
[:paragraph " " street2])
|
|
[:paragraph " " city ", " state " " zip]))))
|
|
[:cell {:colspan 2 :size 13}
|
|
check]]
|
|
|
|
[[:cell {:colspan 12 :leading 74} [:spacer]]]
|
|
|
|
[[:cell]
|
|
[:cell {:colspan 5} [:paragraph
|
|
" " vendor-name "\n"
|
|
" " (:address/street1 (:vendor/address vendor)) "\n"
|
|
(when (not (str/blank? (:address/street2 (:vendor/address vendor))))
|
|
(str " " (:address/street2 (:vendor/address vendor)) "\n")
|
|
)
|
|
" " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]]
|
|
[:cell {:align :right}
|
|
"Paid to:\n"
|
|
"Amount:\n"
|
|
"Date:\n"]
|
|
|
|
[:cell {:colspan 5}
|
|
[:paragraph paid-to]
|
|
[:paragraph amount]
|
|
[:paragraph date]]]
|
|
|
|
[[:cell {:colspan 3} "Memo:"]
|
|
[:cell {:colspan 9} memo]]
|
|
|
|
[[:cell {:colspan 12} [:spacer]]]
|
|
[[:cell {:colspan 12} [:spacer]]]
|
|
[[:cell {:colspan 12} [:spacer]]]
|
|
[[:cell {:colspan 12} [:spacer]]]
|
|
|
|
[[:cell {:colspan 3}]
|
|
[:cell {:align :right :colspan 2}
|
|
"Check:\n"
|
|
"Vendor:\n"
|
|
"Company:\n"
|
|
"Bank Account:\n"
|
|
"Paid To:\n"
|
|
"Amount:\n"
|
|
"Date:\n"]
|
|
|
|
[:cell {:colspan 7}
|
|
[:paragraph check]
|
|
[:paragraph vendor-name]
|
|
[:paragraph (:client/name client)]
|
|
[:paragraph (:bank-account/bank-name bank-account)]
|
|
[:paragraph paid-to]
|
|
[:paragraph amount]
|
|
[:paragraph date]]]
|
|
[[:cell {:colspan 3} "Memo:"]
|
|
[:cell {:colspan 9} memo]]
|
|
])]
|
|
output-stream)
|
|
(.toByteArray output-stream)))
|
|
|
|
(defn make-pdfs [checks]
|
|
(loop [[check & checks] checks]
|
|
(when check
|
|
(let [check-bytes (-> check
|
|
:payment/pdf-data
|
|
(edn/read-string)
|
|
|
|
|
|
|
|
make-check-pdf
|
|
)]
|
|
(s3/put-object :bucket-name (:data-bucket env)
|
|
:key (:payment/s3-key check)
|
|
:input-stream (io/make-input-stream check-bytes {})
|
|
:metadata {:content-type "application/pdf"
|
|
:content-length (count check-bytes)}))
|
|
(recur checks))))
|
|
|
|
(defn merge-pdfs [keys]
|
|
(let [merged-pdf-stream (java.io.ByteArrayOutputStream.)
|
|
uuid (str (UUID/randomUUID))]
|
|
(apply pdf/collate (concat [{:size :letter} merged-pdf-stream] (->> keys
|
|
(map #(s3/get-object (:data-bucket env) %))
|
|
(map :input-stream))))
|
|
(s3/put-object :bucket-name (:data-bucket env)
|
|
:key (str "merged-checks/" uuid ".pdf")
|
|
:input-stream (io/make-input-stream (.toByteArray merged-pdf-stream) {})
|
|
:metadata {:content-length (count (.toByteArray merged-pdf-stream))
|
|
:content-type "application/pdf"})
|
|
(str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/merged-checks/" uuid ".pdf")))
|
|
|
|
#_{:clj-kondo/ignore [:unused-binding]}
|
|
(defmulti invoices->entities (fn [invoices vendor-id client bank-account type index invoice-amounts]
|
|
type))
|
|
(defn invoice-payments [invoices invoice-amounts]
|
|
(->> (for [invoice invoices
|
|
:let [invoice-amount (invoice-amounts (:db/id invoice))]
|
|
:when (and invoice-amount
|
|
(not= :invoice-status/voided (:invoice/status invoice)))]
|
|
[{:invoice-payment/payment (-> invoice :invoice/vendor :db/id str)
|
|
:invoice-payment/amount invoice-amount
|
|
:invoice-payment/invoice (:db/id invoice)}
|
|
[:pay (:db/id invoice) invoice-amount]])
|
|
(reduce into [])))
|
|
|
|
(defn base-payment [invoices vendor client bank-account _ _ invoice-amounts]
|
|
{:db/id (str (:db/id vendor))
|
|
:payment/bank-account (:db/id bank-account)
|
|
:payment/amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
|
|
:payment/vendor (:db/id vendor)
|
|
:payment/client (:db/id client)
|
|
:payment/date (c/to-date (time/now))
|
|
:payment/invoices (map :db/id invoices)})
|
|
|
|
(defmethod invoices->entities :payment-type/check [invoices vendor client bank-account type index invoice-amounts]
|
|
(when (<= (->> invoices
|
|
(map (comp invoice-amounts :db/id))
|
|
(reduce + 0.0))
|
|
0.001)
|
|
(throw (ex-info "The selected invoices do not have an outstanding balance."
|
|
{:validation-error "The selected invoices do not have an outstanding balance."})))
|
|
(let [uuid (str (UUID/randomUUID))
|
|
memo (str "Invoice #'s: "
|
|
(str/join ", "
|
|
(map (fn [i]
|
|
(str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
|
|
invoices)))
|
|
base-payment (base-payment invoices vendor client bank-account type index invoice-amounts)
|
|
payment
|
|
(remove-nils
|
|
(assoc base-payment
|
|
:payment/s3-uuid (when (> (:payment/amount base-payment) 0) uuid)
|
|
:payment/s3-key (when (> (:payment/amount base-payment) 0) (str "checks/" uuid ".pdf"))
|
|
:payment/s3-url (when (> (:payment/amount base-payment) 0) (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/checks/" uuid ".pdf"))
|
|
:payment/check-number (+ index (:bank-account/check-number bank-account))
|
|
:payment/type :payment-type/check
|
|
:payment/memo memo
|
|
:payment/status :payment-status/pending
|
|
:payment/pdf-data (pr-str {:vendor vendor
|
|
:paid-to (or (:vendor/paid-to vendor) (:vendor/name vendor))
|
|
:amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
|
|
:check (str (+ index (:bank-account/check-number bank-account)))
|
|
:memo memo
|
|
:date (date->str (local-now))
|
|
:client (dissoc client :client/bank-accounts :client/square-integration-status :client/ezcater-locations :client/locked-until :client/emails :client/square-auth-token :client/square-locations)
|
|
:bank-account (dissoc bank-account :bank-account/start-date :bank-account/integration-status)
|
|
#_#_:client {:name (:name client)
|
|
:address (:address client)
|
|
:signature-file (:signature-file client)
|
|
:bank {:name (:bank-account/bank-name bank-account)
|
|
:acct (:bank-account/bank-code bank-account)
|
|
:routing (:bank-account/routing bank-account)
|
|
:acct-number (:bank-account/number bank-account)}}})))]
|
|
|
|
(-> []
|
|
(conj payment)
|
|
(into (invoice-payments invoices invoice-amounts)))))
|
|
|
|
|
|
(defmethod invoices->entities :payment-type/debit [invoices vendor client bank-account type index invoice-amounts]
|
|
(when (<= (->> invoices
|
|
(map (comp invoice-amounts :db/id))
|
|
(reduce + 0.0))
|
|
0.001)
|
|
(throw (ex-info "The selected invoices do not have an outstanding balance."
|
|
{:validation-error "The selected invoices do not have an outstanding balance."})))
|
|
(let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts)
|
|
:payment/type :payment-type/debit
|
|
:payment/memo (str "Debit Invoice #'s: "
|
|
(str/join ", "
|
|
(map (fn [i]
|
|
(str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
|
|
invoices)))
|
|
:payment/status :payment-status/pending)]
|
|
(-> []
|
|
(conj payment)
|
|
(into (invoice-payments invoices invoice-amounts)))))
|
|
|
|
|
|
(defmethod invoices->entities :payment-type/balance-credit [invoices invoice-amounts]
|
|
(when (<= (->> invoices
|
|
(map (comp invoice-amounts :db/id))
|
|
(reduce + 0.0))
|
|
0.001)
|
|
(throw (ex-info "The selected invoices do not have an outstanding balance."
|
|
{:validation-error "The selected invoices do not have an outstanding balance."})))
|
|
)
|
|
|
|
(defmethod invoices->entities :payment-type/credit [invoices vendor client bank-account type index invoice-amounts]
|
|
(when (>= (->> invoices
|
|
(map (comp invoice-amounts :db/id))
|
|
(reduce + 0.0))
|
|
0.001)
|
|
(throw (ex-info "The selected invoices do not have an outstanding balance."
|
|
{:validation-error "The selected invoices do not have an outstanding balance."})))
|
|
(let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts)
|
|
:payment/type :payment-type/credit
|
|
:payment/memo (str "Debit Invoice #'s: "
|
|
(str/join ", "
|
|
(map (fn [i]
|
|
(str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
|
|
invoices)))
|
|
:payment/status :payment-status/pending)]
|
|
(-> []
|
|
(conj payment)
|
|
(into (invoice-payments invoices invoice-amounts)))))
|
|
|
|
(defmethod invoices->entities :payment-type/cash [invoices vendor client bank-account type index invoice-amounts]
|
|
(when (<= (->> invoices
|
|
(map (comp invoice-amounts :db/id))
|
|
(reduce + 0.0))
|
|
0.001)
|
|
(throw (ex-info "The selected invoices do not have an outstanding balance."
|
|
{:validation-error "The selected invoices do not have an outstanding balance."})))
|
|
(let [base-payment (base-payment invoices vendor client bank-account type index invoice-amounts)
|
|
transaction-id (str (UUID/randomUUID))
|
|
memo (str "Cash Invoice #'s: "
|
|
(str/join ", "
|
|
(map (fn [i]
|
|
(str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
|
|
invoices)))
|
|
payment (assoc base-payment
|
|
:payment/type :payment-type/cash
|
|
:payment/date (coerce/to-date (last (sort (map :invoice/date invoices))))
|
|
:payment/memo memo
|
|
:payment/status :payment-status/cleared)
|
|
transaction [:upsert-transaction {:db/id (str "transaction-" (:db/id vendor))
|
|
:transaction/amount (- (:payment/amount base-payment))
|
|
:transaction/payment (str (:db/id vendor))
|
|
:transaction/client (:db/id client)
|
|
:transaction/status "POSTED"
|
|
:transaction/bank-account (:db/id bank-account)
|
|
:transaction/id #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
|
|
:transaction/raw-id transaction-id
|
|
:transaction/vendor (:db/id vendor)
|
|
:transaction/description-original memo
|
|
:transaction/date (:payment/date payment)
|
|
:transaction/approval-status :transaction-approval-status/approved
|
|
:transaction/accounts [{:db/id (str "transaction-account-" (:db/id vendor))
|
|
:transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
|
|
:transaction-account/location "A"
|
|
:transaction-account/amount (Math/abs (:payment/amount base-payment))}]}]]
|
|
(-> []
|
|
(conj payment)
|
|
(conj transaction)
|
|
(into (invoice-payments invoices invoice-amounts)))))
|
|
|
|
(defn validate-belonging [client-id invoices bank-account]
|
|
(when-not (apply = client-id (map (comp :db/id :invoice/client) invoices ))
|
|
(throw (ex-info "You can't pay for that invoice from this bank account."
|
|
{:validation-error "You can't pay for that invoice from this bank account."
|
|
:client-id client-id
|
|
:invoices (map :invoice/invoice-number invoices)}))
|
|
)
|
|
(when-not (= client-id (:db/id (:client/_bank-accounts bank-account)))
|
|
(throw (ex-info "The selected bank doesn't belong to this client"
|
|
{:validation-error "The selected bank doesn't belong to this client"
|
|
:client-id client-id
|
|
:invoices (map :invoice/invoice-number invoices)}))))
|
|
|
|
(defn print-checks-internal [invoice-payments client-id bank-account-id type id]
|
|
(let [type (keyword "payment-type" (name type))
|
|
invoices (d-invoices/get-multi (map :invoice-id invoice-payments))
|
|
client (d-clients/get-by-id client-id)
|
|
invoice-amounts (by :invoice-id :amount invoice-payments)
|
|
invoices-grouped-by-vendor (group-by (comp :db/id :invoice/vendor) invoices)
|
|
vendors (->> (pull-many (dc/db conn) d-vendors/default-read (keys invoices-grouped-by-vendor))
|
|
(by :db/id))
|
|
bank-account (d-bank-accounts/get-by-id bank-account-id)
|
|
_ (validate-belonging client-id invoices bank-account)
|
|
_ (when (and (nil? (:bank-account/check-number bank-account))
|
|
(= type :payment-type/check))
|
|
(let [message (str "The bank account " (:bank-account/name bank-account) " does not have a starting check number. Please ask the integreat staff to initialize it.")]
|
|
(throw (ex-info message
|
|
{:validation-error message}))))
|
|
checks (mu/trace ::build-checks []
|
|
(->> (for [[[vendor-id invoices] index] (map vector invoices-grouped-by-vendor (range))]
|
|
(invoices->entities invoices (vendors vendor-id) client bank-account type index invoice-amounts))
|
|
(reduce into [])
|
|
doall))
|
|
checks (if (= type :payment-type/check)
|
|
(conj checks [:plus (:db/id bank-account) :bank-account/check-number (count invoices-grouped-by-vendor)])
|
|
checks)]
|
|
(when (= type :payment-type/check)
|
|
(mu/trace ::making-pdfs [:checks checks]
|
|
(make-pdfs (filter #(and (= :payment-type/check (:payment/type %))
|
|
(> (:payment/amount %) 0.0))
|
|
checks))))
|
|
(let [result (audit-transact (map #(if (map? %)
|
|
(dissoc % :payment/pdf-data)
|
|
%) checks ) id)]
|
|
|
|
(doseq [[_ i] (:tempids result)]
|
|
(solr/touch-with-ledger i)))
|
|
{:invoices (d-invoices/get-multi (map :invoice-id invoice-payments))
|
|
:pdf-url (if (= type :payment-type/check)
|
|
(mu/trace ::merge-pdfs
|
|
[]
|
|
(merge-pdfs (filter identity (map :payment/s3-key checks))))
|
|
nil)}))
|
|
|
|
(defn get-payment-page [context args _]
|
|
(let [[payments checks-count] (d-checks/get-graphql (-> args
|
|
:filters
|
|
(<-graphql)
|
|
(assoc :id (:id context))
|
|
(update :payment-type enum->keyword "payment-type")
|
|
(update :status enum->keyword "payment-status")))]
|
|
|
|
[{:payments (->> payments
|
|
(map (fn [payment]
|
|
(if (seq (:transaction/_payment payment))
|
|
(-> payment
|
|
(set/rename-keys {:transaction/_payment :transaction})
|
|
(update :transaction first))
|
|
payment)))
|
|
(map ->graphql ))
|
|
:total checks-count
|
|
:count (count payments)
|
|
:start (-> args :filters (:start 0) )
|
|
:end (+ (-> args :filters (:start 0) ) (count payments))}]))
|
|
|
|
(defn get-potential-payments [context args _]
|
|
(let [transaction (d-transactions/get-by-id (:transaction_id args))
|
|
_ (assert-can-see-client (:id context) (:transaction/client transaction))
|
|
[payments _] (d-checks/get-graphql {:client-id (:db/id (:transaction/client transaction))
|
|
:bank-account-id (:db/id (:transaction/bank-account transaction))
|
|
:amount (- (:transaction/amount transaction))
|
|
:status :payment-status/pending
|
|
:date-range {:start (time/plus (:transaction/date transaction) (time/days -90))}})]
|
|
(map ->graphql (reverse (sort-by :payment/date payments)))))
|
|
|
|
(defn add-handwritten-check [context args _]
|
|
(let [invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))
|
|
bank-account-id (:bank_account_id args)
|
|
bank-account (d-bank-accounts/get-by-id bank-account-id)
|
|
_ (doseq [invoice invoices]
|
|
(assert-can-see-client (:id context) (:invoice/client invoice)))
|
|
client-id (:db/id (:invoice/client (first invoices)))
|
|
_ (validate-belonging (:db/id (:client/_bank-accounts bank-account)) invoices bank-account)
|
|
_ (assert-not-locked client-id (:date args))
|
|
invoice-payment-lookup (by :invoice_id :amount (:invoice_payments args))
|
|
base-payment (base-payment invoices
|
|
(:invoice/vendor (first invoices))
|
|
(:invoice/client (first invoices))
|
|
bank-account
|
|
:payment-type/check
|
|
0
|
|
invoice-payment-lookup)]
|
|
|
|
(let [result (audit-transact
|
|
(into [(assoc base-payment
|
|
:payment/type :payment-type/check
|
|
:payment/status :payment-status/pending
|
|
:payment/check-number (:check_number args)
|
|
:payment/date (c/to-date (parse (:date args) iso-date)))]
|
|
(invoice-payments invoices invoice-payment-lookup))
|
|
(:id context))]
|
|
(doseq [[_ i] (:tempids result)]
|
|
(solr/touch-with-ledger i)))
|
|
(->graphql
|
|
{:s3-url nil
|
|
:invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))})))
|
|
|
|
|
|
(defn void-payment [context {id :payment_id} _]
|
|
(let [check (d-checks/get-by-id id)]
|
|
(assert (or (= :payment-status/pending (:payment/status check))
|
|
(#{:payment-type/cash :payment-type/debit :payment-type/balance-credit} (:payment/type check)))
|
|
(pr-str check))
|
|
(assert-can-see-client (:id context) (:db/id (:payment/client check)))
|
|
(assert-not-locked (:db/id (:payment/client check)) (:payment/date check))
|
|
(let [removing-payments (mapcat (fn [x]
|
|
(let [invoice (:invoice-payment/invoice x)
|
|
new-balance (+ (:invoice/outstanding-balance invoice)
|
|
(:invoice-payment/amount x))]
|
|
[[:db/retractEntity (:db/id x)]
|
|
[:upsert-invoice {:db/id (:db/id invoice)
|
|
:invoice/outstanding-balance new-balance
|
|
:invoice/status (if (dollars-0? new-balance)
|
|
(:invoice/status invoice)
|
|
:invoice-status/unpaid)}]]))
|
|
(:payment/invoices check))
|
|
updated-payment {:db/id id
|
|
:payment/amount 0.0
|
|
:payment/status :payment-status/voided}]
|
|
(audit-transact (conj removing-payments updated-payment)
|
|
(:id context)))
|
|
(-> (d-checks/get-by-id id)
|
|
(->graphql))))
|
|
|
|
(defn void-payments-internal [all-ids id]
|
|
(audit-transact (->> all-ids
|
|
(dc/q '[:find (pull ?p [:db/id
|
|
{:invoice-payment/_payment [:invoice-payment/amount
|
|
:db/id
|
|
{:invoice-payment/invoice [:db/id :invoice/outstanding-balance]}]}])
|
|
:in $ [?p ...]
|
|
:where
|
|
(not [_ :transaction/payment ?p])
|
|
(not [?p :payment/status :payment-status/voided])
|
|
[?p :payment/client ?c]
|
|
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
|
|
[?p :payment/date ?d]
|
|
[(>= ?d ?lu)]
|
|
]
|
|
(dc/db conn))
|
|
(map first)
|
|
(mapcat (fn [{:keys [:db/id]
|
|
invoices :invoice-payment/_payment}]
|
|
(into
|
|
[{:db/id id
|
|
:payment/amount 0.0
|
|
:payment/status :payment-status/voided}]
|
|
(->> invoices
|
|
(mapcat (fn [{:keys [:invoice-payment/invoice :db/id :invoice-payment/amount]}]
|
|
(let [new-balance (+ (:invoice/outstanding-balance invoice)
|
|
amount)]
|
|
[[:db.fn/retractEntity id]
|
|
[:upsert-invoice {:db/id (:db/id invoice)
|
|
:invoice/outstanding-balance new-balance
|
|
:invoice/status (if (dollars-0? new-balance)
|
|
(:invoice/status invoice)
|
|
:invoice-status/unpaid)}]]))))))))
|
|
id))
|
|
|
|
|
|
(defn void-payments [context args _]
|
|
(assert-admin (:id context))
|
|
(let [args (assoc args :id (:id context))
|
|
ids (some-> args
|
|
:filters
|
|
(assoc :id (:id context))
|
|
(<-graphql)
|
|
(update :payment-type enum->keyword "payment-type")
|
|
(update :status enum->keyword "payment-status")
|
|
(assoc :per-page Integer/MAX_VALUE)
|
|
d-checks/raw-graphql-ids
|
|
:ids)
|
|
specific-ids (d-checks/filter-ids (:ids args))
|
|
all-ids (into (set ids) specific-ids)]
|
|
(log/info "Voiding " (count all-ids) args)
|
|
|
|
(void-payments-internal all-ids (:id context))
|
|
{:message (str "Succesfully voided " (count all-ids))}))
|
|
|
|
(defn get-all-payments [context args _]
|
|
(assert-admin (:id context))
|
|
(map
|
|
->graphql
|
|
(first (d-checks/get-graphql (assoc (<-graphql args) :count Integer/MAX_VALUE)))))
|
|
|
|
(defn print-checks [context args _]
|
|
(assert-can-see-client (:id context) (:client_id args))
|
|
(->graphql
|
|
(print-checks-internal (map (fn [i] {:invoice-id (:invoice_id i)
|
|
:amount (:amount i)})
|
|
(:invoice_payments args))
|
|
(:client_id args)
|
|
(:bank_account_id args)
|
|
(:type args)
|
|
(:id context))))
|
|
|
|
(defn pay-invoices-from-balance [context {invoices :invoices
|
|
client-id :client_id} _]
|
|
(assert-can-see-client (:id context) client-id)
|
|
(let [invoices (d-invoices/get-multi invoices)
|
|
client (d-clients/get-by-id client-id)
|
|
_ (when (> (count (set (map :invoice/vendor invoices))) 1)
|
|
(assert-failure "Balance payments can only be done on one vendor at a time."))
|
|
_ (when (> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.001)
|
|
(assert-failure "There isn't a positive balance to pay from."))
|
|
invoices-to-be-paid (filter
|
|
(fn [i]
|
|
(> (:invoice/outstanding-balance i)
|
|
0.001))
|
|
invoices)
|
|
credit-invoices (filter
|
|
(fn [i]
|
|
(< (:invoice/outstanding-balance i)
|
|
0.001))
|
|
invoices)
|
|
|
|
|
|
total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid))
|
|
_ (when (<= total-to-pay 0.001)
|
|
(assert-failure "You must select invoices that need to be paid."))
|
|
|
|
invoice-amounts (->> invoices-to-be-paid
|
|
(map (fn [i]
|
|
[(:db/id i)
|
|
(:invoice/outstanding-balance i)]))
|
|
(concat (->> credit-invoices
|
|
(reduce
|
|
(fn [[remaining-to-pay invoice-amounts] invoice]
|
|
|
|
(cond (dollars-0? (+ remaining-to-pay (:invoice/outstanding-balance invoice)))
|
|
(reduced (conj invoice-amounts
|
|
[(:db/id invoice)
|
|
(:invoice/outstanding-balance invoice)]))
|
|
|
|
(< (+ remaining-to-pay (:invoice/outstanding-balance invoice)) 0.0)
|
|
(reduced (conj invoice-amounts
|
|
[(:db/id invoice)
|
|
(- remaining-to-pay)]))
|
|
|
|
:else
|
|
[(+ remaining-to-pay (:invoice/outstanding-balance invoice))
|
|
(conj invoice-amounts [(:db/id invoice)
|
|
(:invoice/outstanding-balance invoice)])]))
|
|
[total-to-pay []])))
|
|
(into {}))
|
|
|
|
|
|
|
|
vendor-id (:db/id (:invoice/vendor (first invoices)))
|
|
payment {:db/id (str vendor-id)
|
|
:payment/amount total-to-pay
|
|
:payment/vendor vendor-id
|
|
:payment/client (:db/id client)
|
|
:payment/date (c/to-date (time/now))
|
|
:payment/invoices (map :db/id invoices)
|
|
:payment/type :payment-type/balance-credit
|
|
:payment/status :payment-status/cleared}]
|
|
|
|
(let [result (audit-transact (-> []
|
|
(conj payment)
|
|
(into (invoice-payments invoices invoice-amounts))) (:id context))]
|
|
(doseq [[_ n] (:tempids result)]
|
|
(solr/touch-with-ledger n)))
|
|
(->graphql {:invoices (d-invoices/get-multi (map :db/id invoices))})))
|
|
|
|
(def objects
|
|
{:payment {:fields {:id {:type :id}
|
|
:type {:type :payment_type}
|
|
:original_id {:type 'Int}
|
|
:amount {:type 'String}
|
|
:vendor {:type :vendor}
|
|
:client {:type :client}
|
|
:date {:type 'String}
|
|
:bank_account {:type :bank_account}
|
|
:memo {:type 'String}
|
|
:s3_url {:type 'String}
|
|
:check_number {:type 'Int}
|
|
:status {:type :ident}
|
|
:transaction {:type :transaction}
|
|
:invoices {:type '(list :invoice_payment)}}}
|
|
:invoice_payment
|
|
{:fields {:id {:type :id}
|
|
:amount {:type 'String}
|
|
:invoice_id {:type 'String}
|
|
:payment_id {:type 'String}
|
|
:payment {:type :payment}
|
|
:invoice {:type :invoice}}}
|
|
:payment_page {:fields {:payments {:type '(list :payment)}
|
|
:count {:type 'Int}
|
|
:total {:type 'Int}
|
|
:start {:type 'Int}
|
|
:end {:type 'Int}}}})
|
|
|
|
(def queries
|
|
{:all_payments {:type '(list :payment)
|
|
:args {:client_id {:type :id}
|
|
:client_code {:type 'String}
|
|
:original_id {:type 'Int}
|
|
:statuses {:type '(list String)}}
|
|
:resolve :get-all-payments}
|
|
:payment_page {:type '(list :payment_page)
|
|
:args {:filters {:type :payment_filters}}
|
|
:resolve :get-payment-page}
|
|
:potential_payment_matches {:type '(list :payment)
|
|
:args {:transaction_id {:type :id}}
|
|
:resolve :get-potential-payments}})
|
|
|
|
(def mutations
|
|
{:print_checks {:type :check_result
|
|
:args {:invoice_payments {:type '(list :invoice_payment_amount)}
|
|
:bank_account_id {:type :id}
|
|
:type {:type :payment_type}
|
|
:client_id {:type :id}}
|
|
:resolve :mutation/print-checks}
|
|
:pay_invoices_from_balance {:type :check_result
|
|
:args {:invoices {:type '(list :id)}
|
|
:client_id {:type :id}}
|
|
:resolve :mutation/pay-invoices-from-balance}
|
|
|
|
:add_handwritten_check {:type :check_result
|
|
:args {:invoice_payments {:type '(list :invoice_payment_amount)}
|
|
:date {:type 'String}
|
|
:check_number {:type 'Int}
|
|
:bank_account_id {:type :id}}
|
|
:resolve :mutation/add-handwritten-check}
|
|
:void_payment {:type :payment
|
|
:args {:payment_id {:type :id}}
|
|
:resolve :mutation/void-payment}
|
|
|
|
:void_payments {:type :message
|
|
:args {:filters {:type :payment_filters}
|
|
:ids {:type '(list :id)}}
|
|
:resolve :mutation/void-payments}})
|
|
|
|
(def input-objects
|
|
{:invoice_payment_amount {:fields {:invoice_id {:type :id}
|
|
:amount {:type :money}}}
|
|
:payment_filters {:fields {:client_id {:type :id}
|
|
:vendor_id {:type :id}
|
|
:payment_type {:type :payment_type}
|
|
:status {:type :payment_status}
|
|
:exact_match_id {:type :id}
|
|
:date_range {:type :date_range}
|
|
:amount_lte {:type :money}
|
|
:amount_gte {:type :money}
|
|
:check_number_like {:type 'String}
|
|
:invoice_number {:type 'String}
|
|
:start {:type 'Int}
|
|
:per_page {:type 'Int}
|
|
:sort {:type '(list :sort_item)}}}})
|
|
|
|
(def enums
|
|
{:payment_type {:values [{:enum-value :check}
|
|
{:enum-value :cash}
|
|
{:enum-value :debit}
|
|
{:enum-value :credit}
|
|
{:enum-value :balance_credit}]}
|
|
:payment_status {:values [{:enum-value :voided}
|
|
{:enum-value :pending}
|
|
{:enum-value :cleared}]}})
|
|
|
|
|
|
(def resolvers
|
|
{:get-potential-payments get-potential-payments
|
|
:get-payment-page get-payment-page
|
|
:get-all-payments get-all-payments
|
|
:mutation/void-payment void-payment
|
|
:mutation/void-payments void-payments
|
|
:mutation/print-checks print-checks
|
|
:mutation/pay-invoices-from-balance pay-invoices-from-balance
|
|
:mutation/add-handwritten-check add-handwritten-check})
|
|
|
|
(defn attach [schema]
|
|
(->
|
|
(merge-with merge schema
|
|
{:objects objects
|
|
:queries queries
|
|
:mutations mutations
|
|
:input-objects input-objects
|
|
:enums enums})
|
|
(attach-tracing-resolvers resolvers)))
|