Improvements on validations

This commit is contained in:
Bryce Covert
2020-10-07 07:51:27 -07:00
parent 71fc8f69eb
commit 84eedbf56f
3 changed files with 172 additions and 116 deletions

View File

@@ -9,10 +9,11 @@
[auto-ap.time :refer [parse iso-date]] [auto-ap.time :refer [parse iso-date]]
[auto-ap.utils :refer [dollars=]] [auto-ap.utils :refer [dollars=]]
[datomic.api :as d] [datomic.api :as d]
[auto-ap.datomic :refer [uri remove-nils audit-transact]] [auto-ap.datomic :refer [uri remove-nils audit-transact conn]]
[clj-time.coerce :as coerce] [clj-time.coerce :as coerce]
[clj-time.core :as time] [clj-time.core :as time]
[clojure.set :as set])) [clojure.set :as set]
[clojure.tools.logging :as log]))
(defn get-invoice-page [context args value] (defn get-invoice-page [context args value]
@@ -51,6 +52,8 @@
(throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")})))) (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")}))))
(defn expense-account->entity [{:keys [id account_id amount location]}] (defn expense-account->entity [{:keys [id account_id amount location]}]
(remove-nils #:invoice-expense-account {:amount (Double/parseDouble amount) (remove-nils #:invoice-expense-account {:amount (Double/parseDouble amount)
:db/id id :db/id id
:account account_id :account account_id
@@ -90,16 +93,37 @@
set)] set)]
(set/difference existing-ids specified-ids))) (set/difference existing-ids specified-ids)))
(defn assert-valid-expense-accounts [expense_accounts]
(doseq [expense-account expense_accounts
:let [account (d/entity (d/db conn) (:account_id expense-account))]]
(log/info "ACCOUNT" (:account/location account) )
(when (empty? (:location expense-account))
(throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"})))
(when (and (not (empty? (:account/location account)))
(not= (:location expense-account)
(:account/location account)))
(let [err (str "Account uses location '" (:location expense-account) "' but expects '" (:account/location account) "'")]
(throw (ex-info err
{:validation-error err}))))
(when (and (empty? (:account/location account))
(= "A" (:location expense-account)))
(let [err (str "Account uses location '" (:location expense-account) "', which is reserved for liabilities, equities, and assets.")]
(throw (ex-info err
{:validation-error err}))))
(when (nil? (:account_id expense-account))
(throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"})))))
(defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] (defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value]
(assert-no-conflicting in) (assert-no-conflicting in)
(assert-can-see-client (:id context) client_id) (assert-can-see-client (:id context) client_id)
(doseq [expense-account expense_accounts] (assert-valid-expense-accounts expense_accounts)
(when (empty? (:location expense-account))
(throw (ex-info "Expense account is missing location" {:validation-error "Expense account is missing location"})))
(when (nil? (:account_id expense-account))
(throw (ex-info "Expense account is missing account" {:validation-error "Expense account is missing account"}))))
(let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))] (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))]
(-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"])) (-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"]))
(->graphql)))) (->graphql))))
@@ -112,6 +136,7 @@
(assert-no-conflicting in) (assert-no-conflicting in)
(assert-can-see-client (:id context) client_id) (assert-can-see-client (:id context) client_id)
(assert-bank-account-belongs client_id bank-account-id) (assert-bank-account-belongs client_id bank-account-id)
(assert-valid-expense-accounts (:expense_accounts in))
(let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))] (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))]
(-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"]) (-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"])
:amount total}] :amount total}]
@@ -137,6 +162,8 @@
paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice)) paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice))
_ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice))) _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
deleted (deleted-expense-accounts invoice expense_accounts) deleted (deleted-expense-accounts invoice expense_accounts)
_ (assert-valid-expense-accounts expense_accounts)
updated-invoice (cond-> {:db/id id updated-invoice (cond-> {:db/id id
:invoice/invoice-number invoice_number :invoice/invoice-number invoice_number

View File

@@ -1,24 +1,28 @@
(ns auto-ap.graphql.transactions (ns auto-ap.graphql.transactions
(:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin ident->enum-f snake->kebab enum->keyword]] (:require [auto-ap.datomic
[auto-ap.datomic.transactions :as d-transactions] :refer
[auto-ap.datomic.vendors :as d-vendors] [audit-transact audit-transact-batch conn remove-nils]]
[auto-ap.datomic.accounts :as a]
[auto-ap.datomic.checks :as d-checks] [auto-ap.datomic.checks :as d-checks]
[auto-ap.datomic.transaction-rules :as tr]
[auto-ap.datomic.transactions :as d-transactions]
[auto-ap.graphql.transaction-rules :as g-tr] [auto-ap.graphql.transaction-rules :as g-tr]
[datomic.api :as d] [auto-ap.graphql.utils
[auto-ap.datomic :refer [uri remove-nils audit-transact audit-transact-batch]] :refer
[com.walmartlabs.lacinia :refer [execute]] [->graphql
[com.walmartlabs.lacinia.executor :as executor] <-graphql
[com.walmartlabs.lacinia.resolve :as resolve] assert-admin
[auto-ap.utils :refer [by dollars=]] assert-can-see-client
[auto-ap.time :refer [parse normal-date]] enum->keyword
[auto-ap.datomic.clients :as d-clients] ident->enum-f
snake->kebab]]
[auto-ap.rule-matching :as rm]
[auto-ap.utils :refer [dollars=]]
[clj-time.coerce :as coerce]
[clojure.set :as set] [clojure.set :as set]
[clojure.string :as str] [clojure.string :as str]
[auto-ap.datomic.accounts :as a] [clojure.tools.logging :as log]
[auto-ap.datomic.transaction-rules :as tr] [datomic.api :as d]))
[auto-ap.rule-matching :as rm]
[clj-time.coerce :as coerce]
[clojure.tools.logging :as log]))
(def approval-status->graphql (ident->enum-f :transaction/approval-status)) (def approval-status->graphql (ident->enum-f :transaction/approval-status))
@@ -93,9 +97,33 @@
set)] set)]
(set/difference existing-ids specified-ids))) (set/difference existing-ids specified-ids)))
(defn assert-valid-expense-accounts [accounts]
(doseq [trans-account accounts
:let [account (d/entity (d/db conn) (:account_id trans-account))]]
(when (empty? (:location trans-account))
(throw (ex-info "Account is missing location" {:validation-error "Account is missing location"})))
(when (and (not (empty? (:account/location account)))
(not= (:location trans-account)
(:account/location account)))
(let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")]
(throw (ex-info err
{:validation-error err}))))
(when (and (empty? (:account/location account))
(= "A" (:location trans-account)))
(let [err (str "Account uses location '" (:location trans-account) "', which is reserved for liabilities, equities, and assets.")]
(throw (ex-info err
{:validation-error err}))))
(when (nil? (:account_id trans-account))
(throw (ex-info "Account is missing account" {:validation-error "Account is missing account"})))))
(defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match] :as transaction} :transaction} value] (defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match] :as transaction} :transaction} value]
(let [existing-transaction (d-transactions/get-by-id id) (let [existing-transaction (d-transactions/get-by-id id)
_ (assert-can-see-client (:id context) (:transaction/client existing-transaction) ) _ (assert-can-see-client (:id context) (:transaction/client existing-transaction) )
_ (assert-valid-expense-accounts accounts)
deleted (deleted-accounts existing-transaction accounts) deleted (deleted-accounts existing-transaction accounts)
account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) accounts)) account-total (reduce + 0 (map (fn [x] (Double/parseDouble (:amount x))) accounts))
missing-locations (seq (set/difference missing-locations (seq (set/difference

View File

@@ -190,113 +190,114 @@
(let [change-event [::forms/change ::form] (let [change-event [::forms/change ::form]
{:keys [data] } @(re-frame/subscribe [::forms/form ::form]) {:keys [data] } @(re-frame/subscribe [::forms/form ::form])
locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]) locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))])
{:keys [form field raw-field error-notification submit-button ]} transaction-form {:keys [form-inline form field raw-field error-notification submit-button ]} transaction-form
is-admin? @(re-frame/subscribe [::subs/is-admin?]) is-admin? @(re-frame/subscribe [::subs/is-admin?])
should-disable-for-client? (and (not is-admin?) should-disable-for-client? (and (not is-admin?)
(not= :requires-feedback (:original-status data)))] (not= :requires-feedback (:original-status data)))]
[form {:title "Transaction"} (form-inline {:title "Transaction"}
[:<>
(when (and @(re-frame/subscribe [::subs/is-admin?]) (when (and @(re-frame/subscribe [::subs/is-admin?])
(get-in data [:yodlee-merchant])) (get-in data [:yodlee-merchant]))
[:div.control [:div.control
[:p.help "Merchant"] [:p.help "Merchant"]
[:input.input {:type "text" [:input.input {:type "text"
:disabled true :disabled true
:value (str (get-in data [:yodlee-merchant :name]) :value (str (get-in data [:yodlee-merchant :name])
" - " " - "
(get-in data [:yodlee-merchant :yodlee-id]))}]]) (get-in data [:yodlee-merchant :yodlee-id]))}]])
(when is-admin? (when is-admin?
[field "Matched Rule" (field "Matched Rule"
[:input.input {:type "text" [:input.input {:type "text"
:field [:matched-rule :note] :field [:matched-rule :note]
:disabled "disabled"}]]) :disabled "disabled"}]))
[field "Amount" (field "Amount"
[:input.input {:type "text" [:input.input {:type "text"
:field [:amount] :field [:amount]
:disabled "disabled"}]] :disabled "disabled"}])
[field "Description" (field "Description"
[:input.input {:type "text" [:input.input {:type "text"
:field [:description-original] :field [:description-original]
:disabled "disabled"}]] :disabled "disabled"}])
(cond (cond
(and (seq (:potential-transaction-rule-matches data)) (and (seq (:potential-transaction-rule-matches data))
(not (:matched-rule data)) (not (:matched-rule data))
(not (:payment data)) (not (:payment data))
is-admin?) is-admin?)
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}] [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]
(and (seq (:potential-payment-matches data)) (and (seq (:potential-payment-matches data))
(not (:payment data)) (not (:payment data))
is-admin?) is-admin?)
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}] [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]
(and (not (seq (:potential-payment-matches data))) (and (not (seq (:potential-payment-matches data)))
(not (seq (:potential-transaction-rule-matches data)))) (not (seq (:potential-transaction-rule-matches data))))
[:div [:div
[field "Vendor" (field "Vendor"
[typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors])
:match->text :name :match->text :name
:type "typeahead-entity" :type "typeahead-entity"
:auto-focus true :auto-focus true
:field [:vendor] :field [:vendor]
:disabled (or (boolean (:payment data)) :disabled (or (boolean (:payment data))
should-disable-for-client?)}]] should-disable-for-client?)}])
[field nil (field nil
[expense-accounts-field [expense-accounts-field
{:type "expense-accounts" {:type "expense-accounts"
:field [:accounts] :field [:accounts]
:max (Math/abs (js/parseFloat (:amount data))) :max (Math/abs (js/parseFloat (:amount data)))
:descriptor "credit account" :descriptor "credit account"
:disabled (or (boolean (:payment data)) :disabled (or (boolean (:payment data))
should-disable-for-client?) should-disable-for-client?)
:locations locations}]] :locations locations}])
[field "Approval Status" (field "Approval Status"
[button-radio [button-radio
{:type "button-radio" {:type "button-radio"
:field [:approval-status] :field [:approval-status]
:options [[:unapproved "Unapproved"] :options [[:unapproved "Unapproved"]
[:requires-feedback "Client Review"] [:requires-feedback "Client Review"]
[:approved "Approved"] [:approved "Approved"]
[:excluded "Excluded from Ledger"]] [:excluded "Excluded from Ledger"]]
:disabled should-disable-for-client?}]] :disabled should-disable-for-client?}])
[field "Forecasted-transaction" (field "Forecasted-transaction"
[typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println) [typeahead-entity {:matches @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))])
:match->text :identifier :match->text :identifier
:type "typeahead-entity" :type "typeahead-entity"
:field [:forecast-match]}]] :field [:forecast-match]}])
[error-notification] (error-notification)
(when-not should-disable-for-client? (when-not should-disable-for-client?
[submit-button "Save"])] (submit-button "Save"))]
:else :else
[:div [:div
[field "Approval Status" (field "Approval Status"
[button-radio [button-radio
{:type "button-radio" {:type "button-radio"
:field [:approval-status] :field [:approval-status]
:options [[:unapproved "Unapproved"] :options [[:unapproved "Unapproved"]
[:requires-feedback "Client Review"] [:requires-feedback "Client Review"]
[:approved "Approved"] [:approved "Approved"]
[:excluded "Excluded from Ledger"]] [:excluded "Excluded from Ledger"]]
:disabled should-disable-for-client?}]] :disabled should-disable-for-client?}])
[field "Forecasted-transaction" (field "Forecasted-transaction"
[typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println) [typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println)
:match->text :identifier :match->text :identifier
:type "typeahead-entity" :type "typeahead-entity"
:field [:forecast-match]}]] :field [:forecast-match]}])
[error-notification] (error-notification)
(when-not should-disable-for-client? (when-not should-disable-for-client?
[submit-button "Save"])])])]) (submit-button "Save"))])]))])