From 68444d63114220375c242aeb9c1d1c28137a8629 Mon Sep 17 00:00:00 2001 From: Bryce Date: Mon, 5 Jan 2026 21:34:24 -0800 Subject: [PATCH] allows importing only the invoices that were successful. --- src/clj/auto_ap/ssr/invoice/import.clj | 252 ++++++++++++++----------- 1 file changed, 142 insertions(+), 110 deletions(-) diff --git a/src/clj/auto_ap/ssr/invoice/import.clj b/src/clj/auto_ap/ssr/invoice/import.clj index ece7870a..b4f9ad67 100644 --- a/src/clj/auto_ap/ssr/invoice/import.clj +++ b/src/clj/auto_ap/ssr/invoice/import.clj @@ -1,46 +1,44 @@ (ns auto-ap.ssr.invoice.import - (:require [amazonica.aws.s3 :as s3] - [auto-ap.datomic + (:require + [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 audit-transact conn merge-query observable-query pull-attr pull-many random-tempid]] - [auto-ap.datomic.clients :as d-clients] - [auto-ap.datomic.invoices :as d-invoices] - [auto-ap.datomic.vendors :as d-vendors] - [auto-ap.graphql.utils :refer [assert-can-see-client - assert-not-locked - exception->notification - extract-client-ids]] - [auto-ap.logging :as alog] - [auto-ap.parse :as parse] - [auto-ap.permissions :refer [can? wrap-must]] - [auto-ap.routes.invoice :as route] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.form-cursor :as fc] - [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.invoice.common :refer [default-read]] - [auto-ap.ssr.pos.common :refer [date-range-field*]] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils + [auto-ap.datomic.clients :as d-clients] + [auto-ap.datomic.invoices :as d-invoices] + [auto-ap.datomic.vendors :as d-vendors] + [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked + can-see-client? exception->notification + extract-client-ids]] + [auto-ap.logging :as alog] + [auto-ap.parse :as parse] + [auto-ap.permissions :refer [can? wrap-must]] + [auto-ap.routes.invoice :as route] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.invoice.common :refer [default-read]] + [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 - entity-id html-response main-transformer ref->enum-schema - strip wrap-entity wrap-implied-route-param - wrap-schema-enforce]] - [auto-ap.time :as atime] - [auto-ap.utils :refer [dollars=]] - [bidi.bidi :as bidi] - [clj-time.coerce :as coerce :refer [to-date]] - [clojure.java.io :as io] - [clojure.string :as str] - [com.brunobonacci.mulog :as mu] - [config.core :refer [env]] - [datomic.api :as dc] - [hiccup2.core :as hiccup] - [malli.core :as mc] - [auto-ap.client-routes :as client-routes]) - (:import [java.util UUID])) + entity-id html-response ref->enum-schema strip + wrap-entity wrap-implied-route-param wrap-schema-enforce]] + [auto-ap.time :as atime] + [auto-ap.utils :refer [dollars=]] + [bidi.bidi :as bidi] + [clj-time.coerce :as coerce :refer [to-date]] + [clojure.java.io :as io] + [clojure.string :as str] + [com.brunobonacci.mulog :as mu] + [config.core :refer [env]] + [datomic.api :as dc] + [malli.core :as mc]) + (:import + [java.util UUID])) (defn exact-match-id* [request] @@ -661,17 +659,28 @@ :invoice/status :invoice-status/unpaid})) (defn validate-invoice [invoice user] - (when-not (:invoice/client invoice) - (throw (ex-info (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.") - {:invoice-number (:invoice/invoice-number invoice) - :customer-identifier (:invoice/client-identifier invoice)}))) - (assert-can-see-client user (:invoice/client invoice)) - (doseq [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]] - (when (not (get invoice k)) - (throw (ex-info (str (name k) "not found on invoice " invoice) - invoice)))) + (let [missing-keys (for [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date] + :when (not (get invoice k))] + k + )] + (cond + (not (:invoice/client invoice)) + (do + (alog/warn ::no-client :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] (let [client-count (->> is @@ -688,19 +697,29 @@ (map #(import->invoice % user)) (map #(validate-invoice % user)) 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) - (let [tx (audit-transact potential-invoices user)] - (when-not (seq (dc/q '[:find ?i + (alog/info ::creating-invoice :invoices proposed-invoices) + (let [tx (audit-transact proposed-invoices user)] + #_(when-not (seq (dc/q '[:find ?i :in $ [?i ...] :where [?i :invoice/invoice-number]] (:db-after tx) (map :e (:tx-data tx)))) (throw (ex-info "No new invoices found." {: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] (mu/with-context {:parsing-file filename} @@ -727,6 +746,7 @@ (try (import-uploaded-invoice identity imports) + (catch Exception e (alog/warn ::couldnt-import-upload :error e @@ -734,8 +754,7 @@ (throw (ex-info (ex-message e) {:template (:template ( first imports)) :sample (first imports)} - e)))) - imports) + e))))) (catch Exception e (alog/warn ::couldnt-import-upload :error e) @@ -749,61 +768,74 @@ [file]) results (reduce (fn [result {:keys [filename tempfile]}] - (try - (let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request) )] - (update result :results conj {:filename filename - :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"} + (try + (let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request))] + (alog/info ::failure-error-count :count (count (:errored-invoices i)) ) - [:ul - (for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)] - [:li (name k) ": " (str v)])] - #_(:template r)])])]] - :headers - {"hx-trigger" "invalidated"}) - )) + (-> result + (update :error? #(or % + (boolean (seq (:errored-invoices i))))) + + (update :files conj {:filename filename + :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] (fn [request]