(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))))