Files
integreat/src/clj/auto_ap/graphql/checks.clj
Bryce Covert a50239974b fix.
2019-04-02 19:09:40 -07:00

383 lines
18 KiB
Clojure

(ns auto-ap.graphql.checks
(:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client]]
[datomic.api :as d]
[clojure.edn :as edn]
[com.walmartlabs.lacinia :refer [execute]]
[com.walmartlabs.lacinia.executor :as executor]
[com.walmartlabs.lacinia.resolve :as resolve]
[auto-ap.datomic.checks :as d-checks]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
[auto-ap.datomic :refer [uri remove-nils]]
[auto-ap.utils :refer [by dollars-0?]]
[auto-ap.numeric :refer [num->words]]
[config.core :refer [env]]
[auto-ap.time :refer [parse normal-date iso-date local-now]]
[amazonica.aws.s3 :as s3]
[clojure.string :as str]
[clj-pdf.core :as pdf]
[clj-time.format :as f]
[clj-time.coerce :as c]
[clj-time.core :as time]
[clojure.java.io :as io])
(:import [java.text DecimalFormat]
[java.util UUID]
[java.io ByteArrayOutputStream]))
(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/street2 :address/city :address/state :address/zip]} :client/address} client]
[:cell {:colspan 3 } [: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 7 :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} (str " -- " word-amount " " (str/join "" (take (max
2
(- 97
(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/city :address/state :address/zip ]} :client/address} client]
(list
[:paragraph " " name]
[:paragraph " " street1]
[: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"
" " (: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 5}]
[:cell {:align :right :colspan 2}
"Check:\n"
"Vendor:\n"
"Bank Account:\n"
"Paid To:\n"
"Amount:\n"
"Date:\n"]
[:cell {:colspan 5}
[:paragraph check]
[:paragraph vendor-name]
[: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
(s3/put-object :bucket-name (:data-bucket env)
:key (:payment/s3-key check)
:input-stream (-> check
:payment/pdf-data
(edn/read-string)
make-check-pdf
(io/make-input-stream {}))
:metadata {:content-type "application/pdf"})
(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")))
(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))]]
[{: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 type index 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]
(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 client
:bank-account bank-account
#_#_: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]
(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/cleared)]
(-> []
(conj payment)
(into (invoice-payments invoices invoice-amounts)))))
(defmethod invoices->entities :payment-type/cash [invoices vendor client bank-account type index invoice-amounts]
(let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts)
:payment/type :payment-type/cash
:payment/memo (str "Cash Invoice #'s: "
(str/join ", "
(map (fn [i]
(str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
invoices)))
:payment/status :payment-status/cleared)]
(-> []
(conj payment)
(into (invoice-payments invoices invoice-amounts)))))
(defn print-checks [invoice-payments client-id bank-account-id type]
(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)
vendors (by :db/id (d-vendors/get-graphql {}))
invoice-amounts (by :invoice-id :amount invoice-payments)
invoices-grouped-by-vendor (group-by (comp :db/id :invoice/vendor) invoices)
bank-account (d-bank-accounts/get-by-id bank-account-id)
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 [:inc (:db/id bank-account) :bank-account/check-number (count invoices-grouped-by-vendor)])
checks)]
(when (= type :payment-type/check)
(make-pdfs (filter #(and (= :payment-type/check (:payment/type %))
(> (:payment/amount %) 0.0)
)
checks)))
@(d/transact (d/connect uri) checks)
{:invoices (d-invoices/get-multi (map :invoice-id invoice-payments))
:pdf-url (if (= type :payment-type/check)
(merge-pdfs (filter identity (map :payment/s3-key checks)))
nil)}))
(defn get-payment-page [context args value]
(let [args (assoc args :id (:id context))
payments (map
->graphql
(d-checks/get-graphql (<-graphql args)))
checks-count (d-checks/count-graphql (<-graphql args))]
[{:payments payments
:total checks-count
:count (count payments)
:start (:start args 0)
:end (+ (:start args 0) (count payments))}]))
(defn add-handwritten-check [context args value]
(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)))
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)]
@(d/transact (d/connect uri)
(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)))
(->graphql
{:s3-url nil
:invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))})))
(defn void-check [context {id :payment_id} value]
(let [check (d-checks/get-by-id id)]
(assert (or (= :payment-status/pending (:payment/status check))
(#{:payment-type/cash :payment-type/debit} (:payment/type check))))
(assert-can-see-client (:id context) (:db/id (:payment/client check)))
(let [removing-payments (mapcat (fn [x]
(let [invoice (:invoice-payment/invoice x)
new-balance (+ (:invoice/outstanding-balance invoice)
(:invoice-payment/amount x))]
[[:db.fn/retractEntity (:db/id x)]
{: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}]
@(d/transact (d/connect uri) (conj removing-payments updated-payment)))
(-> (d-checks/get-by-id id)
(->graphql))))