From 3683d582323fa02542c86b4655eb1e205ba5decd Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 25 Mar 2024 17:06:30 -0700 Subject: [PATCH] Validations for new invoices --- .../ssr/invoice/new_invoice_wizard.clj | 135 ++++++++++++++---- 1 file changed, 110 insertions(+), 25 deletions(-) diff --git a/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj b/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj index 5a3886a6..ba24c1c1 100644 --- a/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj +++ b/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj @@ -1,11 +1,15 @@ (ns auto-ap.ssr.invoice.new-invoice-wizard (:require [auto-ap.datomic - :refer [conn pull-attr]] + :refer [audit-transact conn pull-attr pull-ref]] [auto-ap.datomic.accounts :as d-accounts] + [auto-ap.datomic.invoices :as d-invoices] + [auto-ap.graphql.utils :refer [assert-can-see-client + assert-not-locked exception->4xx]] [auto-ap.logging :as alog] [auto-ap.routes.invoice :as route] [auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]] + [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.components :as com] @@ -16,13 +20,54 @@ [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [->db-id apply-middleware-to-all-handlers clj-date-schema - entity-id html-response money wrap-schema-enforce]] + entity-id form-validation-error html-response money strip + wrap-schema-enforce]] [auto-ap.time :as atime] [bidi.bidi :as bidi] + [clj-time.coerce :as coerce] [clj-time.core :as time] [datomic.api :as dc] + [iol-ion.query :refer [dollars=]] [malli.core :as mc] - [malli.util :as mut])) + [malli.util :as mut] + [manifold.deferred :as d])) + +(defn get-vendor [vendor-id] + (dc/pull + (dc/db conn) + [:vendor/terms + :vendor/automatically-paid-when-due + {:vendor/default-account d-accounts/default-read + :vendor/account-overrides + [:vendor-account-override/client + {:vendor-account-override/account d-accounts/default-read}]} + {:vendor/terms-overrides + [:vendor-terms-override/client :vendor-terms-override/terms]}] + vendor-id)) + +(defn check-invoice-expense-account-location [iea] + (let [account-location (pull-attr (dc/db conn) :account/location (:invoice-expense-account/account iea))] + (when (and (seq account-location) + (not= (:invoice-expense-account/location iea) + account-location)) + (throw (ex-info "Exception." {:type (str "expected " account-location)}))) + (when (and (empty? account-location) + (= "A" (:invoice-expense-account/location iea))) + + (throw (ex-info "Exception." {:type "'A' not allowed"}))) + true)) + +(defn check-allowance [account-id] + (let [allowance (:account/invoice-allowance (dc/pull (dc/db conn) '[{[:account/invoice-allowance :xform iol-ion.query/ident] + [:db/ident]}] + account-id))] + (not= :allowance/denied + allowance))) + +(defn check-vendor-default-account [vendor-id] + (some? (:vendor/default-account (get-vendor vendor-id)))) + + (def new-form-schema [:map @@ -30,13 +75,22 @@ [:invoice/date clj-date-schema] [:invoice/due {:optional true} [:maybe clj-date-schema]] [:invoice/scheduled-payment {:optional true} [:maybe clj-date-schema]] - [:invoice/vendor entity-id] + [:invoice/vendor [:and entity-id + [:fn {:error/message "Vendor is missing default expense account"} + check-vendor-default-account]]] + [:invoice/invoice-number [:string {:min 1 :decode/string strip}]] + [:invoice/total money] [:invoice/expense-accounts [:vector {:coerce? true} - [:map - [:invoice-expense-account/account entity-id] - [:invoice-expense-account/location :string] - [:invoice-expense-account/amount money]]]]]) + [:and + [:map + [:invoice-expense-account/account [:and entity-id + [:fn {:error/message "Not an allowed account."} + check-allowance]]] + [:invoice-expense-account/location :string] + [:invoice-expense-account/amount money]] + [:fn {:error/fn (fn [r x] (:type r)) + :error/path [:invoice-expense-account/location]} check-invoice-expense-account-location]]]]]) (defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id] (let [terms-override (->> terms-overrides @@ -68,18 +122,7 @@ vendor)) -(defn get-vendor [vendor-id] - (dc/pull - (dc/db conn) - [:vendor/terms - :vendor/automatically-paid-when-due - {:vendor/default-account d-accounts/default-read - :vendor/account-overrides - [:vendor-account-override/client - {:vendor-account-override/account d-accounts/default-read}]} - {:vendor/terms-overrides - [:vendor-terms-override/client :vendor-terms-override/terms]}] - vendor-id)) + (defrecord BasicDetailsStep [linear-wizard] mm/ModalWizardStep @@ -92,7 +135,7 @@ []) (step-schema [_] - (mut/select-keys (mm/form-schema linear-wizard) #{:invoice/client :invoice/vendor :invoice/date :invoice/due :invoice/scheduled-payment})) + (mut/select-keys (mm/form-schema linear-wizard) #{:invoice/client :invoice/vendor :invoice/date :invoice/due :invoice/scheduled-payment :invoice/total :invoice/invoice-number})) (render-step [this request] (alog/peek ::check (:multi-form-state request)) @@ -292,6 +335,7 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + (defrecord AccountsStep [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -342,6 +386,19 @@ :invoice-expense-account/amount 100}]})) +(defn assert-no-conflicting [{:invoice/keys [invoice-number client vendor]}] + (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice-number + :invoice/vendor (->db-id vendor) + :invoice/client (->db-id client)})) + (form-validation-error (str "Invoice '" invoice-number "' already exists.")))) + +;; TODO warn on account usage based on allowance + +(defn assert-invoice-amounts-add-up [{:keys [:invoice/expense-accounts :invoice/total]}] + (let [expense-account-total (reduce + 0 (map (fn [x] (:invoice-expense-account/amount x)) expense-accounts))] + (when-not (dollars= total expense-account-total) + (form-validation-error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")"))))) + (defrecord NewWizard2 [_ current-step] mm/LinearModalWizard @@ -373,17 +430,45 @@ step-key))) (form-schema [_] new-form-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] + + (let [invoice (:snapshot multi-form-state) + client-id (->db-id (:invoice/client invoice)) + vendor-id (->db-id (:invoice/vendor invoice)) + transaction [:upsert-invoice (-> multi-form-state + :snapshot + (assoc :db/id "invoice") + (assoc + :invoice/outstanding-balance (:invoice/total (:snapshot multi-form-state)) + :invoice/import-status :import-status/imported + :invoice/status :invoice-status/unpaid) + + (update :invoice/expense-accounts + (fn [eas] + (mapv (fn [ea] + (-> ea + (assoc :invoice-expense-account/amount + (:invoice/total (:snapshot multi-form-state))))) + eas))) + (update :invoice/date coerce/to-date) + (update :invoice/due coerce/to-date))]] + + (assert-invoice-amounts-add-up invoice) + (assert-no-conflicting invoice) + (exception->4xx #(assert-can-see-client (:identity request) client-id)) + + (exception->4xx #(assert-not-locked client-id (:invoice/date invoice))) + (let [transaction-result (audit-transact [transaction] (:identity request))] + (solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"])))) (html-response [:div] - :headers {"hx-trigger" "modalclose"}))) + :headers {"hx-trigger" "modalclose,invalidated"}))) (def new-wizard (->NewWizard2 nil nil)) (defn initial-new-wizard-state [request] - (mm/->MultiStepFormState {:TODO nil - :invoice/date (time/plus (time/now) (time/days 12))} + (mm/->MultiStepFormState {:invoice/date (time/now)} [] - {:invoice/date (time/plus (time/now) (time/days 12))})) + {:invoice/date (time/now)})) (defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}] (html-response (location-select* {:name name