From 084df59149b38fec2aac0e1f928d5c19fbd61f5c Mon Sep 17 00:00:00 2001 From: Bryce Date: Fri, 17 Jan 2025 23:10:42 -0800 Subject: [PATCH] Allows paying from credit --- src/clj/auto_ap/graphql/vendors.clj | 1 + src/clj/auto_ap/ssr/invoices.clj | 254 +++++++++++++++++++-------- src/clj/auto_ap/ssr/vendor.clj | 2 + src/cljc/auto_ap/routes/invoice.cljc | 1 + 4 files changed, 187 insertions(+), 71 deletions(-) diff --git a/src/clj/auto_ap/graphql/vendors.clj b/src/clj/auto_ap/graphql/vendors.clj index 343ada6f..7cface3c 100644 --- a/src/clj/auto_ap/graphql/vendors.clj +++ b/src/clj/auto_ap/graphql/vendors.clj @@ -212,3 +212,4 @@ "hidden" (boolean (:vendor/hidden result))})))) #_(rebuild-search-index) + diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index 93facb85..6594fb2b 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -1,58 +1,60 @@ (ns auto-ap.ssr.invoices - (:require [auto-ap.client-routes :as client-routes] - [auto-ap.datomic + (:require + [auto-ap.client-routes :as client-routes] + [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 audit-transact conn merge-query observable-query pull-many]] - [auto-ap.datomic.accounts :as d-accounts] - [auto-ap.datomic.bank-accounts :as d-bank-accounts] - [auto-ap.datomic.invoices :as d-invoices] - [auto-ap.query-params :refer [wrap-copy-qp-pqp]] - [auto-ap.graphql.checks :as gq-checks :refer [base-payment - invoice-payments - print-checks-internal - validate-belonging]] - [auto-ap.graphql.utils :refer [assert-can-see-client - assert-not-locked exception->4xx - exception->notification - extract-client-ids notify-if-locked]] - [auto-ap.logging :as alog] - [auto-ap.permissions :refer [can?]] - [auto-ap.routes.invoice :as route] - [auto-ap.routes.payments :as payment-route] - [auto-ap.routes.utils + [auto-ap.datomic.accounts :as d-accounts] + [auto-ap.datomic.bank-accounts :as d-bank-accounts] + [auto-ap.datomic.clients :as d-clients] + [auto-ap.datomic.invoices :as d-invoices] + [auto-ap.graphql.checks :as gq-checks :refer [base-payment invoice-payments + print-checks-internal + validate-belonging]] + [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked + exception->4xx exception->notification + extract-client-ids notify-if-locked]] + [auto-ap.logging :as alog] + [auto-ap.permissions :refer [can?]] + [auto-ap.query-params :refer [wrap-copy-qp-pqp]] + [auto-ap.routes.invoice :as route] + [auto-ap.routes.payments :as payment-route] + [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated]] - [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]] - [auto-ap.ssr.components.multi-modal :as mm] - [auto-ap.ssr.form-cursor :as fc] - [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] - [auto-ap.ssr.hiccup-helper :as hh] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.invoice.common :refer [default-read]] - [auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard] - [auto-ap.ssr.invoice.import :as invoice-import] - [auto-ap.ssr.pos.common :refer [date-range-field*]] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers clj-date-schema - dissoc-nil-transformer entity-id html-response - main-transformer modal-response money ref->enum-schema - round-money strip wrap-entity wrap-implied-route-param - wrap-merge-prior-hx wrap-schema-enforce form-validation-error assert-schema]] - [auto-ap.time :as atime] - [auto-ap.utils :refer [by dollars=]] - [bidi.bidi :as bidi] - [clj-time.coerce :as coerce] - [clj-time.core :as time] - [clojure.string :as str] - [datomic.api :as dc] - [hiccup.util :as hu] - [malli.core :as mc] - [malli.transform :as mt] - [malli.util :as mut])) + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]] + [auto-ap.ssr.components.multi-modal :as mm] + [auto-ap.ssr.form-cursor :as fc] + [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] + [auto-ap.ssr.hiccup-helper :as hh] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.invoice.common :refer [default-read]] + [auto-ap.ssr.invoice.import :as invoice-import] + [auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard] + [auto-ap.ssr.pos.common :refer [date-range-field*]] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.utils + :refer [apply-middleware-to-all-handlers assert-schema + clj-date-schema dissoc-nil-transformer entity-id + html-response main-transformer modal-response money + ref->enum-schema round-money strip wrap-entity + wrap-implied-route-param wrap-merge-prior-hx + wrap-schema-enforce]] + [auto-ap.time :as atime] + [auto-ap.utils :refer [by dollars-0? dollars=]] + [bidi.bidi :as bidi] + [clj-time.coerce :as coerce] + [clj-time.coerce :as c] + [clj-time.core :as time] + [clojure.string :as str] + [datomic.api :as dc] + [hiccup.util :as hu] + [malli.core :as mc] + [malli.transform :as mt] + [malli.util :as mut])) (defn exact-match-id* [request] @@ -361,29 +363,41 @@ ids)) 0) + outstanding-balances (if (seq ids) + (->> + (dc/q '[:find ?i ?v ?ob + :in $ [?i ...] + :where [?i :invoice/vendor ?v] + [?i :invoice/outstanding-balance ?ob]] + (dc/db conn) + ids))) + vendor-totals (if (seq ids) (->> - (dc/q '[:find ?i ?v ?ob - :in $ [?i ...] - :where [?i :invoice/vendor ?v] - [?i :invoice/outstanding-balance ?ob]] - (dc/db conn) - ids) + outstanding-balances (reduce (fn [acc [_ v ob]] (update acc v (fnil + 0) ob)) {}) (vals))) all-credits-or-debits (or (every? #(<= % 0.0) vendor-totals) (every? #(>= % 0.0) vendor-totals)) - total (reduce + 0.0 vendor-totals)] + at-least-one-positive-payment (some (fn [[_ _ ob]] + (> ob 0.001)) + outstanding-balances) + total (reduce + 0.0 vendor-totals) + paying-credit? (and (> (count ids) 1) + (= 1 (count vendor-totals)) + at-least-one-positive-payment + (dollars-0? total))] - [:div {:hx-target "this" - :hx-get (bidi/path-for ssr-routes/only-routes - ::route/pay-wizard) - :hx-trigger "click from:#pay-button" - :x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}" - } + [:div (cond-> {:hx-target "this" + + :hx-trigger "click from:#pay-button" + :x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}"} + paying-credit? (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/pay-using-credit)) + (not paying-credit? ) (assoc :hx-get (bidi/path-for ssr-routes/only-routes + ::route/pay-wizard))) (com/button {:color :primary :id "pay-button" :disabled (or (= (count (:ids params)) 0) @@ -397,15 +411,22 @@ :x-ref "source" :minimal-loading? true :class "relative"} - (if (> (count ids) 0) - + (cond + paying-credit? + "Pay invoices using credit" + + (> (count ids) 0) + (format "Pay %d invoices ($%,.2f)" (count ids) (or total 0.0)) - "Pay") - (when (or (= 0 (count ids)) - (> selected-client-count 1)) - (com/badge {} "!"))) + + + (or (= 0 (count ids)) + (> selected-client-count 1)) + (list "Pay " (com/badge {} "!")) + :else + "Pay")) [:template {:x-ref "template"} (cond (not all-credits-or-debits) @@ -790,8 +811,95 @@ updated-count (count ids))})}))) -;; TODO -;; Allow for paying balances from set of invoices for one vendor +#_(defn pay-invoices-from-balance [context {invoices :invoices + client-id :client_id} _] + ) + +(defn pay-using-credit [request] + (alog/peek (:form-params request)) + (let [invoices (selected->ids request (:form-params request)) + + _ (alog/peek invoices) + invoices (d-invoices/get-multi invoices) + client->invoices (group-by (comp :db/id :invoice/client) + invoices) + + client-id (first (keys client->invoices)) + _ (when (> (count (keys client->invoices)) 1) + (throw (ex-info "Can only pay from one customer's balance at a time" {:type :form-validation}))) + _ (when-not (can? (:identity request) {:activity :pay :subject :invoice + :client client-id}) + (throw (ex-info "You can't pay these invoices" {:type :form-validation}))) + client (d-clients/get-by-id client-id) + + _ (when (> (count (set (map :invoice/vendor invoices))) 1) + (throw (ex-info "Balance payments can only be for one vendor at a time." {:type :form-validation}))) + _ (when (> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.001) + (throw (ex-info "There isn't a positive balance to pay from" {:type :form-validation}))) + 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) + (throw (ex-info "Select some invoices that need to be paid" {:type :form-validation}))) + + 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} + result (audit-transact (-> [] + (conj payment) + (into (invoice-payments invoices invoice-amounts))) (:identity request))] + + (doseq [[_ n] (:tempids result)] + (solr/touch-with-ledger n)) + (html-response [:div] + :headers {"hx-trigger" (hx/json {:modalclose "" + :invalidated "" + :notification (format "Successfully paid %d invoices." + (count invoices))})}))) + (defn does-amount-exceed-outstanding? [amount outstanding-balance] (let [outstanding-balance (round-money outstanding-balance) @@ -1325,6 +1433,9 @@ (wrap-admin)) ::route/bulk-delete (-> bulk-delete-dialog (wrap-admin)) + + ::route/pay-using-credit (-> pay-using-credit + (wrap-schema-enforce :form-schema query-schema)) ::route/pay-wizard (-> mm/open-wizard-handler (mm/wrap-wizard pay-wizard) @@ -1334,6 +1445,7 @@ (mm/wrap-wizard pay-wizard) (mm/wrap-decode-multi-form-state)) + ::route/pay-wizard-navigate (-> mm/next-handler (mm/wrap-wizard pay-wizard) diff --git a/src/clj/auto_ap/ssr/vendor.clj b/src/clj/auto_ap/ssr/vendor.clj index 4712c052..0519faf1 100644 --- a/src/clj/auto_ap/ssr/vendor.clj +++ b/src/clj/auto_ap/ssr/vendor.clj @@ -36,6 +36,8 @@ (def search (wrap-json-response search)) #_(comment + (solr/delete solr/impl "vendors") + (count (let [valid-ids (->> (dc/q '[:find ?v :in $ :where [?v :vendor/name]] diff --git a/src/cljc/auto_ap/routes/invoice.cljc b/src/cljc/auto_ap/routes/invoice.cljc index 384d3bef..7daaf2e2 100644 --- a/src/cljc/auto_ap/routes/invoice.cljc +++ b/src/cljc/auto_ap/routes/invoice.cljc @@ -25,6 +25,7 @@ "/pay-button" ::pay-button "/pay" {:get ::pay-wizard + "/using-credit" ::pay-using-credit "/navigate" ::pay-wizard-navigate :post ::pay-submit}