allows importing only the invoices that were successful.

This commit is contained in:
2026-01-05 21:34:24 -08:00
parent 8511d30715
commit 68444d6311

View File

@@ -1,46 +1,44 @@
(ns auto-ap.ssr.invoice.import (ns auto-ap.ssr.invoice.import
(:require [amazonica.aws.s3 :as s3] (:require
[auto-ap.datomic [amazonica.aws.s3 :as s3]
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 :refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact conn merge-query observable-query audit-transact conn merge-query observable-query
pull-attr pull-many random-tempid]] pull-attr pull-many random-tempid]]
[auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.invoices :as d-invoices]
[auto-ap.datomic.vendors :as d-vendors] [auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.utils :refer [assert-can-see-client [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked
assert-not-locked can-see-client? exception->notification
exception->notification extract-client-ids]]
extract-client-ids]] [auto-ap.logging :as alog]
[auto-ap.logging :as alog] [auto-ap.parse :as parse]
[auto-ap.parse :as parse] [auto-ap.permissions :refer [can? wrap-must]]
[auto-ap.permissions :refer [can? wrap-must]] [auto-ap.routes.invoice :as route]
[auto-ap.routes.invoice :as route] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com]
[auto-ap.ssr.components :as com] [auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.hx :as hx]
[auto-ap.ssr.hx :as hx] [auto-ap.ssr.invoice.common :refer [default-read]]
[auto-ap.ssr.invoice.common :refer [default-read]] [auto-ap.ssr.pos.common :refer [date-range-field*]]
[auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.svg :as svg]
[auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers clj-date-schema :refer [apply-middleware-to-all-handlers clj-date-schema
entity-id html-response main-transformer ref->enum-schema entity-id html-response ref->enum-schema strip
strip wrap-entity wrap-implied-route-param wrap-entity wrap-implied-route-param wrap-schema-enforce]]
wrap-schema-enforce]] [auto-ap.time :as atime]
[auto-ap.time :as atime] [auto-ap.utils :refer [dollars=]]
[auto-ap.utils :refer [dollars=]] [bidi.bidi :as bidi]
[bidi.bidi :as bidi] [clj-time.coerce :as coerce :refer [to-date]]
[clj-time.coerce :as coerce :refer [to-date]] [clojure.java.io :as io]
[clojure.java.io :as io] [clojure.string :as str]
[clojure.string :as str] [com.brunobonacci.mulog :as mu]
[com.brunobonacci.mulog :as mu] [config.core :refer [env]]
[config.core :refer [env]] [datomic.api :as dc]
[datomic.api :as dc] [malli.core :as mc])
[hiccup2.core :as hiccup] (:import
[malli.core :as mc] [java.util UUID]))
[auto-ap.client-routes :as client-routes])
(:import [java.util UUID]))
(defn exact-match-id* [request] (defn exact-match-id* [request]
@@ -661,17 +659,28 @@
:invoice/status :invoice-status/unpaid})) :invoice/status :invoice-status/unpaid}))
(defn validate-invoice [invoice user] (defn validate-invoice [invoice user]
(when-not (:invoice/client invoice) (let [missing-keys (for [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]
(throw (ex-info (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.") :when (not (get invoice k))]
{:invoice-number (:invoice/invoice-number invoice) k
:customer-identifier (:invoice/client-identifier invoice)}))) )]
(assert-can-see-client user (:invoice/client invoice)) (cond
(doseq [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]] (not (:invoice/client invoice))
(when (not (get invoice k)) (do
(throw (ex-info (str (name k) "not found on invoice " invoice) (alog/warn ::no-client :invoice invoice)
invoice)))) (assoc invoice :error-message (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.")))
invoice) (not (can-see-client? user (:invoice/client invoice)))
(do
(alog/warn ::cant-see-client :invoice invoice )
(assoc invoice :error-message "No access for the client in this file.")
)
(seq missing-keys)
(do
(alog/warn ::mising-keys :keys missing-keys)
(assoc invoice :error-message (str "Missing the key " missing-keys)))
:else
invoice)))
(defn admin-only-if-multiple-clients [is] (defn admin-only-if-multiple-clients [is]
(let [client-count (->> is (let [client-count (->> is
@@ -688,19 +697,29 @@
(map #(import->invoice % user)) (map #(import->invoice % user))
(map #(validate-invoice % user)) (map #(validate-invoice % user))
admin-only-if-multiple-clients admin-only-if-multiple-clients
(mapv d-invoices/code-invoice) )
(mapv (fn [i] [:propose-invoice i])))] errored-invoices (->> potential-invoices
(filter #(:error-message %)))
successful-invoices (->> potential-invoices
(filter #(not (:error-message %))))
proposed-invoices (->> potential-invoices
(filter #(not (:error-message %)))
(mapv d-invoices/code-invoice)
(mapv (fn [i] [:propose-invoice i])))]
(alog/info ::creating-invoice :invoices potential-invoices) (alog/info ::creating-invoice :invoices proposed-invoices)
(let [tx (audit-transact potential-invoices user)] (let [tx (audit-transact proposed-invoices user)]
(when-not (seq (dc/q '[:find ?i #_(when-not (seq (dc/q '[:find ?i
:in $ [?i ...] :in $ [?i ...]
:where [?i :invoice/invoice-number]] :where [?i :invoice/invoice-number]]
(:db-after tx) (:db-after tx)
(map :e (:tx-data tx)))) (map :e (:tx-data tx))))
(throw (ex-info "No new invoices found." (throw (ex-info "No new invoices found."
{:template (:template (first imports))}))) {:template (:template (first imports))})))
tx))) {:tx tx
:errored-invoices errored-invoices
:successful-invoices successful-invoices
:imports imports})))
(defn import-internal [tempfile filename force-client force-location force-vendor force-chatgpt identity] (defn import-internal [tempfile filename force-client force-location force-vendor force-chatgpt identity]
(mu/with-context {:parsing-file filename} (mu/with-context {:parsing-file filename}
@@ -727,6 +746,7 @@
(try (try
(import-uploaded-invoice identity imports) (import-uploaded-invoice identity imports)
(catch Exception e (catch Exception e
(alog/warn ::couldnt-import-upload (alog/warn ::couldnt-import-upload
:error e :error e
@@ -734,8 +754,7 @@
(throw (ex-info (ex-message e) (throw (ex-info (ex-message e)
{:template (:template ( first imports)) {:template (:template ( first imports))
:sample (first imports)} :sample (first imports)}
e)))) e)))))
imports)
(catch Exception e (catch Exception e
(alog/warn ::couldnt-import-upload (alog/warn ::couldnt-import-upload
:error e) :error e)
@@ -749,61 +768,74 @@
[file]) [file])
results (reduce results (reduce
(fn [result {:keys [filename tempfile]}] (fn [result {:keys [filename tempfile]}]
(try (try
(let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request) )] (let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request))]
(update result :results conj {:filename filename (alog/info ::failure-error-count :count (count (:errored-invoices i)) )
:response "success!"
:template (:template (first i))}))
(catch Exception e
(-> result
(assoc :error? true)
(update :results conj {:filename filename
:response (.getMessage e)
:sample (:sample (ex-data e))
:template (:template (ex-data e))})))))
{:error? false :results []}
file)]
(html-response [:div#page-notification.p-4.rounded-lg
{:class (if (:error? results)
"bg-red-50 text-red-700"
"bg-primary-50 text-primary-700")}
[:table
[:thead
[:tr [:td "File"] [:td "Result"]
[:td "Template"]
(if (:error? results)
[:td "Sample match"])]
#_[:tr "Result"]
#_[:tr "Template"]
]
(for [r (:results results)]
[:tr
[:td.p-2.border
{:class (if (:error? results)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:filename r)]
[:td.p-2.border
{:class (if (:error? results)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:response r)]
[:td.p-2.border
{:class (if (:error? results)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
"Template: " (:template r)]
(if (:error? results )
[:td.p-2.border
{:class "bg-red-50 text-red-700 border-red-300"}
[:ul (-> result
(for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)] (update :error? #(or %
[:li (name k) ": " (str v)])] (boolean (seq (:errored-invoices i)))))
#_(:template r)])])]]
:headers (update :files conj {:filename filename
{"hx-trigger" "invalidated"}) :error? (boolean (seq (:errored-invoices i)))
)) :successful-invoices (count (:successful-invoices i))
:errors [:div
[:p.text-green-500 [:b (count (:successful-invoices i)) " succeeded in total."]
]
[:p [:b (count (:errored-invoices i)) " failed in total."]
]
[:ul
(for [e (take 5 (:errored-invoices i))]
[:li (:error-message e)]) ]]
:template (:template (first (:imports i)))})))
(catch Exception e
(-> result
(assoc :error? true)
(update :files conj {:filename filename
:errors "Can't process file"
:response (.getMessage e)
:sample (:sample (ex-data e))
:template (:template (ex-data e))})))))
{:error? false
:files []
}
file)]
(html-response [:div#page-notification.p-4.rounded-lg
[:table
[:thead
[:tr [:td "File"] [:td "Result"]
[:td "Template"]
[:td "Sample"]]
#_[:tr "Result"]
#_[:tr "Template"]]
(for [r (:files results)]
[:tr
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:filename r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
(:errors r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
"Template: " (:template r)]
[:td.p-2.border.align-top
{:class (if (:error? r)
"bg-red-50 text-red-700 border-red-300"
"bg-primary-50 text-primary-700 border-green-500")}
[:ul
(for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)]
[:li (name k) ": " (str v)])]
#_(:template r)]])]]
:headers
{"hx-trigger" "invalidated"})))
#_(defn wrap-test [handler] #_(defn wrap-test [handler]
(fn [request] (fn [request]