(ns auto-ap.ssr.invoice.import (: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 :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])) (defn exact-match-id* [request] (if (nat-int? (:exact-match-id (:query-params request))) [:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"} (com/hidden {:name "exact-match-id" "x-model" "exact_match"}) (com/pill {:color :primary} [:span.inline-flex.space-x-2.items-center [:div "exact match"] [:div.w-3.h-3 (com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"} svg/x)]])] [:div {:id "exact-match-id-tag"}])) (defn filters [request] [:form#invoice-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes ::route/import-table) "hx-target" "#entity-table" "hx-indicator" "#entity-table"} (com/hidden {:name "status" :value (some-> (:status (:query-params request)) name)}) [:fieldset.space-y-6 (com/field {:label "Vendor"} (com/typeahead {:name "vendor" :id "vendor" :url (bidi/path-for ssr-routes/only-routes :vendor-search) :value (:vendor (:query-params request)) :value-fn :db/id :content-fn :vendor/name})) (date-range-field* request) (com/field {:label "Check #"} (com/text-input {:name "check-number" :id "check-number" :class "hot-filter" :value (:check-number (:query-params request)) :placeholder "e.g., 10001" :size :small})) (com/field {:label "Invoice #"} (com/text-input {:name "invoice-number" :id "invoice-number" :class "hot-filter" :value (:invoice-number (:query-params request)) :placeholder "e.g., ABC-456" :size :small})) (com/field {:label "Amount"} [:div.flex.space-x-4.items-baseline (com/money-input {:name "amount-gte" :id "amount-gte" :hx-preserve "true" :class "hot-filter w-20" :value (:amount-gte (:query-params request)) :placeholder "0.01" :size :small}) [:div.align-baseline "to"] (com/money-input {:name "amount-lte" :hx-preserve "true" :id "amount-lte" :class "hot-filter w-20" :value (:amount-lte (:query-params request)) :placeholder "9999.34" :size :small})]) (exact-match-id* request)]]) (defn fetch-ids [db {:keys [query-params route-params] :as request}] (let [valid-clients (extract-client-ids (:clients request) (:client-id request) (when (:client-code request) [:client/code (:client-code request)])) query (if (:exact-match-id query-params) {:query {:find '[?e] :in '[$ ?e [?c ...]] :where '[[?e :invoice/client ?c]]} :args [db (:exact-match-id query-params) valid-clients]} (cond-> {:query {:find [] :in '[$ [?clients ?start ?end]] :where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] [?e :invoice/import-status :import-status/pending]]} :args [db [valid-clients (some-> (:start-date query-params) coerce/to-date) (some-> (:end-date query-params) coerce/to-date)]]} (:client-id query-params) (merge-query {:query {:in ['?client-id] :where ['[?e :invoice/client ?client-id]]} :args [(:client-id query-params)]}) (:client-code query-params) (merge-query {:query {:in ['?client-code] :where ['[?e :invoice/client ?client-id] '[?client-id :client/code ?client-code]]} :args [(:client-code query-params)]}) (:start (:due-range query-params)) (merge-query {:query {:in '[?start-due] :where ['[?e :invoice/due ?due] '[(>= ?due ?start-due)]]} :args [(coerce/to-date (:start (:due-range query-params)))]}) (:end (:due-range query-params)) (merge-query {:query {:in '[?end-due] :where ['[?e :invoice/due ?due] '[(<= ?due ?end-due)]]} :args [(coerce/to-date (:end (:due-range query-params)))]}) (:import-status query-params) (merge-query {:query {:in ['?import-status] :where ['[?e :invoice/import-status ?import-status]]} :args [(:import-status query-params)]}) (:status route-params) (merge-query {:query {:in ['?status] :where ['[?e :invoice/status ?status]]} :args [(:status route-params)]}) (:vendor query-params) (merge-query {:query {:in ['?vendor-id] :where ['[?e :invoice/vendor ?vendor-id]]} :args [(:db/id (:vendor query-params))]}) (:account-id query-params) (merge-query {:query {:in ['?account-id] :where ['[?e :invoice/expense-accounts ?iea ?] '[?iea :invoice-expense-account/account ?account-id]]} :args [(:account-id query-params)]}) (:amount-gte query-params) (merge-query {:query {:in ['?amount-gte] :where ['[?e :invoice/total ?total-filter] '[(>= ?total-filter ?amount-gte)]]} :args [(:amount-gte query-params)]}) (:amount-lte query-params) (merge-query {:query {:in ['?amount-lte] :where ['[?e :invoice/total ?total-filter] '[(<= ?total-filter ?amount-lte)]]} :args [(:amount-lte query-params)]}) (not-empty (:invoice-number query-params)) (merge-query {:query {:in ['?invoice-number-like] :where ['[?e :invoice/invoice-number ?invoice-number] '[(.contains ^String ?invoice-number ?invoice-number-like)]]} :args [(:invoice-number query-params)]}) (:scheduled-payments query-params) (merge-query {:query {:in [] :where ['[?e :invoice/scheduled-payment]]} :args []}) (:unresolved query-params) (merge-query {:query {:in [] :where ['(or-join [?e] (not [?e :invoice/expense-accounts]) (and [?e :invoice/expense-accounts ?ea] (not [?ea :invoice-expense-account/account])))]} :args []}) (seq (:location query-params)) (merge-query {:query {:in ['?location] :where ['[?e :invoice/expense-accounts ?eas] '[?eas :invoice-expense-account/location ?location]]} :args [(:location query-params)]}) (:sort query-params) (add-sorter-fields {"client" ['[?e :invoice/client ?c] '[?c :client/name ?sort-client]] "vendor" ['[?e :invoice/vendor ?v] '[?v :vendor/name ?sort-vendor]] "description-original" ['[?e :transaction/description-original ?sort-description-original]] "location" ['[?e :invoice/expense-accounts ?iea] '[?iea :invoice-expense-account/location ?sort-location]] "date" ['[?e :invoice/date ?sort-date]] "due" ['[(get-else $ ?e :invoice/due #inst "2050-01-01") ?sort-due]] "invoice-number" ['[?e :invoice/invoice-number ?sort-invoice-number]] "total" ['[?e :invoice/total ?sort-total]] "outstanding-balance" ['[?e :invoice/outstanding-balance ?sort-outstanding-balance]]} query-params) true (merge-query {:query {:find ['?sort-default '?e]}})))] (->> (observable-query query) (apply-sort-3 (assoc query-params :default-asc? false)) (apply-pagination query-params)))) (defn hydrate-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) refunds (->> ids (map results) (map first))] refunds)) (defn sum-outstanding [ids] (->> (dc/q {:find ['?id '?o] :in ['$ '[?id ...]] :where ['[?id :invoice/outstanding-balance ?o]]} (dc/db conn) ids) (map last) (reduce + 0.0))) (defn sum-total-amount [ids] (->> (dc/q {:find ['?id '?o] :in ['$ '[?id ...]] :where ['[?id :invoice/total ?o]]} (dc/db conn) ids) (map last) (reduce + 0.0))) (defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count all-ids :all-ids} (fetch-ids db request)] [(->> (hydrate-results ids-to-retrieve db request)) matching-count (sum-outstanding all-ids) (sum-total-amount all-ids)])) (def query-schema (mc/schema [:maybe [:map {:date-range [:date-range :start-date :end-date]} [:sort {:optional true} [:maybe [:any]]] [:per-page {:optional true :default 25} [:maybe :int]] [:start {:optional true :default 0} [:maybe :int]] [:amount-gte {:optional true} [:maybe :double]] [:amount-lte {:optional true} [:maybe :double]] [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] [:check-number {:optional true} [:maybe [:string {:decode/string strip}]]] [:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]] [:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]] [:exact-match-id {:optional true} [:maybe entity-id]] [:all-selected {:optional true :default nil} [:maybe :boolean]] [:selected {:optional true :default nil} [:maybe [:vector {:coerce? true} entity-id]]] [:start-date {:optional true} [:maybe clj-date-schema]] [:end-date {:optional true} [:maybe clj-date-schema]]]])) (defn selected->ids [request params] (let [all-selected (:all-selected params) selected (:selected params) ids (cond all-selected (:ids (fetch-ids (dc/db conn) (-> request (assoc :query-params params) (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) :else selected) ids (->> (dc/q '[:find ?i :in $ [?i ...] :where [?i :invoice/import-status :import-status/pending]] (dc/db conn) ids) (map first))] ids)) (def upload-schema [:map [:force-client {:optional true} [:maybe entity-id]] [:force-vendor {:optional true} [:maybe entity-id]] [:force-chatgpt {:optional true :default false} [:maybe [ :boolean {:decode/string {:enter #(if (= % "on") true (boolean %))}}]]] [:force-location {:optional true} [:maybe [:string {:decode/string strip :min 2 :max 2}]]]]) (defn upload-form [{:keys [form-params form-errors] :as request}] (com/content-card {} [:div.px-4.py-3.space-y-4 [:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"] #_[:div.flex.gap-2.items-baseline "Trouble with the new upload experience?" [:a {:href (bidi/path-for client-routes/routes :import-invoices)} (com/pill {:color :secondary} "Go back to previous version")]]] [:div#page-notification.notification.block {:style {:display "none"}}] [:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import-file) :hx-encoding "multipart/form-data" :hx-target "#page-notification" :hx-swap "outerHTML" :id "upload"} (fc/start-form form-params form-errors [:div.flex.gap-4.items-center (fc/with-field :force-client (com/validated-field {:label "Force client" :errors (fc/field-errors)} [:div.w-96 (com/typeahead {:name (fc/field-name) :error? (fc/error?) :class "w-96" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :company-search) :value (fc/field-value) :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})])) (fc/with-field :force-location (com/validated-field {:label "Force location" :errors (fc/field-errors)} (com/text-input {:name (fc/field-name) :value (fc/field-value) :size 2}))) (fc/with-field :force-vendor (com/validated-field {:label "Force vendor" :errors (fc/field-errors)} [:div.w-96 (com/typeahead {:name (fc/field-name) :error? (fc/error?) :class "w-96" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :vendor-search) :value (fc/field-value) :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})])) (fc/with-field :force-chatgpt (com/validated-field { :errors (fc/field-errors) :label " "} (com/checkbox {:name (fc/field-name) :error? (fc/error?) } "Only use ChatGPT")))]) [:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative { :x-data (hx/json {"files" nil "hovering" false}) ":class" "{'bg-blue-100': !hovering, 'border-blue-300': !hovering, 'text-blue-700': !hovering, 'bg-green-100': hovering, 'border-green-300': hovering, 'text-green-700': hovering }" :x-ref "box"} [:input {:type "file" :name "file" :multiple "multiple" :class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0", :x-on:change "files = $event.target.files; console.log($event.target.files);", :x-on:dragover "hovering = true", :x-on:dragleave "hovering = false", :x-on:drop "hovering = false"}] [:div.flex.flex-col.space-2 [:div [:ul {:x-show "files != null"} [:template {:x-for "f in files" } [:li (com/pill {:color :primary :x-text "f.name"}) ] ]]] [:div.htmx-indicator-hidden "Drop files to upload here"]]] (com/button {:color :primary :class "w-32 mt-3"} "Upload")]])) (def grid-page (helper/build {:id "entity-table" :nav com/main-aside-nav :check-boxes? true :page-specific-nav filters :above-grid upload-form :fetch-page fetch-page :oob-render (fn [request] [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true) (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) :query-schema query-schema :action-buttons (fn [request] (let [[_ _ outstanding total] (:page-results request)] [ (when (can? (:identity request) {:subject :invoice :activity :import}) (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes ::route/bulk-approve)) "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" "hx-include" "#invoice-filters" :color :primary ":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true " } "Approve selected")) (when (can? (:identity request) {:subject :invoice :activity :import}) (com/button {:hx-delete (str (bidi/path-for ssr-routes/only-routes ::route/bulk-disapprove)) "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" "x-init" "" "hx-include" "#invoice-filters" ":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true " :color :red} "Disapprove selected"))])) :row-buttons (fn [request entity] [(when (and (= :import-status/pending (:invoice/import-status entity)) (can? (:identity request) {:subject :invoice :activity :import})) (com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes ::route/disapprove :db/id (:db/id entity)) :hx-confirm "Are you sure you want to remove this invoice?"} svg/thumbs-down)) (when (and (= :import-status/pending (:invoice/import-status entity)) (can? (:identity request) {:subject :invoice :activity :import})) (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes ::route/approve :db/id (:db/id entity))} svg/thumbs-up))]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} "Invoices"]] :title (fn [r] (str (some-> r :route-params :status name str/capitalize (str " ")) "Invoices")) :entity-name "invoices" :route ::route/import-table :headers [{:key "client" :name "Client" :sort-key "client" :hide? (fn [args] (and (= (count (:clients args)) 1) (= 1 (count (:client/locations (:client args)))))) :render (fn [x] [:div.flex.items-center.gap-2 (-> x :invoice/client :client/name) (com/pill {:color :primary} (-> x :invoice/location))])} {:key "uploader" :name "Uploaded by" :sort-key "uploader" :render #(-> % :invoice/uploader :user/name)} {:key "vendor" :name "Vendor" :sort-key "vendor" :render #(-> % :invoice/vendor :vendor/name)} {:key "invoice-number" :name "Invoice number" :sort-key "invoice-number" :render :invoice/invoice-number} {:key "date" :sort-key "date" :name "Date" :show-starting "lg" :render (fn [{:invoice/keys [date]}] (some-> date (atime/unparse-local atime/normal-date)))} {:key "outstanding" :name "Outstanding" :sort-key "outstanding-balance" :class "text-right" :render (fn [{:invoice/keys [outstanding-balance total]}] [:div (some->> outstanding-balance (format "$%,.2f")) (when-not (dollars= outstanding-balance total) [:div.text-xs.text-gray-400 (format "of $%,.2f" total)])])}]})) (def row* (partial helper/row* grid-page)) (defn disapprove [{invoice :entity :as request identity :identity}] (when-not (= :import-status/pending (:invoice/import-status invoice)) (throw (ex-info (str "Cannot disapprove an invoice if it is not pending." (:invoice/import-status invoice)) {:type :notification}))) (exception->notification #(assert-can-see-client identity (:db/id (:invoice/client invoice)))) (audit-transact [[:db/retractEntity (:db/id invoice)]] identity) (html-response (row* (:identity request) invoice {:class "live-removed"}) :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))})) (defn approve [{invoice :entity :as request identity :identity}] (when-not (= :import-status/pending (:invoice/import-status invoice)) (throw (ex-info (str "Cannot approve an invoice if it is not pending." (:invoice/import-status invoice)) {:type :notification}))) (exception->notification #(do (assert-can-see-client identity (:db/id (:invoice/client invoice))) (assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date)))) (audit-transact [ [:upsert-invoice {:db/id (:db/id invoice) :invoice/import-status :import-status/imported}]] identity) (html-response (row* (:identity request) invoice {:class "live-added"}) :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))})) (defn bulk-disapprove [request] (let [ids (selected->ids request (:form-params request)) updates (map (fn [i] [:db/retractEntity i]) ids) ] (audit-transact updates (:identity request) ) (html-response [:div] :headers {"hx-trigger" (hx/json { :notification (format "Successfully disapproved %d invoices." (count ids)) :invalidated "invalidated"})}))) (defn bulk-approve [request] (let [ids (selected->ids request (:form-params request))] (exception->notification #(doseq [i ids :let [invoice (dc/pull (dc/db conn) '[{:invoice/client [:db/id]} :invoice/date] i)]] (assert-can-see-client (:identity request) (-> invoice :invoice/client :db/id)) (assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date)))) (let [transactions (map (fn [i] [:upsert-invoice {:db/id i :invoice/import-status :import-status/imported}]) ids)] (audit-transact transactions (:identity request))) (html-response [:div] :headers {"hx-trigger" (hx/json { :notification (format "Successfully approved %d invoices." (count ids)) :invalidated "invalidated"})}))) #_(defn upload-invoices [{{files :file files-2 "file" client :client client-2 "client" location :location location-2 "location" vendor :vendor vendor-2 "vendor"} :params user :identity}] (let [files (or files files-2) client (or client client-2) location (or location location-2) vendor (some-> (or vendor vendor-2) (Long/parseLong)) ] )) (defn match-vendor [vendor-code forced-vendor vendor-search] (when (and (not forced-vendor) (str/blank? vendor-code)) (if vendor-search (throw (ex-info (format "No vendor found. Searched for '%s'. Please supply an forced vendor." vendor-search) {:vendor-code vendor-code})) (throw (ex-info (str "No vendor found. Please supply an forced vendor.") {:vendor-code vendor-code})))) (let [vendor-id (or forced-vendor (->> (dc/q {:find ['?vendor] :in ['$ '?vendor-name] :where ['[?vendor :vendor/name ?vendor-name]]} (dc/db conn) vendor-code) first first))] (when-not vendor-id (throw (ex-info (str "Vendor matching name \"" vendor-code "\" not found.") {:vendor-code vendor-code}))) (if-let [matching-vendor (->> (dc/q {:find [(list 'pull '?vendor-id d-vendors/default-read)] :in ['$ '?vendor-id]} (dc/db conn) vendor-id) first first)] matching-vendor (throw (ex-info (str "No vendor with the name " vendor-code " was found.") {:vendor-code vendor-code}))))) (defn import->invoice [{:keys [invoice-number source-url customer-identifier account-number total date vendor-code text full-text client-override vendor-search vendor-override location-override import-status]} user] (when-not total (throw (Exception. "Couldn't parse total from file."))) (when-not date (throw (Exception. "Couldn't parse date from file."))) (let [matching-client (cond client-override client-override account-number (:db/id (d-clients/exact-match account-number)) customer-identifier (:db/id (d-clients/best-match customer-identifier))) _ (alog/info ::client-matched :account-number account-number :customer-identifier customer-identifier :client-override client-override :matching (when matching-client (dc/pull (dc/db conn) [:client/name :client/code] matching-client))) matching-vendor (match-vendor vendor-code vendor-override vendor-search) matching-location (or (when-not (str/blank? location-override) location-override) (parse/best-location-match (dc/pull (dc/db conn) [{:client/location-matches [:location-match/location :location-match/matches]} :client/default-location :client/locations] matching-client) text full-text))] #:invoice {:db/id (random-tempid) :invoice/uploader (-> user :db/id) :invoice/client matching-client :invoice/client-identifier (or account-number customer-identifier) :invoice/vendor (:db/id matching-vendor) :invoice/source-url source-url :invoice/invoice-number invoice-number :invoice/total (Double/parseDouble total) :invoice/date (to-date date) :invoice/location matching-location :invoice/import-status (or import-status :import-status/pending) :invoice/outstanding-balance (Double/parseDouble total) :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)))) invoice) (defn admin-only-if-multiple-clients [is] (let [client-count (->> is (map :invoice/client) set count)] (map #(assoc % :invoice/source-url-admin-only (boolean (> client-count 1))) is))) (defn import-uploaded-invoice [user imports] (alog/info ::importing-uploaded :count (count imports) :bc (or user "NOO")) (let [potential-invoices (->> imports (map #(import->invoice % user)) (map #(validate-invoice % user)) admin-only-if-multiple-clients (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 :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))) (defn import-internal [tempfile filename force-client force-location force-vendor force-chatgpt identity] (mu/with-context {:parsing-file filename} (try (let [extension (last (str/split (.getName (io/file filename)) #"\.")) s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension) _ (s3/put-object (:data-bucket env) s3-location (io/input-stream tempfile) {:content-type (if (= "csv" extension) "text/csv" "application/pdf") :content-length (.length tempfile)}) imports (->> (if force-chatgpt (parse/glimpse2 (.getPath tempfile)) (parse/parse-file (.getPath tempfile) filename :allow-glimpse? true)) (map #(assoc % :client-override force-client :location-override force-location :vendor-override force-vendor :source-url (str "https://" (:data-bucket env) "/" s3-location))))] (try (import-uploaded-invoice identity imports) (catch Exception e (alog/warn ::couldnt-import-upload :error e :template (:template ( first imports))) (throw (ex-info (ex-message e) {:template (:template ( first imports)) :sample (first imports)} e)))) imports) (catch Exception e (alog/warn ::couldnt-import-upload :error e) (throw e))))) (defn import-file [request] #_(html-response [:div]) (let [{:keys [file force-client force-vendor force-location force-chatgpt]} (:multipart-params request) file (if (vector? file) file [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"} [: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] (clojure.pprint/pprint (:multipart-params request)) (handler request ))) (def key->handler (apply-middleware-to-all-handlers {::route/import-page (-> (helper/page-route grid-page) (wrap-implied-route-param :status nil)) ::route/import-table (-> (helper/table-route grid-page) (wrap-implied-route-param :status nil)) ::route/disapprove (-> disapprove (wrap-entity [:route-params :db/id] default-read) (wrap-schema-enforce :route-params [:map [:db/id entity-id]])) ::route/approve (-> approve (wrap-entity [:route-params :db/id] default-read) (wrap-schema-enforce :route-params [:map [:db/id entity-id]])) ::route/bulk-disapprove (-> bulk-disapprove (wrap-schema-enforce :form-schema query-schema)) ::route/bulk-approve (-> bulk-approve (wrap-schema-enforce :form-schema query-schema)) ::route/import-file (-> import-file (wrap-schema-enforce :multipart-schema upload-schema))} (fn [a] (-> a (wrap-must {:subject :invoice :activity :import})))))