diff --git a/e2e/bulk-code-transactions.spec.ts b/e2e/bulk-code-transactions.spec.ts index 90dec175..4c3bf9e9 100644 --- a/e2e/bulk-code-transactions.spec.ts +++ b/e2e/bulk-code-transactions.spec.ts @@ -375,7 +375,6 @@ test.describe('Bulk Code Transactions - Account Distribution', () => { await addNewAccount(page); await selectAccountFromTypeahead(page, 0, 'test-account'); - // "Shared" should be valid for accounts without fixed location await setAccountLocation(page, 0, 'Shared'); await setAccountPercentage(page, 0, '100'); @@ -385,3 +384,60 @@ test.describe('Bulk Code Transactions - Account Distribution', () => { await page.waitForSelector('table tbody tr'); }); }); + +test.describe('Bulk Code Transactions - Vendor Pre-population', () => { + test('should pre-populate default account when vendor is selected', async ({ page }) => { + await navigateToTransactions(page); + await selectTransactionByIndex(page, 0); + await openBulkCodeModal(page); + + // Select vendor (test vendor has default-account set to test-account) + const testInfo = await getTestInfo(page); + const vendorId = testInfo.accounts.vendor; + + // The vendor typeahead dispatches change from its parent div + // We need to set the hidden input and dispatch change on the container + const vendorContainer = page.locator('div[hx-post*="vendor-changed"]').first(); + const vendorHidden = vendorContainer.locator('input[type="hidden"]').first(); + + await vendorHidden.evaluate((el: HTMLInputElement, value: string) => { + const newInput = document.createElement('input'); + newInput.type = 'hidden'; + newInput.name = el.name; + newInput.value = value; + el.parentNode.replaceChild(newInput, el); + }, vendorId.toString()); + + // Dispatch change on the container to trigger HTMX + await vendorContainer.evaluate((el: HTMLElement) => { + el.dispatchEvent(new Event('change', { bubbles: true })); + }); + + // Wait for HTMX response + await page.waitForResponse(response => response.url().includes('/vendor-changed') && response.status() === 200); + await page.waitForTimeout(500); + + // Account should be pre-populated - check for account row + const accountRows = page.locator('#account-entries tbody tr'); + const rowCount = await accountRows.count(); + + // Should have at least 1 account row (the default account) plus the new-row button + expect(rowCount).toBeGreaterThanOrEqual(2); + + // The account should have a hidden input with the test-account ID + const accountHidden = page.locator('input[type="hidden"][name*="[account]"]').first(); + const accountValue = await accountHidden.inputValue(); + expect(accountValue).toBe(testInfo.accounts['test-account'].toString()); + + // Percentage should be 100 + const percentageInput = page.locator('input[name*="percentage"]').first(); + const percentageValue = await percentageInput.inputValue(); + expect(percentageValue).toBe('100'); + + // Submit should succeed + await submitBulkCodeForm(page); + await closeBulkCodeModal(page); + + await page.waitForSelector('table tbody tr'); + }); +}); diff --git a/iol_ion/src/iol_ion/query.clj b/iol_ion/src/iol_ion/query.clj index 8873ff86..82d5b276 100644 --- a/iol_ion/src/iol_ion/query.clj +++ b/iol_ion/src/iol_ion/query.clj @@ -14,7 +14,7 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn dollars= [amt1 amt2] - (dollars-0? (- amt1 amt2) )) + (dollars-0? (- amt1 amt2))) (defn localize [d] (time/to-time-zone d (time/time-zone-for-id "America/Los_Angeles"))) @@ -22,7 +22,6 @@ (defn local-now [] (localize (time/now))) - (defn recent-date ([] (recent-date 90)) @@ -32,16 +31,16 @@ (def excel-formatter (f/with-zone (f/formatter "MM/dd/yyyy") (time/time-zone-for-id "America/Los_Angeles"))) (defn excel-date [d] (->> d - (coerce/to-date-time) - localize - (f/unparse excel-formatter ))) + (coerce/to-date-time) + localize + (f/unparse excel-formatter))) (def iso-formatter (f/with-zone (f/formatter "yyyy-MM-dd") (time/time-zone-for-id "America/Los_Angeles"))) (defn iso-date [d] (->> d - (coerce/to-date-time) - localize - (f/unparse iso-formatter ))) + (coerce/to-date-time) + localize + (f/unparse iso-formatter))) (defn sales-orders-in-range [db client start end] (let [end (or end #inst "2050-01-01")] @@ -53,9 +52,6 @@ [client start] [client end])))) - - - (defn can-see-client? [identity client] (when (not client) (println "WARNING - permission checking for null client")) @@ -63,11 +59,9 @@ ((set (map :db/id (:user/clients identity))) (:db/id client)) ((set (map :db/id (:user/clients identity))) client))) - (defn ->pattern [x] (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) - (defn dom [^java.util.Date x] (-> x (.toInstant) @@ -85,8 +79,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :sales-order/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-charges [db clients start end] @@ -94,8 +88,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :charge/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-sales-refunds [db clients start end] @@ -103,8 +97,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :sales-refund/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-expected-deposits [db clients start end] @@ -112,8 +106,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :expected-deposit/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-cash-drawer-shifts [db clients start end] @@ -121,8 +115,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :cash-drawer-shift/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-invoices [db clients start end] @@ -130,17 +124,17 @@ :let [c (entid db c)] r (seq (dc/index-range db :invoice/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-transactions [db clients start end] (for [c clients :let [c (entid db c)] r (seq (dc/index-range db - :transaction/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + :transaction/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-ledger [db clients start end] @@ -148,8 +142,8 @@ :let [c (entid db c)] r (seq (dc/index-range db :journal-entry/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn scan-payments [db clients start end] @@ -157,15 +151,14 @@ :let [c (entid db c)] r (seq (dc/index-range db :payment/client+date - [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] - [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [c (or start #inst "2001-01-01T08:00:00.000-00:00")] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00")]))] [(:e r) (first (:v r)) (second (:v r))])) (defn ident [x] (:db/ident x)) -(deftype Line [^Long id ^Long client-id ^Long account-id ^String location ^java.util.Date date ^Double debit ^Double credit ^Double running-balance] - ) +(deftype Line [^Long id ^Long client-id ^Long account-id ^String location ^java.util.Date date ^Double debit ^Double credit ^Double running-balance]) (defmethod print-method Line [entity writer] (.write writer (format "Line %d: client:%d account:%d location:%s date:%s" @@ -175,18 +168,16 @@ (.-location entity) (iso-date (.-date entity))))) - (defn ->line [{[current-client current-account current-location current-date debit credit running-balance] :v id :e}] - (Line. id current-client current-account current-location current-date debit credit running-balance) - ) + (Line. id current-client current-account current-location current-date debit credit running-balance)) + +(defn compare-account [^Line l1 ^Line l2] -(defn compare-account [^Line l1 ^Line l2] - (let [a (compare (.-date l1) (.-date l2))] (if (not= 0 a) - a + a (compare (.-id l1) (.-id l2))))) (defn account-sets [db client-id] @@ -194,7 +185,7 @@ (seq) (map ->line) (partition-by (fn set-partition [^Line l] - [(.-account-id l) (.-location l)]))) ] + [(.-account-id l) (.-location l)])))] (->> running-balance-set (sort compare-account)))) @@ -205,35 +196,35 @@ (take-while (fn until-date [^Line l] (let [^java.util.Date d (.-date l)] (<= (.compareTo ^java.util.Date d end) 0)))) - last) ] + last)] :when (and z (.-id z))] [(.-client-id z) (.-account-id z) (.-location z) (.-date z) (.-running-balance z)])) -#_(doseq [[ n] (dc/q '[:find ?cd :where [?c :client/code ?cd] [?c :client/groups "NTG"]] (dc/db auto-ap.datomic/conn))] - (println n) - (dc/q '[:find ?code ?name ?afc ?an ?l ?d2 ?balance - :in $ ?end ?group - :where - [(clj-time.coerce/to-date-time ?end) ?end2] - [(iol-ion.query/localize ?end2) ?end3] - [(clj-time.coerce/to-date ?end3) ?end4] - (or - [?c :client/groups ?group] - [?c :client/code ?group]) - [?c :client/name ?name] - [?c :client/code ?code] - [?c :client/bank-accounts ?b] - [(iol-ion.query/account-snapshot $ ?c ?end4) [?x ...]] - [(untuple ?x) [_ ?a ?l ?date ?balance]] - [(not= nil ?a)] - [(iol-ion.query/excel-date ?date) ?d2] - (or-join [?a ?afc ?an] - (and [?a :account/name ?an] - [?a :account/numeric-code ?afc]) - (and [?a :bank-account/name ?an] - [?a :bank-account/numeric-code ?afc]))] - (dc/db auto-ap.datomic/conn) - #inst "2024-10-10" n)) +#_(doseq [[n] (dc/q '[:find ?cd :where [?c :client/code ?cd] [?c :client/groups "NTG"]] (dc/db auto-ap.datomic/conn))] + (println n) + (dc/q '[:find ?code ?name ?afc ?an ?l ?d2 ?balance + :in $ ?end ?group + :where + [(clj-time.coerce/to-date-time ?end) ?end2] + [(iol-ion.query/localize ?end2) ?end3] + [(clj-time.coerce/to-date ?end3) ?end4] + (or + [?c :client/groups ?group] + [?c :client/code ?group]) + [?c :client/name ?name] + [?c :client/code ?code] + [?c :client/bank-accounts ?b] + [(iol-ion.query/account-snapshot $ ?c ?end4) [?x ...]] + [(untuple ?x) [_ ?a ?l ?date ?balance]] + [(not= nil ?a)] + [(iol-ion.query/excel-date ?date) ?d2] + (or-join [?a ?afc ?an] + (and [?a :account/name ?an] + [?a :account/numeric-code ?afc]) + (and [?a :bank-account/name ?an] + [?a :bank-account/numeric-code ?afc]))] + (dc/db auto-ap.datomic/conn) + #inst "2024-10-10" n)) (defn detailed-account-snapshot ([db client-id ^java.util.Date end] @@ -266,12 +257,11 @@ :credits 0.0 :current-balance 0.0})))] :when client-id] - (do + (do [client-id account-id location debits credits current-balance count sample])))) - -(comment - (->> +(comment + (->> (detailed-account-snapshot (dc/db auto-ap.datomic/conn) (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) [:client/code "NGOP"]) @@ -280,65 +270,65 @@ (into #{}) seq) -(account-snapshot (dc/db auto-ap.datomic/conn) - (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) - [:client/code "NGOP"]) - #inst "2022-01-01") + (account-snapshot (dc/db auto-ap.datomic/conn) + (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) + [:client/code "NGOP"]) + #inst "2022-01-01") -(def orig (->> [:client/code "NGOP"] - (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn)) - (account-sets (dc/db auto-ap.datomic/conn)) - (mapcat (fn [ls] - ls)) - (filter (fn [l] (nil? (.-location l)))) - (into #{}))) + (def orig (->> [:client/code "NGOP"] + (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn)) + (account-sets (dc/db auto-ap.datomic/conn)) + (mapcat (fn [ls] + ls)) + (filter (fn [l] (nil? (.-location l)))) + (into #{}))) -(.-location orig) + (.-location orig) -(def orig (into [] (take 5000 (mapcat (fn [ls] - (map #(.-id %) ls)) (account-sets (dc/db auto-ap.datomic/conn) - (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) - [:client/code "NGOP"])))))) + (def orig (into [] (take 5000 (mapcat (fn [ls] + (map #(.-id %) ls)) (account-sets (dc/db auto-ap.datomic/conn) + (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) + [:client/code "NGOP"])))))) + + (def n (into [] (take 5000 (mapcat (fn [ls] + (map #(.-id %) ls)) (account-sets (dc/db auto-ap.datomic/conn) + (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) + [:client/code "NGOP"])))))) - (def n (into [] (take 5000 (mapcat (fn [ls] - (map #(.-id %) ls)) (account-sets (dc/db auto-ap.datomic/conn) - (auto-ap.datomic/pull-id (dc/db auto-ap.datomic/conn) - [:client/code "NGOP"])))))) - (= orig n) -#_(seq (dc/q '[:find ?c ?a ?l ?date ?balance - :in $ - :where [?c :client/code "NGOP"] - [(iol-ion.query/account-snapshot $ ?c #inst "2023-01-01") [?x ...]] - [(untuple ?x) [_ ?a ?l ?date ?balance]]] - (dc/db auto-ap.datomic/conn))) + #_(seq (dc/q '[:find ?c ?a ?l ?date ?balance + :in $ + :where [?c :client/code "NGOP"] + [(iol-ion.query/account-snapshot $ ?c #inst "2023-01-01") [?x ...]] + [(untuple ?x) [_ ?a ?l ?date ?balance]]] + (dc/db auto-ap.datomic/conn))) -#_(->> (seq (dc/q '[:find ?code ?name ?afc ?an ?l ?d2 ?balance ?end4 - :in $ ?end ?group - :where - [(clj-time.coerce/to-date-time ?end) ?end2] - [(iol-ion.query/localize ?end2) ?end3] - [(clj-time.coerce/to-date ?end3) ?end4] - (or - [?c :client/groups ?group] - [?c :client/code ?group]) - [?c :client/name ?name] - [?c :client/code ?code] - [?c :client/bank-accounts ?b] - [(iol-ion.query/account-snapshot $ ?c ?end4) [?x ...]] - [(untuple ?x) [_ ?a ?l ?date ?balance]] - [(iol-ion.query/excel-date ?date) ?d2] - [(not= nil ?a)] - (or-join [?a ?afc ?an] - (and [?a :account/name ?an] - [?a :account/numeric-code ?afc]) - (and [?a :bank-account/name ?an] - [?a :bank-account/numeric-code ?afc]))] - (dc/db auto-ap.datomic/conn) - #inst "2024-09-23" - "NGKG")) - (filter (fn [[_ _ afc]] - (= 12990 afc))) - (map (fn [[_ _ _ _ _ _ a]] - (Math/round a))))) \ No newline at end of file + #_(->> (seq (dc/q '[:find ?code ?name ?afc ?an ?l ?d2 ?balance ?end4 + :in $ ?end ?group + :where + [(clj-time.coerce/to-date-time ?end) ?end2] + [(iol-ion.query/localize ?end2) ?end3] + [(clj-time.coerce/to-date ?end3) ?end4] + (or + [?c :client/groups ?group] + [?c :client/code ?group]) + [?c :client/name ?name] + [?c :client/code ?code] + [?c :client/bank-accounts ?b] + [(iol-ion.query/account-snapshot $ ?c ?end4) [?x ...]] + [(untuple ?x) [_ ?a ?l ?date ?balance]] + [(iol-ion.query/excel-date ?date) ?d2] + [(not= nil ?a)] + (or-join [?a ?afc ?an] + (and [?a :account/name ?an] + [?a :account/numeric-code ?afc]) + (and [?a :bank-account/name ?an] + [?a :bank-account/numeric-code ?afc]))] + (dc/db auto-ap.datomic/conn) + #inst "2024-09-23" + "NGKG")) + (filter (fn [[_ _ afc]] + (= 12990 afc))) + (map (fn [[_ _ _ _ _ _ a]] + (Math/round a))))) \ No newline at end of file diff --git a/iol_ion/src/iol_ion/tx.clj b/iol_ion/src/iol_ion/tx.clj index 1d90c284..40f4f421 100644 --- a/iol_ion/src/iol_ion/tx.clj +++ b/iol_ion/src/iol_ion/tx.clj @@ -11,7 +11,6 @@ (def pull-many iol-ion.utils/pull-many) (def remove-nils iol-ion.utils/remove-nils) - ;; TODO expected-deposit ledger entry #_(defmethod entity-change->ledger :expected-deposit [db [type id]] @@ -33,9 +32,6 @@ :location "A" :account :account/ccp}]})) - - - (defn regenerate-literals [] (require 'com.github.ivarref.gen-fn) (spit diff --git a/iol_ion/src/iol_ion/tx/propose_invoice.clj b/iol_ion/src/iol_ion/tx/propose_invoice.clj index 53481c58..21d74782 100644 --- a/iol_ion/src/iol_ion/tx/propose_invoice.clj +++ b/iol_ion/src/iol_ion/tx/propose_invoice.clj @@ -13,11 +13,11 @@ (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice)))) - [ locked-until] (first (dc/q '[:find ?locked-until - :in $ ?c - :where [?c :client/locked-until ?locked-until]] - db - (:invoice/client invoice))) + [locked-until] (first (dc/q '[:find ?locked-until + :in $ ?c + :where [?c :client/locked-until ?locked-until]] + db + (:invoice/client invoice))) is-locked? (cond (not locked-until) false (not (:invoice/date invoice)) true diff --git a/iol_ion/src/iol_ion/tx/reset_rels.clj b/iol_ion/src/iol_ion/tx/reset_rels.clj index 1110fcef..2762562b 100644 --- a/iol_ion/src/iol_ion/tx/reset_rels.clj +++ b/iol_ion/src/iol_ion/tx/reset_rels.clj @@ -4,11 +4,11 @@ (defn reset-rels [db e a vs] (assert (every? :db/id vs) (format "In order to reset attribute %s, every value must have :db/id" a)) (let [ids (when-not (string? e) - (->> (dc/q '[:find ?z - :in $ ?e ?a - :where [?e ?a ?z]] - db e a) - (map first))) + (->> (dc/q '[:find ?z + :in $ ?e ?a + :where [?e ?a ?z]] + db e a) + (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) @@ -16,6 +16,6 @@ (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] - [:db/retract e a i ])) retract-ids)) + [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs))))) diff --git a/iol_ion/src/iol_ion/tx/reset_scalars.clj b/iol_ion/src/iol_ion/tx/reset_scalars.clj index bcd79e5f..c48e4ad8 100644 --- a/iol_ion/src/iol_ion/tx/reset_scalars.clj +++ b/iol_ion/src/iol_ion/tx/reset_scalars.clj @@ -2,7 +2,7 @@ (:require [datomic.api :as dc])) (defn reset-scalars [db e a vs] - + (let [extant (when-not (string? e) (->> (dc/q '[:find ?z :in $ ?e ?a @@ -12,5 +12,5 @@ retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] - (into (map (fn [i] [:db/retract e a i ]) retracts)) + (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new))))) diff --git a/iol_ion/src/iol_ion/tx/upsert_entity.clj b/iol_ion/src/iol_ion/tx/upsert_entity.clj index e3b2f8f3..3ef5801d 100644 --- a/iol_ion/src/iol_ion/tx/upsert_entity.clj +++ b/iol_ion/src/iol_ion/tx/upsert_entity.clj @@ -5,7 +5,6 @@ ) (:import [java.util UUID])) - (defn -random-tempid [] (str (UUID/randomUUID))) @@ -36,7 +35,6 @@ ;; :else ;; v)) - (defn upsert-entity [db entity] (when-not (or (:db/id entity) (:db/ident entity)) @@ -90,7 +88,7 @@ ops ;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq) - + (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) diff --git a/iol_ion/src/iol_ion/tx/upsert_invoice.clj b/iol_ion/src/iol_ion/tx/upsert_invoice.clj index cbbae5b1..4fe1ba68 100644 --- a/iol_ion/src/iol_ion/tx/upsert_invoice.clj +++ b/iol_ion/src/iol_ion/tx/upsert_invoice.clj @@ -4,13 +4,12 @@ (defn -remove-nils [m] (let [result (reduce-kv - (fn [m k v] - (if (not (nil? v)) - (assoc m k v) - m - )) - {} - m)] + (fn [m k v] + (if (not (nil? v)) + (assoc m k v) + m)) + {} + m)] (if (seq result) result nil))) @@ -33,41 +32,38 @@ invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or - (not (:invoice/total entity)) - (= true (:invoice/exclude-from-ledger entity)) - (= :import-status/pending (:db/ident (:invoice/import-status entity))) - (= :invoice-status/voided (:db/ident (:invoice/status entity))) - (< -0.001 (:invoice/total entity) 0.001)) - - (-remove-nils - {:journal-entry/source "invoice" - :journal-entry/client (:db/id (:invoice/client entity)) - :journal-entry/date (:invoice/date entity) - :journal-entry/original-entity raw-invoice-id - :journal-entry/vendor (:db/id (:invoice/vendor entity)) - :journal-entry/amount (Math/abs (:invoice/total entity)) + (not (:invoice/total entity)) + (= true (:invoice/exclude-from-ledger entity)) + (= :import-status/pending (:db/ident (:invoice/import-status entity))) + (= :invoice-status/voided (:db/ident (:invoice/status entity))) + (< -0.001 (:invoice/total entity) 0.001)) - :journal-entry/line-items (into [(cond-> {:db/id (str raw-invoice-id "-" 0) - :journal-entry-line/account :account/accounts-payable - :journal-entry-line/location "A" - } - credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) - (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] - (map-indexed (fn [i ea] - (cond-> - {:db/id (str raw-invoice-id "-" (inc i)) - :journal-entry-line/account (:db/id (:invoice-expense-account/account ea)) - :journal-entry-line/location (or (:invoice-expense-account/location ea) "HQ") - } - credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) - (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) - (:invoice/expense-accounts entity))) - :journal-entry/cleared (and (< (:invoice/outstanding-balance entity) 0.01) - (every? #(= :payment-status/cleared (:payment/status %)) (:invoice/payments entity)) - )})))) + (-remove-nils + {:journal-entry/source "invoice" + :journal-entry/client (:db/id (:invoice/client entity)) + :journal-entry/date (:invoice/date entity) + :journal-entry/original-entity raw-invoice-id + :journal-entry/vendor (:db/id (:invoice/vendor entity)) + :journal-entry/amount (Math/abs (:invoice/total entity)) + + :journal-entry/line-items (into [(cond-> {:db/id (str raw-invoice-id "-" 0) + :journal-entry-line/account :account/accounts-payable + :journal-entry-line/location "A"} + credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) + (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] + (map-indexed (fn [i ea] + (cond-> + {:db/id (str raw-invoice-id "-" (inc i)) + :journal-entry-line/account (:db/id (:invoice-expense-account/account ea)) + :journal-entry-line/location (or (:invoice-expense-account/location ea) "HQ")} + credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) + (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) + (:invoice/expense-accounts entity))) + :journal-entry/cleared (and (< (:invoice/outstanding-balance entity) 0.01) + (every? #(= :payment-status/cleared (:payment/status %)) (:invoice/payments entity)))})))) (defn current-date [db] - (let [ last-tx (dc/t->tx (dc/basis-t db)) + (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q '[:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]] db @@ -80,15 +76,15 @@ invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) - invoice-id - (:db/id invoice)) - client-id (-> (dc/pull (:db-after with-invoice) - [{:invoice/client [:db/id]}] + invoice-id + (:db/id invoice)) + client-id (-> (dc/pull (:db-after with-invoice) + [{:invoice/client [:db/id]}] invoice-id) - :invoice/client + :invoice/client :db/id)] (into upserted-entity - (if journal-entry + (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]] {:db/id client-id diff --git a/iol_ion/src/iol_ion/tx/upsert_ledger.clj b/iol_ion/src/iol_ion/tx/upsert_ledger.clj index 9fd95220..0238eaaa 100644 --- a/iol_ion/src/iol_ion/tx/upsert_ledger.clj +++ b/iol_ion/src/iol_ion/tx/upsert_ledger.clj @@ -10,7 +10,7 @@ next-jel (->> (dc/index-pull db {:index :avet :selector [:db/id :journal-entry-line/client+account+location+date] :start [:journal-entry-line/client+account+location+date - (:journal-entry-line/client+account+location+date jel) + (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and @@ -24,9 +24,8 @@ (def extant-read '[:db/id :journal-entry/date :journal-entry/client {:journal-entry/line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}]) - (defn current-date [db] - (let [ last-tx (dc/t->tx (dc/basis-t db)) + (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q '[:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]] db @@ -51,7 +50,7 @@ (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] - (dc/pull db extant-read [:journal-entry/external-id external-id]))) ] + (dc/pull db extant-read [:journal-entry/external-id external-id])))] (cond-> [[:upsert-entity (into (-> ledger-entry @@ -59,11 +58,11 @@ (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) - (update :journal-entry/line-items + (update :journal-entry/line-items (fn [lis] (mapv #(-> % (assoc :journal-entry-line/date (:journal-entry/date ledger-entry)) - (assoc :journal-entry-line/client (:journal-entry/client ledger-entry))) + (assoc :journal-entry-line/client (:journal-entry/client ledger-entry))) lis)))))] {:db/id (:journal-entry/client ledger-entry) :client/ledger-last-change (current-date db)}]))) diff --git a/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj index 3db00f80..93b8b60a 100644 --- a/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj +++ b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj @@ -27,11 +27,10 @@ (get m :ledger-side/debit) (assoc :journal-entry-line/debit (get m :ledger-side/debit)) (get m :ledger-side/credit) (assoc :journal-entry-line/credit (get m :ledger-side/credit)))) aggregated) - + total-debits (reduce + 0.0 (map #(get % :ledger-side/debit 0.0) aggregated)) total-credits (reduce + 0.0 (map #(get % :ledger-side/credit 0.0) aggregated)) -_ (clojure.pprint/pprint [total-debits total-credits]) - ] + _ (clojure.pprint/pprint [total-debits total-credits])] (when (and (seq line-items) (= (Math/round (* 1000 total-debits)) (Math/round (* 1000 total-credits)))) @@ -60,11 +59,10 @@ _ (clojure.pprint/pprint [total-debits total-credits]) journal-entry (summary->journal-entry db-after summary-id)] upserted-summary #_(into upserted-summary - (if journal-entry - [[:upsert-ledger journal-entry]] - (concat - [[:db/retractEntity [:journal-entry/original-entity (:db/id summary)]]] + (if journal-entry + [[:upsert-ledger journal-entry]] + (concat + [[:db/retractEntity [:journal-entry/original-entity (:db/id summary)]]] - - (when client-id [{:db/id client-id - :client/ledger-last-change (current-date db)}])))))) + (when client-id [{:db/id client-id + :client/ledger-last-change (current-date db)}])))))) diff --git a/iol_ion/src/iol_ion/tx/upsert_transaction.clj b/iol_ion/src/iol_ion/tx/upsert_transaction.clj index 80c3559a..525df1a3 100644 --- a/iol_ion/src/iol_ion/tx/upsert_transaction.clj +++ b/iol_ion/src/iol_ion/tx/upsert_transaction.clj @@ -81,73 +81,70 @@ [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]])))) - #_(comment ;; If transactions are failing, it is likely that there are multiple bank accounts linked ;; to yodlee or plaid. here is how i debugged - (upsert-transaction (dc/db auto-ap.datomic/conn) {:transaction/matched-rule 17592233159891, - :db/id "34411061-4656-4e77-8cc0-2f2769b4324c", - :transaction/status "POSTED", - :transaction/description-original "Rotten Robbie #03", - :transaction/approval-status {:db/id 17592231963877, - :db/ident :transaction-approval-status/approved}, - :transaction/plaid-merchant {:db/id "223ceae4-d9e7-4e7f-92be-4fb00676088b", - :plaid-merchant/name "Rotten Robbie"}, - :transaction/bank-account 17592232681223, - :transaction/vendor 17592232627053, - :transaction/date #inst "2024-02-24T08:00:00Z", - :transaction/client 17592232577980, - :transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996", - :transaction/amount -84.43, - :transaction/accounts [{:db/id "cad8463f-2dfe-47dc-ab17-831e87a633d5", - :transaction-account/account 17592231963549, - :transaction-account/location "CB", - :transaction-account/amount 84.43}], - :transaction/raw-id "gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP"}) + (upsert-transaction (dc/db auto-ap.datomic/conn) {:transaction/matched-rule 17592233159891, + :db/id "34411061-4656-4e77-8cc0-2f2769b4324c", + :transaction/status "POSTED", + :transaction/description-original "Rotten Robbie #03", + :transaction/approval-status {:db/id 17592231963877, + :db/ident :transaction-approval-status/approved}, + :transaction/plaid-merchant {:db/id "223ceae4-d9e7-4e7f-92be-4fb00676088b", + :plaid-merchant/name "Rotten Robbie"}, + :transaction/bank-account 17592232681223, + :transaction/vendor 17592232627053, + :transaction/date #inst "2024-02-24T08:00:00Z", + :transaction/client 17592232577980, + :transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996", + :transaction/amount -84.43, + :transaction/accounts [{:db/id "cad8463f-2dfe-47dc-ab17-831e87a633d5", + :transaction-account/account 17592231963549, + :transaction-account/location "CB", + :transaction-account/amount 84.43}], + :transaction/raw-id "gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP"}) - ["upsert-transaction"] - (user/init-repl) + ["upsert-transaction"] + (user/init-repl) - (def my-transaction {:transaction/bank-account 17592232681223, - :transaction/date #inst "2024-02-24T08:00:00.000-00:00", - :transaction/matched-rule 17592233159891, - :transaction/client 17592232577980, - :transaction/status "POSTED", - :transaction/plaid-merchant - {:plaid-merchant/name "Rotten Robbie", :db/id "b2776792-9e2b-46e8-a9c8-bf80abea359e"}, - :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc", - :transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996", - :transaction/description-original "Rotten Robbie #03", - :transaction/approval-status {:db/id 17592231963877, :db/ident :transaction-approval-status/approved}, :transaction/amount -84.43, - :transaction/accounts [{:db/id "c402c7b3-c11b-484b-b670-bd48f79a3e5f", :transaction-account/account 17592231963549, :transaction-account/amount 84.43, :transaction-account/location "CB"}], - :transaction/raw-id "gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP", - :transaction/vendor 17592232627053}) + (def my-transaction {:transaction/bank-account 17592232681223, + :transaction/date #inst "2024-02-24T08:00:00.000-00:00", + :transaction/matched-rule 17592233159891, + :transaction/client 17592232577980, + :transaction/status "POSTED", + :transaction/plaid-merchant + {:plaid-merchant/name "Rotten Robbie", :db/id "b2776792-9e2b-46e8-a9c8-bf80abea359e"}, + :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc", + :transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996", + :transaction/description-original "Rotten Robbie #03", + :transaction/approval-status {:db/id 17592231963877, :db/ident :transaction-approval-status/approved}, :transaction/amount -84.43, + :transaction/accounts [{:db/id "c402c7b3-c11b-484b-b670-bd48f79a3e5f", :transaction-account/account 17592231963549, :transaction-account/amount 84.43, :transaction-account/location "CB"}], + :transaction/raw-id "gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP", + :transaction/vendor 17592232627053}) - (def my-journal {:journal-entry/alternate-description "Rotten Robbie #03", - :journal-entry/date #inst "2024-02-24T08:00:00.000-00:00", - :journal-entry/original-entity "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc", - :journal-entry/client 17592232577980, - :journal-entry/line-items [{:journal-entry-line/credit 84.43, :journal-entry-line/account 17592232681223, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-0", :journal-entry-line/location "A"} - {:journal-entry-line/account 17592231963549, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-1", :journal-entry-line/debit 84.43, :journal-entry-line/location "CB"} - {:journal-entry-line/account 17592231963549, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-2", :journal-entry-line/debit 84.43, :journal-entry-line/location "CB"}], - :journal-entry/source "transaction", - :journal-entry/cleared true, - :journal-entry/amount 84.43, - :journal-entry/vendor 17592232627053}) - (dc/pull (dc/db auto-ap.datomic/conn) '[*] [:transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996"]) - wl - (user/init-repl) + (def my-journal {:journal-entry/alternate-description "Rotten Robbie #03", + :journal-entry/date #inst "2024-02-24T08:00:00.000-00:00", + :journal-entry/original-entity "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc", + :journal-entry/client 17592232577980, + :journal-entry/line-items [{:journal-entry-line/credit 84.43, :journal-entry-line/account 17592232681223, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-0", :journal-entry-line/location "A"} + {:journal-entry-line/account 17592231963549, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-1", :journal-entry-line/debit 84.43, :journal-entry-line/location "CB"} + {:journal-entry-line/account 17592231963549, :db/id "ac2efd80-bb03-48b2-b0d0-6b47a5c119dc-2", :journal-entry-line/debit 84.43, :journal-entry-line/location "CB"}], + :journal-entry/source "transaction", + :journal-entry/cleared true, + :journal-entry/amount 84.43, + :journal-entry/vendor 17592232627053}) + (dc/pull (dc/db auto-ap.datomic/conn) '[*] [:transaction/id "11a4a13e713d63f476009027e9a53e217e13d0192a37df8ab96c0eed4bdbe996"]) + wl + (user/init-repl) - (or (when-let [original-entity (:journal-entry/original-entity my-journal)] - (dc/pull (dc/db auto-ap.datomic/conn) iol-ion.tx.upsert-ledger/extant-read [:journal-entry/original-entity original-entity])) - (when-let [external-id (:journal-entry/external-id my-journal)] - (dc/pull (dc/db auto-ap.datomic/conn) iol-ion.tx.upsert-ledger/extant-read [:journal-entry/external-id external-id]))) + (or (when-let [original-entity (:journal-entry/original-entity my-journal)] + (dc/pull (dc/db auto-ap.datomic/conn) iol-ion.tx.upsert-ledger/extant-read [:journal-entry/original-entity original-entity])) + (when-let [external-id (:journal-entry/external-id my-journal)] + (dc/pull (dc/db auto-ap.datomic/conn) iol-ion.tx.upsert-ledger/extant-read [:journal-entry/external-id external-id]))) - @(dc/transact auto-ap.datomic/conn [[:upsert-entity my-transaction] - [:upsert-ledger my-journal]]) + @(dc/transact auto-ap.datomic/conn [[:upsert-entity my-transaction] + [:upsert-ledger my-journal]]) - (auto-ap.datomic/pull-attr (dc/db auto-ap.datomic/conn) :bank-account/code 17592232681223) - (auto-ap.datomic/pull-attr (dc/db auto-ap.datomic/conn) :bank-account/code 17592232681228) - - ) \ No newline at end of file + (auto-ap.datomic/pull-attr (dc/db auto-ap.datomic/conn) :bank-account/code 17592232681223) + (auto-ap.datomic/pull-attr (dc/db auto-ap.datomic/conn) :bank-account/code 17592232681228)) \ No newline at end of file diff --git a/iol_ion/src/iol_ion/utils.clj b/iol_ion/src/iol_ion/utils.clj index 7d81eb66..b7e34813 100644 --- a/iol_ion/src/iol_ion/utils.clj +++ b/iol_ion/src/iol_ion/utils.clj @@ -10,11 +10,11 @@ (by f identity xs)) ([f fv xs] (reduce - #(assoc %1 (f %2) (fv %2)) - {} - xs))) + #(assoc %1 (f %2) (fv %2)) + {} + xs))) -(defn pull-many [db read ids ] +(defn pull-many [db read ids] (->> (dc/q '[:find (pull ?e r) :in $ [?e ...] r] db @@ -24,13 +24,12 @@ (defn remove-nils [m] (let [result (reduce-kv - (fn [m k v] - (if (not (nil? v)) - (assoc m k v) - m - )) - {} - m)] + (fn [m k v] + (if (not (nil? v)) + (assoc m k v) + m)) + {} + m)] (if (seq result) result nil))) diff --git a/resources/public/output.css b/resources/public/output.css index b35ba9f9..fc6d4583 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1 +1 @@ -/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.start-0{inset-inline-start:0}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[99\]{z-index:99}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.row-span-2{grid-row:span 2/span 2}.m-0{margin:0}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.me-2{-webkit-margin-end:.5rem;margin-inline-end:.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-8{margin-right:2rem}.ms-3{-webkit-margin-start:.75rem;margin-inline-start:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-0{margin-top:0}.box-content{box-sizing:initial}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-32{height:8rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[350px\]{height:350px}.h-\[49rem\]{height:49rem}.h-\[600px\]{height:600px}.h-\[70vh\]{height:70vh}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-0{max-height:0}.max-h-96{max-height:24rem}.max-h-\[300px\]{max-height:300px}.max-h-\[600px\]{max-height:600px}.max-h-\[700px\]{max-height:700px}.max-h-\[inherit\]{max-height:inherit}.max-h-full{max-height:100%}.max-h-screen{max-height:100vh}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-11{width:2.75rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-72{width:18rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[10em\]{width:10em}.w-\[20em\]{width:20em}.w-\[300px\]{width:300px}.w-\[30em\]{width:30em}.w-\[5em\]{width:5em}.w-\[600px\]{width:600px}.w-\[700px\]{width:700px}.w-\[748px\]{width:748px}.w-\[7em\]{width:7em}.w-\[850px\]{width:850px}.w-\[8em\]{width:8em}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-screen{width:100vw}.w-28{width:7rem}.min-w-full{min-width:100%}.min-w-0{min-width:0}.max-w-2xl{max-width:42rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[24em\]{max-width:24em}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-1\/2{flex-basis:50%}.\!translate-y-0{--tw-translate-y:0px!important}.\!translate-y-0,.\!translate-y-32{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.\!translate-y-32{--tw-translate-y:8rem!important}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.translate-y-0{--tw-translate-y:0px}.translate-y-0,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-95,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-none{transform:none}@keyframes gentleGrow{0%{transform:scale(1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:scale(1.1);animation-timing-function:cubic-bezier(0,0,.2,1)}to{transform:scale(1);animation-timing-function:cubic-bezier(.8,0,1,1)}}.animate-gg{animation:gentleGrow 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes shake{0%{transform:translateX(0)}12.5%{transform:translateX(-5px)}25%{transform:translateX(0)}37.5%{transform:translateX(5px)}50%{transform:translateX(0)}62.5%{transform:translateX(-5px)}75%{transform:translateX(5px)}87.5%{transform:translateX(5px)}to{transform:translateX(0)}}.animate-shake{animation:shake .5s ease-out 1}@keyframes slideUp{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.animate-slideUp{animation:slideUp .5s ease-out forwards}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-row{grid-auto-flow:row}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.justify-items-stretch{justify-items:stretch}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-center{align-self:center}.self-stretch{align-self:stretch}.justify-self-end{justify-self:end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-b-lg{border-bottom-right-radius:.5rem}.rounded-b-lg,.rounded-l-lg{border-bottom-left-radius:.5rem}.rounded-l-lg{border-top-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-green-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(97 145 37/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity))}.border-emerald-300{--tw-border-opacity:1;border-color:rgb(110 231 183/var(--tw-border-opacity))}.border-slate-300{--tw-border-opacity:1;border-color:rgb(203 213 225/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(255 154 154/var(--tw-border-opacity))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(180 198 252/var(--tw-border-opacity))}.\!bg-primary-200{--tw-bg-opacity:1!important;background-color:rgb(201 225 171/var(--tw-bg-opacity))!important}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(246 245 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(255 3 3/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(252 233 106/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(253 253 234/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity))}.bg-indigo-50\/40{background-color:#f0f5ff66}.\!bg-opacity-0{--tw-bg-opacity:0!important}.\!bg-opacity-100{--tw-bg-opacity:1!important}.\!bg-opacity-50{--tw-bg-opacity:0.5!important}.bg-opacity-50{--tw-bg-opacity:0.5}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pr-6{padding-right:1.5rem}.ps-10{-webkit-padding-start:2.5rem;padding-inline-start:2.5rem}.ps-3{-webkit-padding-start:.75rem;padding-inline-start:.75rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.pt-1{padding-top:.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[0\.6rem\]{font-size:.6rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(0 94 140/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-300{--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(24 36 9/var(--tw-text-opacity))}.text-purple-600{--tw-text-opacity:1;color:rgb(126 58 242/var(--tw-text-opacity))}.text-red-300{--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(153 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(159 88 10/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(99 49 18/var(--tw-text-opacity))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.\!opacity-0{opacity:0!important}.\!opacity-100{opacity:1!important}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.outline{outline-style:solid}.outline-0{outline-width:0}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-added .swipe-left-swap,.htmx-added.swipe-left-swap{opacity:1!important;--tw-scale-x:1!important;--tw-scale-y:1!important;--tw-translate-x:-50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.swipe-left-swap{opacity:1;--tw-scale-x:1;--tw-scale-y:1;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-settling.htmx-added .swipe-left-swap,.htmx-settling.htmx-added.swipe-left-swap{opacity:0!important;--tw-scale-x:.75!important;--tw-scale-y:.75!important;--tw-translate-x:50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.htmx-settling .slide-up-settle,.htmx-settling.slide-up-settle{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up-settle{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hidden .slide-up,.htmx-added .slide-up{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}.live-removed{animation:pulse-red .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-removed{animation:pulse-dark-red .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}@keyframes pulse-red{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-red{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-request .htmx-indicator-invisible{visibility:hidden!important}.htmx-indicator-invisible{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.arrow,.arrow:before{position:absolute;width:24px;height:24px;background:inherit}.arrow{visibility:hidden}.arrow:before{visibility:visible;content:"";transform:rotate(45deg)}.arrow{bottom:-4px}.ct-series-a .ct-bar{stroke:#79b52e;fill:#79b52e}.ct-series-b .ct-bar{stroke:#ff0303;fill:#ff0303}.ct-series-c .ct-bar{stroke:#009cea;fill:#009cea}.ct-series-d .ct-bar{stroke:#f48017;fill:#f48017}.ct-series-e .ct-bar{stroke:#9c27b0;fill:#9c27b0}[x-cloak]{display:none}.tippy-box[data-theme~=dropdown] .tippy-content{padding:0}.tippy-box[data-theme~=dropdown]{background-color:unset!important}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:start-\[2px\]:after{content:var(--tw-content);inset-inline-start:2px}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:h-5:after{content:var(--tw-content);height:1.25rem}.after\:w-5:after{content:var(--tw-content);width:1.25rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.indeterminate\:bg-gray-300:indeterminate{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:border-green-300:hover{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.hover\:bg-blue-100:hover{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-200:hover{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:bg-neutral-100:focus{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-100:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(204 235 251/var(--tw-ring-opacity))}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(148 196 88/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group.raw .group-\[\.raw\]\:sticky{position:sticky}.group.raw .group-\[\.raw\]\:top-0{top:0}.group.raw .group-\[\.raw\]\:z-10{z-index:10}.group.raw .group-\[\.raw\]\:block{display:block}.group.raw .group-\[\.raw\]\:hidden{display:none}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.implied .group-\[\&\.implied\]\:bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}.peer:checked~.peer-checked\:bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.peer:checked~.peer-checked\:after\:translate-x-full:after{content:var(--tw-content);--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.peer:hover~.peer-hover\:block{display:block}.peer:focus~.peer-focus\:outline-none{outline:2px solid #0000;outline-offset:2px}.peer:focus~.peer-focus\:ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.peer:focus~.peer-focus\:ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.data-\[active\]\:border-blue-600[data-active]{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.data-\[active\]\:text-blue-600[data-active]{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.htmx-swapping\:-translate-x-2\/3.htmx-swapping{--tw-translate-x:-66.666667%}.htmx-swapping\:-translate-x-2\/3.htmx-swapping,.htmx-swapping\:translate-x-2\/3.htmx-swapping{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:translate-x-2\/3.htmx-swapping{--tw-translate-x:66.666667%}.htmx-swapping\:scale-0.htmx-swapping{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:opacity-0.htmx-swapping{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4.htmx-swapping{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4.htmx-swapping{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping{transition-timing-function:cubic-bezier(.4,0,1,1)}.htmx-swapping .htmx-swapping\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-swapping .htmx-swapping\:-translate-x-2\/3,.htmx-swapping .htmx-swapping\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-swapping .htmx-swapping\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:opacity-0{opacity:0}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0{opacity:0}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.htmx-added\:-translate-x-2\/3.htmx-added{--tw-translate-x:-66.666667%}.htmx-added\:-translate-x-2\/3.htmx-added,.htmx-added\:translate-x-2\/3.htmx-added{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:translate-x-2\/3.htmx-added{--tw-translate-x:66.666667%}.htmx-added\:scale-0.htmx-added{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:opacity-0.htmx-added{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4.htmx-added{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4.htmx-added{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:scale-75.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:scale-75.htmx-added{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:opacity-0.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:opacity-0.htmx-added{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:ease-out.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:ease-out.htmx-added{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .htmx-added\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-added .htmx-added\:-translate-x-2\/3,.htmx-added .htmx-added\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-added .htmx-added\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:opacity-0{opacity:0}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:scale-75,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:scale-75{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:opacity-0,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:opacity-0{opacity:0}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:ease-out,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:is([dir=rtl] .peer:checked~.rtl\:peer-checked\:after\:-translate-x-full):after{content:var(--tw-content);--tw-translate-x:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-400){--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-green-800){--tw-border-opacity:1;border-color:rgb(48 72 18/var(--tw-border-opacity))}:is(.dark .dark\:border-primary-500){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-400){--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-100){--tw-text-opacity:1;color:rgb(204 235 251/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-green-400){--tw-text-opacity:1;color:rgb(148 196 88/var(--tw-text-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .hover\:dark\:border-green-800):hover{--tw-border-opacity:1;border-color:rgb(48 72 18/var(--tw-border-opacity))}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-800:hover){--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .hover\:dark\:bg-gray-800):hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .hover\:dark\:text-green-400):hover{--tw-text-opacity:1;color:rgb(148 196 88/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}:is(.dark .peer:focus~.dark\:peer-focus\:ring-blue-800){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:h-\[600px\]{height:600px}.md\:h-\[800px\]{height:800px}.md\:w-\[750px\]{width:750px}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:p-12{padding:3rem}}@media (min-width:1024px){.lg\:block{display:block}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:h-\[900px\]{height:900px}.lg\:h-\[640px\]{height:640px}.lg\:h-\[600px\]{height:600px}.lg\:w-96{width:24rem}.lg\:w-\[850px\]{width:850px}.lg\:w-\[920px\]{width:920px}.lg\:w-\[900px\]{width:900px}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-baseline{align-items:baseline}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}}@media (min-width:1280px){.xl\:table-cell{display:table-cell}}@media (min-width:1536px){.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.\[\&\.active\]\:bg-primary-300.active{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.\[\&\.active\]\:bg-primary-500.active{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}:is(.dark .\[\&\.active\]\:dark\:bg-primary-700).active{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.\[\&\.implied\]\:text-gray-500.implied{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))} \ No newline at end of file +/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.start-0{inset-inline-start:0}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[99\]{z-index:99}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.row-span-2{grid-row:span 2/span 2}.m-0{margin:0}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.me-2{-webkit-margin-end:.5rem;margin-inline-end:.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-8{margin-right:2rem}.ms-3{-webkit-margin-start:.75rem;margin-inline-start:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-0{margin-top:0}.box-content{box-sizing:initial}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-32{height:8rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[350px\]{height:350px}.h-\[49rem\]{height:49rem}.h-\[600px\]{height:600px}.h-\[70vh\]{height:70vh}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.h-9{height:2.25rem}.max-h-0{max-height:0}.max-h-96{max-height:24rem}.max-h-\[300px\]{max-height:300px}.max-h-\[600px\]{max-height:600px}.max-h-\[700px\]{max-height:700px}.max-h-\[inherit\]{max-height:inherit}.max-h-full{max-height:100%}.max-h-screen{max-height:100vh}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-11{width:2.75rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-72{width:18rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[10em\]{width:10em}.w-\[20em\]{width:20em}.w-\[300px\]{width:300px}.w-\[30em\]{width:30em}.w-\[5em\]{width:5em}.w-\[600px\]{width:600px}.w-\[700px\]{width:700px}.w-\[748px\]{width:748px}.w-\[7em\]{width:7em}.w-\[850px\]{width:850px}.w-\[8em\]{width:8em}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-screen{width:100vw}.w-28{width:7rem}.min-w-full{min-width:100%}.min-w-0{min-width:0}.max-w-2xl{max-width:42rem}.max-w-\[12rem\]{max-width:12rem}.max-w-\[24em\]{max-width:24em}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-1\/2{flex-basis:50%}.\!translate-y-0{--tw-translate-y:0px!important}.\!translate-y-0,.\!translate-y-32{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.\!translate-y-32{--tw-translate-y:8rem!important}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.translate-y-0{--tw-translate-y:0px}.translate-y-0,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-95,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-none{transform:none}@keyframes gentleGrow{0%{transform:scale(1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:scale(1.1);animation-timing-function:cubic-bezier(0,0,.2,1)}to{transform:scale(1);animation-timing-function:cubic-bezier(.8,0,1,1)}}.animate-gg{animation:gentleGrow 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes shake{0%{transform:translateX(0)}12.5%{transform:translateX(-5px)}25%{transform:translateX(0)}37.5%{transform:translateX(5px)}50%{transform:translateX(0)}62.5%{transform:translateX(-5px)}75%{transform:translateX(5px)}87.5%{transform:translateX(5px)}to{transform:translateX(0)}}.animate-shake{animation:shake .5s ease-out 1}@keyframes slideUp{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.animate-slideUp{animation:slideUp .5s ease-out forwards}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-row{grid-auto-flow:row}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.justify-items-stretch{justify-items:stretch}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-center{align-self:center}.self-stretch{align-self:stretch}.justify-self-end{justify-self:end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-b-lg{border-bottom-right-radius:.5rem}.rounded-b-lg,.rounded-l-lg{border-bottom-left-radius:.5rem}.rounded-l-lg{border-top-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-green-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(97 145 37/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.border-amber-300{--tw-border-opacity:1;border-color:rgb(252 211 77/var(--tw-border-opacity))}.border-emerald-300{--tw-border-opacity:1;border-color:rgb(110 231 183/var(--tw-border-opacity))}.border-slate-300{--tw-border-opacity:1;border-color:rgb(203 213 225/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(255 154 154/var(--tw-border-opacity))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(180 198 252/var(--tw-border-opacity))}.\!bg-primary-200{--tw-bg-opacity:1!important;background-color:rgb(201 225 171/var(--tw-bg-opacity))!important}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(246 245 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(255 3 3/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.bg-yellow-200{--tw-bg-opacity:1;background-color:rgb(252 233 106/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(253 253 234/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-emerald-100{--tw-bg-opacity:1;background-color:rgb(209 250 229/var(--tw-bg-opacity))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity))}.bg-indigo-50\/40{background-color:#f0f5ff66}.\!bg-opacity-0{--tw-bg-opacity:0!important}.\!bg-opacity-100{--tw-bg-opacity:1!important}.\!bg-opacity-50{--tw-bg-opacity:0.5!important}.bg-opacity-50{--tw-bg-opacity:0.5}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-8{padding-top:2rem;padding-bottom:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-6{padding-left:1.5rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pr-6{padding-right:1.5rem}.ps-10{-webkit-padding-start:2.5rem;padding-inline-start:2.5rem}.ps-3{-webkit-padding-start:.75rem;padding-inline-start:.75rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.pt-1{padding-top:.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[0\.6rem\]{font-size:.6rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(0 94 140/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-300{--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgb(24 36 9/var(--tw-text-opacity))}.text-purple-600{--tw-text-opacity:1;color:rgb(126 58 242/var(--tw-text-opacity))}.text-red-300{--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(153 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(159 88 10/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(99 49 18/var(--tw-text-opacity))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity))}.text-emerald-800{--tw-text-opacity:1;color:rgb(6 95 70/var(--tw-text-opacity))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.\!opacity-0{opacity:0!important}.\!opacity-100{opacity:1!important}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.outline{outline-style:solid}.outline-0{outline-width:0}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-added .swipe-left-swap,.htmx-added.swipe-left-swap{opacity:1!important;--tw-scale-x:1!important;--tw-scale-y:1!important;--tw-translate-x:-50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.swipe-left-swap{opacity:1;--tw-scale-x:1;--tw-scale-y:1;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-settling.htmx-added .swipe-left-swap,.htmx-settling.htmx-added.swipe-left-swap{opacity:0!important;--tw-scale-x:.75!important;--tw-scale-y:.75!important;--tw-translate-x:50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.htmx-settling .slide-up-settle,.htmx-settling.slide-up-settle{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up-settle{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hidden .slide-up,.htmx-added .slide-up{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}.live-removed{animation:pulse-red .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-removed{animation:pulse-dark-red .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}@keyframes pulse-red{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-red{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-request .htmx-indicator-invisible{visibility:hidden!important}.htmx-indicator-invisible{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.arrow,.arrow:before{position:absolute;width:24px;height:24px;background:inherit}.arrow{visibility:hidden}.arrow:before{visibility:visible;content:"";transform:rotate(45deg)}.arrow{bottom:-4px}.ct-series-a .ct-bar{stroke:#79b52e;fill:#79b52e}.ct-series-b .ct-bar{stroke:#ff0303;fill:#ff0303}.ct-series-c .ct-bar{stroke:#009cea;fill:#009cea}.ct-series-d .ct-bar{stroke:#f48017;fill:#f48017}.ct-series-e .ct-bar{stroke:#9c27b0;fill:#9c27b0}[x-cloak]{display:none}.tippy-box[data-theme~=dropdown] .tippy-content{padding:0}.tippy-box[data-theme~=dropdown]{background-color:unset!important}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:start-\[2px\]:after{content:var(--tw-content);inset-inline-start:2px}.after\:top-\[2px\]:after{content:var(--tw-content);top:2px}.after\:h-5:after{content:var(--tw-content);height:1.25rem}.after\:w-5:after{content:var(--tw-content);width:1.25rem}.after\:rounded-full:after{content:var(--tw-content);border-radius:9999px}.after\:border:after{content:var(--tw-content);border-width:1px}.after\:border-gray-300:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.after\:bg-white:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.after\:transition-all:after{content:var(--tw-content);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.indeterminate\:bg-gray-300:indeterminate{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:border-green-300:hover{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.hover\:bg-blue-100:hover{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-200:hover{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:bg-neutral-100:focus{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-100:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(204 235 251/var(--tw-ring-opacity))}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(148 196 88/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group.raw .group-\[\.raw\]\:sticky{position:sticky}.group.raw .group-\[\.raw\]\:top-0{top:0}.group.raw .group-\[\.raw\]\:z-10{z-index:10}.group.raw .group-\[\.raw\]\:block{display:block}.group.raw .group-\[\.raw\]\:hidden{display:none}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.implied .group-\[\&\.implied\]\:bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}.peer:checked~.peer-checked\:bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.peer:checked~.peer-checked\:after\:translate-x-full:after{content:var(--tw-content);--tw-translate-x:100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.peer:checked~.peer-checked\:after\:border-white:after{content:var(--tw-content);--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.peer:hover~.peer-hover\:block{display:block}.peer:focus~.peer-focus\:outline-none{outline:2px solid #0000;outline-offset:2px}.peer:focus~.peer-focus\:ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.peer:focus~.peer-focus\:ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.data-\[active\]\:border-blue-600[data-active]{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.data-\[active\]\:text-blue-600[data-active]{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.htmx-swapping\:-translate-x-2\/3.htmx-swapping{--tw-translate-x:-66.666667%}.htmx-swapping\:-translate-x-2\/3.htmx-swapping,.htmx-swapping\:translate-x-2\/3.htmx-swapping{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:translate-x-2\/3.htmx-swapping{--tw-translate-x:66.666667%}.htmx-swapping\:scale-0.htmx-swapping{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:opacity-0.htmx-swapping{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4.htmx-swapping{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4.htmx-swapping{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75.htmx-swapping{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0.htmx-swapping{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in.htmx-swapping{transition-timing-function:cubic-bezier(.4,0,1,1)}.htmx-swapping .htmx-swapping\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-swapping .htmx-swapping\:-translate-x-2\/3,.htmx-swapping .htmx-swapping\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-swapping .htmx-swapping\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:opacity-0{opacity:0}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:translate-x-1\/4{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:-translate-x-1\/4{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:scale-75,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:scale-75{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:opacity-0,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:opacity-0{opacity:0}.group\/transition.backward .htmx-swapping .group-\[\.backward\]\/transition\:htmx-swapping\:ease-in,.group\/transition.forward .htmx-swapping .group-\[\.forward\]\/transition\:htmx-swapping\:ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.htmx-added\:-translate-x-2\/3.htmx-added{--tw-translate-x:-66.666667%}.htmx-added\:-translate-x-2\/3.htmx-added,.htmx-added\:translate-x-2\/3.htmx-added{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:translate-x-2\/3.htmx-added{--tw-translate-x:66.666667%}.htmx-added\:scale-0.htmx-added{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:opacity-0.htmx-added{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4.htmx-added{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4.htmx-added{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:scale-75.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:scale-75.htmx-added{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:opacity-0.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:opacity-0.htmx-added{opacity:0}.group\/transition.backward .group-\[\.backward\]\/transition\:htmx-added\:ease-out.htmx-added,.group\/transition.forward .group-\[\.forward\]\/transition\:htmx-added\:ease-out.htmx-added{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .htmx-added\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-added .htmx-added\:-translate-x-2\/3,.htmx-added .htmx-added\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-added .htmx-added\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:opacity-0{opacity:0}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:-translate-x-1\/4{--tw-translate-x:-25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:translate-x-1\/4{--tw-translate-x:25%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:scale-75,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:scale-75{--tw-scale-x:.75;--tw-scale-y:.75;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:opacity-0,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:opacity-0{opacity:0}.group\/transition.backward .htmx-added .group-\[\.backward\]\/transition\:htmx-added\:ease-out,.group\/transition.forward .htmx-added .group-\[\.forward\]\/transition\:htmx-added\:ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:is([dir=rtl] .peer:checked~.rtl\:peer-checked\:after\:-translate-x-full):after{content:var(--tw-content);--tw-translate-x:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-400){--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-green-800){--tw-border-opacity:1;border-color:rgb(48 72 18/var(--tw-border-opacity))}:is(.dark .dark\:border-primary-500){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-400){--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-100){--tw-text-opacity:1;color:rgb(204 235 251/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-green-400){--tw-text-opacity:1;color:rgb(148 196 88/var(--tw-text-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .hover\:dark\:border-green-800):hover{--tw-border-opacity:1;border-color:rgb(48 72 18/var(--tw-border-opacity))}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-800:hover){--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .hover\:dark\:bg-gray-800):hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .hover\:dark\:text-green-400):hover{--tw-text-opacity:1;color:rgb(148 196 88/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}:is(.dark .peer:focus~.dark\:peer-focus\:ring-blue-800){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}@media (min-width:640px){.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:h-\[600px\]{height:600px}.md\:h-\[800px\]{height:800px}.md\:w-\[750px\]{width:750px}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:p-12{padding:3rem}}@media (min-width:1024px){.lg\:block{display:block}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:h-\[900px\]{height:900px}.lg\:h-\[640px\]{height:640px}.lg\:h-\[600px\]{height:600px}.lg\:w-96{width:24rem}.lg\:w-\[850px\]{width:850px}.lg\:w-\[920px\]{width:920px}.lg\:w-\[900px\]{width:900px}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:items-baseline{align-items:baseline}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}}@media (min-width:1280px){.xl\:table-cell{display:table-cell}}@media (min-width:1536px){.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.\[\&\.active\]\:bg-primary-300.active{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.\[\&\.active\]\:bg-primary-500.active{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}:is(.dark .\[\&\.active\]\:dark\:bg-primary-700).active{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.\[\&\.implied\]\:text-gray-500.implied{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))} \ No newline at end of file diff --git a/src/clj/amazonica/aws/textract.clj b/src/clj/amazonica/aws/textract.clj index 3278a264..a0ed389e 100644 --- a/src/clj/amazonica/aws/textract.clj +++ b/src/clj/amazonica/aws/textract.clj @@ -1,12 +1,11 @@ (ns amazonica.aws.textract (:require [amazonica.core :as amz]) - (:import [com.amazonaws.services.textract AmazonTextractClient ])) + (:import [com.amazonaws.services.textract AmazonTextractClient])) -#_ -(import '[com.amazonaws.services.textract AmazonTextractClient ]) -#_(import '[com.amazonaws.services.textract.model S3Object ]) -#_(import '[com.amazonaws.services.textract.model StartExpenseAnalysisRequest ]) -#_(import '[com.amazonaws.services.textract.model GetExpenseAnalysisRequest ]) +#_(import '[com.amazonaws.services.textract AmazonTextractClient]) +#_(import '[com.amazonaws.services.textract.model S3Object]) +#_(import '[com.amazonaws.services.textract.model StartExpenseAnalysisRequest]) +#_(import '[com.amazonaws.services.textract.model GetExpenseAnalysisRequest]) #_(import '[com.amazonaws.services.textract.model DocumentLocation]) (amz/set-client AmazonTextractClient *ns*) diff --git a/src/clj/auto_ap/background/metrics.clj b/src/clj/auto_ap/background/metrics.clj index 760d50d7..608f128d 100644 --- a/src/clj/auto_ap/background/metrics.clj +++ b/src/clj/auto_ap/background/metrics.clj @@ -28,7 +28,6 @@ [(str "container:" (:DockerId container-data)) (str "ip:" (-> container-data :Networks first :IPv4Addresses first))]) - (mount/defstate container-tags :start (get-container-tags) :stop nil) diff --git a/src/clj/auto_ap/cursor.clj b/src/clj/auto_ap/cursor.clj index 28508791..c6e867a3 100644 --- a/src/clj/auto_ap/cursor.clj +++ b/src/clj/auto_ap/cursor.clj @@ -5,14 +5,11 @@ (path [cursor]) (state [cursor])) - (defprotocol ITransact (-transact! [cursor f])) - (declare to-cursor cursor?) - (deftype ValCursor [value state path] IDeref (deref [_] @@ -23,9 +20,8 @@ ITransact (-transact! [_ f] (get-in - (swap! state (if (empty? path) f #(update-in % path f))) - path))) - + (swap! state (if (empty? path) f #(update-in % path f))) + path))) (deftype MapCursor [value state path] Counted @@ -53,14 +49,13 @@ ITransact (-transact! [cursor f] (get-in - (swap! state (if (empty? path) f #(update-in % path f))) - path)) + (swap! state (if (empty? path) f #(update-in % path f))) + path)) Seqable (seq [this] (for [[k v] @this] [k (to-cursor v state (conj path k) nil)]))) - (deftype VecCursor [value state path] Counted (count [_] @@ -91,29 +86,25 @@ ITransact (-transact! [cursor f] (get-in - (swap! state (if (empty? path) f #(update-in % path f))) - path)) + (swap! state (if (empty? path) f #(update-in % path f))) + path)) Seqable (seq [this] (for [[v i] (map vector @this (range))] (to-cursor v state (conj path i) nil)))) - (defn- to-cursor ([v state path value] (cond (cursor? v) v (map? v) (MapCursor. value state path) (vector? v) (VecCursor. value state path) - :else (ValCursor. value state path) - ))) - + :else (ValCursor. value state path)))) (defn cursor? [c] "Returns true if c is a cursor." (satisfies? ICursor c)) - (defn cursor [v] "Creates cursor from supplied value v. If v is an ordinary data structure, it is wrapped into atom. If v is an atom, @@ -123,7 +114,6 @@ (if (instance? Atom v) v (atom v)) [] nil)) - (defn synthetic-cursor [v prefix] (let [internal-cursor (cursor v)] (reify ICursor @@ -132,14 +122,12 @@ (state [this] (state internal-cursor))))) - (defn transact! [cursor f] "Changes value beneath cursor by passing it to a single-argument function f. Old value will be passed as function argument. Function result will be the new value." (-transact! cursor f)) - (defn update! [cursor v] "Replaces value supplied by cursor with value v." (-transact! cursor (constantly v))) diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index 80b9271e..d66cd4c3 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -5,11 +5,11 @@ [iol-ion.tx.propose-invoice] [iol-ion.tx.reset-rels] [iol-ion.tx.reset-scalars] - [iol-ion.tx.upsert-entity] - [iol-ion.tx.upsert-invoice] - [iol-ion.tx.upsert-ledger] - [iol-ion.tx.upsert-transaction] - [iol-ion.tx.upsert-sales-summary-ledger] + [iol-ion.tx.upsert-entity] + [iol-ion.tx.upsert-invoice] + [iol-ion.tx.upsert-ledger] + [iol-ion.tx.upsert-transaction] + [iol-ion.tx.upsert-sales-summary-ledger] [com.github.ivarref.gen-fn :refer [gen-fn! datomic-fn]] [auto-ap.utils :refer [default-pagination-size by]] [clojure.edn :as edn] @@ -27,8 +27,8 @@ (def uri (:datomic-url env)) #_(mount/defstate client - :start (dc/client (:client-config env)) - :stop nil) + :start (dc/client (:client-config env)) + :stop nil) (mount/defstate conn :start (dc/connect uri) @@ -38,21 +38,20 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_(defn create-database [] - (d/create-database uri)) + (d/create-database uri)) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_(defn drop-database [] - (d/delete-database uri)) + (d/delete-database uri)) (defn remove-nils [m] (let [result (reduce-kv - (fn [m k v] - (if (not (nil? v)) - (assoc m k v) - m - )) - {} - m)] + (fn [m k v] + (if (not (nil? v)) + (assoc m k v) + m)) + {} + m)] (if (seq result) result nil))) @@ -80,7 +79,7 @@ :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "A vendor's email address"} - + {:db/ident :vendor/phone :db/valueType :db.type/string :db/cardinality :db.cardinality/one @@ -102,14 +101,13 @@ :db/valueType :db.type/ref :db/isComponent true :db/cardinality :db.cardinality/one - :db/doc "The vendor's secondary contact"} + :db/doc "The vendor's secondary contact"} {:db/ident :vendor/address :db/valueType :db.type/ref :db/cardinality :db.cardinality/one :db/isComponent true :db.install/_attribute :db.part/db - :db/doc "The vendor's address"} - ]) + :db/doc "The vendor's address"}]) (def client-schema [{:db/ident :client/original-id @@ -151,8 +149,7 @@ :db/doc "Bank accounts for the client"}]) (def address-schema - [ - {:db/ident :address/street1 + [{:db/ident :address/street1 :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "123 main st"} @@ -174,8 +171,7 @@ :db/doc "95014"}]) (def contact-schema - [ - {:db/ident :contact/name + [{:db/ident :contact/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "John Smith"} @@ -188,8 +184,6 @@ :db/cardinality :db.cardinality/one :db/doc "hello@example.com"}]) - - (def bank-account-schema [{:db/ident :bank-account/external-id :db/valueType :db.type/long @@ -296,7 +290,6 @@ :db/cardinality :db.cardinality/many :db/isComponent true :db/doc "The expense account categories for this invoice"} - {:db/ident :invoice-status/paid} {:db/ident :invoice-status/unpaid} @@ -312,17 +305,15 @@ :db/valueType :db.type/long :db/cardinality :db.cardinality/one :db/doc "The code for the expense account"} - {:db/ident :invoice-expense-account/location + {:db/ident :invoice-expense-account/location :db/valueType :db.type/string :db/cardinality :db.cardinality/one - :db/doc "Location for this expense account"} + :db/doc "Location for this expense account"} {:db/ident :invoice-expense-account/amount :db/valueType :db.type/double :db/cardinality :db.cardinality/one :db/doc "The amount that this contributes to"}]) - - (def payment-schema [{:db/ident :payment/original-id :db/valueType :db.type/long @@ -373,9 +364,8 @@ :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "raw data used to generate check pdf"} - - ;; relations +;; relations {:db/ident :payment/vendor :db/valueType :db.type/ref :db/cardinality :db.cardinality/one @@ -400,8 +390,7 @@ {:db/ident :payment-type/cash} {:db/ident :payment-type/check} - {:db/ident :payment-type/debit} - ]) + {:db/ident :payment-type/debit}]) (def invoice-payment-schema [{:db/ident :invoice-payment/original-id @@ -414,7 +403,7 @@ :db/valueType :db.type/double :db/cardinality :db.cardinality/one :db/doc "The amount that was paid to this invoice"} - + ;; relations {:db/ident :invoice-payment/invoice :db/valueType :db.type/ref @@ -481,8 +470,7 @@ :db/cardinality :db.cardinality/one :db/doc "The check number that was parsed from the description"} - - ;; relations +;; relations {:db/ident :transaction/vendor :db/valueType :db.type/ref :db/cardinality :db.cardinality/one @@ -498,8 +486,7 @@ {:db/ident :transaction/payment :db/valueType :db.type/ref :db/cardinality :db.cardinality/one - :db/doc "The payment that this transaction matched to"} - ]) + :db/doc "The payment that this transaction matched to"}]) (def user-schema [{:db/ident :user/original-id @@ -531,12 +518,10 @@ ;;enums {:db/ident :user-role/admin} {:db/ident :user-role/user} - {:db/ident :user-role/none} - ]) + {:db/ident :user-role/none}]) (def base-schema - [ address-schema contact-schema vendor-schema client-schema bank-account-schema invoice-schema invoice-expense-account-schema payment-schema invoice-payment-schema transaction-schema user-schema]) - + [address-schema contact-schema vendor-schema client-schema bank-account-schema invoice-schema invoice-expense-account-schema payment-schema invoice-payment-schema transaction-schema user-schema]) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn migrate-vendors [_] @@ -590,19 +575,19 @@ (defn add-sorter-fields-2 [q sort-map args] (reduce - (fn [q {:keys [sort-key]}] - (merge-query q - {:query {:find [(last (last (sort-map - sort-key - (println "Warning, trying to sort by unsupported field" sort-key))))] - :where (sort-map - sort-key - (println "Warning, trying to sort by unsupported field" sort-key))}})) - q - (:sort args))) + (fn [q {:keys [sort-key]}] + (merge-query q + {:query {:find [(last (last (sort-map + sort-key + (println "Warning, trying to sort by unsupported field" sort-key))))] + :where (sort-map + sort-key + (println "Warning, trying to sort by unsupported field" sort-key))}})) + q + (:sort args))) (defn apply-sort-3 [args results] - + (let [sort-bys (conj (into [] (:sort args)) {:sort-key "default" :asc (if (contains? args :default-asc?) (:default-asc? args) @@ -611,7 +596,7 @@ comparator (fn [xs ys] (reduce (fn [_ i] - + (let [comparison (if (:asc (nth sort-bys i)) (compare (nth xs i) (nth ys i)) (compare (nth ys i) (nth xs i)))] @@ -625,18 +610,18 @@ ;; TODO replace COULD JUST BE SORT-3 (defn apply-sort-4 [args results] - + (let [sort-bys (-> [] (into (:sort args)) (conj {:sort-key "default" :asc (if (contains? args :default-asc?) - (:default-asc? args) - true)}) + (:default-asc? args) + true)}) (conj {:sort-key "e" :asc true})) length (count sort-bys) comparator (fn [xs ys] (reduce (fn [_ i] - + (let [comparison (if (:asc (nth sort-bys i)) (compare (nth xs i) (nth ys i)) (compare (nth ys i) (nth xs i)))] @@ -657,7 +642,7 @@ (defn apply-pagination [args results] {:ids (->> results (drop (or (:start args) 0)) - (take (or (:count args ) + (take (or (:count args) (:per-page args) default-pagination-size)) (map last)) @@ -669,8 +654,8 @@ (reduce (fn [full-tx batch] (let [batch (conj (vec batch) {:db/id "datomic.tx" - :audit/user (str (:user/role id) "-" (:user/name id)) - :audit/batch batch-id}) + :audit/user (str (:user/role id) "-" (:user/name id)) + :audit/batch batch-id}) _ (mu/log ::transacting-batch :batch batch-id :count (count batch)) @@ -687,18 +672,17 @@ (partition-all 200 txes)))) (defn audit-transact [txes id] - (try + (try @(dc/transact-async conn (conj txes {:db/id "datomic.tx" - :audit/user (str (:user/role id) "-" (:user/name id))})) + :audit/user (str (:user/role id) "-" (:user/name id))})) (catch Exception e (mu/log ::transaction-error :exception e :level :error :tx txes) - (throw e) - ))) + (throw e)))) -(defn pull-many [db read ids ] +(defn pull-many [db read ids] (->> (dc/q '[:find (pull ?e r) :in $ [?e ...] r] db @@ -706,22 +690,22 @@ read) (map first))) -(defn pull-many-by-id [db read ids ] +(defn pull-many-by-id [db read ids] (into {} (map (fn [[e]] [(:db/id e) e])) (dc/q '[:find (pull ?e r) - :in $ [?e ...] r] - db - ids - read))) + :in $ [?e ...] r] + db + ids + read))) (defn random-tempid [] (str (UUID/randomUUID))) (defn pull-id [db id] (if (sequential? id) - (ffirst (dc/q '[:find ?i + (ffirst (dc/q '[:find ?i :in $ [?a ?v] :where [?i ?a ?v]] db @@ -734,170 +718,163 @@ (defn pull-ref [db k id] (:db/id (pull-attr db k id))) +#_(comment + (dc/pull (dc/db conn) '[*] 175921860633685) + (upsert-entity (dc/db conn) {:db/id 175921860633685 :invoice/invoice-number nil :invoice/date #inst "2021-01-01" :invoice/expense-accounts [:reset-rels [{:db/id "new" :invoice-expense-account/amount 1}]]}) + (upsert-entity (dc/db conn) {:invoice/client #:db{:id 79164837221949}, + :invoice/status #:db{:id 101155069755470, :ident :invoice-status/paid}, + :invoice/due #inst "2020-12-23T08:00:00.000-00:00", + :invoice/invoice-number "12648", + :invoice/import-status + :import-status/imported, + :invoice/vendor nil, + :invoice/date #inst "2020-12-16T08:00:00.000-00:00", + :entity/migration-key 17592234924273, + :db/id 175921860633685, + :invoice/outstanding-balance 0.0, + :invoice/expense-accounts + [{:entity/migration-key 17592234924274, + :invoice-expense-account/location nil + :invoice-expense-account/amount 360.0, + :invoice-expense-account/account #:db{:id 92358976759248}}]}) -#_(comment - (dc/pull (dc/db conn) '[*] 175921860633685) - - (upsert-entity (dc/db conn) {:db/id 175921860633685 :invoice/invoice-number nil :invoice/date #inst "2021-01-01" :invoice/expense-accounts [:reset-rels [{:db/id "new" :invoice-expense-account/amount 1}]]}) - - (upsert-entity (dc/db conn) {:invoice/client #:db{:id 79164837221949}, - :invoice/status #:db{:id 101155069755470, :ident :invoice-status/paid}, - :invoice/due #inst "2020-12-23T08:00:00.000-00:00", - :invoice/invoice-number "12648", - :invoice/import-status - :import-status/imported, - :invoice/vendor nil, - :invoice/date #inst "2020-12-16T08:00:00.000-00:00", - :entity/migration-key 17592234924273, - :db/id 175921860633685, - :invoice/outstanding-balance 0.0, - :invoice/expense-accounts - [{:entity/migration-key 17592234924274, - :invoice-expense-account/location nil - :invoice-expense-account/amount 360.0, - :invoice-expense-account/account #:db{:id 92358976759248}}],}) - - - - #_(dc/pull (dc/db conn) auto-ap.datomic.clients 79164837221904) - (upsert-entity (dc/db conn) {:client/name "20Twenty - WG Development LLC", - :client/square-locations - [{:db/id 83562883711605, - :entity/migration-key 17592258901782, - :square-location/square-id "L2579ATQ0X1ET", - :square-location/name "20Twenty", - :square-location/client-location "WG"}], - :client/square-auth-token - "EAAAEEr749Ea6AdPTdngsmUPwIM3ETbPwcx3QQl_NS0KWuIL-JNzAg4f3W9DGQhb", - :client/bank-accounts - [{:bank-account/sort-order 2, - :bank-account/include-in-reports true, - :bank-account/number "3467", - :bank-account/code "20TY-WFCC3467", - :bank-account/locations ["WG"], - :entity/migration-key 17592245102834, - :bank-account/current-balance 11160.289999999979, - :bank-account/name "Wells Fargo CC - 3467", - :db/id 83562883732805, - :bank-account/start-date #inst "2021-12-01T08:00:00.000-00:00", - :bank-account/visible true, - :bank-account/type - #:db{:id 101155069755504, :ident :bank-account-type/credit}, - :bank-account/intuit-bank-account #:db{:id 105553116286744}, - :bank-account/integration-status - {:db/id 74766790691480, - :entity/migration-key 17592267080690, - :integration-status/last-updated #inst "2022-08-23T03:47:44.892-00:00", - :integration-status/last-attempt #inst "2022-08-23T03:47:44.892-00:00", + #_(dc/pull (dc/db conn) auto-ap.datomic.clients 79164837221904) + (upsert-entity (dc/db conn) {:client/name "20Twenty - WG Development LLC", + :client/square-locations + [{:db/id 83562883711605, + :entity/migration-key 17592258901782, + :square-location/square-id "L2579ATQ0X1ET", + :square-location/name "20Twenty", + :square-location/client-location "WG"}], + :client/square-auth-token + "EAAAEEr749Ea6AdPTdngsmUPwIM3ETbPwcx3QQl_NS0KWuIL-JNzAg4f3W9DGQhb", + :client/bank-accounts + [{:bank-account/sort-order 2, + :bank-account/include-in-reports true, + :bank-account/number "3467", + :bank-account/code "20TY-WFCC3467", + :bank-account/locations ["WG"], + :entity/migration-key 17592245102834, + :bank-account/current-balance 11160.289999999979, + :bank-account/name "Wells Fargo CC - 3467", + :db/id 83562883732805, + :bank-account/start-date #inst "2021-12-01T08:00:00.000-00:00", + :bank-account/visible true, + :bank-account/type + #:db{:id 101155069755504, :ident :bank-account-type/credit}, + :bank-account/intuit-bank-account #:db{:id 105553116286744}, + :bank-account/integration-status + {:db/id 74766790691480, + :entity/migration-key 17592267080690, + :integration-status/last-updated #inst "2022-08-23T03:47:44.892-00:00", + :integration-status/last-attempt #inst "2022-08-23T03:47:44.892-00:00", + :integration-status/state + #:db{:id 101155069755529, :ident :integration-state/success}}, + :bank-account/bank-name "Wells Fargo"} + {:bank-account/sort-order 0, + :bank-account/include-in-reports true, + :bank-account/numeric-code 11301, + :bank-account/check-number 301, + :bank-account/number "1734742859", + :bank-account/code "20TY-WF2882", + :bank-account/locations ["WG"], + :bank-account/bank-code "11-4288/1210 4285", + :entity/migration-key 17592241193004, + :bank-account/current-balance -47342.54000000085, + :bank-account/name "Wells Fargo Main - 2859", + :db/id 83562883732846, + :bank-account/start-date #inst "2021-12-01T08:00:00.000-00:00", + :bank-account/visible true, + :bank-account/type + #:db{:id 101155069755468, :ident :bank-account-type/check}, + :bank-account/intuit-bank-account #:db{:id 105553116286745}, + :bank-account/routing "121042882", + :bank-account/integration-status + {:db/id 74766790691458, + :entity/migration-key 17592267080255, + :integration-status/last-updated #inst "2022-08-23T03:46:45.879-00:00", + :integration-status/last-attempt #inst "2022-08-23T03:46:45.879-00:00", + :integration-status/state + #:db{:id 101155069755529, :ident :integration-state/success}}, + :bank-account/bank-name "Wells Fargo"} + {:bank-account/sort-order 1, + :bank-account/include-in-reports true, + :bank-account/numeric-code 20101, + :bank-account/yodlee-account-id 27345526, + :bank-account/number "41006", + :bank-account/code "20TY-Amex41006", + :bank-account/locations ["WG"], + :entity/migration-key 17592241193006, + :bank-account/current-balance 9674.069999999963, + :bank-account/name "Amex - 41006", + :db/id 83562883732847, + :bank-account/visible true, + :bank-account/type + #:db{:id 101155069755504, :ident :bank-account-type/credit}, + :bank-account/bank-name "American Express"} + {:bank-account/sort-order 3, + :bank-account/include-in-reports true, + :bank-account/numeric-code 11101, + :bank-account/code "20TY-0", + :bank-account/locations ["WG"], + :entity/migration-key 17592241193005, + :bank-account/current-balance 0.0, + :bank-account/name "CASH", + :db/id 83562883732848, + :bank-account/visible true, + :bank-account/type + #:db{:id 101155069755469, :ident :bank-account-type/cash}}], + :entity/migration-key 17592241193003, + :db/id 79164837221904, + :client/address + {:db/id 105553116285906, + :entity/migration-key 17592250661126, + :address/street1 "1389 Lincoln Ave", + :address/city "San Jose", + :address/state "CA", + :address/zip "95125"}, + :client/code "NY", + :client/locations ["WE" "NG"], + :client/square-integration-status + {:db/id 74766790691447, + :entity/migration-key 17592267072653, + :integration-status/last-updated #inst "2022-08-23T13:09:16.082-00:00", + :integration-status/last-attempt #inst "2022-08-23T13:08:47.018-00:00", :integration-status/state - #:db{:id 101155069755529, :ident :integration-state/success}}, - :bank-account/bank-name "Wells Fargo"} - {:bank-account/sort-order 0, - :bank-account/include-in-reports true, - :bank-account/numeric-code 11301, - :bank-account/check-number 301, - :bank-account/number "1734742859", - :bank-account/code "20TY-WF2882", - :bank-account/locations ["WG"], - :bank-account/bank-code "11-4288/1210 4285", - :entity/migration-key 17592241193004, - :bank-account/current-balance -47342.54000000085, - :bank-account/name "Wells Fargo Main - 2859", - :db/id 83562883732846, - :bank-account/start-date #inst "2021-12-01T08:00:00.000-00:00", - :bank-account/visible true, - :bank-account/type - #:db{:id 101155069755468, :ident :bank-account-type/check}, - :bank-account/intuit-bank-account #:db{:id 105553116286745}, - :bank-account/routing "121042882", - :bank-account/integration-status - {:db/id 74766790691458, - :entity/migration-key 17592267080255, - :integration-status/last-updated #inst "2022-08-23T03:46:45.879-00:00", - :integration-status/last-attempt #inst "2022-08-23T03:46:45.879-00:00", - :integration-status/state - #:db{:id 101155069755529, :ident :integration-state/success}}, - :bank-account/bank-name "Wells Fargo"} - {:bank-account/sort-order 1, - :bank-account/include-in-reports true, - :bank-account/numeric-code 20101, - :bank-account/yodlee-account-id 27345526, - :bank-account/number "41006", - :bank-account/code "20TY-Amex41006", - :bank-account/locations ["WG"], - :entity/migration-key 17592241193006, - :bank-account/current-balance 9674.069999999963, - :bank-account/name "Amex - 41006", - :db/id 83562883732847, - :bank-account/visible true, - :bank-account/type - #:db{:id 101155069755504, :ident :bank-account-type/credit}, - :bank-account/bank-name "American Express"} - {:bank-account/sort-order 3, - :bank-account/include-in-reports true, - :bank-account/numeric-code 11101, - :bank-account/code "20TY-0", - :bank-account/locations ["WG"], - :entity/migration-key 17592241193005, - :bank-account/current-balance 0.0, - :bank-account/name "CASH", - :db/id 83562883732848, - :bank-account/visible true, - :bank-account/type - #:db{:id 101155069755469, :ident :bank-account-type/cash}}], - :entity/migration-key 17592241193003, - :db/id 79164837221904, - :client/address - {:db/id 105553116285906, - :entity/migration-key 17592250661126, - :address/street1 "1389 Lincoln Ave", - :address/city "San Jose", - :address/state "CA", - :address/zip "95125"}, - :client/code "NY", - :client/locations ["WE" "NG"], - :client/square-integration-status - {:db/id 74766790691447, - :entity/migration-key 17592267072653, - :integration-status/last-updated #inst "2022-08-23T13:09:16.082-00:00", - :integration-status/last-attempt #inst "2022-08-23T13:08:47.018-00:00", - :integration-status/state - #:db{:id 101155069755529, :ident :integration-state/success}}}) - - ) + #:db{:id 101155069755529, :ident :integration-state/success}}})) (defn install-functions [] @(dc/transact conn - (edn/read-string {:readers {'db/id id-literal - 'db/fn construct}} (slurp (io/resource "functions.edn"))))) + (edn/read-string {:readers {'db/id id-literal + 'db/fn construct}} (slurp (io/resource "functions.edn"))))) (defn all-schema [] (edn/read-string (slurp (io/resource "schema.edn")))) (defn transact-schema [conn] @(dc/transact conn - (edn/read-string (slurp (io/resource "schema.edn")))) + (edn/read-string (slurp (io/resource "schema.edn")))) ;; this is temporary for any new stuff that needs to be asserted for cloud migration. @(dc/transact conn - (edn/read-string (slurp (io/resource "cloud-migration-schema.edn"))))) + (edn/read-string (slurp (io/resource "cloud-migration-schema.edn"))))) (defn backoff [n] - (let [base-timeout 500 + (let [base-timeout 500 max-timeout 300000 ; 5 minutes max-retries 10 backoff-time (* base-timeout (Math/pow 2 (min n max-retries)))] (min (+ backoff-time (rand-int base-timeout)) max-timeout))) (defn transact-with-backoff - ([tx ] (transact-with-backoff tx 0)) + ([tx] (transact-with-backoff tx 0)) ([tx attempt] - (try + (try @(dc/transact conn tx) (catch Exception e (if (< attempt 10) - (do + (do (Thread/sleep (backoff attempt)) (mu/log ::transact-failed :exception e @@ -923,7 +900,6 @@ (into #{} (map :db/id (:user/clients id []))))) - (defn query2 [query] (apply dc/q (:query query) (:args query))) @@ -933,14 +909,14 @@ (defn observable-query [query] (mu/with-context {:query (pr-str (:query query)) :args (pr-str (:args query))} - (mu/trace ::query - [] - (let [query-results (dc/query {:query (:query query) - :args (:args query) - :query-stats true - :io-context ::hello})] - (alog/info ::query-stats - :io-stats (pr-str (:io-stats query-results)) - :query-stats (pr-str (:query-stats query-results))) - (:ret query-results))))) + (mu/trace ::query + [] + (let [query-results (dc/query {:query (:query query) + :args (:args query) + :query-stats true + :io-context ::hello})] + (alog/info ::query-stats + :io-stats (pr-str (:io-stats query-results)) + :query-stats (pr-str (:query-stats query-results))) + (:ret query-results))))) diff --git a/src/clj/auto_ap/datomic/accounts.clj b/src/clj/auto_ap/datomic/accounts.clj index 51f9d0b6..28ed5d45 100644 --- a/src/clj/auto_ap/datomic/accounts.clj +++ b/src/clj/auto_ap/datomic/accounts.clj @@ -12,18 +12,18 @@ [datomic.api :as dc])) (defn <-datomic [a] - (-> a + (-> a (update :account/applicability :db/ident) (update :account/invoice-allowance :db/ident) (update :account/vendor-allowance :db/ident))) (def default-read ['* {:account/type [:db/ident :db/id] - :account/applicability [:db/ident :db/id] + :account/applicability [:db/ident :db/id] :account/invoice-allowance [:db/ident :db/id] :account/vendor-allowance [:db/ident :db/id] - :account/client-overrides [:db/id - :account-client-override/name - {:account-client-override/client [:db/id :client/name]}]}]) + :account/client-overrides [:db/id + :account-client-override/name + {:account-client-override/client [:db/id :client/name]}]}]) (defn clientize [a client] (if-let [override-name (->> a @@ -52,44 +52,44 @@ (map <-datomic))))) (defn get-for-vendor [vendor-id client-id] - (if client-id + (if client-id (->> - (dc/q '[:find (pull ?e r) - :in $ ?v ?c r - :where (or-join [?v ?c ?e] - (and [?v :vendor/account-overrides ?ao] - [?ao :vendor-account-override/client ?c] - [?ao :vendor-account-override/account ?e]) - (and [?v :vendor/account-overrides ?ao] - (not [?ao :vendor-account-override/client ?c]) - [?v :vendor/default-account ?e]) - (and (not [?v :vendor/account-overrides]) - [?v :vendor/default-account ?e]))] + (dc/q '[:find (pull ?e r) + :in $ ?v ?c r + :where (or-join [?v ?c ?e] + (and [?v :vendor/account-overrides ?ao] + [?ao :vendor-account-override/client ?c] + [?ao :vendor-account-override/account ?e]) + (and [?v :vendor/account-overrides ?ao] + (not [?ao :vendor-account-override/client ?c]) + [?v :vendor/default-account ?e]) + (and (not [?v :vendor/account-overrides]) + [?v :vendor/default-account ?e]))] - (dc/db conn ) + (dc/db conn) vendor-id client-id default-read) - (map first) - (map <-datomic) - first) + (map first) + (map <-datomic) + first) (<-datomic (dc/q '[:find (pull ?e r) :in $ ?v r :where [?v :vendor/default-account ?e]] - (dc/db conn ) + (dc/db conn) vendor-id default-read)))) (defn get-account-by-numeric-code-and-sets [numeric-code _] (->> - (dc/q {:find ['(pull ?e [* {:account/type [:db/ident :db/id]}])] - :in ['$ '?numeric-code] - :where ['[?e :account/numeric-code ?numeric-code]]} - (dc/db conn) numeric-code) - (map first) - (map <-datomic) - (first))) + (dc/q {:find ['(pull ?e [* {:account/type [:db/ident :db/id]}])] + :in ['$ '?numeric-code] + :where ['[?e :account/numeric-code ?numeric-code]]} + (dc/db conn) numeric-code) + (map first) + (map <-datomic) + (first))) (defn raw-graphql-ids [db args] (let [query (cond-> {:query {:find [] @@ -111,23 +111,21 @@ :args [(re-pattern (str "(?i)" (:name-like args)))]}) true - (merge-query {:query {:find ['?sort-default '?e ] + (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :account/name] '[?e :account/numeric-code ?sort-default]]}}))] - (cond->> (query2 query) true (apply-sort-3 args) true (apply-pagination args)))) - (defn graphql-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) accounts (->> ids - (map results) - (map first) - (map <-datomic))] + (map results) + (map first) + (map <-datomic))] accounts)) (defn get-graphql [args] diff --git a/src/clj/auto_ap/datomic/bank_accounts.clj b/src/clj/auto_ap/datomic/bank_accounts.clj index bf23f336..aa9ca405 100644 --- a/src/clj/auto_ap/datomic/bank_accounts.clj +++ b/src/clj/auto_ap/datomic/bank_accounts.clj @@ -13,15 +13,12 @@ (def default-read '[* {:client/_bank-accounts [:db/id]}]) (defn <-datomic [x] - (->> x - (map #(update % :bank-account/type :db/ident)) - )) - - + (->> x + (map #(update % :bank-account/type :db/ident)))) (defn get-by-id [id] - (->> [(dc/pull (dc/db conn ) default-read id)] - (<-datomic) + (->> [(dc/pull (dc/db conn) default-read id)] + (<-datomic) (first))) diff --git a/src/clj/auto_ap/datomic/checks.clj b/src/clj/auto_ap/datomic/checks.clj index 332c54f3..589ffb54 100644 --- a/src/clj/auto_ap/datomic/checks.clj +++ b/src/clj/auto_ap/datomic/checks.clj @@ -16,22 +16,22 @@ (defn <-datomic [result] (-> result - (update :payment/date c/from-date) - (update :payment/status :db/ident) - (update :payment/type :db/ident) - (update :transaction/_payment (fn [transactions] - (mapv (fn [transaction] - (update transaction :transaction/date c/from-date)) - transactions))) - (rename-keys {:invoice-payment/_payment :payment/invoices}))) + (update :payment/date c/from-date) + (update :payment/status :db/ident) + (update :payment/type :db/ident) + (update :transaction/_payment (fn [transactions] + (mapv (fn [transaction] + (update transaction :transaction/date c/from-date)) + transactions))) + (rename-keys {:invoice-payment/_payment :payment/invoices}))) (def default-read '[* - {:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]} - {:payment/client [:client/name :db/id :client/code]} - {:payment/bank-account [*]} - {:payment/vendor [:vendor/name {:vendor/default-account - [:account/name :account/numeric-code :db/id]} :db/id {:vendor/primary-contact [*]} {:vendor/address [*]}]} - {:payment/status [:db/ident]} + {:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]} + {:payment/client [:client/name :db/id :client/code]} + {:payment/bank-account [*]} + {:payment/vendor [:vendor/name {:vendor/default-account + [:account/name :account/numeric-code :db/id]} :db/id {:vendor/primary-contact [*]} {:vendor/address [*]}]} + {:payment/status [:db/ident]} {:payment/type [:db/ident]} {:transaction/_payment [:db/id :transaction/date]}]) @@ -60,7 +60,7 @@ (:sort args) (add-sorter-fields {"client" ['[?e :payment/client ?c] '[?c :client/name ?sort-client]] "vendor" ['[?e :payment/vendor ?v] - '[?v :vendor/name ?sort-vendor]] + '[?v :vendor/name ?sort-vendor]] "bank-account" ['[?e :payment/bank-account ?ba] '[?ba :bank-account/name ?sort-bank-account]] "check-number" ['[(get-else $ ?e :payment/check-number 0) ?sort-check-number]] @@ -73,7 +73,7 @@ :where []} :args [(:exact-match-id args)]}) - (:vendor-id args) + (:vendor-id args) (merge-query {:query {:in ['?vendor-id] :where ['[?e :payment/vendor ?vendor-id]]} :args [(:vendor-id args)]}) @@ -100,13 +100,13 @@ :where ['[?e :payment/bank-account ?bank-account-id]]} :args [(:bank-account-id args)]}) - (:amount-gte args) + (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :payment/amount ?a] '[(>= ?a ?amount-gte)]]} :args [(:amount-gte args)]}) - (:amount-lte args) + (:amount-lte args) (merge-query {:query {:in ['?amount-lte] :where ['[?e :payment/amount ?a] '[(<= ?a ?amount-lte)]]} @@ -118,7 +118,6 @@ '[(iol-ion.query/dollars= ?transaction-amount ?amount)]]} :args [(:amount args)]}) - (:status args) (merge-query {:query {:in ['?status] :where ['[?e :payment/status ?status]]} @@ -137,7 +136,6 @@ true (merge-query {:query {:find ['?sort-default '?e]}})))] - (cond->> (observable-query query) true (apply-sort-3 (assoc args :default-asc? false)) true (apply-pagination args))))) @@ -146,9 +144,9 @@ (let [results (->> (pull-many db default-read ids) (group-by :db/id)) payments (->> ids - (map results) - (map first) - (mapv <-datomic))] + (map results) + (map first) + (mapv <-datomic))] payments)) (defn get-graphql [args] @@ -169,7 +167,7 @@ [])) (defn get-by-id [id] - (->> + (->> (dc/pull (dc/db conn) default-read id) (<-datomic))) diff --git a/src/clj/auto_ap/datomic/clients.clj b/src/clj/auto_ap/datomic/clients.clj index 5e55b2f1..64515333 100644 --- a/src/clj/auto_ap/datomic/clients.clj +++ b/src/clj/auto_ap/datomic/clients.clj @@ -122,8 +122,6 @@ Long/parseLong (#(hash-map :db/id %))))) - - (defn exact-match [identifier] (when (and identifier (not-empty identifier)) (some-> (solr/query solr/impl "clients" @@ -170,7 +168,6 @@ matching-ids) (set (map :db/id (:clients args)))) - query (cond-> {:query {:find [] :in ['$] :where []} @@ -179,7 +176,6 @@ (merge-query {:query {:in ['[?e ...]]} :args [(set valid-ids)]}) - (:sort args) (add-sorter-fields {"name" ['[?e :client/name ?sort-name]]} args) @@ -195,7 +191,6 @@ (map cleanse))] results)) - (defn get-graphql-page [args] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] diff --git a/src/clj/auto_ap/datomic/expected_deposit.clj b/src/clj/auto_ap/datomic/expected_deposit.clj index 8e390c97..a06292b6 100644 --- a/src/clj/auto_ap/datomic/expected_deposit.clj +++ b/src/clj/auto_ap/datomic/expected_deposit.clj @@ -36,7 +36,7 @@ "date" ['[?e :expected-deposit/date ?sort-date]] "total" ['[?e :expected-deposit/total ?sort-total]] "fee" ['[?e :expected-deposit/fee ?sort-fee]]} - args) + args) true (merge-query {:query {:in ['[?xx ...]] @@ -58,13 +58,13 @@ '[?client-id :client/code ?client-code]]} :args [(:client-code args)]}) - (:total-gte args) + (:total-gte args) (merge-query {:query {:in ['?total-gte] :where ['[?e :expected-deposit/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte args)]}) - (:total-lte args) + (:total-lte args) (merge-query {:query {:in ['?total-lte] :where ['[?e :expected-deposit/total ?a] '[(<= ?a ?total-lte)]]} @@ -76,7 +76,6 @@ '[(iol-ion.query/dollars= ?expected-deposit-total ?total)]]} :args [(:total args)]}) - (:start (:date-range args)) (merge-query {:query {:in '[?start-date] :where ['[?e :expected-deposit/date ?date] @@ -92,7 +91,7 @@ true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :expected-deposit/date ?sort-default]]}}))] - + (cond->> (query2 query) true (apply-sort-3 args) true (apply-pagination args)))) @@ -101,26 +100,26 @@ (let [results (->> (pull-many db default-read ids) (group-by :db/id)) payments (->> ids - (map results) - (map first) - (mapv <-datomic) - (map (fn get-totals [ed] - (assoc ed :totals - (->> (dc/q '[:find ?d4 (count ?c) (sum ?a) - :in $ ?ed - :where [?ed :expected-deposit/charges ?c] - [?c :charge/total ?a] - [?o :sales-order/charges ?c] - [?o :sales-order/date ?d] - [(clj-time.coerce/from-date ?d) ?d2] - [(auto-ap.time/localize ?d2) ?d3] - [(clj-time.coerce/to-local-date ?d3) ?d4]] - (dc/db conn) - (:db/id ed)) - (map (fn [[date count amount]] - {:date (c/to-date-time date) - :count count - :amount amount})))))))] + (map results) + (map first) + (mapv <-datomic) + (map (fn get-totals [ed] + (assoc ed :totals + (->> (dc/q '[:find ?d4 (count ?c) (sum ?a) + :in $ ?ed + :where [?ed :expected-deposit/charges ?c] + [?c :charge/total ?a] + [?o :sales-order/charges ?c] + [?o :sales-order/date ?d] + [(clj-time.coerce/from-date ?d) ?d2] + [(auto-ap.time/localize ?d2) ?d3] + [(clj-time.coerce/to-local-date ?d3) ?d4]] + (dc/db conn) + (:db/id ed)) + (map (fn [[date count amount]] + {:date (c/to-date-time date) + :count count + :amount amount})))))))] payments)) (defn get-graphql [args] diff --git a/src/clj/auto_ap/datomic/invoices.clj b/src/clj/auto_ap/datomic/invoices.clj index f585123a..130bb337 100644 --- a/src/clj/auto_ap/datomic/invoices.clj +++ b/src/clj/auto_ap/datomic/invoices.clj @@ -34,16 +34,15 @@ (defn <-datomic [x] (-> x - (update :invoice/date coerce/from-date) - (update :invoice/due coerce/from-date) - (update :invoice/scheduled-payment coerce/from-date) - (update :invoice/status :db/ident) - (update :invoice/expense-accounts (fn [eas] - (map - #(update % :invoice-expense-account/account d-accounts/clientize (:db/id (:invoice/client x))) - eas))) - (rename-keys {:invoice-payment/_invoice :invoice/payments}))) - + (update :invoice/date coerce/from-date) + (update :invoice/due coerce/from-date) + (update :invoice/scheduled-payment coerce/from-date) + (update :invoice/status :db/ident) + (update :invoice/expense-accounts (fn [eas] + (map + #(update % :invoice-expense-account/account d-accounts/clientize (:db/id (:invoice/client x))) + eas))) + (rename-keys {:invoice-payment/_invoice :invoice/payments}))) (defn raw-graphql-ids ([args] @@ -63,37 +62,29 @@ valid-clients]} (cond-> {:query {:find [] :in '[$ [?clients ?start ?end]] - :where '[ - [(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] - ]} + :where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]} :args [db [valid-clients (some-> (:start (:date-range args)) coerce/to-date) (some-> (:end (:date-range args)) coerce/to-date)]]} - (:client-id args) (merge-query {:query {:in ['?client-id] :where ['[?e :invoice/client ?client-id]]} - :args [ (:client-id args)]}) + :args [(:client-id args)]}) (:client-code args) (merge-query {:query {:in ['?client-code] :where ['[?e :invoice/client ?client-id] '[?client-id :client/code ?client-code]]} - :args [ (:client-code args)]}) + :args [(:client-code args)]}) (:original-id args) (merge-query {:query {:in ['?original-id] - :where [ - '[?e :invoice/client ?c] + :where ['[?e :invoice/client ?c] '[?c :client/original-id ?original-id]]} - :args [ (cond-> (:original-id args) - (string? (:original-id args)) Long/parseLong )]}) - - - - + :args [(cond-> (:original-id args) + (string? (:original-id args)) Long/parseLong)]}) (:start (:due-range args)) (merge-query {:query {:in '[?start-due] :where ['[?e :invoice/due ?due] @@ -104,34 +95,33 @@ :where ['[?e :invoice/due ?due] '[(<= ?due ?end-due)]]} :args [(coerce/to-date (:end (:due-range args)))]}) - (:import-status args) (merge-query {:query {:in ['?import-status] :where ['[?e :invoice/import-status ?import-status]]} - :args [ (keyword "import-status" (:import-status args))]}) + :args [(keyword "import-status" (:import-status args))]}) (:status args) (merge-query {:query {:in ['?status] :where ['[?e :invoice/status ?status]]} - :args [ (:status args)]}) + :args [(:status args)]}) (:vendor-id args) (merge-query {:query {:in ['?vendor-id] :where ['[?e :invoice/vendor ?vendor-id]]} - :args [ (:vendor-id args)]}) + :args [(:vendor-id args)]}) (:account-id args) (merge-query {:query {:in ['?account-id] :where ['[?e :invoice/expense-accounts ?iea ?] '[?iea :invoice-expense-account/account ?account-id]]} - :args [ (:account-id args)]}) + :args [(:account-id args)]}) - (:amount-gte args) + (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :invoice/total ?total-filter] '[(>= ?total-filter ?amount-gte)]]} :args [(:amount-gte args)]}) - (:amount-lte args) + (:amount-lte args) (merge-query {:query {:in ['?amount-lte] :where ['[?e :invoice/total ?total-filter] '[(<= ?total-filter ?amount-lte)]]} @@ -151,7 +141,7 @@ (:unresolved args) (merge-query {:query {:in [] :where ['(or-join [?e] - (not [?e :invoice/expense-accounts ]) + (not [?e :invoice/expense-accounts]) (and [?e :invoice/expense-accounts ?ea] (not [?ea :invoice-expense-account/account])))]} :args []}) @@ -165,7 +155,7 @@ (:sort args) (add-sorter-fields {"client" ['[?e :invoice/client ?c] '[?c :client/name ?sort-client]] "vendor" ['[?e :invoice/vendor ?v] - '[?v :vendor/name ?sort-vendor]] + '[?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]] @@ -176,16 +166,15 @@ "outstanding-balance" ['[?e :invoice/outstanding-balance ?sort-outstanding-balance]]} args) true - (merge-query {:query {:find ['?sort-default '?e ]}}) ))] + (merge-query {:query {:find ['?sort-default '?e]}})))] (->> (observable-query query) - (apply-sort-3 args) - (apply-pagination args))))) - + (apply-sort-3 args) + (apply-pagination args))))) (defn graphql-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) - + invoices (->> ids (map results) (map first) @@ -193,40 +182,38 @@ invoices)) (defn sum-outstanding [ids] - - (->> - (dc/q {:find ['?id '?o] - :in ['$ '[?id ...]] - :where ['[?id :invoice/outstanding-balance ?o]]} - (dc/db conn) - 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))) + + (->> + (dc/q {:find ['?id '?o] + :in ['$ '[?id ...]] + :where ['[?id :invoice/total ?o]]} + (dc/db conn) + ids) + (map last) + (reduce + + + 0.0))) (defn get-graphql [args] - + (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args) outstanding (sum-outstanding ids-to-retrieve) total-amount (sum-total-amount ids-to-retrieve)] - [(->> (graphql-results ids-to-retrieve db args)) matching-count outstanding @@ -239,56 +226,51 @@ (defn get-multi [ids] (map <-datomic - (pull-many (dc/db conn) default-read ids ))) - - + (pull-many (dc/db conn) default-read ids))) (defn find-conflicting [{:keys [:invoice/invoice-number :invoice/vendor :invoice/client :db/id]}] - + (->> (dc/q - {:find [(list 'pull '?e default-read)] - :in ['$ '?invoice-number '?vendor '?client '?invoice-id] - :where '[[?e :invoice/invoice-number ?invoice-number] - [?e :invoice/vendor ?vendor] - [?e :invoice/client ?client] - (not [?e :invoice/status :invoice-status/voided]) - [(not= ?e ?invoice-id)]]} - (dc/db conn) invoice-number vendor client (or id 0)) + {:find [(list 'pull '?e default-read)] + :in ['$ '?invoice-number '?vendor '?client '?invoice-id] + :where '[[?e :invoice/invoice-number ?invoice-number] + [?e :invoice/vendor ?vendor] + [?e :invoice/client ?client] + (not [?e :invoice/status :invoice-status/voided]) + [(not= ?e ?invoice-id)]]} + (dc/db conn) invoice-number vendor client (or id 0)) (map first) (map <-datomic))) - (defn get-existing-set [] (let [vendored-results (set (dc/q {:find ['?vendor '?client '?invoice-number] :in ['$] :where '[[?e :invoice/invoice-number ?invoice-number] [?e :invoice/vendor ?vendor] [?e :invoice/client ?client] - (not [?e :invoice/status :invoice-status/voided]) - ]} + (not [?e :invoice/status :invoice-status/voided])]} (dc/db conn))) vendorless-results (->> (dc/q {:find ['?client '?invoice-number] :in ['$] :where '[[?e :invoice/invoice-number ?invoice-number] (not [?e :invoice/vendor]) [?e :invoice/client ?client] - (not [?e :invoice/status :invoice-status/voided]) - ]} + (not [?e :invoice/status :invoice-status/voided])]} (dc/db conn)) (mapv (fn [[client invoice-number]] - [nil client invoice-number]) ) + [nil client invoice-number])) set)] (into vendored-results vendorless-results))) - + (defn filter-ids [ids] - (if ids - (->> - (dc/q {:find ['?e] - :in ['$ '[?e ...]] - :where ['[?e :invoice/date]]} - (dc/db conn) ids) - (map first) - vec) + (if ids + (->> + (dc/q {:find ['?e] + :in ['$ '[?e ...]] + :where ['[?e :invoice/date]]} + (dc/db conn) ids) + (map first) + vec) [])) (defn code-invoice @@ -317,7 +299,7 @@ client-id)))) [schedule-payment-dom] (map first (dc/q '[:find ?dom :in $ ?v ?c - :where [?v :vendor/schedule-payment-dom ?sp ] + :where [?v :vendor/schedule-payment-dom ?sp] [?sp :vendor-schedule-payment-dom/client ?c] [?sp :vendor-schedule-payment-dom/dom ?dom]] db diff --git a/src/clj/auto_ap/datomic/ledger.clj b/src/clj/auto_ap/datomic/ledger.clj index cd30e80b..11bfeed5 100644 --- a/src/clj/auto_ap/datomic/ledger.clj +++ b/src/clj/auto_ap/datomic/ledger.clj @@ -34,8 +34,8 @@ (some-> (:start (:date-range args)) coerce/to-date) (some-> (:end (:date-range args)) coerce/to-date)]]} - (:only-external args) - (merge-query {:query {:where ['(not [?e :journal-entry/original-entity ])]}}) + (:only-external args) + (merge-query {:query {:where ['(not [?e :journal-entry/original-entity])]}}) (seq (:external-id-like args)) (merge-query {:query {:in ['?external-id-like] @@ -48,12 +48,11 @@ :where ['[?e :journal-entry/source ?source]]} :args [(:source args)]}) - (:vendor-id args) + (:vendor-id args) (merge-query {:query {:in ['?vendor-id] :where ['[?e :journal-entry/vendor ?vendor-id]]} :args [(:vendor-id args)]}) - (or (seq (:numeric-code args)) (:bank-account-id args) (not-empty (:location args))) @@ -70,36 +69,35 @@ :args [(vec (for [{:keys [from to]} (:numeric-code args)] [(or from 0) (or to 99999)]))]}) - - (:amount-gte args) + (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :journal-entry/amount ?a] '[(>= ?a ?amount-gte)]]} :args [(:amount-gte args)]}) - (:amount-lte args) + (:amount-lte args) (merge-query {:query {:in ['?amount-lte] :where ['[?e :journal-entry/amount ?a] '[(<= ?a ?amount-lte)]]} :args [(:amount-lte args)]}) - (:bank-account-id args) + (:bank-account-id args) (merge-query {:query {:in ['?a] :where ['[?li :journal-entry-line/account ?a]]} :args [(:bank-account-id args)]}) - (:account-id args) + (:account-id args) (merge-query {:query {:in ['?a2] :where ['[?e :journal-entry/line-items ?li2] '[?li2 :journal-entry-line/account ?a2]]} :args [(:account-id args)]}) - (not-empty (:location args)) + (not-empty (:location args)) (merge-query {:query {:in ['?location] :where ['[?li :journal-entry-line/location ?location]]} :args [(:location args)]}) - (not-empty (:locations args)) + (not-empty (:locations args)) (merge-query {:query {:in ['[?location ...]] :where ['[?li :journal-entry-line/location ?location]]} :args [(:locations args)]}) @@ -118,7 +116,7 @@ (merge-query {:query {:find ['?sort-default '?e]}})))] (->> (observable-query query) (apply-sort-4 (assoc args :default-asc? true)) - (apply-pagination args)))) + (apply-pagination args)))) (defn graphql-results [ids db _] (let [results (->> (pull-many db '[* {:journal-entry/client [:client/name :client/code :db/id] @@ -134,15 +132,15 @@ (update je :journal-entry/line-items (fn [jels] (map - #(update % :journal-entry-line/account d-accounts/clientize (:db/id (:journal-entry/client je))) - jels))))) + #(update % :journal-entry-line/account d-accounts/clientize (:db/id (:journal-entry/client je))) + jels))))) (filter (fn [je] (every? - (fn [jel] - (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] - (or (nil? include-in-reports) - (true? include-in-reports)))) - (:journal-entry/line-items je)))) + (fn [jel] + (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] + (or (nil? include-in-reports) + (true? include-in-reports)))) + (:journal-entry/line-items je)))) (group-by :db/id))] (->> ids (map results) @@ -156,7 +154,7 @@ matching-count])) (defn filter-ids [ids] - (if ids + (if ids (->> (dc/q {:find ['?e] :in ['$ '[?e ...]] :where ['[?e :journal-entry/date]]} diff --git a/src/clj/auto_ap/datomic/sales_orders.clj b/src/clj/auto_ap/datomic/sales_orders.clj index 09f9c409..ba3528f6 100644 --- a/src/clj/auto_ap/datomic/sales_orders.clj +++ b/src/clj/auto_ap/datomic/sales_orders.clj @@ -23,9 +23,9 @@ (update :sales-order/charges (fn [cs] (map (fn [c] (-> c - (update :charge/processor :db/ident) - (set/rename-keys {:expected-deposit/_charges :expected-deposit}) - (update :expected-deposit first))) + (update :charge/processor :db/ident) + (set/rename-keys {:expected-deposit/_charges :expected-deposit}) + (update :expected-deposit first))) cs))))) (def default-read '[:db/id @@ -43,8 +43,7 @@ :sales-order/source, :sales-order/reference-link, {:sales-order/client [:client/name :db/id :client/code] - :sales-order/charges [ - :charge/type-name, + :sales-order/charges [:charge/type-name, :charge/total, :charge/tax, :charge/tip, @@ -63,7 +62,6 @@ (set/intersection #{(:client-id args)} visible-clients) - (:client-code args) (set/intersection #{(pull-id db [:client/code (:client-code args)])} visible-clients) @@ -79,7 +77,7 @@ :where '[[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} :args [db [selected-clients (some-> (:start (:date-range args)) c/to-date) - (some-> (:end (:date-range args)) c/to-date )]]} + (some-> (:end (:date-range args)) c/to-date)]]} (:sort args) (add-sorter-fields-2 {"client" ['[?e :sales-order/client ?c] '[?c :client/name ?sort-client]] @@ -108,13 +106,13 @@ '[?chg :charge/type-name ?type-name]]} :args [(:type-name args)]}) - (:total-gte args) + (:total-gte args) (merge-query {:query {:in ['?total-gte] :where ['[?e :sales-order/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte args)]}) - (:total-lte args) + (:total-lte args) (merge-query {:query {:in ['?total-lte] :where ['[?e :sales-order/total ?a] '[(<= ?a ?total-lte)]]} @@ -136,7 +134,7 @@ (defn graphql-results [ids db _] (let [results (->> (pull-many db default-read ids) - (group-by :db/id)) + (group-by :db/id)) payments (->> ids (map results) (map first) @@ -146,14 +144,14 @@ (defn summarize-orders [ids] (let [[total tax] (->> - (dc/q {:find ['(sum ?t) '(sum ?tax)] - :with ['?id] - :in ['$ '[?id ...]] - :where ['[?id :sales-order/total ?t] - '[?id :sales-order/tax ?tax]]} - (dc/db conn) - ids) - first)] + (dc/q {:find ['(sum ?t) '(sum ?tax)] + :with ['?id] + :in ['$ '[?id ...]] + :where ['[?id :sales-order/total ?t] + '[?id :sales-order/tax ?tax]]} + (dc/db conn) + ids) + first)] {:total total :tax tax})) diff --git a/src/clj/auto_ap/datomic/transaction_rules.clj b/src/clj/auto_ap/datomic/transaction_rules.clj index 959ee04a..f341a6f8 100644 --- a/src/clj/auto_ap/datomic/transaction_rules.clj +++ b/src/clj/auto_ap/datomic/transaction_rules.clj @@ -49,7 +49,7 @@ "note" ['[?e :transaction-rule/note ?sort-note]] "amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]] "amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]} - args) + args) (seq (:clients args)) (merge-query {:query {:in ['[?xx ...]] @@ -78,7 +78,6 @@ (merge-query {:query {:find ['?e] :where ['[?e :transaction-rule/transaction-approval-status]]}}))] - (cond->> (query2 query) true (apply-sort-3 args) true (apply-pagination args)))) @@ -99,13 +98,13 @@ matching-count])) (defn get-by-id [id] - (->> + (->> (dc/pull (dc/db conn) default-read id) (<-datomic))) (defn get-all [] (mapv first - (dc/q {:find [(list 'pull '?e default-read )] + (dc/q {:find [(list 'pull '?e default-read)] :in ['$] :where ['[?e :transaction-rule/transaction-approval-status]]} (dc/db conn)))) diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index 90207718..4a472631 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -37,7 +37,6 @@ (map first) set))) - (defn raw-graphql-ids ([args] (raw-graphql-ids (dc/db conn) args)) ([db args] @@ -87,7 +86,6 @@ :where ['[?e :transaction/vendor ?vendor-id]]} :args [(:vendor-id args)]}) - (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :transaction/amount ?a] diff --git a/src/clj/auto_ap/datomic/users.clj b/src/clj/auto_ap/datomic/users.clj index bac3ef54..a2f2f70e 100644 --- a/src/clj/auto_ap/datomic/users.clj +++ b/src/clj/auto_ap/datomic/users.clj @@ -4,15 +4,15 @@ [datomic.api :as dc] [datomic.api :as d])) -(defn find-or-insert! [{:keys [:user/provider :user/provider-id ] :as new-user}] +(defn find-or-insert! [{:keys [:user/provider :user/provider-id] :as new-user}] (let [is-first-user? (not (seq (dc/q [:find '?e :in '$ :where '[?e :user/provider]] (dc/db conn)))) user-id (ffirst (dc/q '[:find ?e - :in $ ?provider ?provider-id - :where [?e :user/provider ?provider] - [?e :user/provider-id ?provider-id]] + :in $ ?provider ?provider-id + :where [?e :user/provider ?provider] + [?e :user/provider-id ?provider-id]] (dc/db conn) provider provider-id)) result @(dc/transact conn [[:upsert-entity (cond-> (assoc new-user :db/id (or user-id "user") :user/last-login (java.util.Date.)) diff --git a/src/clj/auto_ap/datomic/vendors.clj b/src/clj/auto_ap/datomic/vendors.clj index c583351c..b02b5da9 100644 --- a/src/clj/auto_ap/datomic/vendors.clj +++ b/src/clj/auto_ap/datomic/vendors.clj @@ -18,29 +18,29 @@ (:vendor/legal-entity-tin-type a) (update :vendor/legal-entity-tin-type :db/ident) (:vendor/legal-entity-1099-type a) (update :vendor/legal-entity-1099-type :db/ident) true (assoc :usage (:vendor-usage/_vendor a)) - true (dissoc :vendor-usage/_vendor ))) + true (dissoc :vendor-usage/_vendor))) (defn cleanse [id vendor] (let [clients (if-let [clients (limited-clients id)] (set (map :db/id clients)) nil)] (if clients - (-> vendor + (-> vendor (update :vendor/account-overrides (fn [aos] (->> aos (filter #(clients (:db/id (:vendor-account-override/client %)))) (map #(update % :vendor-account-override/account d-accounts/clientize (:db/id (:vendor-account-override/client %))))))) (update :vendor/terms-overrides (fn [to] (filter #(clients (:db/id (:vendor-terms-override/client %))) to))) (update :vendor/schedule-payment-dom (fn [to] (filter #(clients (:db/id (:vendor-schedule-payment-dom/client %))) to)))) - (-> vendor + (-> vendor (update :vendor/account-overrides (fn [aos] (->> aos (map #(update % :vendor-account-override/account d-accounts/clientize (:db/id (:vendor-account-override/client %))))))))))) (def default-read '[* {:vendor/account-overrides [* {:vendor-account-override/client [:client/name :db/id] - :vendor-account-override/account [:account/name :account/numeric-code :db/id - {:account/client-overrides [:account-client-override/client :account-client-override/name]}]}] + :vendor-account-override/account [:account/name :account/numeric-code :db/id + {:account/client-overrides [:account-client-override/client :account-client-override/name]}]}] :vendor/terms-overrides [* {:vendor-terms-override/client [:client/name :client/code :db/id]}] :vendor/schedule-payment-dom [* {:vendor-schedule-payment-dom/client [:client/name :client/code :db/id]}] :vendor/automatically-paid-when-due [:db/id :client/name] @@ -50,14 +50,13 @@ :vendor/plaid-merchant [:db/id :plaid-merchant/name] :vendor-usage/_vendor [:vendor-usage/client :vendor-usage/count]}]) - (defn raw-graphql-ids [db args] (let [query (cond-> {:query {:find [] :in ['$] :where []} :args [db]} (:sort args) (add-sorter-fields {"name" ['[?e :vendor/name ?sort-name]]} - args) + args) (not (str/blank? (:name-like args))) (merge-query {:query {:in ['?name-like] @@ -70,25 +69,22 @@ (merge-query {:query {:find ['?e] :where ['[?e :vendor/name]]}}))] - (cond->> (query2 query) true (apply-sort-3 args) true (apply-pagination args)))) (defn trim-usage [v limited-clients] (->> (if limited-clients - (update v :usage (fn [usages] - (->> usages - (filter (comp (set (map :db/id limited-clients)) :db/id :vendor-usage/client)) - (map (fn [u] {:client-id (:db/id (:vendor-usage/client u)) - :count (:vendor-usage/count u)}))))) + (update v :usage (fn [usages] + (->> usages + (filter (comp (set (map :db/id limited-clients)) :db/id :vendor-usage/client)) + (map (fn [u] {:client-id (:db/id (:vendor-usage/client u)) + :count (:vendor-usage/count u)}))))) - (update v :usage (fn [usages] - (->> usages - (map (fn [u] {:client-id (:db/id (:vendor-usage/client u)) - :count (:vendor-usage/count u)})))))) - - )) + (update v :usage (fn [usages] + (->> usages + (map (fn [u] {:client-id (:db/id (:vendor-usage/client u)) + :count (:vendor-usage/count u)})))))))) (defn graphql-results [ids db args] (let [results (->> (pull-many db default-read ids) @@ -104,9 +100,7 @@ (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] [(->> (graphql-results ids-to-retrieve db args)) - matching-count]) - - ) + matching-count])) (defn get-graphql-by-id [args id] (->> (dc/q {:find [(list 'pull '?e default-read)] @@ -120,29 +114,28 @@ first)) (defn get-by-id [id] - + (->> (dc/q '[:find (pull ?e [* - {:vendor/default-account [:account/name :db/id :account/location] - :vendor/legal-entity-tin-type [:db/ident :db/id] - :vendor/legal-entity-1099-type [:db/ident :db/id] - :vendor/plaid-merchant [:db/id :plaid-merchant/name] - :vendor/account-overrides [* {:vendor-account-override/client [:client/name :db/id] - :vendor-account-override/account [:account/name :account/numeric-code :db/id]}] - :vendor/terms-overrides [* {:vendor-terms-override/client [:client/name :db/id]}] - :vendor/schedule-payment-dom [* {:vendor-schedule-payment-dom/client [:client/name :db/id]}] - :vendor/automatically-paid-when-due [:db/id :client/name]}]) - :in $ ?e - :where [?e]] - (dc/db conn) - id) + {:vendor/default-account [:account/name :db/id :account/location] + :vendor/legal-entity-tin-type [:db/ident :db/id] + :vendor/legal-entity-1099-type [:db/ident :db/id] + :vendor/plaid-merchant [:db/id :plaid-merchant/name] + :vendor/account-overrides [* {:vendor-account-override/client [:client/name :db/id] + :vendor-account-override/account [:account/name :account/numeric-code :db/id]}] + :vendor/terms-overrides [* {:vendor-terms-override/client [:client/name :db/id]}] + :vendor/schedule-payment-dom [* {:vendor-schedule-payment-dom/client [:client/name :db/id]}] + :vendor/automatically-paid-when-due [:db/id :client/name]}]) + :in $ ?e + :where [?e]] + (dc/db conn) + id) (map first) (map <-datomic) (first))) - (defn terms-for-client-id [vendor client-id] (or - (->> + (->> (filter (fn [to] (= (:db/id (:vendor-terms-override/client to)) @@ -153,8 +146,8 @@ (:vendor/terms vendor))) (defn account-for-client-id [vendor client-id] - (or - (->> + (or + (->> (filter (fn [to] (= (:db/id (:vendor-account-override/client to)) @@ -165,7 +158,7 @@ (:vendor/default-account vendor))) (defn automatically-paid-for-client-id? [vendor client-id] - (->> + (->> (:vendor/automatically-paid-when-due vendor) (filter (fn [client] diff --git a/src/clj/auto_ap/datomic/yodlee_merchants.clj b/src/clj/auto_ap/datomic/yodlee_merchants.clj index da234046..7705dc04 100644 --- a/src/clj/auto_ap/datomic/yodlee_merchants.clj +++ b/src/clj/auto_ap/datomic/yodlee_merchants.clj @@ -6,8 +6,8 @@ (defn get-merchants [_] ;; TODO admin? (->> - (dc/q {:find ['(pull ?e [:yodlee-merchant/name :yodlee-merchant/yodlee-id :db/id])] - :in ['$] - :where [['?e :yodlee-merchant/name]]} - (dc/db conn)) - (mapv first))) + (dc/q {:find ['(pull ?e [:yodlee-merchant/name :yodlee-merchant/yodlee-id :db/id])] + :in ['$] + :where [['?e :yodlee-merchant/name]]} + (dc/db conn)) + (mapv first))) diff --git a/src/clj/auto_ap/ezcater/core.clj b/src/clj/auto_ap/ezcater/core.clj index 900a214a..30f603e0 100644 --- a/src/clj/auto_ap/ezcater/core.clj +++ b/src/clj/auto_ap/ezcater/core.clj @@ -1,6 +1,6 @@ (ns auto-ap.ezcater.core (:require - [auto-ap.datomic :refer [conn random-tempid]] + [auto-ap.datomic :refer [conn random-tempid]] [datomic.api :as dc] [clj-http.client :as client] [venia.core :as v] @@ -20,42 +20,41 @@ :body (json/write-str {"query" (v/graphql-query q)}) :as :json}) :body - :data - )) + :data)) (defn get-caterers [integration] (:caterers (query integration {:venia/queries [{:query/data - [:caterers [:name :uuid [:address [:name :street]]]]}]} ))) + [:caterers [:name :uuid [:address [:name :street]]]]}]}))) (defn get-subscriptions [integration] (->> (query integration {:venia/queries [{:query/data - [:subscribers [:id [:subscriptions [:parentId :parentEntity :eventEntity :eventKey]] ]]}]} ) + [:subscribers [:id [:subscriptions [:parentId :parentEntity :eventEntity :eventKey]]]]}]}) :subscribers first :subscriptions)) (defn get-integrations [] (map first (dc/q '[:find (pull ?i [:ezcater-integration/api-key - :ezcater-integration/subscriber-uuid - :db/id - :ezcater-integration/integration-status [:db/id]]) - :in $ - :where [?i :ezcater-integration/api-key]] - (dc/db conn)))) + :ezcater-integration/subscriber-uuid + :db/id + :ezcater-integration/integration-status [:db/id]]) + :in $ + :where [?i :ezcater-integration/api-key]] + (dc/db conn)))) (defn mark-integration-status [integration integration-status] @(dc/transact conn - [{:db/id (:db/id integration) - :ezcater-integration/integration-status (assoc integration-status - :db/id (or (-> integration :ezcater-integration/integration-status :db/id) - (random-tempid)))}])) + [{:db/id (:db/id integration) + :ezcater-integration/integration-status (assoc integration-status + :db/id (or (-> integration :ezcater-integration/integration-status :db/id) + (random-tempid)))}])) (defn upsert-caterers ([integration] @(dc/transact conn (for [caterer (get-caterers integration)] - {:db/id (:db/id integration) + {:db/id (:db/id integration) :ezcater-integration/caterers [{:ezcater-caterer/name (str (:name caterer) " (" (:street (:address caterer)) ")") :ezcater-caterer/search-terms (str (:name caterer) " " (:street (:address caterer))) :ezcater-caterer/uuid (:uuid caterer)}]})))) @@ -64,14 +63,14 @@ ([integration] (let [extant (get-subscriptions integration) to-ensure (set (map first (dc/q '[:find ?cu - :in $ - :where [_ :client/ezcater-locations ?el] - [?el :ezcater-location/caterer ?c] - [?c :ezcater-caterer/uuid ?cu]] - (dc/db conn)))) + :in $ + :where [_ :client/ezcater-locations ?el] + [?el :ezcater-location/caterer ?c] + [?c :ezcater-caterer/uuid ?cu]] + (dc/db conn)))) to-create (set/difference - to-ensure - (set (map :parentId extant)))] + to-ensure + (set (map :parentId extant)))] (doseq [parentId to-create] (query integration {:venia/operation {:operation/type :mutation @@ -94,7 +93,6 @@ :eventKey 'cancelled}} [[:subscription [:parentId :parentEntity :eventEntity :eventKey]]]]]}))))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn upsert-ezcater ([] (upsert-ezcater (get-integrations))) @@ -115,12 +113,11 @@ (defn get-caterer [caterer-uuid] (dc/pull (dc/db conn) - '[:ezcater-caterer/name - {:ezcater-integration/_caterers [:ezcater-integration/api-key]} - {:ezcater-location/_caterer [:ezcater-location/location - {:client/_ezcater-locations [:client/code]}]}] - [:ezcater-caterer/uuid caterer-uuid])) - + '[:ezcater-caterer/name + {:ezcater-integration/_caterers [:ezcater-integration/api-key]} + {:ezcater-location/_caterer [:ezcater-location/location + {:client/_ezcater-locations [:client/code]}]}] + [:ezcater-caterer/uuid caterer-uuid])) (defn round-carry-cents [f] (with-precision 2 (double (.setScale (bigdec f) 2 java.math.RoundingMode/HALF_UP)))) @@ -135,126 +132,118 @@ 0.15M :else 0.07M)] - (round-carry-cents - (* commision% - 0.01M - (+ - (-> order :totals :subTotal :subunits ) - (reduce + - 0 - (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order))))))))) - -(defn ccp-fee [order] - (round-carry-cents - (* 0.000299M - (+ - (-> order :totals :subTotal :subunits ) - (-> order :totals :salesTax :subunits ) + (round-carry-cents + (* commision% + 0.01M + (+ + (-> order :totals :subTotal :subunits) (reduce + 0 - (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order)))))))) + (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order))))))))) + +(defn ccp-fee [order] + (round-carry-cents + (* 0.000299M + (+ + (-> order :totals :subTotal :subunits) + (-> order :totals :salesTax :subunits) + (reduce + + 0 + (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order)))))))) (defn order->sales-order [{{:keys [timestamp]} :event {:keys [orderItems]} :catererCart :keys [client-code client-location uuid] :as order}] (let [adjustment (round-carry-cents (- (+ (-> order :totals :subTotal :subunits (* 0.01)) (-> order :totals :salesTax :subunits (* 0.01))) - (-> order :catererCart :totals :catererTotalDue ) + (-> order :catererCart :totals :catererTotalDue) (commision order) (ccp-fee order))) service-charge (+ (commision order) (ccp-fee order)) tax (-> order :totals :salesTax :subunits (* 0.01)) tip (-> order :totals :tip :subunits (* 0.01))] #:sales-order - {:date (atime/localize (coerce/to-date-time timestamp)) - :external-id (str "ezcater/order/" client-code "-" client-location "-" uuid) - :client [:client/code client-code] - :location client-location - :reference-link (str (url/url "https://ezmanage.ezcater.com/orders/" uuid )) - :line-items [#:order-line-item - {:external-id (str "ezcater/order/" client-code "-" client-location "-" uuid "-" 0) - :item-name "EZCater Catering" - :category "EZCater Catering" - :discount adjustment - :tax tax - :total (+ (-> order :totals :subTotal :subunits (* 0.01)) - tax - tip)}] - :charges [#:charge - {:type-name "CARD" - :date (atime/localize (coerce/to-date-time timestamp)) - :client [:client/code client-code] - :location client-location - :external-id (str "ezcater/charge/" uuid) - :processor :ccp-processor/ezcater - :total (+ (-> order :totals :subTotal :subunits (* 0.01)) - tax - tip) - :tip tip}] - - :total (+ (-> order :totals :subTotal :subunits (* 0.01)) - tax - tip) - :discount adjustment - :service-charge service-charge - :tax tax - :tip tip - :returns 0.0 - :vendor :vendor/ccp-ezcater})) + {:date (atime/localize (coerce/to-date-time timestamp)) + :external-id (str "ezcater/order/" client-code "-" client-location "-" uuid) + :client [:client/code client-code] + :location client-location + :reference-link (str (url/url "https://ezmanage.ezcater.com/orders/" uuid)) + :line-items [#:order-line-item + {:external-id (str "ezcater/order/" client-code "-" client-location "-" uuid "-" 0) + :item-name "EZCater Catering" + :category "EZCater Catering" + :discount adjustment + :tax tax + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip)}] + :charges [#:charge + {:type-name "CARD" + :date (atime/localize (coerce/to-date-time timestamp)) + :client [:client/code client-code] + :location client-location + :external-id (str "ezcater/charge/" uuid) + :processor :ccp-processor/ezcater + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip) + :tip tip}] + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip) + :discount adjustment + :service-charge service-charge + :tax tax + :tip tip + :returns 0.0 + :vendor :vendor/ccp-ezcater})) (defn get-by-id [integration id] - (query - integration - {:venia/queries [[:order {:id id} - [:uuid - :orderNumber - :orderSourceType - [:caterer - [:name - :uuid - [:address [:street]]]] - [:event - [:timestamp - :catererHandoffFoodTime - :orderType]] - [:catererCart [[:orderItems - [:name - :quantity - :posItemId - [:totalInSubunits - [:currency - :subunits]]]] - [:totals - [:catererTotalDue]] - [:feesAndDiscounts - {:type 'DELIVERY_FEE} - [[:cost - [:currency - :subunits]]]]]] - [:totals [[:customerTotalDue - [ - :currency - :subunits - ]] - [:pointOfSaleIntegrationFee - [ - :currency - :subunits - ]] - [:tip - [:currency - :subunits]] - [:salesTax - [ - :currency - :subunits - ]] - [:salesTaxRemittance - [:currency - :subunits - ]] - [:subTotal - [:currency - :subunits]]]]]]]})) + (query + integration + {:venia/queries [[:order {:id id} + [:uuid + :orderNumber + :orderSourceType + [:caterer + [:name + :uuid + [:address [:street]]]] + [:event + [:timestamp + :catererHandoffFoodTime + :orderType]] + [:catererCart [[:orderItems + [:name + :quantity + :posItemId + [:totalInSubunits + [:currency + :subunits]]]] + [:totals + [:catererTotalDue]] + [:feesAndDiscounts + {:type 'DELIVERY_FEE} + [[:cost + [:currency + :subunits]]]]]] + [:totals [[:customerTotalDue + [:currency + :subunits]] + [:pointOfSaleIntegrationFee + [:currency + :subunits]] + [:tip + [:currency + :subunits]] + [:salesTax + [:currency + :subunits]] + [:salesTaxRemittance + [:currency + :subunits]] + [:subTotal + [:currency + :subunits]]]]]]]})) (defn lookup-order [json] (let [caterer (get-caterer (get json "parent_id")) @@ -262,25 +251,25 @@ client (-> caterer :ezcater-location/_caterer first :client/_ezcater-locations :client/code) location (-> caterer :ezcater-location/_caterer first :ezcater-location/location)] (if (and client location) - (doto - (-> (get-by-id integration (get json "entity_id")) - (:order) - (assoc :client-code client - :client-location location)) + (doto + (-> (get-by-id integration (get json "entity_id")) + (:order) + (assoc :client-code client + :client-location location)) (#(alog/info ::order-details :detail %))) (alog/warn ::caterer-no-longer-has-location :json json)))) (defn import-order [json] ;; {"id" "bf3dcf5c-a68f-42d9-9084-049133e03d3d", "parent_type" "Caterer", "parent_id" "91541331-d7ae-4634-9e8b-ccbbcfb2ce70", "entity_type" "Order", "entity_id" "9ab05fee-a9c5-483b-a7f2-14debde4b7a8", "key" "accepted", "occurred_at" "2022-07-21T19:21:07.549Z"} (alog/info - ::try-import-order - :json json) + ::try-import-order + :json json) @(dc/transact conn (filter identity - [(some-> json - (lookup-order) - (order->sales-order) - (update :sales-order/date coerce/to-date) - (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]))) + [(some-> json + (lookup-order) + (order->sales-order) + (update :sales-order/date coerce/to-date) + (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]))) (defn upsert-recent [] (upsert-ezcater) @@ -289,17 +278,17 @@ (filter #(= 7 (time/day-of-week %))))) (time/days 1))) orders-to-update (doall (for [[order uuid] (dc/q '[:find ?eid ?uuid - :in $ ?start - :where [?e :sales-order/vendor :vendor/ccp-ezcater] - [?e :sales-order/date ?d] - [(>= ?d ?start)] - [?e :sales-order/external-id ?eid] - [?e :sales-order/client ?c] - [?c :client/ezcater-locations ?l] - [?l :ezcater-location/caterer ?c2] - [?c2 :ezcater-caterer/uuid ?uuid]] - (dc/db conn) - last-sunday) + :in $ ?start + :where [?e :sales-order/vendor :vendor/ccp-ezcater] + [?e :sales-order/date ?d] + [(>= ?d ?start)] + [?e :sales-order/external-id ?eid] + [?e :sales-order/client ?c] + [?c :client/ezcater-locations ?l] + [?l :ezcater-location/caterer ?c2] + [?c2 :ezcater-caterer/uuid ?uuid]] + (dc/db conn) + last-sunday) :let [_ (alog/info ::considering :order order) id (last (str/split order #"/")) @@ -313,29 +302,29 @@ "occurred_at" "2022-07-21T19:21:07.549Z"} ezcater-order (lookup-order lookup-map) extant-order (dc/pull (dc/db conn) '[:sales-order/total - :sales-order/tax - :sales-order/tip - :sales-order/discount - :sales-order/external-id - {:sales-order/charges [:charge/tax - :charge/tip - :charge/total - :charge/external-id] - :sales-order/line-items [:order-line-item/external-id - :order-line-item/total - :order-line-item/tax - :order-line-item/discount]}] - [:sales-order/external-id order]) + :sales-order/tax + :sales-order/tip + :sales-order/discount + :sales-order/external-id + {:sales-order/charges [:charge/tax + :charge/tip + :charge/total + :charge/external-id] + :sales-order/line-items [:order-line-item/external-id + :order-line-item/total + :order-line-item/tax + :order-line-item/discount]}] + [:sales-order/external-id order]) updated-order (-> (order->sales-order ezcater-order) (select-keys - #{:sales-order/total - :sales-order/tax - :sales-order/tip - :sales-order/discount - :sales-order/charges - :sales-order/external-id - :sales-order/line-items}) + #{:sales-order/total + :sales-order/tax + :sales-order/tip + :sales-order/discount + :sales-order/charges + :sales-order/external-id + :sales-order/line-items}) (update :sales-order/line-items (fn [c] (map #(select-keys % #{:order-line-item/external-id diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 3174155f..abbfcb22 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -34,15 +34,14 @@ (clojure.lang IPersistentMap))) (def integreat-schema - { - :scalars {:id {:parse #(cond (number? %) + {:scalars {:id {:parse #(cond (number? %) % % (Long/parseLong %)) - + :serialize #(.toString %)} - :ident {:parse (fn [x] {:db/ident x}) + :ident {:parse (fn [x] {:db/ident x}) :serialize #(or (:ident %) (:db/ident %) %)} :iso_date {:parse #(time/parse % time/iso-date) :serialize #(time/unparse % time/iso-date)} @@ -65,29 +64,28 @@ :else %) :serialize #(cond (double? %) - (str %) - - (int? %) - (str %) - - :else - %)} - - :percentage {:parse #(cond (and (string? %) - (not (str/blank? %))) - (Double/parseDouble %) + (str %) (int? %) - (double %) + (str %) :else - %) + %)} + + :percentage {:parse #(cond (and (string? %) + (not (str/blank? %))) + (Double/parseDouble %) + + (int? %) + (double %) + + :else + %) :serialize #(if (double? %) (str %) %)}} :objects - { - :message + {:message {:fields {:message {:type 'String}}} :search_result @@ -128,8 +126,7 @@ :email {:type 'String} :phone {:type 'String}}} - - :address + :address {:fields {:id {:type :id} :street1 {:type 'String} :street2 {:type 'String} @@ -184,7 +181,6 @@ :legal_entity_tin_type {:type :tin_type} :legal_entity_1099_type {:type :type_1099}}} - :reminder {:fields {:id {:type 'Int} :email {:type 'String} @@ -193,13 +189,13 @@ :scheduled {:type 'String} :sent {:type 'String} :vendor {:type :vendor}}} - + :yodlee_merchant {:fields {:id {:type :id} :yodlee_id {:type 'String} :name {:type 'String}}} :plaid_merchant {:fields {:id {:type :id} - :name {:type 'String}}} + :name {:type 'String}}} :intuit_bank_account {:fields {:id {:type :id} :external_id {:type 'String} @@ -222,8 +218,6 @@ :accounts {:type '(list :percentage_account)} :transaction_approval_status {:type :transaction_approval_status}}} - - :user {:fields {:id {:type :id} :name {:type 'String} @@ -264,18 +258,12 @@ :start {:type 'Int} :end {:type 'Int}}} - - - :transaction_rule_page {:fields {:transaction_rules {:type '(list :transaction_rule)} + :transaction_rule_page {:fields {:transaction_rules {:type '(list :transaction_rule)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} - - - - :vendor_page {:fields {:vendors {:type '(list :vendor)} :count {:type 'Int} :total {:type 'Int} @@ -283,10 +271,10 @@ :end {:type 'Int}}} :account_page {:fields {:accounts {:type '(list :account)} - :count {:type 'Int} - :total {:type 'Int} - :start {:type 'Int} - :end {:type 'Int}}} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int}}} :reminder_page {:fields {:reminders {:type '(list :reminder)} :count {:type 'Int} @@ -303,7 +291,7 @@ :paid {:type 'String} :unpaid {:type 'String}}} - :upcoming_transaction {:fields {:amount {:type :money} + :upcoming_transaction {:fields {:amount {:type :money} :identifier {:type 'String} :date {:type :iso_date}}} @@ -320,14 +308,13 @@ :potential_transaction_rule_matches {:type '(list :transaction_rule) :args {:transaction_id {:type :id}} :resolve :get-transaction-rule-matches} - :test_transaction_rule {:type '(list :transaction) - :args {:transaction_rule {:type :edit_transaction_rule}} - :resolve :test-transaction-rule} + :args {:transaction_rule {:type :edit_transaction_rule}} + :resolve :test-transaction-rule} :run_transaction_rule {:type '(list :transaction) - :args {:transaction_rule_id {:type :id}} + :args {:transaction_rule_id {:type :id}} :resolve :run-transaction-rule} :invoice_stats {:type '(list :invoice_stat) @@ -337,7 +324,6 @@ :cash_flow {:type :cash_flow_result :args {:client_id {:type :id}} :resolve :get-cash-flow} - :all_accounts {:type '(list :account) :args {} @@ -351,11 +337,7 @@ :allowance {:type :account_allowance} :client_id {:type :id} :vendor_id {:type :id}} - :resolve :search-account} - - - - + :resolve :search-account} :yodlee_merchants {:type '(list :yodlee_merchant) :args {} @@ -376,16 +358,13 @@ :description {:type 'String}} :resolve :get-transaction-rule-page} - - - :vendor {:type :vendor_page :args {:name_like {:type 'String} :start {:type 'Int} :per_page {:type 'Int} :sort {:type '(list :sort_item)}} :resolve :get-vendor} - + :vendor_by_id {:type :vendor :args {:id {:type :id}} :resolve :vendor-by-id} @@ -395,8 +374,7 @@ :resolve :account-for-vendor}} :input-objects - { - :sort_item + {:sort_item {:fields {:sort_key {:type 'String} :sort_name {:type 'String} :asc {:type 'Boolean}}} @@ -495,8 +473,7 @@ :name {:type 'String} :client_overrides {:type '(list :edit_account_client_override)}}}} - :enums { - :processor {:values [{:enum-value :na} + :enums {:processor {:values [{:enum-value :na} {:enum-value :doordash} {:enum-value :koala} {:enum-value :ezcater} @@ -533,9 +510,7 @@ {:enum-value :equity} {:enum-value :revenue}]}} :mutations - { - - :delete_transaction_rule + {:delete_transaction_rule {:type :id :args {:transaction_rule_id {:type :id}} :resolve :mutation/delete-transaction-rule} @@ -553,9 +528,7 @@ :upsert_transaction_rule {:type :transaction_rule :args {:transaction_rule {:type :edit_transaction_rule}} - :resolve :mutation/upsert-transaction-rule} - }}) - + :resolve :mutation/upsert-transaction-rule}}}) (defn snake->kebab [s] (str/replace s #"_" "-")) @@ -571,65 +544,64 @@ (defn ->graphql [m] (walk/postwalk - (fn [node] - (cond + (fn [node] + (cond - (keyword? node) - (snake node) + (keyword? node) + (snake node) - :else - node)) - m)) + :else + node)) + m)) - -(defn get-expense-account-stats [_ {:keys [client_id] } _] +(defn get-expense-account-stats [_ {:keys [client_id]} _] (let [query (cond-> {:query {:find ['?account '?account-name '(sum ?amount)] - :in ['$] - :where []} - :args [(dc/db conn) client_id]} - client_id (merge-query {:query {:in ['?c]} - - :args [client_id]}) - (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) + :in ['$] + :where []} + :args [(dc/db conn) client_id]} + client_id (merge-query {:query {:in ['?c]} - true (merge-query {:query {:where ['[?i :invoice/client ?c] - '[?i :invoice/expense-accounts ?expense-account] - '[?expense-account :invoice-expense-account/account ?account] - '[?account :account/name ?account-name] - '[?expense-account :invoice-expense-account/amount ?amount]]}})) + :args [client_id]}) + (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) + + true (merge-query {:query {:where ['[?i :invoice/client ?c] + '[?i :invoice/expense-accounts ?expense-account] + '[?expense-account :invoice-expense-account/account ?account] + '[?account :account/name ?account-name] + '[?expense-account :invoice-expense-account/amount ?amount]]}})) result (query2 query)] (for [[account-id account-name total] result] {:account {:id account-id :name account-name} :total total}))) -(defn get-invoice-stats [_ {:keys [client_id] } _] +(defn get-invoice-stats [_ {:keys [client_id]} _] (let [query (cond-> {:query {:find ['?name '(sum ?outstanding-balance) '(sum ?total)] - :in ['$] - :where []} - :args [(dc/db conn) client_id]} - client_id (merge-query {:query {:in ['?c]} - :args [client_id]}) - (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) + :in ['$] + :where []} + :args [(dc/db conn) client_id]} + client_id (merge-query {:query {:in ['?c]} + :args [client_id]}) + (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) - true (merge-query {:query {:where ['[?i :invoice/client ?c] - '[?i :invoice/outstanding-balance ?outstanding-balance] - '[?i :invoice/total ?total] - '[?i :invoice/due ?date] - '[(.toInstant ^java.util.Date ?date) ?d2] - '[(.between java.time.temporal.ChronoUnit/DAYS (java.time.Instant/now) ?d2 ) ?d3] - '(or-join [?d3 ?name] - (and [(<= ?d3 0)] - [(ground :due) ?name]) - (and [(<= ?d3 30)] - [(ground :due-30) ?name]) - (and [(<= ?d3 60)] - [(ground :due-30) ?name]) - (and [(> ?d3 60)] - [(ground :due-later) ?name]))]}})) + true (merge-query {:query {:where ['[?i :invoice/client ?c] + '[?i :invoice/outstanding-balance ?outstanding-balance] + '[?i :invoice/total ?total] + '[?i :invoice/due ?date] + '[(.toInstant ^java.util.Date ?date) ?d2] + '[(.between java.time.temporal.ChronoUnit/DAYS (java.time.Instant/now) ?d2) ?d3] + '(or-join [?d3 ?name] + (and [(<= ?d3 0)] + [(ground :due) ?name]) + (and [(<= ?d3 30)] + [(ground :due-30) ?name]) + (and [(<= ?d3 60)] + [(ground :due-30) ?name]) + (and [(> ?d3 60)] + [(ground :due-later) ?name]))]}})) result (->> (query2 query) (group-by first))] - + (for [[id name] [[:due "Due"] [:due-30 "0-30 days"] [:due-60 "31-60 days"] [:due-later ">60 days"]] - :let [[[_ outstanding-balance total] ] (id result nil) + :let [[[_ outstanding-balance total]] (id result nil) outstanding-balance (or outstanding-balance 0) total (or total 0)]] {:name name :unpaid outstanding-balance :paid (if (= :due id) @@ -637,7 +609,7 @@ (- total outstanding-balance))}))) (defn has-fulfilled? [id date recent-fulfillments] - + (seq (transduce (filter (fn [[potential-id potential-date]] (let [date (coerce/to-date-time date) @@ -652,7 +624,7 @@ (defn get-cash-flow [_ {:keys [client_id]} _] (when client_id - (let [{:client/keys [week-a-credits week-a-debits week-b-credits week-b-debits forecasted-transactions ]} (dc/pull (dc/db conn) '[*] client_id) + (let [{:client/keys [week-a-credits week-a-debits week-b-credits week-b-debits forecasted-transactions]} (dc/pull (dc/db conn) '[*] client_id) total-cash (reduce (fn [total [credit debit]] (- (+ total credit) @@ -685,9 +657,9 @@ :where ['[?p :payment/client ?client] '[?p :payment/status :payment-status/pending] '[?p :payment/amount ?amount] - '(or - [?p :payment/type :payment-type/debit] - [?p :payment/type :payment-type/check])]} + '(or + [?p :payment/type :payment-type/debit] + [?p :payment/type :payment-type/check])]} (dc/db conn) client_id (coerce/to-date (t/plus (time/local-now) (t/days 180)))))) recent-fulfillments (dc/q {:find '[?f ?d] :in '[$ ?client ?min-date] @@ -710,7 +682,7 @@ :date (coerce/to-date-time next)}) is-week-a? (fn [d] (= 0 (mod (t/in-weeks (t/interval first-week-a d)) 2)))] - + {:beginning_balance total-cash :outstanding_payments outstanding-checks :invoices_due_soon (mapv (fn [[due outstanding invoice-number vendor-id vendor-name]] @@ -735,31 +707,29 @@ :date (coerce/to-date-time date)}) (take (* 7 4) (time/day-of-week-seq 1))) (filter #(< (:amount %) 0) forecasted-transactions))}))) - (def schema (-> integreat-schema (attach-tracing-resolvers - { - :get-all-accounts gq-accounts/get-all-graphql - :get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page - :get-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches - :get-expense-account-stats get-expense-account-stats - :get-invoice-stats get-invoice-stats - :get-cash-flow get-cash-flow - :get-yodlee-merchants ym/get-yodlee-merchants - :get-intuit-bank-accounts gq-intuit-bank-accounts/get-intuit-bank-accounts - :vendor-by-id gq-vendors/get-by-id - :account-for-vendor gq-accounts/default-for-vendor - :mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule - :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule - :test-transaction-rule gq-transaction-rules/test-transaction-rule - :run-transaction-rule gq-transaction-rules/run-transaction-rule - :mutation/upsert-vendor gq-vendors/upsert-vendor - :mutation/merge-vendors gq-vendors/merge-vendors - :get-vendor gq-vendors/get-graphql - :search-vendor gq-vendors/search - :search-account gq-accounts/search}) + {:get-all-accounts gq-accounts/get-all-graphql + :get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page + :get-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches + :get-expense-account-stats get-expense-account-stats + :get-invoice-stats get-invoice-stats + :get-cash-flow get-cash-flow + :get-yodlee-merchants ym/get-yodlee-merchants + :get-intuit-bank-accounts gq-intuit-bank-accounts/get-intuit-bank-accounts + :vendor-by-id gq-vendors/get-by-id + :account-for-vendor gq-accounts/default-for-vendor + :mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule + :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule + :test-transaction-rule gq-transaction-rules/test-transaction-rule + :run-transaction-rule gq-transaction-rules/run-transaction-rule + :mutation/upsert-vendor gq-vendors/upsert-vendor + :mutation/merge-vendors gq-vendors/merge-vendors + :get-vendor gq-vendors/get-graphql + :search-vendor gq-vendors/search + :search-account gq-accounts/search}) gq-checks/attach gq-ledger/attach gq-plaid/attach @@ -772,30 +742,28 @@ gq-sales-orders/attach schema/compile)) - - (defn simplify "Converts all ordered maps nested within the map into standard hash maps, and sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems." [m] (walk/postwalk - (fn [node] - (cond - (instance? IPersistentMap node) - (into {} node) + (fn [node] + (cond + (instance? IPersistentMap node) + (into {} node) - (seq? node) - (vec node) + (seq? node) + (vec node) - (keyword? node) - (kebab node) + (keyword? node) + (kebab node) - :else - node)) - m)) + :else + node)) + m)) (defn query-name [q] - (try + (try (str/join "__" (map name (:operations (p/operations (p/parse-query schema q))))) (catch Exception _ "unknown query"))) @@ -805,32 +773,32 @@ (query id q nil)) ([id q v] (statsd/increment "query.graphql.count" {:tags #{(str "query:" (query-name q))}}) - (statsd/time! [(str "query.graphql.time" ) {:tags #{(str "query:" (query-name q))}}] - (mu/with-context {:query-name (query-name q) :user id :query q} - (mu/trace ::executing-query - [] - (try - (let [[result time] (time-it (simplify (execute schema q (dissoc v - :clients) {:id id - :clients (:clients v) - :log-context (or (mu/local-context) {})})))] - - (when (seq (:errors result)) - (throw (ex-info "GraphQL error" {:result result}))) - result) + (statsd/time! [(str "query.graphql.time") {:tags #{(str "query:" (query-name q))}}] + (mu/with-context {:query-name (query-name q) :user id :query q} + (mu/trace ::executing-query + [] + (try + (let [[result time] (time-it (simplify (execute schema q (dissoc v + :clients) {:id id + :clients (:clients v) + :log-context (or (mu/local-context) {})})))] - (catch Exception e - (if-let [v (or (:validation-error (ex-data e)) - (:validation-error (ex-data (.getCause e))))] - - (do - (alog/warn ::query-validation - :exception e) - (throw e) - #_{:errors [{:message v}]}) - (do - (alog/error ::query-error - :exception e) + (when (seq (:errors result)) + (throw (ex-info "GraphQL error" {:result result}))) + result) - (throw e)))))))))) + (catch Exception e + (if-let [v (or (:validation-error (ex-data e)) + (:validation-error (ex-data (.getCause e))))] + + (do + (alog/warn ::query-validation + :exception e) + (throw e) + #_{:errors [{:message v}]}) + (do + (alog/error ::query-error + :exception e) + + (throw e)))))))))) diff --git a/src/clj/auto_ap/graphql/accounts.clj b/src/clj/auto_ap/graphql/accounts.clj index cec20fee..048ecc55 100644 --- a/src/clj/auto_ap/graphql/accounts.clj +++ b/src/clj/auto_ap/graphql/accounts.clj @@ -18,7 +18,6 @@ [iol-ion.tx :refer [random-tempid]] [com.brunobonacci.mulog :as mu])) - (defn get-all-graphql [context args _] (assert-admin (:id context)) (let [args (assoc args :id (:id context)) diff --git a/src/clj/auto_ap/graphql/checks.clj b/src/clj/auto_ap/graphql/checks.clj index abe0e26b..4ed55ba3 100644 --- a/src/clj/auto_ap/graphql/checks.clj +++ b/src/clj/auto_ap/graphql/checks.clj @@ -97,7 +97,6 @@ [:line {:line-width 0.15 :color [50 50 50]}]] [:cell {:colspan 3}]] - [[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"] [:cell {:colspan 5 :leading 11.5} (split-memo memo) [:line {:line-width 0.15 :color [50 50 50]}]] @@ -186,8 +185,6 @@ :payment/pdf-data (edn/read-string) - - make-check-pdf)] (s3/put-object :bucket-name (:data-bucket env) :key (:payment/s3-key check) @@ -277,7 +274,6 @@ (conj payment) (into (invoice-payments invoices invoice-amounts))))) - (defmethod invoices->entities :payment-type/debit [invoices vendor client bank-account type index invoice-amounts date] (when (<= (->> invoices (map (comp invoice-amounts :db/id)) @@ -297,7 +293,6 @@ (conj payment) (into (invoice-payments invoices invoice-amounts))))) - (defmethod invoices->entities :payment-type/balance-credit [invoices invoice-amounts] (when (<= (->> invoices (map (comp invoice-amounts :db/id)) @@ -488,7 +483,6 @@ {:s3-url nil :invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))}))) - (defn void-payment [context {id :payment_id} _] (let [check (d-checks/get-by-id id)] (assert (or (= :payment-status/pending (:payment/status check)) @@ -549,7 +543,6 @@ :invoice-status/unpaid)}]])))))))) id)) - (defn void-payments [context args _] (assert-admin (:id context)) (let [args (assoc args :clients (:clients context)) @@ -607,7 +600,6 @@ 0.001)) invoices) - total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid)) _ (when (<= total-to-pay 0.001) (assert-failure "You must select invoices that need to be paid.")) @@ -637,8 +629,6 @@ [total-to-pay []]))) (into {})) - - vendor-id (:db/id (:invoice/vendor (first invoices))) payment {:db/id (str vendor-id) :payment/amount total-to-pay @@ -751,7 +741,6 @@ {:enum-value :pending} {:enum-value :cleared}]}}) - (def resolvers {:get-potential-payments get-potential-payments :get-payment-page get-payment-page diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index d7e22aef..ac6ef937 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -19,9 +19,9 @@ (defn get-admin-client [context {:keys [id]} _] (assert-admin (:id context)) (->graphql - (-> (d-clients/get-by-id id) - (update :client/bank-accounts (fn [bas] - (map #(set/rename-keys % {:bank-account/use-date-instead-of-post-date? :use-date-instead-of-post-date}) bas)))))) + (-> (d-clients/get-by-id id) + (update :client/bank-accounts (fn [bas] + (map #(set/rename-keys % {:bank-account/use-date-instead-of-post-date? :use-date-instead-of-post-date}) bas)))))) (defn get-client-page [context args _] (assert-admin (:id context)) @@ -29,7 +29,7 @@ [clients clients-count] (d-clients/get-graphql-page (assoc (<-graphql (:filters args)) :clients (:clients context))) clients (->> clients - + (map (fn [c] (update c :client/bank-accounts (fn [bas] (map #(set/rename-keys % {:bank-account/use-date-instead-of-post-date? :use-date-instead-of-post-date}) bas))))) @@ -47,13 +47,6 @@ bank-accounts))))))] (result->page clients clients-count :clients (:filters args)))) - - - - - - - (def objects {:location_match {:fields {:location {:type 'String} @@ -102,15 +95,13 @@ :yodlee_provider_accounts {:type '(list :yodlee_provider_account)} :plaid_items {:type '(list :plaid_item)}}} - :client_page + :client_page {:fields {:clients {:type '(list :client)} :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} :end {:type 'Int}}} - - :bank_account {:fields {:id {:type :id} :integration_status {:type :integration_status} @@ -139,9 +130,7 @@ :forecasted_transaction {:fields {:identifier {:type 'String} :id {:type :id} :day_of_month {:type 'Int} - :amount {:type :money}}} - -}) + :amount {:type :money}}}}) (def queries {:client {:type '(list :client) @@ -158,12 +147,12 @@ {}) (def input-objects - { :client_filters + {:client_filters {:fields {:code {:type 'String} :name_like {:type 'String} :start {:type 'Int} :per_page {:type 'Int} - :sort {:type '(list :sort_item)}}} }) + :sort {:type '(list :sort_item)}}}}) (def enums {:bank_account_type {:values [{:enum-value :check} @@ -173,11 +162,10 @@ (def resolvers {:get-client get-client :get-admin-client get-admin-client - :get-client-page get-client-page }) - + :get-client-page get-client-page}) (defn attach [schema] - (-> + (-> (merge-with merge schema {:objects objects :queries queries diff --git a/src/clj/auto_ap/graphql/expected_deposit.clj b/src/clj/auto_ap/graphql/expected_deposit.clj index 50ac678a..1e2477ec 100644 --- a/src/clj/auto_ap/graphql/expected_deposit.clj +++ b/src/clj/auto_ap/graphql/expected_deposit.clj @@ -11,7 +11,7 @@ (defn get-all-expected-deposits [context args _] (assert-admin (:id context)) (map - (comp ->graphql status->graphql) + (comp ->graphql status->graphql) (first (d-expected-deposit/get-graphql (assoc (<-graphql args) :count Integer/MAX_VALUE))))) (defn get-expected-deposit-page [context args _] diff --git a/src/clj/auto_ap/graphql/ezcater.clj b/src/clj/auto_ap/graphql/ezcater.clj index 373e2bd6..50ce20f1 100644 --- a/src/clj/auto_ap/graphql/ezcater.clj +++ b/src/clj/auto_ap/graphql/ezcater.clj @@ -17,15 +17,14 @@ {:name name :id id})) - (def objects {:ezcater_caterer {:fields {:name {:type 'String} :id {:type :id}}}}) (def queries {:search_ezcater_caterer {:type '(list :search_result) - :args {:query {:type 'String}} - :resolve :search-ezcater-caterer}}) + :args {:query {:type 'String}} + :resolve :search-ezcater-caterer}}) (def enums {}) diff --git a/src/clj/auto_ap/graphql/import_batch.clj b/src/clj/auto_ap/graphql/import_batch.clj index 147343cf..57cf67ba 100644 --- a/src/clj/auto_ap/graphql/import_batch.clj +++ b/src/clj/auto_ap/graphql/import_batch.clj @@ -40,7 +40,6 @@ (merge-query {:query {:find ['?e] :where ['[?e :import-batch/date]]}}))] - (cond->> (query2 query) true (apply-sort-3 args) true (apply-pagination args)))) @@ -66,9 +65,8 @@ (map #(update % :import-batch/date coerce/to-date-time))) matching-count :data args))) - (defn attach [schema] - (-> + (-> (merge-with merge schema {:objects {:import_batch {:fields {:user_name {:type 'String} :id {:type :id} @@ -83,12 +81,10 @@ :count {:type 'Int} :total {:type 'Int} :start {:type 'Int} - :end {:type 'Int}}} - -} + :end {:type 'Int}}}} :queries {:import_batch_page {:type :import_batch_page :args {:filters {:type :import_batch_filters}} - + :resolve :get-import-batch-page}} :mutations {} :input-objects {:import_batch_filters {:fields {:start {:type 'Int} diff --git a/src/clj/auto_ap/graphql/intuit_bank_accounts.clj b/src/clj/auto_ap/graphql/intuit_bank_accounts.clj index 9fc1ab50..f47c2eb9 100644 --- a/src/clj/auto_ap/graphql/intuit_bank_accounts.clj +++ b/src/clj/auto_ap/graphql/intuit_bank_accounts.clj @@ -7,6 +7,6 @@ (defn get-intuit-bank-accounts [context _ _] (assert-admin (:id context)) (->graphql (map first (dc/q '[:find (pull ?e [*]) - :in $ - :where [?e :intuit-bank-account/external-id]] - (dc/db conn))))) + :in $ + :where [?e :intuit-bank-account/external-id]] + (dc/db conn))))) diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index d6ab1e3a..0035df06 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -174,8 +174,6 @@ (let [error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")] (throw (ex-info error {:validation-error error})))))) - - (defn add-invoice [context {{:keys [expense_accounts client_id vendor_id] :as in} :invoice} _] (assert-no-conflicting in) (assert-can-see-client (:id context) client_id) @@ -193,8 +191,6 @@ (when-not ((set (map :db/id (:client/bank-accounts (d-clients/get-by-id client-id)))) bank-account-id) (throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."})))) - - (defn add-and-print-invoice [context {{:keys [total client_id vendor_id] :as in} :invoice bank-account-id :bank_account_id type :type} _] (mu/trace ::validating-invoice [:invoice in] (do @@ -261,7 +257,6 @@ (-> (d-invoices/get-by-id id) (->graphql (:id context))))) - (defn get-ids-matching-filters [args] (let [ids (some-> args :filters @@ -448,8 +443,6 @@ [])] accounts))) - - (defn bulk-change-invoices [context args _] (assert-admin (:id context)) (when-not (:client_id args) diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index 367151e7..4bac4e66 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -38,9 +38,9 @@ _ (when (:client_id (:filters args)) (assert-can-see-client (:id context) (:client_id (:filters args)))) clients (or (and (:client_id (:filters args)) - [{:db/id (:client_id (:filters args))}]) - (:clients context)) - + [{:db/id (:client_id (:filters args))}]) + (:clients context)) + [journal-entries journal-entries-count] (l/get-graphql (assoc (<-graphql (:filters args)) :clients clients)) @@ -55,12 +55,10 @@ (let [args (assoc args :id (:id context)) [journal-entries journal-entries-count] (l/get-graphql (assoc (<-graphql (:filters args)) :per-page Integer/MAX_VALUE - :clients (:clients context))) + :clients (:clients context)))] - - ] {:csv_content_b64 (Base64/encodeBase64String - (.getBytes + (.getBytes (with-open [w (java.io.StringWriter.)] (csv/write-csv w (into [["Client" "Vendor" "Date" "Journal Entry" "Journal Entry Line" "Account Code" "Account Name" "Account Type" "Debit" "Credit" "Net"]] @@ -83,22 +81,19 @@ (-> li :journal-entry-line/account :bank-account/numeric-code)) (or (-> li :journal-entry-line/account :account/name) (-> li :journal-entry-line/account :bank-account/name)) - (some-> account-type name ) + (some-> account-type name) (-> li :journal-entry-line/debit) (-> li :journal-entry-line/credit) (if (#{:account-type/asset :account-type/dividend :account-type/expense} account-type) (- (or (-> li :journal-entry-line/debit) 0.0) (or (-> li :journal-entry-line/credit) 0.0)) - (- (or (-> li :journal-entry-line/credit) 0.0) (or (-> li :journal-entry-line/debit) 0.0))) - - ])) - (:journal-entry/line-items j)) - )))) + (- (or (-> li :journal-entry-line/credit) 0.0) (or (-> li :journal-entry-line/debit) 0.0)))])) + + (:journal-entry/line-items j)))))) :quote? (constantly true)) (.toString w))))})) - (defn roll-up-until ([lookup-account all-ledger-entries end-date] (roll-up-until lookup-account all-ledger-entries end-date nil)) @@ -107,57 +102,56 @@ (filter (fn [[d]] (if start-date (and - (>= (compare d start-date) 0) - (<= (compare d end-date) 0)) + (>= (compare d start-date) 0) + (<= (compare d end-date) 0)) (<= (compare d end-date) 0)))) (reduce - (fn [acc [_ _ account location debit credit]] - (-> acc - (update-in [[location account] :debit] (fnil + 0.0) debit) - (update-in [[location account] :credit] (fnil + 0.0) credit) - (update-in [[location account] :count] (fnil + 0) 1)) - ) - {}) + (fn [acc [_ _ account location debit credit]] + (-> acc + (update-in [[location account] :debit] (fnil + 0.0) debit) + (update-in [[location account] :credit] (fnil + 0.0) credit) + (update-in [[location account] :count] (fnil + 0) 1))) + {}) (reduce-kv - (fn [acc [location account-id] {:keys [debit credit count]}] - (let [account (lookup-account account-id) - account-type (:account_type account)] - - (conj acc (merge {:id (str account-id "-" location) - :location (or location "") - :count count - :debits debit - :credits credit - :amount (if account-type (if (#{:account-type/asset - :account-type/dividend - :account-type/expense} account-type) - (- debit credit) - (- credit debit)) - 0.0)} - account)))) - [])))) + (fn [acc [location account-id] {:keys [debit credit count]}] + (let [account (lookup-account account-id) + account-type (:account_type account)] + + (conj acc (merge {:id (str account-id "-" location) + :location (or location "") + :count count + :debits debit + :credits credit + :amount (if account-type (if (#{:account-type/asset + :account-type/dividend + :account-type/expense} account-type) + (- debit credit) + (- credit debit)) + 0.0)} + account)))) + [])))) (defn full-ledger-for-client [client-id] - (->> (dc/q - {:find ['?d '?jel '?account '?location '?debit '?credit] - :in ['$ '?client-id] - :where '[[?e :journal-entry/client ?client-id] - [?e :journal-entry/date ?d] - [?e :journal-entry/line-items ?jel] - (or-join [?e] - (and [?e :journal-entry/original-entity ?i] - (or-join [?e ?i] - (and - [?i :transaction/bank-account ?b] - (or [?b :bank-account/include-in-reports true] - (not [?b :bank-account/include-in-reports]))) - (not [?i :transaction/bank-account]))) - (not [?e :journal-entry/original-entity ])) - [(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account] - [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ] - [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit] - [(get-else $ ?jel :journal-entry-line/location "") ?location]]} - (dc/db conn) client-id) + (->> (dc/q + {:find ['?d '?jel '?account '?location '?debit '?credit] + :in ['$ '?client-id] + :where '[[?e :journal-entry/client ?client-id] + [?e :journal-entry/date ?d] + [?e :journal-entry/line-items ?jel] + (or-join [?e] + (and [?e :journal-entry/original-entity ?i] + (or-join [?e ?i] + (and + [?i :transaction/bank-account ?b] + (or [?b :bank-account/include-in-reports true] + (not [?b :bank-account/include-in-reports]))) + (not [?i :transaction/bank-account]))) + (not [?e :journal-entry/original-entity])) + [(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account] + [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit] + [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit] + [(get-else $ ?jel :journal-entry-line/location "") ?location]]} + (dc/db conn) client-id) (sort-by first))) (defn get-balance-sheet [context args _] @@ -180,30 +174,29 @@ [client-id (build-account-lookup client-id)])) (into {}))] (alog/info ::balance-sheet :params args) - + (cond-> {:balance-sheet-accounts (mapcat - #(roll-up-until (lookup-account %) (all-ledger-entries %) end-date ) - client-ids) - } + #(roll-up-until (lookup-account %) (all-ledger-entries %) end-date) + client-ids)} (:include_comparison args) (assoc :comparable-balance-sheet-accounts (mapcat - #(roll-up-until (lookup-account %) (all-ledger-entries %) comparable-date ) - client-ids)) + #(roll-up-until (lookup-account %) (all-ledger-entries %) comparable-date) + client-ids)) true ->graphql))) (defn get-profit-and-loss-raw [client-ids periods] - (let [ all-ledger-entries (->> client-ids - (map (fn [client-id] - [client-id (full-ledger-for-client client-id)])) - (into {})) + (let [all-ledger-entries (->> client-ids + (map (fn [client-id] + [client-id (full-ledger-for-client client-id)])) + (into {})) lookup-account (->> client-ids (map (fn [client-id] [client-id (build-account-lookup client-id)])) (into {}))] - (->graphql {:periods + (->graphql {:periods (->> periods (mapv (fn [{:keys [start end]}] {:accounts (mapcat - #(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start) ) + #(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start)) client-ids)})))}))) (defn get-profit-and-loss [context args _] @@ -216,12 +209,9 @@ (assert-can-see-client (:id context) client-id)) _ (when (and (:include_deltas args) (:column_per_location args)) - (throw (ex-info "Please select one of 'Include deltas' or 'Column per location'" {:validation-error "Please select one of 'Include deltas' or 'Column per location'"}))) ] + (throw (ex-info "Please select one of 'Include deltas' or 'Column per location'" {:validation-error "Please select one of 'Include deltas' or 'Column per location'"})))] (get-profit-and-loss-raw client-ids (:periods args)))) - - - ;; profit and loss based off of index #_(defn get-profit-and-loss [context args _] (let [client-id (:client_id args) @@ -239,17 +229,17 @@ :in $ [?c ...] :where (or-join [?c ?a ?l] - (and - [?a :account/numeric-code] - (not [?a :account/location]) - [?c :client/locations ?l]) (and - [?a :account/numeric-code] - [?a :account/location ?l] - [?c :client/locations ?l]) + [?a :account/numeric-code] + (not [?a :account/location]) + [?c :client/locations ?l]) (and - [?c :client/bank-accounts ?a] - [(ground "A") ?l]))] + [?a :account/numeric-code] + [?a :account/location ?l] + [?c :client/locations ?l]) + (and + [?c :client/bank-accounts ?a] + [(ground "A") ?l]))] (dc/db conn) client-ids) lookup-account (->> client-ids @@ -257,49 +247,48 @@ [client-id (build-account-lookup client-id)])) (into {}))] (->graphql - {:periods - (->> (:periods args) - (mapv (fn [{:keys [start end]}] - (let [start (coerce/to-date start) - end (coerce/to-date end)] - {:accounts (mapcat - (fn [[c a l]] - (let [start-point (->> (dc/index-pull db - {:index :avet - :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date] - :start [:journal-entry-line/client+account+location+date [c a l start]] - :reverse true - :limit 1}) - (take-while (fn [result] - (= [c a l] - (take 3 (:journal-entry-line/client+account+location+date result))))) - (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}] - (>= (compare date start) 0))) - first) - end-point (->> (dc/index-pull db - {:index :avet - :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date] - :start [:journal-entry-line/client+account+location+date [c a l end]] - :reverse true - :limit 1}) - (take-while (fn [result] - (= [c a l] - (take 3 (:journal-entry-line/client+account+location+date result))))) - (take 1) - (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}] - (>= (compare date end) 0))) - first)] - (when end-point - [(merge {:id (str a "-" l) - :location (or l "") - :count 0 - :debits 0 - :credits 0 - :amount (- (or (:journal-entry-line/running-balance end-point) 0.0) - (or (:journal-entry-line/running-balance start-point) 0.0)) - } - ((lookup-account c) a))]))) - all-used-account-locations)}))))}))) + {:periods + (->> (:periods args) + (mapv (fn [{:keys [start end]}] + (let [start (coerce/to-date start) + end (coerce/to-date end)] + {:accounts (mapcat + (fn [[c a l]] + (let [start-point (->> (dc/index-pull db + {:index :avet + :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date] + :start [:journal-entry-line/client+account+location+date [c a l start]] + :reverse true + :limit 1}) + (take-while (fn [result] + (= [c a l] + (take 3 (:journal-entry-line/client+account+location+date result))))) + (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}] + (>= (compare date start) 0))) + first) + end-point (->> (dc/index-pull db + {:index :avet + :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date] + :start [:journal-entry-line/client+account+location+date [c a l end]] + :reverse true + :limit 1}) + (take-while (fn [result] + (= [c a l] + (take 3 (:journal-entry-line/client+account+location+date result))))) + (take 1) + (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}] + (>= (compare date end) 0))) + first)] + (when end-point + [(merge {:id (str a "-" l) + :location (or l "") + :count 0 + :debits 0 + :credits 0 + :amount (- (or (:journal-entry-line/running-balance end-point) 0.0) + (or (:journal-entry-line/running-balance start-point) 0.0))} + ((lookup-account c) a))]))) + all-used-account-locations)}))))}))) (defn profit-and-loss-pdf [context args value] (let [data (get-profit-and-loss context args value) @@ -320,10 +309,9 @@ (->graphql result))) - (defn assoc-error [f] (fn [entry] - (try + (try (f entry) (catch Exception e (assoc entry :error (.getMessage e) @@ -333,13 +321,13 @@ (defn all-ids-not-locked [all-ids] (->> all-ids (dc/q '[:find ?t - :in $ [?t ...] - :where - [?t :journal-entry/client ?c] - [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] - [?t :journal-entry/date ?d] - [(>= ?d ?lu)]] - (dc/db conn)) + :in $ [?t ...] + :where + [?t :journal-entry/client ?c] + [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] + [?t :journal-entry/date ?d] + [(>= ?d ?lu)]] + (dc/db conn)) (map first))) (defn delete-external-ledger [context args _] @@ -353,8 +341,8 @@ (#(l/raw-graphql-ids (dc/db conn) %)) :ids) _ (alog/info ::trying-to-delete - :count (count ids) - :sample (take 3 ids)) + :count (count ids) + :sample (take 3 ids)) specific-ids (l/filter-ids (:ids args)) all-ids (all-ids-not-locked (into (set ids) specific-ids))] (if (> (count all-ids) 1000) @@ -364,7 +352,7 @@ (audit-transact-batch (map (fn [i] [:db/retractEntity i]) - all-ids) + all-ids) (:id context)) {:message (str "Succesfully deleted " (count all-ids) " ledger entries.")})))) @@ -372,15 +360,15 @@ (assert-admin (:id context)) (let [used-vendor-names (set (map :vendor_name (:entries args))) all-vendors (mu/trace ::get-all-vendors - [] - (->> (dc/q '[:find ?e - :in $ [?name ...] - :where [?e :vendor/name ?name]] - (dc/db conn) - used-vendor-names) - (map first) - (pull-many (dc/db conn) [:db/id :vendor/name]) - (by :vendor/name))) + [] + (->> (dc/q '[:find ?e + :in $ [?name ...] + :where [?e :vendor/name ?name]] + (dc/db conn) + used-vendor-names) + (map first) + (pull-many (dc/db conn) [:db/id :vendor/name]) + (by :vendor/name))) client-locked-lookup (mu/trace ::get-all-clients [] (->> (dc/q '[:find ?code ?locked-until :in $ @@ -389,18 +377,18 @@ (dc/db conn)) (into {}))) all-client-bank-accounts (mu/trace ::get-all-client-bank-accounts - [] - (->> (dc/q '[:find ?code ?ba-code - :in $ - :where [?c :client/code ?code] - [?c :client/bank-accounts ?ba] - [?ba :bank-account/code ?ba-code]] - (dc/db conn)) - (reduce - (fn [acc [code ba-code]] - (update acc code (fnil conj #{}) ba-code)) - {}))) - + [] + (->> (dc/q '[:find ?code ?ba-code + :in $ + :where [?c :client/code ?code] + [?c :client/bank-accounts ?ba] + [?ba :bank-account/code ?ba-code]] + (dc/db conn)) + (reduce + (fn [acc [code ba-code]] + (update acc code (fnil conj #{}) ba-code)) + {}))) + all-client-locations (mu/trace ::get-all-client-locations [] (->> (dc/q '[:find ?code ?location @@ -409,160 +397,158 @@ [?c :client/locations ?location]] (dc/db conn)) (reduce - (fn [acc [code ba-code]] - (update acc code (fnil conj #{"HQ" "A"}) ba-code)) - {}))) - + (fn [acc [code ba-code]] + (update acc code (fnil conj #{"HQ" "A"}) ba-code)) + {}))) + new-hidden-vendors (reduce - (fn [new-vendors {:keys [vendor_name]}] - (if (or (all-vendors vendor_name) - (new-vendors vendor_name)) - new-vendors - (assoc new-vendors vendor_name - {:vendor/name vendor_name - :vendor/hidden true - :db/id vendor_name}))) - {} - (:entries args)) + (fn [new-vendors {:keys [vendor_name]}] + (if (or (all-vendors vendor_name) + (new-vendors vendor_name)) + new-vendors + (assoc new-vendors vendor_name + {:vendor/name vendor_name + :vendor/hidden true + :db/id vendor_name}))) + {} + (:entries args)) _ (mu/trace ::upsert-new-vendors - [] - (audit-transact-batch (vec (vals new-hidden-vendors)) (:id context))) + [] + (audit-transact-batch (vec (vals new-hidden-vendors)) (:id context))) all-vendors (->> (dc/q '[:find ?e - :in $ [?name ...] - :where [?e :vendor/name ?name]] - (dc/db conn) - used-vendor-names) + :in $ [?name ...] + :where [?e :vendor/name ?name]] + (dc/db conn) + used-vendor-names) (map first) (pull-many (dc/db conn) [:db/id :vendor/name]) (by :vendor/name)) all-accounts (mu/trace ::get-all-accounts [] (transduce (map (comp str :account/numeric-code)) conj #{} (a/get-accounts))) transaction (mu/trace ::build-transaction - [:count (count (:entries args))] - (doall (map - (assoc-error (fn [entry] - (let [vendor (all-vendors (:vendor_name entry))] - (when-not (client-locked-lookup (:client_code entry)) - (throw (ex-info (str "Client '" (:client_code entry )"' not found.") {:status :error}) )) - (when-not vendor - (throw (ex-info (str "Vendor '" (:vendor_name entry) "' not found.") {:status :error}))) - (when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry)) - (throw (ex-info (str "Date must be MM/dd/yyyy") {:status :error}))) - (when-let [locked-until (client-locked-lookup (:client_code entry))] - (when (and (not (t/after? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))) - (coerce/to-date-time locked-until))) - (not (t/equal? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))) - (coerce/to-date-time locked-until)))) - (throw (ex-info (str "Client's data is locked until " locked-until) {:status :error})))) - - (when-not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry))) - (reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line_items entry)))) - (throw (ex-info (str "Debits '" - (reduce (fnil + 0.0 0.0) 0 (map :debit (:line_items entry))) - "' and credits '" - (reduce (fnil + 0.0 0.0) 0 (map :credit (:line_items entry))) - "' do not add up.") - {:status :error}))) - (when (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry))) - 0.0) - (throw (ex-info (str "Cannot have ledger entries that total $0.00") - {:status :ignored}))) - (assoc entry - :status :success - :tx - [:upsert-ledger - (remove-nils - {:journal-entry/source (:source entry) - :journal-entry/client [:client/code (:client_code entry)] - :journal-entry/date (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))) - :journal-entry/external-id (:external_id entry) - :journal-entry/vendor (:db/id (all-vendors (:vendor_name entry))) - :journal-entry/amount (:amount entry) - :journal-entry/note (:note entry) - :journal-entry/cleared-against (:cleared_against entry) + [:count (count (:entries args))] + (doall (map + (assoc-error (fn [entry] + (let [vendor (all-vendors (:vendor_name entry))] + (when-not (client-locked-lookup (:client_code entry)) + (throw (ex-info (str "Client '" (:client_code entry) "' not found.") {:status :error}))) + (when-not vendor + (throw (ex-info (str "Vendor '" (:vendor_name entry) "' not found.") {:status :error}))) + (when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry)) + (throw (ex-info (str "Date must be MM/dd/yyyy") {:status :error}))) + (when-let [locked-until (client-locked-lookup (:client_code entry))] + (when (and (not (t/after? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))) + (coerce/to-date-time locked-until))) + (not (t/equal? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))) + (coerce/to-date-time locked-until)))) + (throw (ex-info (str "Client's data is locked until " locked-until) {:status :error})))) - :journal-entry/line-items - (mapv (fn [ea] - (let [debit (or (:debit ea) 0.0) - credit (or (:credit ea) 0.0)] - (when (and (not (get - (get all-client-locations (:client_code entry)) - (:location ea))) - (not= "A" (:location ea))) - (throw (ex-info (str "Location '" (:location ea) "' not found.") - {:status :error}))) - (when (and (<= debit 0.0) - (<= credit 0.0)) - (throw (ex-info (str "Line item amount " (or debit credit) " must be greater than 0.") - {:status :error}))) - (when (and (not (all-accounts (:account_identifier ea))) - (not (get - (get all-client-bank-accounts (:client_code entry)) - (:account_identifier ea)))) - (throw (ex-info (str "Account '" (:account_identifier ea) "' not found.") - {:status :error}))) - (let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea)) - (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))] - (when (and matching-account - (:account/location matching-account) - (not= (:account/location matching-account) - (:location ea))) - (throw (ex-info (str "Account '" - (:account/numeric-code matching-account) - "' requires location '" - (:account/location matching-account) - "' but got '" - (:location ea) - "'") - {:status :error}))) - (when (and matching-account - (not (:account/location matching-account)) - (= "A" (:location ea))) - (throw (ex-info (str "Account '" - (:account/numeric-code matching-account) - "' cannot use location '" - (:location ea) - "'") - {:status :error}))) - (remove-nils (cond-> {:db/id (random-tempid) - :journal-entry-line/location (:location ea) - :journal-entry-line/debit (when (> debit 0) - debit) - :journal-entry-line/credit (when (> credit 0) - credit)} - matching-account (assoc :journal-entry-line/account (:db/id matching-account)) - (not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)])))))) - (:line_items entry)) - - :journal-entry/cleared true})])))) - (:entries args)))) + (when-not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry))) + (reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line_items entry)))) + (throw (ex-info (str "Debits '" + (reduce (fnil + 0.0 0.0) 0 (map :debit (:line_items entry))) + "' and credits '" + (reduce (fnil + 0.0 0.0) 0 (map :credit (:line_items entry))) + "' do not add up.") + {:status :error}))) + (when (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry))) + 0.0) + (throw (ex-info (str "Cannot have ledger entries that total $0.00") + {:status :ignored}))) + (assoc entry + :status :success + :tx + [:upsert-ledger + (remove-nils + {:journal-entry/source (:source entry) + :journal-entry/client [:client/code (:client_code entry)] + :journal-entry/date (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))) + :journal-entry/external-id (:external_id entry) + :journal-entry/vendor (:db/id (all-vendors (:vendor_name entry))) + :journal-entry/amount (:amount entry) + :journal-entry/note (:note entry) + :journal-entry/cleared-against (:cleared_against entry) + + :journal-entry/line-items + (mapv (fn [ea] + (let [debit (or (:debit ea) 0.0) + credit (or (:credit ea) 0.0)] + (when (and (not (get + (get all-client-locations (:client_code entry)) + (:location ea))) + (not= "A" (:location ea))) + (throw (ex-info (str "Location '" (:location ea) "' not found.") + {:status :error}))) + (when (and (<= debit 0.0) + (<= credit 0.0)) + (throw (ex-info (str "Line item amount " (or debit credit) " must be greater than 0.") + {:status :error}))) + (when (and (not (all-accounts (:account_identifier ea))) + (not (get + (get all-client-bank-accounts (:client_code entry)) + (:account_identifier ea)))) + (throw (ex-info (str "Account '" (:account_identifier ea) "' not found.") + {:status :error}))) + (let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea)) + (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))] + (when (and matching-account + (:account/location matching-account) + (not= (:account/location matching-account) + (:location ea))) + (throw (ex-info (str "Account '" + (:account/numeric-code matching-account) + "' requires location '" + (:account/location matching-account) + "' but got '" + (:location ea) + "'") + {:status :error}))) + (when (and matching-account + (not (:account/location matching-account)) + (= "A" (:location ea))) + (throw (ex-info (str "Account '" + (:account/numeric-code matching-account) + "' cannot use location '" + (:location ea) + "'") + {:status :error}))) + (remove-nils (cond-> {:db/id (random-tempid) + :journal-entry-line/location (:location ea) + :journal-entry-line/debit (when (> debit 0) + debit) + :journal-entry-line/credit (when (> credit 0) + credit)} + matching-account (assoc :journal-entry-line/account (:db/id matching-account)) + (not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)])))))) + (:line_items entry)) + + :journal-entry/cleared true})])))) + (:entries args)))) errors (filter #(= (:status %) :error) transaction) ignored (filter #(= (:status %) :ignored) transaction) success (filter #(= (:status %) :success) transaction) retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external_id x)]]) success) ignore-retraction (->> ignored - (map :external_id ) + (map :external_id) (dc/q '[:find ?je - :in $ [?ei ...] - :where [?je :journal-entry/external-id ?ei]] - (dc/db conn) - ) + :in $ [?ei ...] + :where [?je :journal-entry/external-id ?ei]] + (dc/db conn)) (map first) (map (fn [je] [:db/retractEntity je])))] (alog/info ::manual-import :errors (count errors) :sample (take 3 errors)) - (mu/trace ::retraction-tx - [:count (count retraction)] - (audit-transact-batch retraction (:id context))) + [:count (count retraction)] + (audit-transact-batch retraction (:id context))) (mu/trace ::ignore-retraction-tx - [:count (count ignore-retraction)] - (when (seq ignore-retraction) - (audit-transact-batch ignore-retraction (:id context)))) - (let [invalidated + [:count (count ignore-retraction)] + (when (seq ignore-retraction) + (audit-transact-batch ignore-retraction (:id context)))) + (let [invalidated (mu/trace ::success-tx [:count (count success)] (for [[_ n] (:tempids (audit-transact-batch (map :tx success) (:id context)))] @@ -573,7 +559,7 @@ [:count (count invalidated)] (doseq [n invalidated] (solr/touch n))))) - + {:successful (map (fn [x] {:external_id (:external_id x)}) success) :ignored (map (fn [x] {:external_id (:external_id x)}) @@ -582,7 +568,6 @@ :errors (map (fn [x] {:external_id (:external_id x) :error (:error x)}) errors)})) - (defn get-journal-detail-report [context input _] (let [category-totals (atom {}) base-categories (into [] @@ -597,20 +582,19 @@ :clients [{:db/id client-id}]) {:filters {:location location :date_range (:date_range input) - :from_numeric_code (l-reports/min-numeric-code category ) - :to_numeric_code (l-reports/max-numeric-code category ) + :from_numeric_code (l-reports/min-numeric-code category) + :to_numeric_code (l-reports/max-numeric-code category) :per_page Integer/MAX_VALUE}} nil) :journal_entries (mapcat (fn [je] (->> (je :line_items) (filter (fn [jel] - (when-let [account (account-lookup (:id (:account jel)))] - (and - (l-reports/account-belongs-in-category? (:numeric_code account) category) - (= location (:location jel))))) - ) - (map (fn [jel ] + (when-let [account (account-lookup (:id (:account jel)))] + (and + (l-reports/account-belongs-in-category? (:numeric_code account) category) + (= location (:location jel)))))) + (map (fn [jel] {:date (:date je) :debit (:debit jel) :credit (:credit jel) @@ -621,18 +605,18 @@ (into [])) _ (swap! category-totals assoc-in [client-id location category] (- (or (reduce + 0.0 (map #(or (:credit %) 0.0) all-journal-entries)) 0.0) - (or (reduce + 0.0 (map #(or (:debit %) 0.0) all-journal-entries)) 0.0)) ) + (or (reduce + 0.0 (map #(or (:debit %) 0.0) all-journal-entries)) 0.0))) journal-entries-by-account (group-by #(account-lookup (get-in % [:account :id])) all-journal-entries)] [account journal-entries] (conj (vec journal-entries-by-account) [nil all-journal-entries]) :let [journal-entries (first (reduce - (fn [[acc last-je] je] - (let [next-je (assoc je :running_balance - (- (+ (or (:running_balance last-je 0.0) 0.0) - (or (:credit je 0.0) 0.0)) - (or (:debit je 0.0) 0.0)))] - [(conj acc next-je) next-je])) - [] - (sort-by :date journal-entries)))]] + (fn [[acc last-je] je] + (let [next-je (assoc je :running_balance + (- (+ (or (:running_balance last-je 0.0) 0.0) + (or (:credit je 0.0) 0.0)) + (or (:debit je 0.0) 0.0)))] + [(conj acc next-je) next-je])) + [] + (sort-by :date journal-entries)))]] {:category (->graphql category) :client_id client-id :location location @@ -641,7 +625,7 @@ :journal_entries (when account journal-entries) :total (- (or (reduce + 0.0 (map #(or (:credit %) 0.0) journal-entries)) 0.0) (or (reduce + 0.0 (map #(or (:debit %) 0.0) journal-entries)) 0.0))})) - result {:categories + result {:categories (into base-categories (for [client-id (:client_ids input) :let [_ (assert-can-see-client (:id context) client-id) @@ -675,15 +659,12 @@ line))}] result)) - - (defn journal-detail-report-pdf [context args value] (let [data (get-journal-detail-report context args value) result (print-journal-detail-report (:id context) args data)] (->graphql result))) - (def objects {:balance_sheet_account {:fields {:id {:type 'String} @@ -847,7 +828,7 @@ (def input-objects {:numeric_code_range {:fields {:from {:type 'Int} - :to {:type 'Int}}} + :to {:type 'Int}}} :ledger_filters {:fields {:client_id {:type :id} :vendor_id {:type :id} @@ -874,25 +855,23 @@ :credit {:type :money}}} :import_ledger_entry {:fields {:source {:type 'String} - :external_id {:type 'String} + :external_id {:type 'String} :client_code {:type 'String} :date {:type 'String} :vendor_name {:type 'String} :amount {:type :money} :note {:type 'String} :cleared_against {:type 'String} - :line_items {:type '(list :import_ledger_line_item)}}} - }) + :line_items {:type '(list :import_ledger_line_item)}}}}) (def enums {:ledger_category {:values [{:enum-value :sales} - {:enum-value :cogs} - {:enum-value :payroll} + {:enum-value :cogs} + {:enum-value :payroll} {:enum-value :controllable} {:enum-value :fixed_overhead} {:enum-value :ownership_controllable}]}}) - (def resolvers {:get-ledger-page get-ledger-page :get-balance-sheet get-balance-sheet diff --git a/src/clj/auto_ap/graphql/plaid.clj b/src/clj/auto_ap/graphql/plaid.clj index a678215d..9f8d174e 100644 --- a/src/clj/auto_ap/graphql/plaid.clj +++ b/src/clj/auto_ap/graphql/plaid.clj @@ -21,13 +21,11 @@ :name (first name)})) [])) - - (defn attach [schema] - (-> + (-> (merge-with merge schema {:objects {:plaid_link_result - {:fields {:token {:type 'String}} } + {:fields {:token {:type 'String}}} :plaid_item {:fields {:external_id {:type 'String} @@ -50,7 +48,7 @@ :name {:type 'String} :number {:type 'String}}}} :queries {:search_plaid_merchants {:type '(list :plaid_merchant) - :args {:query {:type 'String}} - :resolve :search-plaid-merchants}}}) + :args {:query {:type 'String}} + :resolve :search-plaid-merchants}}}) (attach-tracing-resolvers {:search-plaid-merchants search-merchants}))) diff --git a/src/clj/auto_ap/graphql/sales_orders.clj b/src/clj/auto_ap/graphql/sales_orders.clj index 660c30c8..2193254b 100644 --- a/src/clj/auto_ap/graphql/sales_orders.clj +++ b/src/clj/auto_ap/graphql/sales_orders.clj @@ -1,6 +1,6 @@ (ns auto-ap.graphql.sales-orders (:require [auto-ap.datomic.sales-orders :as d-sales-orders2] - [auto-ap.graphql.utils :refer [->graphql <-graphql result->page assert-admin] ] + [auto-ap.graphql.utils :refer [->graphql <-graphql result->page assert-admin]] [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [auto-ap.graphql.utils :refer [attach-tracing-resolvers]])) @@ -14,19 +14,18 @@ (defn get-all-sales-orders [context args _] (assert-admin (:id context)) (map - ->graphql - (first (d-sales-orders2/get-graphql (assoc (<-graphql args) :count Integer/MAX_VALUE))))) - + ->graphql + (first (d-sales-orders2/get-graphql (assoc (<-graphql args) :count Integer/MAX_VALUE))))) (def objects {:sales_order_page {:fields {:sales_orders {:type '(list :sales_order)} - :count {:type 'Int} - :total {:type 'Int} - :start {:type 'Int} - :end {:type 'Int} - :sales_order_total {:type :money} - :sales_order_tax {:type :money}}} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int} + :sales_order_total {:type :money} + :sales_order_tax {:type :money}}} :sales_order {:fields {:id {:type :id} @@ -93,8 +92,7 @@ (def resolvers {:get-all-sales-orders get-all-sales-orders - :get-sales-order-page get-sales-orders-page - }) + :get-sales-order-page get-sales-orders-page}) (defn attach [schema] (-> diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index ed2a1c5e..4544973a 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -29,9 +29,8 @@ (defn get-transaction-rule-matches [context args _] (if (= "admin" (:user/role (:id context))) (let [transaction (update (d-transactions/get-by-id (:transaction_id args)) :transaction/date c/to-date) - all-rules (tr/get-all-for-client (:db/id (:transaction/client transaction))) + all-rules (tr/get-all-for-client (:db/id (:transaction/client transaction)))] - ] (mu/log ::counted :count (count all-rules)) (doto (map ->graphql (rm/get-matching-rules transaction all-rules)) (#(println (count %))))) @@ -43,7 +42,7 @@ :account account_id :location location}) -(defn delete-transaction-rule [context {:keys [transaction_rule_id ]} _] +(defn delete-transaction-rule [context {:keys [transaction_rule_id]} _] (assert-admin (:id context)) (let [existing-transaction-rule (tr/get-by-id transaction_rule_id)] (when-not (:transaction-rule/description existing-transaction-rule) @@ -59,62 +58,59 @@ (. java.util.regex.Pattern (compile description java.util.regex.Pattern/CASE_INSENSITIVE)) (catch Exception e (throw (ex-info (ex-message e) {:validation-error (ex-message e)})))) - _ (when-not (dollars= 1.0 account-total) + _ (when-not (dollars= 1.0 account-total) (let [error (str "Account total (" account-total ") does not reach 100%")] (throw (ex-info error {:validation-error error})))) _ (when (and (str/blank? description) - (nil? yodlee_merchant_id)) + (nil? yodlee_merchant_id)) (let [error (str "You must provide a description or a yodlee merchant")] (throw (ex-info error {:validation-error error})))) _ (doseq [a accounts :let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account_id a)) - client (dc/pull (dc/db conn) [:client/locations] client_id) - ]] + client (dc/pull (dc/db conn) [:client/locations] client_id)]] (when (and location (not= location (:location a))) (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] - (throw (ex-info err {:validation-error err}) ))) - + (throw (ex-info err {:validation-error err})))) + (when (and (not location) (not (get (into #{"Shared"} (:client/locations client)) (:location a)))) (let [err (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")] - (throw (ex-info err {:validation-error err}) )))) + (throw (ex-info err {:validation-error err}))))) rule-id (if id id "transaction-rule") transaction [[:upsert-entity #:transaction-rule {:db/id (or rule-id (random-tempid)) - :description description - :note note - :client client_id - :bank-account bank_account_id - :yodlee-merchant yodlee_merchant_id - :dom-lte dom_lte - :dom-gte dom_gte - :amount-lte amount_lte - :amount-gte amount_gte - :vendor vendor_id - :transaction-approval-status - (some->> transaction_approval_status - name - snake->kebab - (keyword "transaction-approval-status")) - :transaction-rule/accounts (map transaction-rule-account->entity accounts)}]] - + :description description + :note note + :client client_id + :bank-account bank_account_id + :yodlee-merchant yodlee_merchant_id + :dom-lte dom_lte + :dom-gte dom_gte + :amount-lte amount_lte + :amount-gte amount_gte + :vendor vendor_id + :transaction-approval-status + (some->> transaction_approval_status + name + snake->kebab + (keyword "transaction-approval-status")) + :transaction-rule/accounts (map transaction-rule-account->entity accounts)}]] transaction-result (audit-transact transaction (:id context))] (-> (tr/get-by-id (or (-> transaction-result :tempids (get "transaction-rule")) - id)) + id)) ((ident->enum-f :transaction-rule/transaction-approval-status)) (->graphql)))) (defn -test-transaction-rule [id {:keys [:transaction-rule/description :transaction-rule/client :transaction-rule/bank-account :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte :transaction-rule/yodlee-merchant]} include-coded? count] (let [query (cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name] :transaction/bank-account [:bank-account/name] - :transaction/payment [:db/id]} - ])] - :in ['$ ] + :transaction/payment [:db/id]}])] + :in ['$] :where []} - :args [(dc/db conn)]} + :args [(dc/db conn)]} description (merge-query {:query {:in ['?descr] :where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]} @@ -170,23 +166,22 @@ :where ['[?e :transaction/client ?client-id]]} :args [(:db/id client)]}) - (not include-coded?) (merge-query {:query {:where ['[or [?e :transaction/approval-status :transaction-approval-status/unapproved] [(missing? $ ?e :transaction/approval-status)]]]}}) true (merge-query {:query {:where ['[?e :transaction/id]]}}))] - (->> - (query2 query) - (transduce (comp - (take (or count 15)) - (map first) - (map #(dissoc % :transaction/id)) - (map (fn [x] - (update x :transaction/date c/from-date))) - (map ->graphql)) - conj [])))) + (->> + (query2 query) + (transduce (comp + (take (or count 15)) + (map first) + (map #(dissoc % :transaction/id)) + (map (fn [x] + (update x :transaction/date c/from-date))) + (map ->graphql)) + conj [])))) (defn test-transaction-rule [{:keys [id]} {{:keys [description client_id bank_account_id amount_lte amount_gte dom_lte dom_gte yodlee_merchant_id]} :transaction_rule} _] (assert-admin id) @@ -200,7 +195,6 @@ :yodlee-merchant (when yodlee_merchant_id {:db/id yodlee_merchant_id})} true 15)) - (defn run-transaction-rule [{:keys [id]} {:keys [transaction_rule_id count]} _] (assert-admin id) (-test-transaction-rule id (tr/get-by-id transaction_rule_id) false count)) diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index a248ea73..a968a0e5 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -66,14 +66,14 @@ (defn get-ids-matching-filters [args] (alog/info ::getting-ids-matching-filters - :args args) + :args args) (let [ids (some-> (:filters args) (assoc :clients (:clients args)) (assoc :id (:id args)) (<-graphql) (update :approval-status enum->keyword "transaction-approval-status") (assoc :per-page Integer/MAX_VALUE) - (d-transactions/raw-graphql-ids ) + (d-transactions/raw-graphql-ids) :ids) specific-ids (d-transactions/filter-ids (seq (:ids args)))] (if (seq (:ids args)) @@ -83,13 +83,13 @@ (defn all-ids-not-locked [all-ids] (->> all-ids (dc/q '[:find ?t - :in $ [?t ...] - :where - [?t :transaction/client ?c] - [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] - [?t :transaction/date ?d] - [(>= ?d ?lu)]] - (dc/db conn)) + :in $ [?t ...] + :where + [?t :transaction/client ?c] + [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] + [?t :transaction/date ?d] + [(>= ?d ?lu)]] + (dc/db conn)) (map first))) (defn bulk-change-status [context args _] (let [_ (assert-admin (:id context)) @@ -98,47 +98,46 @@ all-ids-not-locked)] (alog/info ::bulk-change-status - :count (count all-ids) - :sample (take 3 all-ids) - :status (:status args) - ) + :count (count all-ids) + :sample (take 3 all-ids) + :status (:status args)) (audit-transact-batch (->> all-ids (mapv (fn [t] [:upsert-transaction {:db/id t :transaction/approval-status (enum->keyword (:status args) "transaction-approval-status")}]))) - + (:id context)) - {:message (str "Succesfully changed " (count all-ids) " transactions to be " (name (:status args) ) ".")})) + {:message (str "Succesfully changed " (count all-ids) " transactions to be " (name (:status args)) ".")})) ;; TODO very similar to rule-matching (defn maybe-code-accounts [transaction account-rules valid-locations] (with-precision 2 (let [accounts (vec (mapcat - (fn [ar] - (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) - (:transaction/amount transaction) - 100))))] - (if (= "Shared" (:location ar)) - (->> valid-locations - (map - (fn [cents location] - {:db/id (random-tempid) - :transaction-account/account (:account_id ar) - :transaction-account/amount (* 0.01 cents) - :transaction-account/location location}) - (rm/spread-cents cents-to-distribute (count valid-locations)))) - [(cond-> {:db/id (random-tempid) - :transaction-account/account (:account_id ar) - :transaction-account/amount (* 0.01 cents-to-distribute)} - (:location ar) (assoc :transaction-account/location (:location ar)))]))) - account-rules)) + (fn [ar] + (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) + (:transaction/amount transaction) + 100))))] + (if (= "Shared" (:location ar)) + (->> valid-locations + (map + (fn [cents location] + {:db/id (random-tempid) + :transaction-account/account (:account_id ar) + :transaction-account/amount (* 0.01 cents) + :transaction-account/location location}) + (rm/spread-cents cents-to-distribute (count valid-locations)))) + [(cond-> {:db/id (random-tempid) + :transaction-account/account (:account_id ar) + :transaction-account/amount (* 0.01 cents-to-distribute)} + (:location ar) (assoc :transaction-account/location (:location ar)))]))) + account-rules)) accounts (mapv - (fn [a] - (update a :transaction-account/amount - #(with-precision 2 - (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) - accounts) + (fn [a] + (update a :transaction-account/amount + #(with-precision 2 + (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) + accounts) leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction)) (Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts))))) *math-context*)) @@ -152,13 +151,13 @@ (when-not (seq (:clients context)) (throw (ex-info "Client is required" {:validation-error "Client is required"}))) - (let [args (assoc args :clients (:clients context) :id (:id context)) + (let [args (assoc args :clients (:clients context) :id (:id context)) client->locations (->> (:clients context) - (map :db/id ) + (map :db/id) (dc/q - '[:find (pull ?e [:db/id :client/locations]) - :in $ [?e ...]] - (dc/db conn)) + '[:find (pull ?e [:db/id :client/locations]) + :in $ [?e ...]] + (dc/db conn)) (map (fn [[client]] [(:db/id client) (:client/locations client)])) (into {})) @@ -166,41 +165,40 @@ transactions (pull-many (dc/db conn) [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids)) account-total (reduce + 0 (map (fn [x] (:percentage x)) (:accounts args)))] (alog/info ::bulk-coding-transactions - :count (count transactions) - :sample (take 3 transactions)) + :count (count transactions) + :sample (take 3 transactions)) (when - (and - (seq (:accounts args)) - (not (dollars= 1.0 account-total))) - (let [error (str "Account total (" account-total ") does not reach 100%")] - (throw (ex-info error {:validation-error error})))) + (and + (seq (:accounts args)) + (not (dollars= 1.0 account-total))) + (let [error (str "Account total (" account-total ") does not reach 100%")] + (throw (ex-info error {:validation-error error})))) (doseq [a (:accounts args) :let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account_id a))]] (when (and location (not= location (:location a))) (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] - (throw (ex-info err {:validation-error err}) ))) + (throw (ex-info err {:validation-error err})))) (doseq [[_ locations] client->locations] (when (and (not location) (not (get (into #{"Shared"} locations) (:location a)))) (let [err (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")] - (throw (ex-info err {:validation-error err}) ))))) + (throw (ex-info err {:validation-error err})))))) (audit-transact-batch - (map (fn [t] - (let [locations (client->locations (-> t :transaction/client :db/id))] - (doto - [:upsert-transaction (cond-> t - (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) - (:vendor args) (assoc :transaction/vendor (:vendor args)) - (seq (:accounts args)) (assoc :transaction/accounts (maybe-code-accounts t (:accounts args) locations)))] - clojure.pprint/pprint))) - transactions) - (:id context)) + (map (fn [t] + (let [locations (client->locations (-> t :transaction/client :db/id))] + (doto + [:upsert-transaction (cond-> t + (:approval_status args) (assoc :transaction/approval-status (enum->keyword (:approval_status args) "transaction-approval-status")) + (:vendor args) (assoc :transaction/vendor (:vendor args)) + (seq (:accounts args)) (assoc :transaction/accounts (maybe-code-accounts t (:accounts args) locations)))] + clojure.pprint/pprint))) + transactions) + (:id context)) {:message (str "Successfully coded " (count all-ids) " transactions.")})) - (defn delete-transactions [context args _] (let [_ (assert-admin (:id context)) args (assoc args :clients (:clients context)) @@ -208,24 +206,24 @@ db (dc/db conn)] (alog/info ::bulk-delete-transactions - :count (count all-ids) - :sample (take 3 all-ids)) + :count (count all-ids) + :sample (take 3 all-ids)) (audit-transact-batch - (mapcat (fn [i] - (let [transaction (dc/pull db [:transaction/payment - :transaction/expected-deposit - :db/id] i) - payment-id (-> transaction :transaction/payment :db/id) - expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] - (cond->> [[:db/retractEntity [:journal-entry/original-entity i]]] - payment-id (into [{:db/id payment-id - :payment/status :payment-status/pending} - [:db/retract (:db/id transaction) :transaction/payment payment-id]]) - expected-deposit-id (into [{:db/id expected-deposit-id - :expected-deposit/status :expected-deposit-status/pending} - [:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]])))) - all-ids) - (:id context)) + (mapcat (fn [i] + (let [transaction (dc/pull db [:transaction/payment + :transaction/expected-deposit + :db/id] i) + payment-id (-> transaction :transaction/payment :db/id) + expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] + (cond->> [[:db/retractEntity [:journal-entry/original-entity i]]] + payment-id (into [{:db/id payment-id + :payment/status :payment-status/pending} + [:db/retract (:db/id transaction) :transaction/payment payment-id]]) + expected-deposit-id (into [{:db/id expected-deposit-id + :expected-deposit/status :expected-deposit-status/pending} + [:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]])))) + all-ids) + (:id context)) (audit-transact-batch (mapcat (fn [i] (let [transaction-tx (if (:suppress args) @@ -242,21 +240,21 @@ (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + _ (assert-can-see-client (:id context) (:transaction/client transaction)) matches-set (i-transactions/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction) (:db/id (:transaction/client transaction)))] (->graphql (for [matches matches-set] - (for [[_ invoice-id ] matches] + (for [[_ invoice-id] matches] (d-invoices/get-by-id invoice-id)))))) (defn get-potential-unpaid-invoices-matches [context args _] (assert-power-user (:id context)) (let [transaction (d-transactions/get-by-id (:transaction_id args)) - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + _ (assert-can-see-client (:id context) (:transaction/client transaction)) matches-set (i-transactions/match-transaction-to-unpaid-invoices (:transaction/amount transaction) (:db/id (:transaction/client transaction)))] (->graphql (for [matches matches-set] - (for [[_ invoice-id ] matches] + (for [[_ invoice-id] matches] (d-invoices/get-by-id invoice-id)))))) (defn unlink-transaction [context args _] @@ -264,20 +262,20 @@ args (assoc args :id (:id context)) transaction-id (:transaction_id args) transaction (dc/pull (dc/db conn) - [:transaction/approval-status - :transaction/status - :transaction/date - :transaction/location - :transaction/vendor - :transaction/accounts - :transaction/client [:db/id] - {:transaction/payment [:payment/date {:payment/status [:db/ident]} :db/id]} ] - transaction-id) - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + [:transaction/approval-status + :transaction/status + :transaction/date + :transaction/location + :transaction/vendor + :transaction/accounts + :transaction/client [:db/id] + {:transaction/payment [:payment/date {:payment/status [:db/ident]} :db/id]}] + transaction-id) + _ (assert-can-see-client (:id context) (:transaction/client transaction)) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction)) _ (when (:transaction/payment transaction) (assert-not-locked (:db/id (:transaction/client transaction)) (-> transaction :transaction/payment :payment/date))) - payment (-> transaction :transaction/payment ) + payment (-> transaction :transaction/payment) is-autopay-payment? (some->> (dc/q {:find ['?sp] :in ['$ '?payment] :where ['[?ip :invoice-payment/payment ?payment] @@ -286,8 +284,7 @@ (dc/db conn) (:db/id payment)) seq (map first) - (every? #(instance? java.util.Date %))) - ] + (every? #(instance? java.util.Date %)))] (alog/info ::unlinking :transaction (pr-str transaction) :autopay is-autopay-payment? :payment (pr-str payment)) @@ -295,49 +292,47 @@ (throw (ex-info "Payment can't be undone because it isn't cleared." {:validation-error "Payment can't be undone because it isn't cleared."}))) (if is-autopay-payment? (audit-transact - (-> [{:db/id (:db/id payment) - :payment/status :payment-status/pending} - [:upsert-transaction - {:db/id transaction-id - :transaction/approval-status :transaction-approval-status/unapproved - :transaction/payment nil - :transaction/vendor nil - :transaction/location nil - :transaction/accounts nil}] + (-> [{:db/id (:db/id payment) + :payment/status :payment-status/pending} + [:upsert-transaction + {:db/id transaction-id + :transaction/approval-status :transaction-approval-status/unapproved + :transaction/payment nil + :transaction/vendor nil + :transaction/location nil + :transaction/accounts nil}] - [:db/retractEntity (:db/id payment) ]] + [:db/retractEntity (:db/id payment)]] - (into (map (fn [[invoice-payment]] - [:db/retractEntity invoice-payment]) - (dc/q {:find ['?ip] - :in ['$ '?p] - :where ['[?ip :invoice-payment/payment ?p]]} - (dc/db conn) - (:db/id payment) )))) + (into (map (fn [[invoice-payment]] + [:db/retractEntity invoice-payment]) + (dc/q {:find ['?ip] + :in ['$ '?p] + :where ['[?ip :invoice-payment/payment ?p]]} + (dc/db conn) + (:db/id payment))))) (:id context)) (audit-transact - [{:db/id (:db/id payment) - :payment/status :payment-status/pending} - [:upsert-transaction - {:db/id transaction-id - :transaction/approval-status :transaction-approval-status/unapproved - :transaction/payment nil - :transaction/vendor nil - :transaction/location nil - :transaction/accounts nil}]] + [{:db/id (:db/id payment) + :payment/status :payment-status/pending} + [:upsert-transaction + {:db/id transaction-id + :transaction/approval-status :transaction-approval-status/unapproved + :transaction/payment nil + :transaction/vendor nil + :transaction/location nil + :transaction/accounts nil}]] (:id context))) (-> (d-transactions/get-by-id transaction-id) approval-status->graphql ->graphql))) - (defn transaction-account->entity [{:keys [id account_id amount location]}] #:transaction-account {:amount amount :db/id (or id (random-tempid)) :account account_id :location location}) - (defn assert-valid-expense-accounts [accounts] (doseq [trans-account accounts :let [account (dc/pull (dc/db conn) @@ -351,7 +346,7 @@ (:account/location account))) (let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")] (throw (ex-info err - {:validation-error err})))) + {:validation-error err})))) (when (and (empty? (:account/location account)) (= "A" (:location trans-account))) @@ -359,13 +354,12 @@ (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 memo forecast_match]} :transaction} _] (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) _ (assert-not-locked (:db/id (:transaction/client existing-transaction)) (:transaction/date existing-transaction)) account-total (reduce + 0 (map (fn [x] (:amount x)) accounts)) @@ -378,17 +372,17 @@ set (conj "A") (conj "HQ"))))] - + (when (and (not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total)) (or (and (= approval_status :unapproved) (> (count accounts) 0)) - (not= approval_status :unapproved))) + (not= approval_status :unapproved))) (let [error (str "Expense account total (" account-total ") does not equal transaction total (" (Math/abs (:transaction/amount existing-transaction)) ")")] - (throw (ex-info error {:validation-error error})))) + (throw (ex-info error {:validation-error error})))) (when missing-locations - (throw (ex-info (str "Location '" (str/join ", " missing-locations) "' not found on client.") {})) ) - + (throw (ex-info (str "Location '" (str/join ", " missing-locations) "' not found on client.") {}))) + (audit-transact (cond-> [[:upsert-transaction {:db/id id :transaction/vendor vendor_id :transaction/memo memo @@ -413,8 +407,8 @@ (defn match-transaction [context {:keys [transaction_id payment_id]} _] (let [transaction (d-transactions/get-by-id transaction_id) payment (d-checks/get-by-id payment_id) - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) - _ (assert-can-see-client (:id context) (:payment/client payment) ) + _ (assert-can-see-client (:id context) (:transaction/client transaction)) + _ (assert-can-see-client (:id context) (:payment/client payment)) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction))] (when (not= (:db/id (:transaction/client transaction)) (:db/id (:payment/client payment))) @@ -423,7 +417,7 @@ (when-not (dollars= (- (:transaction/amount transaction)) (:payment/amount payment)) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) - (audit-transact (into + (audit-transact (into [{:db/id (:db/id payment) :payment/status :payment-status/cleared :payment/date (coerce/to-date (first (sort [(:payment/date payment) @@ -431,14 +425,14 @@ [:upsert-transaction {:db/id (:db/id transaction) - :transaction/payment (:db/id payment) - :transaction/vendor (:db/id (:payment/vendor payment)) - :transaction/location "A" - :transaction/approval-status :transaction-approval-status/approved - :transaction/accounts [{:db/id (random-tempid) - :transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) - :transaction-account/location "A" - :transaction-account/amount (Math/abs (:transaction/amount transaction))}]}]]) + :transaction/payment (:db/id payment) + :transaction/vendor (:db/id (:payment/vendor payment)) + :transaction/location "A" + :transaction/approval-status :transaction-approval-status/approved + :transaction/accounts [{:db/id (random-tempid) + :transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) + :transaction-account/location "A" + :transaction-account/amount (Math/abs (:transaction/amount transaction))}]}]]) (:id context))) (solr/touch-with-ledger transaction_id) (-> (d-transactions/get-by-id transaction_id) @@ -448,7 +442,7 @@ (defn match-transaction-autopay-invoices [context {:keys [transaction_id autopay_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + _ (assert-can-see-client (:id context) (:transaction/client transaction)) db (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) autopay_invoice_ids)) invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/total %) autopay_invoice_ids)) @@ -474,9 +468,9 @@ (:db/id (:transaction/bank-account transaction)) (:db/id (:transaction/client transaction)))] (alog/info ::adding-payment-from-autopay-invoice - :payment (pr-str payment-tx)) + :payment (pr-str payment-tx)) (audit-transact payment-tx (:id context))) - (solr/touch-with-ledger transaction_id) + (solr/touch-with-ledger transaction_id) (-> (d-transactions/get-by-id transaction_id) approval-status->graphql ->graphql))) @@ -485,8 +479,8 @@ (defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) - - _ (assert-can-see-client (:id context) (:transaction/client transaction) ) + + _ (assert-can-see-client (:id context) (:transaction/client transaction)) _ (assert-not-locked (:db/id (:transaction/client transaction)) (:transaction/date transaction)) db (dc/db conn) invoice-clients (set (map #(pull-ref db :invoice/client %) unpaid_invoice_ids)) @@ -502,17 +496,17 @@ (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) (when (:transaction/payment transaction) (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"}))) - + (let [payment-tx (i-transactions/add-new-payment (dc/pull db [:transaction/amount :transaction/date :db/id] transaction_id) - (map (fn [id] - (let [entity (dc/pull db [:invoice/vendor :db/id :invoice/total] id)] - [(or (-> entity :invoice/vendor :db/id) - (-> entity :invoice/vendor)) - (-> entity :db/id) - (-> entity :invoice/total)])) - unpaid_invoice_ids) - (:db/id (:transaction/bank-account transaction)) - (:db/id (:transaction/client transaction)))] + (map (fn [id] + (let [entity (dc/pull db [:invoice/vendor :db/id :invoice/total] id)] + [(or (-> entity :invoice/vendor :db/id) + (-> entity :invoice/vendor)) + (-> entity :db/id) + (-> entity :invoice/total)])) + unpaid_invoice_ids) + (:db/id (:transaction/bank-account transaction)) + (:db/id (:transaction/client transaction)))] (audit-transact payment-tx (:id context))) (solr/touch-with-ledger transaction_id) @@ -527,9 +521,8 @@ :count Integer/MAX_VALUE} nil) (filter #(not (:payment %))) - (map :id )) + (map :id)) - transaction_ids) _ (mu/log ::here :txids transaction_ids) transaction_ids (all-ids-not-locked transaction_ids) @@ -553,17 +546,16 @@ (audit-transact (mapv (fn [t] [:upsert-transaction (remove-nils (rm/apply-rule {:db/id (:db/id t) - :transaction/amount (:transaction/amount t)} - transaction-rule + :transaction/amount (:transaction/amount t)} + transaction-rule - (or (-> t :transaction/bank-account :bank-account/locations) - (-> t :transaction/client :client/locations))))]) + (or (-> t :transaction/bank-account :bank-account/locations) + (-> t :transaction/client :client/locations))))]) transactions) (:id context)) (doseq [n transactions] - (solr/touch-with-ledger (:db/id n))) - ) + (solr/touch-with-ledger (:db/id n)))) (transduce (comp (map d-transactions/get-by-id) @@ -571,12 +563,12 @@ (map ->graphql)) conj [] - transaction_ids )) + transaction_ids)) (def objects {:transaction {:fields {:id {:type :id} :amount {:type 'String} - :memo {:type 'String} + :memo {:type 'String} :is_locked {:type 'Boolean} :description_original {:type 'String} :description_simple {:type 'String} @@ -628,8 +620,8 @@ :resolve :mutation/bulk-code-transactions} :delete_transactions {:type :message :args {:filters {:type :transaction_filters} - :ids {:type '(list :id)} - :suppress {:type 'Boolean}} + :ids {:type '(list :id)} + :suppress {:type 'Boolean}} :resolve :mutation/delete-transactions} :edit_transaction {:type :transaction :args {:transaction {:type :edit_transaction}} @@ -711,9 +703,8 @@ :mutation/match-transaction-unpaid-invoices match-transaction-unpaid-invoices :mutation/match-transaction-rules match-transaction-rules}) - (defn attach [schema] - (-> + (-> (merge-with merge schema {:objects objects :queries queries diff --git a/src/clj/auto_ap/graphql/utils.clj b/src/clj/auto_ap/graphql/utils.clj index 056f6ac7..8211626f 100644 --- a/src/clj/auto_ap/graphql/utils.clj +++ b/src/clj/auto_ap/graphql/utils.clj @@ -13,7 +13,6 @@ [iol-ion.query :refer [entid]] [slingshot.slingshot :refer [throw+]])) - (defn snake->kebab [s] (str/replace s #"_" "-")) @@ -107,8 +106,6 @@ (#{"manager" "user" "power-user" "read-only"} (:user/role id)) (:user/clients id []))) - - (defn result->page [results result-count key args] {key (map ->graphql results) :total result-count @@ -197,7 +194,6 @@ (= :client/code (first x))) [(entid (dc/db conn) x)] - (sequential? x) (mapcat coerce-client-ids x) @@ -218,14 +214,14 @@ e))))) (defn exception->4xx [f] - (try + (try (f) (catch Throwable e -(throw+ (ex-info (.getMessage e) {:type :form-validation - :form-validation-errors [(.getMessage e)]})) + (throw+ (ex-info (.getMessage e) {:type :form-validation + :form-validation-errors [(.getMessage e)]})) #_(throw (ex-info (.getMessage e) - {:type :notification} - e))))) + {:type :notification} + e))))) (defn notify-if-locked [client-id date] (try diff --git a/src/clj/auto_ap/graphql/vendors.clj b/src/clj/auto_ap/graphql/vendors.clj index 7cface3c..454f1bc1 100644 --- a/src/clj/auto_ap/graphql/vendors.clj +++ b/src/clj/auto_ap/graphql/vendors.clj @@ -130,7 +130,6 @@ :vendor/schedule-payment-dom schedule-payment-dom :vendor/automatically-paid-when-due (:automatically_paid_when_due in)))] - transaction-result (audit-transact [transaction] (:id context)) new-vendor (d-vendors/get-by-id (or (-> transaction-result :tempids (get "vendor")) id))] @@ -160,7 +159,6 @@ (audit-transact transaction (:id context)) to)) - (defn get-graphql [context args _] (assert-admin (:id context)) (let [args (assoc args :id (:id context)) @@ -187,7 +185,6 @@ (if-let [query (not-empty (cleanse-query (:query args)))] (let [search-query (str "name:(" query ")")] - (for [{:keys [id name]} (solr/query solr/impl "vendors" {"query" (cond-> search-query (not (is-admin? (:id context))) (str " hidden:false")) "fields" "id, name"})] diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj index 8f02a10b..d5afdfa8 100644 --- a/src/clj/auto_ap/handler.clj +++ b/src/clj/auto_ap/handler.clj @@ -70,7 +70,6 @@ :headers {} :body ""}) - (defn home-handler [{:keys [identity]}] (if identity {:status 302 @@ -78,7 +77,6 @@ {:status 302 :headers {"Location" "/login"}})) - (def match->handler-lookup (-> {:not-found not-found :home home-handler} @@ -90,15 +88,13 @@ (merge yodlee2/match->handler) (merge auth/match->handler) (merge invoices/match->handler) - (merge exports/match->handler) - )) + (merge exports/match->handler))) (def match->handler (fn [route] (or (get match->handler-lookup route) route))) - (def route-handler (make-handler all-routes match->handler)) @@ -125,7 +121,6 @@ uri :request-method method)) - (def auth-backend (jws-backend {:secret (:jwt-secret env) :options {:alg :hs512}})) (defn wrap-logging [handler] @@ -159,8 +154,6 @@ :exception e) (throw e))))))) - - (defn wrap-idle-session-timeout [handler] (fn [request] diff --git a/src/clj/auto_ap/import/common.clj b/src/clj/auto_ap/import/common.clj index a296294b..472dc02b 100644 --- a/src/clj/auto_ap/import/common.clj +++ b/src/clj/auto_ap/import/common.clj @@ -9,21 +9,21 @@ (random-tempid))) (defn wrap-integration [f bank-account] - (try + (try (let [result (f)] @(dc/transact-async conn [{:db/id bank-account - :bank-account/integration-status - {:db/id (bank-account->integration-id bank-account) - :integration-status/state :integration-state/success - :integration-status/last-attempt (java.util.Date.) - :integration-status/last-updated (java.util.Date.)}}]) + :bank-account/integration-status + {:db/id (bank-account->integration-id bank-account) + :integration-status/state :integration-state/success + :integration-status/last-attempt (java.util.Date.) + :integration-status/last-updated (java.util.Date.)}}]) result) (catch Exception e @(dc/transact-async conn [{:db/id bank-account - :bank-account/integration-status - {:db/id (bank-account->integration-id bank-account) - :integration-status/state :integration-state/failed - :integration-status/last-attempt (java.util.Date.) - :integration-status/message (.getMessage e)}}]) + :bank-account/integration-status + {:db/id (bank-account->integration-id bank-account) + :integration-status/state :integration-state/failed + :integration-status/last-attempt (java.util.Date.) + :integration-status/message (.getMessage e)}}]) (alog/warn ::integration-failed :error e) nil))) diff --git a/src/clj/auto_ap/import/intuit.clj b/src/clj/auto_ap/import/intuit.clj index ff4dd3e0..3b67f203 100644 --- a/src/clj/auto_ap/import/intuit.clj +++ b/src/clj/auto_ap/import/intuit.clj @@ -12,15 +12,15 @@ [datomic.api :as dc] [iol-ion.utils :refer [remove-nils]])) -(defn get-intuit-bank-accounts - ( [db] - (dc/q '[:find ?external-id ?ba ?c - :in $ - :where - [?c :client/bank-accounts ?ba] - [?ba :bank-account/intuit-bank-account ?iab] - [?iab :intuit-bank-account/external-id ?external-id]] - db)) +(defn get-intuit-bank-accounts + ([db] + (dc/q '[:find ?external-id ?ba ?c + :in $ + :where + [?c :client/bank-accounts ?ba] + [?ba :bank-account/intuit-bank-account ?iab] + [?iab :intuit-bank-account/external-id ?external-id]] + db)) ([db & client-codes] (dc/q '[:find ?external-id ?ba ?c :in $ [?cc ...] @@ -32,7 +32,6 @@ db client-codes))) - (defn intuit->transaction [transaction] (let [check-number (when (not (str/blank? (:Num transaction))) (try @@ -46,7 +45,6 @@ :transaction/status "POSTED"} check-number (assoc :transaction/check-number check-number)))) - (defn intuits->transactions [transactions bank-account-id client-id] (->> transactions (map intuit->transaction) diff --git a/src/clj/auto_ap/import/intuit_test.clj b/src/clj/auto_ap/import/intuit_test.clj index 4fe46121..fefbf1fc 100644 --- a/src/clj/auto_ap/import/intuit_test.clj +++ b/src/clj/auto_ap/import/intuit_test.clj @@ -11,10 +11,9 @@ (t/is (= #inst "2021-01-01T00:00:00-08:00" (:transaction/date (sut/intuit->transaction (assoc base-transaction :Date "2021-01-01"))))) (t/is (= #inst "2021-06-01T00:00:00-07:00" (:transaction/date (sut/intuit->transaction (assoc base-transaction :Date "2021-06-01"))))))) - (t/deftest intuits->transactions (t/testing "should give unique ids to duplicates" (t/is (= ["2021-10-11T00:00:00.000-07:00-123-this is a description-45.23-0-345" "2021-10-11T00:00:00.000-07:00-123-this is a description-45.23-1-345"] (map :transaction/raw-id (sut/intuits->transactions [base-transaction base-transaction] - 123 - 345)))))) + 123 + 345)))))) diff --git a/src/clj/auto_ap/import/manual.clj b/src/clj/auto_ap/import/manual.clj index 8f54b0b7..41885b4b 100644 --- a/src/clj/auto_ap/import/manual.clj +++ b/src/clj/auto_ap/import/manual.clj @@ -7,8 +7,6 @@ [clojure.data.csv :as csv] [datomic.api :as dc])) - - (def columns [:status :raw-date :description-original :high-level-category nil nil :amount nil nil nil nil nil :bank-account-code :client-code]) (defn tabulate-data [data] @@ -33,12 +31,12 @@ (defn import-batch [transactions user] (let [bank-account-code->client (into {} - (dc/q '[:find ?bac ?c - :in $ - :where - [?c :client/bank-accounts ?ba] - [?ba :bank-account/code ?bac]] - (dc/db conn))) + (dc/q '[:find ?bac ?c + :in $ + :where + [?c :client/bank-accounts ?ba] + [?ba :bank-account/code ?bac]] + (dc/db conn))) bank-account-code->bank-account (into {} (dc/q '[:find ?bac ?ba :in $ @@ -46,9 +44,9 @@ (dc/db conn))) import-batch (t/start-import-batch :import-source/manual user) transactions (->> transactions - (map (fn [t] - (manual->transaction t bank-account-code->bank-account bank-account-code->client))) - (t/apply-synthetic-ids ))] + (map (fn [t] + (manual->transaction t bank-account-code->bank-account bank-account-code->client))) + (t/apply-synthetic-ids))] (try (doseq [transaction transactions] (when-not (seq (:errors transaction)) diff --git a/src/clj/auto_ap/import/manual/common.clj b/src/clj/auto_ap/import/manual/common.clj index 41464ab5..d12a793d 100644 --- a/src/clj/auto_ap/import/manual/common.clj +++ b/src/clj/auto_ap/import/manual/common.clj @@ -22,9 +22,9 @@ (defn parse-date [{:keys [raw-date]}] (when-not - (re-find #"\d{1,2}/\d{1,2}/\d{4}" raw-date) + (re-find #"\d{1,2}/\d{1,2}/\d{4}" raw-date) (throw (Exception. (str "Date " raw-date " must match MM/dd/yyyy")))) - (try + (try (parse-u/parse-value :clj-time "MM/dd/yyyy" raw-date) (catch Exception e diff --git a/src/clj/auto_ap/import/plaid.clj b/src/clj/auto_ap/import/plaid.clj index d77223fc..6041c638 100644 --- a/src/clj/auto_ap/import/plaid.clj +++ b/src/clj/auto_ap/import/plaid.clj @@ -16,7 +16,7 @@ [manifold.deferred :as de] [manifold.executor :as ex])) -(defn get-plaid-accounts +(defn get-plaid-accounts ([db] (-> (dc/q '[:find ?ba ?c ?external-id ?t :in $ @@ -40,7 +40,6 @@ db client-codes)))) - (defn plaid->transaction [t plaid-merchant->vendor-id] (alog/info ::trying-transaction :transaction t) (cond-> #:transaction {:description-original (:name t) @@ -57,7 +56,7 @@ :db/id (random-tempid)}) (not (str/blank? (:check_number t))) (assoc :transaction/check-number (Long/parseLong (:check_number t))) #_#_(plaid-merchant->vendor-id (:merchant_name t)) (assoc :transaction/default-vendor - (plaid-merchant->vendor-id (:merchant_name t))))) + (plaid-merchant->vendor-id (:merchant_name t))))) (defn build-plaid-merchant->vendor-id [] (into {} @@ -66,23 +65,22 @@ :where [?v :vendor/plaid-merchant ?pm] [?pm :plaid-merchant/name ?pmn]] - (dc/db conn )))) - + (dc/db conn)))) (def single-thread (ex/fixed-thread-executor 1)) (defn rebuild-search-index [] (de/future-with - single-thread - (auto-ap.solr/index-documents-raw - auto-ap.solr/impl - "plaid_merchants" - (for [[result] (dc/qseq {:query '[:find (pull ?v [:plaid-merchant/name :db/id]) - :in $ - :where [?v :plaid-merchant/name]] - :args [(dc/db conn)]})] - {"id" (:db/id result) - "name" (:plaid-merchant/name result)})))) + single-thread + (auto-ap.solr/index-documents-raw + auto-ap.solr/impl + "plaid_merchants" + (for [[result] (dc/qseq {:query '[:find (pull ?v [:plaid-merchant/name :db/id]) + :in $ + :where [?v :plaid-merchant/name]] + :args [(dc/db conn)]})] + {"id" (:db/id result) + "name" (:plaid-merchant/name result)})))) (defn upsert-accounts [] (try @@ -96,20 +94,16 @@ (remove-nils {:plaid-account/external-id (:account_id a) :plaid-account/last-synced (coerce/to-date (coerce/to-date-time (-> item :status :transactions :last_successful_update))) :plaid-account/balance (or (some-> a - :balances - :current - double) - 0.0)})))) + :balances + :current + double) + 0.0)})))) (catch Exception e (alog/warn ::couldnt-upsert-account :error e)))) - (catch Exception e (alog/warn ::couldnt-upsert-accounts :error e)))) - - - (defn import-plaid-int [] (let [_ (upsert-accounts) import-batch (t/start-import-batch :import-source/plaid "Automated plaid user") @@ -119,8 +113,8 @@ (try (doseq [[bank-account-id client-id external-id access-token] (get-plaid-accounts (dc/db conn)) :let [transaction-result (wrap-integration #(p/get-transactions access-token external-id start end) - bank-account-id) - accounts-by-id (by :account_id (:accounts transaction-result))] + bank-account-id) + accounts-by-id (by :account_id (:accounts transaction-result))] transaction (:transactions transaction-result)] (when (not (:pending transaction)) (t/import-transaction! import-batch (assoc (plaid->transaction (assoc transaction diff --git a/src/clj/auto_ap/import/transactions.clj b/src/clj/auto_ap/import/transactions.clj index ae08abe1..c9d435c5 100644 --- a/src/clj/auto_ap/import/transactions.clj +++ b/src/clj/auto_ap/import/transactions.clj @@ -21,7 +21,7 @@ (if (and client-id bank-account-id amount) (let [[matching-checks] (d-checks/get-graphql {:client-id client-id :clients [client-id] - :bank-account-id bank-account-id + :bank-account-id bank-account-id :amount (- amount) :status :payment-status/pending})] (if (= 1 (count matching-checks)) @@ -29,7 +29,6 @@ nil)) nil)) - (defn transaction->existing-payment [_ check-number client-id bank-account-id amount id] (alog/info ::searching :client-id client-id @@ -46,7 +45,7 @@ check-number (-> (d-checks/get-graphql {:client-id client-id :clients [client-id] - :bank-account-id bank-account-id + :bank-account-id bank-account-id :check-number check-number :amount (- amount) :status :payment-status/pending}) @@ -70,12 +69,12 @@ (group-by first) ;; group by vendors vals) considerations (for [candidate-invoices candidate-invoices-vendor-groups - invoice-count (range 1 32) - consideration (partition invoice-count 1 candidate-invoices) - :when (dollars= (reduce (fn [acc [_ _ amount]] - (+ acc amount)) 0.0 consideration) - (- amount))] - consideration)] + invoice-count (range 1 32) + consideration (partition invoice-count 1 candidate-invoices) + :when (dollars= (reduce (fn [acc [_ _ amount]] + (+ acc amount)) 0.0 consideration) + (- amount))] + consideration)] (alog/info ::unfulfilled-autoapayment-considerations :count (count considerations) :amount amount) @@ -85,13 +84,13 @@ (alog/info ::searching-unpaid-invoice :client-id client-id :amount amount) - (try + (try (let [candidate-invoices-vendor-groups (->> (dc/q {:find ['?vendor-id '?e '?outstanding-balance '?d] :in ['$ '?client-id] :where ['[?e :invoice/client ?client-id] '[?e :invoice/status :invoice-status/unpaid] '(not [_ :invoice-payment/invoice ?e]) - '[?e :invoice/vendor ?vendor-id] + '[?e :invoice/vendor ?vendor-id] '[?e :invoice/outstanding-balance ?outstanding-balance] '[?e :invoice/date ?d]]} (dc/db conn) client-id) @@ -110,10 +109,10 @@ :amount amount :count (count considerations)) considerations) - (catch Exception e - (alog/error ::cant-get-considerations - :error e) - []))) + (catch Exception e + (alog/error ::cant-get-considerations + :error e) + []))) (defn match-transaction-to-single-unfulfilled-autopayments [amount client-id] (let [considerations (match-transaction-to-unfulfilled-autopayments amount client-id)] @@ -134,10 +133,10 @@ :transaction/location "A" :transaction/accounts [#:transaction-account - {:db/id (random-tempid) - :account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) - :location "A" - :amount (Math/abs (:transaction/amount transaction))}])]] + {:db/id (random-tempid) + :account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) + :location "A" + :amount (Math/abs (:transaction/amount transaction))}])]] (conj {:payment/bank-account bank-account-id :payment/client client-id @@ -169,30 +168,26 @@ (= 1234 (extract-check-number {:transaction/description-original "Check abc 1234"})) -(= 1234 (extract-check-number {:transaction/description-original "Check abc 4/10 1234"})) -(= 1234 (extract-check-number {:transaction/description-original "Check abc 4/10 1234 12/3"})) + (= 1234 (extract-check-number {:transaction/description-original "Check abc 4/10 1234"})) + (= 1234 (extract-check-number {:transaction/description-original "Check abc 4/10 1234 12/3"})) -(not= 1234 (extract-check-number {:transaction/description-original "Checkcard 4/10 1234"})) - - ) + (not= 1234 (extract-check-number {:transaction/description-original "Checkcard 4/10 1234"}))) (defn find-expected-deposit [client-id amount date] - (when date + (when date (-> (dc/q - '[:find (pull ?ed [:db/id {:expected-deposit/vendor [:db/id]}]) - :in $ ?c ?a ?d-start + '[:find (pull ?ed [:db/id {:expected-deposit/vendor [:db/id]}]) + :in $ ?c ?a ?d-start :where [?ed :expected-deposit/client ?c] (not [?ed :expected-deposit/status :expected-deposit-status/cleared]) [?ed :expected-deposit/date ?d] [(>= ?d ?d-start)] [?ed :expected-deposit/total ?a2] - [(auto-ap.utils/dollars= ?a2 ?a)] - ] + [(auto-ap.utils/dollars= ?a2 ?a)]] (dc/db conn) client-id amount (coerce/to-date (t/plus date (t/days -10)))) ffirst))) - (defn categorize-transaction [transaction bank-account existing] (cond (= :transaction-approval-status/suppressed (existing (:transaction/id transaction))) :suppressed @@ -235,7 +230,6 @@ (assoc transaction :transaction/check-number check-number) transaction)) - (defn maybe-clear-payment [{:transaction/keys [check-number client bank-account amount id] :as transaction}] (when-let [existing-payment (transaction->existing-payment transaction check-number client bank-account amount id)] (assoc transaction @@ -245,10 +239,10 @@ :transaction/vendor (:db/id (:payment/vendor existing-payment)) :transaction/location "A" :transaction/accounts [#:transaction-account - {:db/id (random-tempid) - :account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) - :location "A" - :amount (Math/abs (double amount))}]))) + {:db/id (random-tempid) + :account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) + :location "A" + :amount (Math/abs (double amount))}]))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn maybe-autopay-invoices [{:transaction/keys [amount client bank-account] :as transaction}] @@ -266,8 +260,7 @@ :transaction-account/amount amount :transaction-account/location "A"}] :transaction/approval-status :transaction-approval-status/approved - :transaction/vendor (:db/id (:expected-deposit/vendor expected-deposit)) - )))) + :transaction/vendor (:db/id (:expected-deposit/vendor expected-deposit)))))) (defn maybe-code [{:transaction/keys [client amount] :as transaction} apply-rules valid-locations] (mu/trace @@ -304,7 +297,6 @@ (maybe-apply-default-vendor) remove-nils))) - (defn get-existing [bank-account] (into {} (dc/q '[:find ?tid ?as2 @@ -317,7 +309,7 @@ (defprotocol ImportBatch (import-transaction! [this transaction]) - (get-stats [this ]) + (get-stats [this]) (get-pending-balance [this bank-account]) (finish! [this]) (fail! [this error])) @@ -326,21 +318,21 @@ :db/id :bank-account/locations :bank-account/start-date - {:client/_bank-accounts [:client/code :client/locked-until :client/locations :db/id]} ]) + {:client/_bank-accounts [:client/code :client/locked-until :client/locations :db/id]}]) (defn start-import-batch [source user] (let [stats (atom {:import-batch/imported 0 - :import-batch/suppressed 0 - :import-batch/error 0 - :import-batch/not-ready 0 - :import-batch/extant 0}) + :import-batch/suppressed 0 + :import-batch/error 0 + :import-batch/not-ready 0 + :import-batch/extant 0}) pending-balance (atom {}) - extant-cache (atom (cache/ttl-cache-factory {} :ttl 60000 )) + extant-cache (atom (cache/ttl-cache-factory {} :ttl 60000)) import-id (get (:tempids @(dc/transact-async conn [{:db/id "import-batch" - :import-batch/date (coerce/to-date (t/now)) - :import-batch/source source - :import-batch/status :import-status/started - :import-batch/user-name user}])) "import-batch") + :import-batch/date (coerce/to-date (t/now)) + :import-batch/source source + :import-batch/status :import-status/started + :import-batch/user-name user}])) "import-batch") rule-applying-function (rm/rule-applying-fn (tr/get-all))] (alog/info ::starting-transaction-import :source source) @@ -349,17 +341,17 @@ (import-transaction! [_ transaction] (let [bank-account (dc/pull (dc/db conn) bank-account-pull - (:transaction/bank-account transaction)) + (:transaction/bank-account transaction)) extant (get (swap! extant-cache cache/through-cache (:transaction/bank-account transaction) get-existing) (:transaction/bank-account transaction)) action (categorize-transaction transaction bank-account extant)] - (try + (try (when (not= "POSTED" (:transaction/status transaction)) (swap! pending-balance (fn [pb] - (update pb + (update pb (:transaction/bank-account transaction) - (fnil + 0.0) + (fnil + 0.0) (:transaction/amount transaction))))) (catch Exception e (alog/warn ::cant-capture-pending @@ -372,7 +364,7 @@ :error :import-batch/error :not-ready :import-batch/not-ready) inc)) (when (= :import action) - (try + (try (let [result (audit-transact [[:upsert-transaction (transaction->txs transaction bank-account rule-applying-function)] {:db/id import-id :import-batch/entry (:db/id transaction)}] @@ -390,14 +382,14 @@ (get-stats [_] @stats) (get-pending-balance [_ bank-account] - (or (get @pending-balance bank-account) - 0.0)) + (or (get @pending-balance bank-account) + 0.0)) (fail! [_ error] (alog/error ::cant-complete-import :import-id import-id :error error) - + @(dc/transact-async conn [(merge {:db/id import-id :import-batch/status :import-status/completed :import-batch/error-message (str error)} @@ -407,12 +399,10 @@ (alog/info ::finished :import-id import-id :source source :stats (pr-str @stats)) @(dc/transact conn [(merge {:db/id import-id - :import-batch/status :import-status/completed} - @stats)]))))) + :import-batch/status :import-status/completed} + @stats)]))))) - - -(defn synthetic-key [{:transaction/keys [date bank-account description-original amount client] } index] +(defn synthetic-key [{:transaction/keys [date bank-account description-original amount client]} index] (str (str (some-> date coerce/to-date-time atime/localize)) "-" bank-account "-" description-original "-" amount "-" index "-" client)) (defn apply-synthetic-ids [transactions] @@ -424,7 +414,7 @@ (let [raw-id (synthetic-key transaction index)] (assoc transaction :transaction/id #_{:clj-kondo/ignore [:unresolved-var]} - (di/sha-256 raw-id) + (di/sha-256 raw-id) :transaction/raw-id raw-id :db/id (random-tempid)))) (range) diff --git a/src/clj/auto_ap/import/yodlee2.clj b/src/clj/auto_ap/import/yodlee2.clj index 0bf9df65..947fd9df 100644 --- a/src/clj/auto_ap/import/yodlee2.clj +++ b/src/clj/auto_ap/import/yodlee2.clj @@ -73,10 +73,10 @@ (alog/info ::finished-import) (t/finish! import-batch) (doseq [[_ bank-account _ _ ya] account-lookup] - (try + (try @(dc/transact auto-ap.datomic/conn - [{:db/id ya - :yodlee-account/pending-balance (t/get-pending-balance import-batch bank-account)}]) + [{:db/id ya + :yodlee-account/pending-balance (t/get-pending-balance import-batch bank-account)}]) (catch Exception e (alog/error ::cant-persist-yodlee-account-pending-balance :error e))))) @@ -95,5 +95,4 @@ nil) (Thread/sleep 10000))))) - (def import-yodlee2 (allow-once import-yodlee2-int)) diff --git a/src/clj/auto_ap/intuit/core.clj b/src/clj/auto_ap/intuit/core.clj index ed69df79..3aa995e0 100644 --- a/src/clj/auto_ap/intuit/core.clj +++ b/src/clj/auto_ap/intuit/core.clj @@ -15,7 +15,6 @@ ;; (def base-url "https://sandbox-quickbooks.api.intuit.com/v3") - (def prod-client-id "ABFRwAiOqQiLN66HKplXfyRE3ipD390DHsrUquflRCiOa81mxa") (def prod-client-secret "xDUj04GeQXpLvrhxep1jjDYwjJWbzzOPrirUQTKF") @@ -27,21 +26,18 @@ ;; "accessToken":, ;; - - (def prod-company-id "123146163906404") - (def prod-base-url "https://quickbooks.api.intuit.com/v3") (defn set-access-token [t] - (s3/put-object :bucket-name (:data-bucket env) + (s3/put-object :bucket-name (:data-bucket env) :key (str "intuit/access-token") :input-stream (io/make-input-stream (.getBytes t) {}) :metadata {:content-type "application/text" :content-length (count (.getBytes t))})) (defn set-refresh-token [t] - (s3/put-object :bucket-name (:data-bucket env) + (s3/put-object :bucket-name (:data-bucket env) :key (str "intuit/refresh-token") :input-stream (io/make-input-stream (.getBytes t) {}) :metadata {:content-type "application/text" @@ -53,7 +49,6 @@ :bucket-name "data.prod.app.integreatconsult.com" :key "intuit/refresh-token"))))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn init-tokens [access refresh] (set-access-token access) @@ -74,17 +69,16 @@ (defn get-basic-auth [] (Base64/encodeBase64String (.getBytes (str prod-client-id ":" prod-client-secret)))) - (defn get-fresh-access-token [] (let [refresh-token (lookup-refresh-token) - response (:body (client/post (str "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer" ) + response (:body (client/post (str "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer") - {:headers {"Accept" "application/json" - "Content-Type" "application/x-www-form-urlencoded" - "Authorization" (str "Basic " (get-basic-auth))} - :form-params {"grant_type" "refresh_token" - "refresh_token" refresh-token} - :as :json}))] + {:headers {"Accept" "application/json" + "Content-Type" "application/x-www-form-urlencoded" + "Authorization" (str "Basic " (get-basic-auth))} + :form-params {"grant_type" "refresh_token" + "refresh_token" refresh-token} + :as :json}))] (set-access-token (:access_token response)) (set-refresh-token (:refresh_token response)) (:access_token response))) @@ -94,21 +88,20 @@ (defn with-auth [t token] (assoc t "Authorization" (str "Bearer " token))) -#_(client/get (str base-url "/company/4620816365202617680") - {:headers base-headers - :as :json}) +#_(client/get (str base-url "/company/4620816365202617680") + {:headers base-headers + :as :json}) (defn get-bank-accounts-raw [token] - (->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query" ) + (->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query") {:headers (with-auth prod-base-headers token) :as :json :query-params {"query" "SELECT * From Account maxresults 1000"}})) :QueryResponse)) - (defn get-bank-accounts [token] - (->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query" ) + (->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query") {:headers (with-auth prod-base-headers token) :as :json @@ -116,7 +109,7 @@ :QueryResponse :Account #_(filter - #(#{"Bank" "Credit Card"} (:AccountType %))) + #(#{"Bank" "Credit Card"} (:AccountType %))) (map (juxt :Id :Name :CurrentBalance :MetaData)) (map (fn [[id name current-balance metadata]] {:id id @@ -124,10 +117,9 @@ :last-updated (c/to-date-time (-> metadata :LastUpdatedTime)) :current-balance (try (double current-balance) (catch Exception _ nil))})))) - (defn get-all-transactions [start end] (let [token (get-fresh-access-token)] - (:body (client/get (str prod-base-url "/company/" prod-company-id "/reports/TransactionList" "?minorversion=63&start_date=" start "&end_date=" end) + (:body (client/get (str prod-base-url "/company/" prod-company-id "/reports/TransactionList" "?minorversion=63&start_date=" start "&end_date=" end) {:headers (with-auth prod-base-headers token) :as :json})))) diff --git a/src/clj/auto_ap/jobs/bulk_journal_import.clj b/src/clj/auto_ap/jobs/bulk_journal_import.clj index bc633edc..4f787660 100644 --- a/src/clj/auto_ap/jobs/bulk_journal_import.clj +++ b/src/clj/auto_ap/jobs/bulk_journal_import.clj @@ -12,12 +12,11 @@ (defn line->id [{:keys [source id client-code]}] (str client-code "-" source "-" id)) - (defn csv->graphql-rows [lines] (for [lines (partition-by line->id (drop 1 lines)) - :let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]] + :let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]] {:source source - :external_id (line->id line) + :external_id (line->id line) :client_code client-code :date date :note note @@ -33,8 +32,8 @@ {:account_identifier account-identifier :location (some-> location str/trim) :debit (if (str/blank? debit) - 0.0 - (Double/parseDouble debit)) + 0.0 + (Double/parseDouble debit)) :credit (if (str/blank? credit) 0.0 (Double/parseDouble credit))}) diff --git a/src/clj/auto_ap/jobs/close_auto_invoices.clj b/src/clj/auto_ap/jobs/close_auto_invoices.clj index fb1891fd..f531feb7 100644 --- a/src/clj/auto_ap/jobs/close_auto_invoices.clj +++ b/src/clj/auto_ap/jobs/close_auto_invoices.clj @@ -20,8 +20,7 @@ (mapv (fn [[i]] {:db/id i :invoice/outstanding-balance 0.0 - :invoice/status :invoice-status/paid})) - )) + :invoice/status :invoice-status/paid})))) (alog/info ::closed :count (count invoices-to-close)))) diff --git a/src/clj/auto_ap/jobs/core.clj b/src/clj/auto_ap/jobs/core.clj index b906f838..7c30c0c3 100644 --- a/src/clj/auto_ap/jobs/core.clj +++ b/src/clj/auto_ap/jobs/core.clj @@ -1,7 +1,7 @@ (ns auto-ap.jobs.core (:require [auto-ap.utils :refer [heartbeat]] [mount.core :as mount] - [auto-ap.datomic :refer [conn ]] + [auto-ap.datomic :refer [conn]] [auto-ap.logging :as alog] [nrepl.server :refer [start-server]] [auto-ap.background.metrics :refer [metrics-setup container-tags container-data logging-context]] @@ -13,8 +13,8 @@ :service name} (mu/trace ::execute-background-job [] - (try - (mount/start (mount/only #{#'conn #'metrics-setup #'container-tags #'logging-context #'container-data })) + (try + (mount/start (mount/only #{#'conn #'metrics-setup #'container-tags #'logging-context #'container-data})) (start-server :port 9000 :bind "0.0.0.0" #_#_:handler (cider-nrepl-handler)) ((heartbeat f name)) (alog/info ::stopping :job name) diff --git a/src/clj/auto_ap/jobs/import_uploaded_invoices.clj b/src/clj/auto_ap/jobs/import_uploaded_invoices.clj index 9ba7b834..cf1150fd 100644 --- a/src/clj/auto_ap/jobs/import_uploaded_invoices.clj +++ b/src/clj/auto_ap/jobs/import_uploaded_invoices.clj @@ -18,7 +18,7 @@ (javax.mail Session) (javax.mail.internet MimeMessage))) -(defn send-email-about-failed-message [mail-bucket mail-key message] +(defn send-email-about-failed-message [mail-bucket mail-key message] (let [target-key (str "failed-emails/" mail-key ".eml") target-url (str "https://" (:data-bucket env) "/" target-key)] (alog/info ::sending-failure-email :who (:import-failure-destination-emails env)) @@ -29,7 +29,6 @@ :body {:html (str "
You can download the original email here.

" message "

") :text (str "
You can download the original email here: " target-url)}}}))) - (defn process-sqs [] (alog/info ::fetching-sqs) (doseq [message (:messages (sqs/receive-message {:queue-url (:invoice-import-queue-url env) @@ -79,27 +78,20 @@ (defn -main [& _] (execute "import-uploaded-invoices" process-sqs)) - -(comment +(comment (with-open [i (io/output-stream "/tmp/bryce.pdf")] - (clojure.java.io/copy + (clojure.java.io/copy (-> (s3/get-object :bucket-name (:data-bucket env) - :key "invoice-files/f0e73dcb-e5e5-4c81-b82b-319b7caedacf.pdf" - - ) + :key "invoice-files/f0e73dcb-e5e5-4c81-b82b-319b7caedacf.pdf") + :input-stream) i)) (parse/parse-file "/tmp/bryce.pdf" "/tmp/bryce.pdf") - - (-> (clojure.java.shell/sh "pdftotext" "-layout" "/tmp/bryce.pdf" "-") - :out - ) + (-> (clojure.java.shell/sh "pdftotext" "-layout" "/tmp/bryce.pdf" "-") + :out) - 1 - (user/init-repl) - - ) \ No newline at end of file + (user/init-repl)) \ No newline at end of file diff --git a/src/clj/auto_ap/jobs/insight_outcome_recommendation.clj b/src/clj/auto_ap/jobs/insight_outcome_recommendation.clj index 6e13aab1..d9217c29 100644 --- a/src/clj/auto_ap/jobs/insight_outcome_recommendation.clj +++ b/src/clj/auto_ap/jobs/insight_outcome_recommendation.clj @@ -23,39 +23,39 @@ [?t :transaction/client ?c]]))) (defn get-pinecone [transaction-id] - (-> - (http2/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch" + (-> + (http2/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch" url/url (assoc :query {:ids transaction-id}) str) {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} :as :json :keywordize? false}) - :body - :vectors - ((keyword (str transaction-id))) - :values)) + :body + :vectors + ((keyword (str transaction-id))) + :values)) (defn get-pinecone-similarities [transaction-id] (if-let [vector (get-pinecone transaction-id)] - (filter - (fn [{:keys [score]}] - (> score 0.95)) - (-> - (http2/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query" - url/url - str) - {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} - :form-params {"vector" vector - "topK" 200, - "includeMetadata" true - "namespace" ""} - :content-type :json - :as :json}) - :body - :matches - (doto (#(alog/info ::similarities-found :transaction transaction-id :count (count %) :sample (take 3 %)))))) - + (filter + (fn [{:keys [score]}] + (> score 0.95)) + (-> + (http2/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query" + url/url + str) + {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} + :form-params {"vector" vector + "topK" 200, + "includeMetadata" true + "namespace" ""} + :content-type :json + :as :json}) + :body + :matches + (doto (#(alog/info ::similarities-found :transaction transaction-id :count (count %) :sample (take 3 %)))))) + (do (alog/info ::no-matches-for :transaction transaction-id) []))) diff --git a/src/clj/auto_ap/jobs/load_historical_sales.clj b/src/clj/auto_ap/jobs/load_historical_sales.clj index fa5d92da..8558c6ca 100644 --- a/src/clj/auto_ap/jobs/load_historical_sales.clj +++ b/src/clj/auto_ap/jobs/load_historical_sales.clj @@ -10,16 +10,15 @@ [config.core :refer [env]] [datomic.api :as dc])) - (defn historical-load-sales [client days] (alog/info ::new-sales-loading :client (:client/code client) :days days) (let [client (dc/pull (dc/db auto-ap.datomic/conn) - square3/square-read - client) - days (cond-> days (string? days) ( #(Long/parseLong %)))] + square3/square-read + client) + days (cond-> days (string? days) (#(Long/parseLong %)))] (doseq [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] - + (println "orders") (doseq [d (per/periodic-seq (time/plus (time/today) (time/days (- days))) (time/plus (time/today) (time/days 2)) @@ -28,14 +27,13 @@ @(square3/upsert client square-location (coerce/to-date-time d) (coerce/to-date-time (time/plus d (time/days 1))))) (println "refunds") - @(square3/upsert-refunds client square-location) - @(square3/upsert-payouts client square-location (time/plus (time/now) (time/days (- days))) (time/now))))) - + @(square3/upsert-refunds client square-location) + @(square3/upsert-payouts client square-location (time/plus (time/now) (time/days (- days))) (time/now))))) (defn load-historical-sales [args] (let [{:keys [days client]} args - client (cond-> client - ( string? client) ( #( Long/parseLong %)))] + client (cond-> client + (string? client) (#(Long/parseLong %)))] (historical-load-sales client days))) (defn -main [& _] diff --git a/src/clj/auto_ap/jobs/ntg.clj b/src/clj/auto_ap/jobs/ntg.clj index 9b2cabee..b417955e 100644 --- a/src/clj/auto_ap/jobs/ntg.clj +++ b/src/clj/auto_ap/jobs/ntg.clj @@ -28,19 +28,17 @@ (defn read-xml [stream] (-> (slurp stream) (.getBytes) - (java.io.ByteArrayInputStream. ) + (java.io.ByteArrayInputStream.) xml/parse zip/xml-zip)) - - (defn mark-key [k] (s3/copy-object {:source-bucket-name bucket-name :destination-bucket-name bucket-name :destination-key (str/replace-first k "pending" "imported") :source-key k}) #_(s3/delete-object {:bucket-name bucket-name - :key k})) + :key k})) (defn is-csv-file? [x] (= "dat" (last (str/split x #"[\\.]")))) @@ -54,7 +52,7 @@ (and (str/includes? k "GeneralProduce") (str/includes? k "FRANCHISEE") (is-csv-file? k)) - :general-produce + :general-produce :else :unknown)) @@ -66,15 +64,15 @@ [k input-stream clients] (log/info ::parsing-general-produce :key k) (let [missing-client-hints (atom #{})] - (try + (try (->> (read-csv input-stream) (drop 1) #_(filter (fn [[_ _ _ _ _ _ _ _ _ _ _ break-flag]] (= "Y" break-flag))) - (map (fn [[_ location-hint invoice-number ship-date invoice-total ]] + (map (fn [[_ location-hint invoice-number ship-date invoice-total]] (let [matching-client (and location-hint (parse/exact-match clients location-hint)) - location (parse/best-location-match matching-client location-hint location-hint ) + location (parse/best-location-match matching-client location-hint location-hint) vendor (d/pull (d/db conn) '[:vendor/default-account] :vendor/general-produce)] (when-not (and matching-client (not (@missing-client-hints location-hint)) @@ -99,8 +97,7 @@ (-> vendor :vendor/default-account :db/id) :invoice-expense-account/location location :invoice-expense-account/amount (Math/abs (Double/parseDouble invoice-total)) - :db/id (random-tempid) - }]}))) + :db/id (random-tempid)}]}))) (filter :invoice/client) (reduce (fn [[seen-so-far list] i] (let [k [(:invoice/invoice-number i) (:invoice/client i)]] @@ -108,8 +105,7 @@ [seen-so-far list] [(conj seen-so-far k) (conj list i)]))) [#{} []]) - (second) - ) + (second)) (catch Exception e (log/error ::cant-import-general-produce :error e) @@ -123,8 +119,8 @@ (defn zip-seq [zipper] (->> (zip/xml-zip (zip/node zipper)) - (iterate zip/next ) - (take-while (complement zip/end?)))) + (iterate zip/next) + (take-while (complement zip/end?)))) (defmethod extract-invoice-details :cintas [k input-stream clients] @@ -160,10 +156,10 @@ atime/localize (atime/unparse atime/iso-date) (atime/parse atime/iso-date)))) - location (parse/best-location-match matching-client location-hint location-hint ) + location (parse/best-location-match matching-client location-hint location-hint) due (-> invoice-date - (time/plus (time/days 30)) - (coerce/to-date)) + (time/plus (time/days 30)) + (coerce/to-date)) total (->> node-seq (filter (fn [zipper] (= (:tag (zip/node zipper)) @@ -178,7 +174,7 @@ :content first Double/parseDouble) - invoice {:db/id (random-tempid ) + invoice {:db/id (random-tempid) :invoice/vendor :vendor/cintas :invoice/import-status :import-status/imported :invoice/status :invoice-status/unpaid @@ -188,37 +184,36 @@ :invoice/total total :invoice/outstanding-balance total :invoice/invoice-number (->> node-seq - (map zip/node) - (filter (fn [node] - (= (:tag node) - :InvoiceDetailRequestHeader))) - first - (#(-> % :attrs :invoiceID))) + (map zip/node) + (filter (fn [node] + (= (:tag node) + :InvoiceDetailRequestHeader))) + first + (#(-> % :attrs :invoiceID))) :invoice/due due :invoice/scheduled-payment (when-not ((into #{} (->> matching-client :client/feature-flags)) "manually-pay-cintas") - due) + due) :invoice/date (coerce/to-date invoice-date) :invoice/expense-accounts [{:invoice-expense-account/account (-> vendor :vendor/default-account :db/id) :invoice-expense-account/location location :invoice-expense-account/amount (Math/abs total) - :db/id (random-tempid) - }]}] + :db/id (random-tempid)}]}] (log/info ::cintas-invoice-importing :invoice invoice) [invoice]) - (do + (do ;; disabling logging for cintas #_(log/warn ::missing-client - :client-hint location-hint) + :client-hint location-hint) [])))) (defn mark-error [k] - (s3/copy-object {:source-bucket-name bucket-name + (s3/copy-object {:source-bucket-name bucket-name :destination-bucket-name bucket-name :source-key k :destination-key (str "ntg-invoices/error/" @@ -232,17 +227,17 @@ (s3/copy-object {:source-bucket-name bucket-name :destination-bucket-name bucket-name :source-key k - :destination-key invoice-key }) + :destination-key invoice-key}) invoice-key)) (defn get-all-keys ([] - (let [first-page-result (s3/list-objects-v2 {:bucket-name bucket-name + (let [first-page-result (s3/list-objects-v2 {:bucket-name bucket-name :prefix "ntg-invoices/pending"})] (lazy-seq (concat (:object-summaries first-page-result) (get-all-keys (:next-continuation-token first-page-result)))))) - ([next-token ] - (when next-token - (let [page-result (s3/list-objects-v2 {:bucket-name bucket-name + ([next-token] + (when next-token + (let [page-result (s3/list-objects-v2 {:bucket-name bucket-name :prefix "ntg-invoices/pending" :continuation-token next-token})] (println "getting next page " next-token) @@ -250,60 +245,58 @@ (lazy-seq (concat (:object-summaries page-result) (get-all-keys (:next-continuation-token page-result))))))))) (defn recent? [k] - (time/after? (:last-modified k) (time/plus (time/now) (time/days -15))) - ) + (time/after? (:last-modified k) (time/plus (time/now) (time/days -15)))) (defn import-ntg-invoices ([] (import-ntg-invoices (->> (get-all-keys) (filter recent?) (map :key)))) ([keys] - (let [clients (map first (d/q '[:find (pull ?c [:client/code - :db/id - :client/feature-flags - {:client/location-matches [:location-match/matches :location-match/location]} - :client/name - :client/matches - :client/locations]) - :where [?c :client/code]] - (d/db conn)))] - (log/info ::found-invoice-keys - :keys keys ) - (let [transaction (->> keys - (mapcat (fn [k] - (try - (let [invoice-key (copy-readable-version k) - invoice-url (str "https://" bucket-name "/" invoice-key)] - (with-open [is (-> (s3/get-object {:bucket-name bucket-name - :key k}) - :input-stream)] - (->> (extract-invoice-details k - is - clients) - (set) - (map (fn [i] - (log/info ::importing-invoice - :invoice i) - i)) - (mapv (fn [i] - (if (= :vendor/cintas (:invoice/vendor i)) - [:propose-invoice (assoc i :invoice/source-url invoice-url)] - [:propose-invoice i])))))) - (catch Exception e - (log/error ::cant-load-file - :key k - :exception e) - (mark-error k) - [])))) - (into []))] - (doseq [t transaction] - (audit-transact [t] {:user/name "sysco importer" :user/role "admin"})) - (log/info ::success - :count (count transaction) - :sample (take 3 transaction))) - (doseq [k keys] - (mark-key k))))) - + (let [clients (map first (d/q '[:find (pull ?c [:client/code + :db/id + :client/feature-flags + {:client/location-matches [:location-match/matches :location-match/location]} + :client/name + :client/matches + :client/locations]) + :where [?c :client/code]] + (d/db conn)))] + (log/info ::found-invoice-keys + :keys keys) + (let [transaction (->> keys + (mapcat (fn [k] + (try + (let [invoice-key (copy-readable-version k) + invoice-url (str "https://" bucket-name "/" invoice-key)] + (with-open [is (-> (s3/get-object {:bucket-name bucket-name + :key k}) + :input-stream)] + (->> (extract-invoice-details k + is + clients) + (set) + (map (fn [i] + (log/info ::importing-invoice + :invoice i) + i)) + (mapv (fn [i] + (if (= :vendor/cintas (:invoice/vendor i)) + [:propose-invoice (assoc i :invoice/source-url invoice-url)] + [:propose-invoice i])))))) + (catch Exception e + (log/error ::cant-load-file + :key k + :exception e) + (mark-error k) + [])))) + (into []))] + (doseq [t transaction] + (audit-transact [t] {:user/name "sysco importer" :user/role "admin"})) + (log/info ::success + :count (count transaction) + :sample (take 3 transaction))) + (doseq [k keys] + (mark-key k))))) (defn -main [& _] (execute "ntg" import-ntg-invoices)) diff --git a/src/clj/auto_ap/jobs/register_invoice_import.clj b/src/clj/auto_ap/jobs/register_invoice_import.clj index e5821988..3b269ec6 100644 --- a/src/clj/auto_ap/jobs/register_invoice_import.clj +++ b/src/clj/auto_ap/jobs/register_invoice_import.clj @@ -18,7 +18,7 @@ (def bucket (:data-bucket env)) (defn s3->csv [url] - (try + (try (->> (-> (s3/get-object {:bucket-name bucket :key (str "bulk-import/" url)}) :input-stream @@ -26,9 +26,9 @@ csv/read-csv)) (catch Exception e (alog/error - :file-not-found - :error e - :url url) + :file-not-found + :error e + :url url) (throw e)))) (defn register-invoice-import* [data] @@ -45,106 +45,100 @@ (reduce + 0.0 (->> values (map (fn [[_ _ _ _ amount]] - (- (Double/parseDouble amount)))))) - ])) + (- (Double/parseDouble amount))))))])) (into {}))] - (->> - (for [[i - invoice-expense-account-id - target-account - target-date - amount - _ - location] (drop 1 data) - :let [invoice-id (i->invoice-id i) + (->> + (for [[i + invoice-expense-account-id + target-account + target-date + amount + _ + location] (drop 1 data) + :let [invoice-id (i->invoice-id i) - invoice (dc/pull db '[*] invoice-id) - current-total (:invoice/total invoice) - target-total (invoice-totals invoice-id) ;; TODO should include expense accounts not visible - new-account? (not (boolean (or (some-> invoice-expense-account-id not-empty Long/parseLong) - (:db/id (first (:invoice/expense-accounts invoice)))))) + invoice (dc/pull db '[*] invoice-id) + current-total (:invoice/total invoice) + target-total (invoice-totals invoice-id) ;; TODO should include expense accounts not visible + new-account? (not (boolean (or (some-> invoice-expense-account-id not-empty Long/parseLong) + (:db/id (first (:invoice/expense-accounts invoice)))))) - invoice-expense-account-id (or (some-> invoice-expense-account-id not-empty Long/parseLong) - (:db/id (first (:invoice/expense-accounts invoice))) - (str (UUID/randomUUID))) - invoice-expense-account (when-not new-account? - (or (dc/pull db '[*] invoice-expense-account-id) - (dc/pull db '[*] [:invoice-expense-account/original-id invoice-expense-account-id]))) - current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account)) - target-account-id (Long/parseLong (str/trim target-account)) + invoice-expense-account-id (or (some-> invoice-expense-account-id not-empty Long/parseLong) + (:db/id (first (:invoice/expense-accounts invoice))) + (str (UUID/randomUUID))) + invoice-expense-account (when-not new-account? + (or (dc/pull db '[*] invoice-expense-account-id) + (dc/pull db '[*] [:invoice-expense-account/original-id invoice-expense-account-id]))) + current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account)) + target-account-id (Long/parseLong (str/trim target-account)) - target-date (coerce/to-date (atime/parse target-date atime/normal-date)) - current-date (:invoice/date invoice) - + target-date (coerce/to-date (atime/parse target-date atime/normal-date)) + current-date (:invoice/date invoice) - current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0) - target-expense-account-amount (- (Double/parseDouble amount)) + current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0) + target-expense-account-amount (- (Double/parseDouble amount)) + current-expense-account-location (:invoice-expense-account/location invoice-expense-account) + target-expense-account-location location - current-expense-account-location (:invoice-expense-account/location invoice-expense-account) - target-expense-account-location location + [[_ _ invoice-payment]] (vec (dc/q + '[:find ?p ?a ?ip + :in $ ?i + :where [?ip :invoice-payment/invoice ?i] + [?ip :invoice-payment/amount ?a] + [?ip :invoice-payment/payment ?p]] + db invoice-id))] + :when current-total] + [(when (not (dollars= current-total target-total)) + {:db/id invoice-id + :invoice/total target-total}) - [[_ _ invoice-payment]] (vec (dc/q - '[:find ?p ?a ?ip - :in $ ?i - :where [?ip :invoice-payment/invoice ?i] - [?ip :invoice-payment/amount ?a] - [?ip :invoice-payment/payment ?p] - ] - db invoice-id))] - :when current-total] + (when (and (not (dollars= 0.0 target-total)) + (= :invoice-status/voided (:db/ident (:invoice/status invoice)))) + {:db/id invoice-id + :invoice/total target-total + :invoice/status :invoice-status/paid}) - [ - (when (not (dollars= current-total target-total)) - {:db/id invoice-id - :invoice/total target-total}) + (when new-account? + {:db/id invoice-id + :invoice/expense-accounts invoice-expense-account-id}) - (when (and (not (dollars= 0.0 target-total)) - (= :invoice-status/voided (:db/ident (:invoice/status invoice)))) - {:db/id invoice-id - :invoice/total target-total - :invoice/status :invoice-status/paid}) + (when (and target-date (not= current-date target-date)) + {:db/id invoice-id + :invoice/date target-date}) - (when new-account? - {:db/id invoice-id - :invoice/expense-accounts invoice-expense-account-id}) + (when (and + (not (dollars= current-total target-total)) + invoice-payment) + [:db/retractEntity invoice-payment]) - (when (and target-date (not= current-date target-date)) - {:db/id invoice-id - :invoice/date target-date}) + (when (or new-account? + (not (dollars= current-expense-account-amount target-expense-account-amount))) + {:db/id invoice-expense-account-id + :invoice-expense-account/amount target-expense-account-amount}) - (when (and - (not (dollars= current-total target-total)) - invoice-payment) - [:db/retractEntity invoice-payment]) + (when (not= current-expense-account-location + target-expense-account-location) + {:db/id invoice-expense-account-id + :invoice-expense-account/location target-expense-account-location}) - (when (or new-account? - (not (dollars= current-expense-account-amount target-expense-account-amount))) - {:db/id invoice-expense-account-id - :invoice-expense-account/amount target-expense-account-amount}) - - (when (not= current-expense-account-location - target-expense-account-location) - {:db/id invoice-expense-account-id - :invoice-expense-account/location target-expense-account-location}) - - (when (not= current-account-id target-account-id ) - {:db/id invoice-expense-account-id - :invoice-expense-account/account target-account-id})]) - (mapcat identity) - (filter identity) - vec))) + (when (not= current-account-id target-account-id) + {:db/id invoice-expense-account-id + :invoice-expense-account/account target-account-id})]) + (mapcat identity) + (filter identity) + vec))) (defn register-invoice-import [args] (let [{:keys [invoice-url]} args data (s3->csv invoice-url)] (alog/info ::rows - :count (count data)) + :count (count data)) (doseq [n (partition-all 50 (register-invoice-import* data))] (alog/info ::transacting - :count (count n) - :sample (take 2 n)) + :count (count n) + :sample (take 2 n)) (audit-transact n {:user/name "register-invoice-import" :user/role "admin"})))) diff --git a/src/clj/auto_ap/jobs/restore_from_backup.clj b/src/clj/auto_ap/jobs/restore_from_backup.clj index 50fbc645..305f113b 100644 --- a/src/clj/auto_ap/jobs/restore_from_backup.clj +++ b/src/clj/auto_ap/jobs/restore_from_backup.clj @@ -20,32 +20,29 @@ (def buffered (ex/fixed-thread-executor 100)) (defn order-of-insert [entity-dependencies] - (loop [entity-dependencies entity-dependencies + (loop [entity-dependencies entity-dependencies order []] (let [next-order (for [[entity deps] entity-dependencies :when (not (seq deps))] entity) next-deps (reduce - (fn [entity-dependencies next-entity] - (into {} - (map - (fn [[k v]] - [k (disj v next-entity)]) - entity-dependencies))) - (apply dissoc entity-dependencies next-order) - next-order)] + (fn [entity-dependencies next-entity] + (into {} + (map + (fn [[k v]] + [k (disj v next-entity)]) + entity-dependencies))) + (apply dissoc entity-dependencies next-order) + next-order)] (if (seq next-deps) (recur next-deps (into order next-order)) (into order next-order))))) - - - (def loaded (atom #{})) (defn upsert-batch [batch context] - (de/future-with request-pool + (de/future-with request-pool (mu/with-context context (transact-with-backoff batch)) batch)) @@ -68,39 +65,39 @@ (mu/with-context {:entity entity} (mu/log ::starting) @(s/consume (fn [batch] - (mu/with-context {:entity entity} - (try - (swap! so-far #(+ % (count batch))) - (mu/log ::loaded :count (count batch) - :so-far @so-far) - (catch Exception e - (mu/log ::error - :exception e) - (throw e))))) - (->> (partition-all 1000 entities) - (s/->source) - (s/onto buffered) - (s/map (fn [entities] - (when @die? - (reset! die? false) - (throw (Exception. "dead"))) - (upsert-batch entities {:entity entity - :service "restore-from-backup" - :background-job "restore-from-backup"}))) - (s/buffer 20) - (s/realize-each))) + (mu/with-context {:entity entity} + (try + (swap! so-far #(+ % (count batch))) + (mu/log ::loaded :count (count batch) + :so-far @so-far) + (catch Exception e + (mu/log ::error + :exception e) + (throw e))))) + (->> (partition-all 1000 entities) + (s/->source) + (s/onto buffered) + (s/map (fn [entities] + (when @die? + (reset! die? false) + (throw (Exception. "dead"))) + (upsert-batch entities {:entity entity + :service "restore-from-backup" + :background-job "restore-from-backup"}))) + (s/buffer 20) + (s/realize-each))) (swap! loaded conj entity)))) -(defn load-from-backup +(defn load-from-backup ([backup-id connection] (load-from-backup backup-id connection nil)) ([backup-id connection starting-at] (let [schema (edn/read-string (slurp (pull-file backup-id "schema.edn"))) full-dependencies (edn/read-string (slurp (pull-file backup-id "full-dependencies.edn"))) entity-dependencies (edn/read-string (slurp (pull-file backup-id "entity-dependencies.edn")))] @(dc/transact connection [{:db/ident :entity/migration-key - :db/unique :db.unique/identity - :db/cardinality :db.cardinality/one - :db/valueType :db.type/long}]) + :db/unique :db.unique/identity + :db/cardinality :db.cardinality/one + :db/valueType :db.type/long}]) @(dc/transact connection (map (fn [s] (set/rename-keys s {:db/id :entity/migration-key})) @@ -108,14 +105,13 @@ ;; TEMP - this has been fixed in current export (ezcater-olaciotn) @(dc/transact connection [{:entity/migration-key 17592257603901 :vendor/name "unknown"} - {:entity/migration-key 17592232621701} - {:entity/migration-key 17592263907739} - {:entity/migration-key 17592271516922}]) - + {:entity/migration-key 17592232621701} + {:entity/migration-key 17592263907739} + {:entity/migration-key 17592271516922}]) (doseq [entity (cond->> (order-of-insert entity-dependencies) true (filter #(not= "audit" %)) - starting-at (drop-while #(not= starting-at %))) + starting-at (drop-while #(not= starting-at %))) :let [_ (reset! so-far 0) _ (mu/log ::querying :entity entity) entities (mu/trace ::file-pulled @@ -136,9 +132,8 @@ (mu/log ::refresh-running-balance-cache-complete) (mu/log ::done)) - (defn -main [& _] - (try + (try (println "restore") (execute "restore-from-backup" #(restore-fresh-from-backup (:args env))) (catch Exception e @@ -151,13 +146,11 @@ (throw e)))) ;; cloud load -#_(comment +#_(comment ;; /datomic-backup/079df203-eae0-4acf-94d5-8608ba8b8a9a - (load-from-backup "079df203-eae0-4acf-94d5-8608ba8b8a9a" auto-ap.datomic/conn ["charge"]) + (load-from-backup "079df203-eae0-4acf-94d5-8608ba8b8a9a" auto-ap.datomic/conn ["charge"]) - (load-entity "charge" (ednl/slurp "/tmp/tmp-edn")) + (load-entity "charge" (ednl/slurp "/tmp/tmp-edn"))) - - ) ;; => nil diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index e759078b..def61fdc 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -39,17 +39,14 @@ (dc/db conn) number))) - (defn delete-all [] @(dc/transact-async conn - (->> - (dc/q '[:find ?ss - :where [?ss :sales-summary/date]] - (dc/db conn)) - (map (fn [[ ss]] - [:db/retractEntity ss]))))) - - + (->> + (dc/q '[:find ?ss + :where [?ss :sales-summary/date]] + (dc/db conn)) + (map (fn [[ss]] + [:db/retractEntity ss]))))) (defn dirty-sales-summaries [c] (let [client-id (dc/entid (dc/db conn) c)] @@ -99,53 +96,53 @@ "food app refunds" 41400}) (defn get-payment-items [c date] - (->> - (dc/q '[:find ?processor ?type-name (sum ?total) - :with ?c - :in $ [?clients ?start-date ?end-date] - :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] - [?e :sales-order/charges ?c] - [?c :charge/type-name ?type-name] - (or-join [?c ?processor] - (and [?c :charge/processor ?p] - [?p :db/ident ?processor]) - (and - (not [?c :charge/processor]) - [(ground :ccp-processor/na) ?processor])) - [?c :charge/total ?total]] - (dc/db conn) - [[c] date date]) - (reduce - (fn [acc [processor type-name total]] - (update - acc - (cond (= type-name "CARD") - "Card Payments" - (= type-name "CASH") - "Cash Payments" - (#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name) - "Gift Card Payments" - (#{:ccp-processor/toast - #_:ccp-processor/ezcater - #_:ccp-processor/koala - :ccp-processor/doordash - :ccp-processor/grubhub - :ccp-processor/uber-eats} processor) - "Food App Payments" - :else - "Unknown") - (fnil + 0.0) - total)) - {}) - (map (fn [[k v]] - {:db/id (str (java.util.UUID/randomUUID)) - :sales-summary-item/sort-order 0 - :sales-summary-item/category k - - :ledger-mapped/amount (if (= "Card Payments" k) - (- v (get-fee c date)) - v) - :ledger-mapped/ledger-side :ledger-side/debit})))) + (->> + (dc/q '[:find ?processor ?type-name (sum ?total) + :with ?c + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/type-name ?type-name] + (or-join [?c ?processor] + (and [?c :charge/processor ?p] + [?p :db/ident ?processor]) + (and + (not [?c :charge/processor]) + [(ground :ccp-processor/na) ?processor])) + [?c :charge/total ?total]] + (dc/db conn) + [[c] date date]) + (reduce + (fn [acc [processor type-name total]] + (update + acc + (cond (= type-name "CARD") + "Card Payments" + (= type-name "CASH") + "Cash Payments" + (#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name) + "Gift Card Payments" + (#{:ccp-processor/toast + #_:ccp-processor/ezcater + #_:ccp-processor/koala + :ccp-processor/doordash + :ccp-processor/grubhub + :ccp-processor/uber-eats} processor) + "Food App Payments" + :else + "Unknown") + (fnil + 0.0) + total)) + {}) + (map (fn [[k v]] + {:db/id (str (java.util.UUID/randomUUID)) + :sales-summary-item/sort-order 0 + :sales-summary-item/category k + + :ledger-mapped/amount (if (= "Card Payments" k) + (- v (get-fee c date)) + v) + :ledger-mapped/ledger-side :ledger-side/debit})))) (defn get-discounts [c date] (when-let [discount (ffirst (dc/q '[:find (sum ?discount) @@ -162,7 +159,7 @@ :ledger-mapped/ledger-side :ledger-side/debit})) (defn get-refund-items [c date] - (->> + (->> (dc/q '[:find ?type-name (sum ?t) :with ?e :in $ [?clients ?start-date ?end-date] @@ -173,26 +170,24 @@ (dc/db conn) [[c] date date]) (reduce - (fn [acc [type-name total]] - (update - acc - (cond (= type-name "CARD") - "Card Refunds" - (= type-name "CASH") - "Cash Refunds" - :else - "Food App Refunds") - (fnil + 0.0) - total)) - {}) + (fn [acc [type-name total]] + (update + acc + (cond (= type-name "CARD") + "Card Refunds" + (= type-name "CASH") + "Cash Refunds" + :else + "Food App Refunds") + (fnil + 0.0) + total)) + {}) (map (fn [[k v]] - {:db/id (str (java.util.UUID/randomUUID)) - :sales-summary-item/sort-order 3 - :sales-summary-item/category k - :ledger-mapped/amount v - :ledger-mapped/ledger-side :ledger-side/credit})))) - - + {:db/id (str (java.util.UUID/randomUUID)) + :sales-summary-item/sort-order 3 + :sales-summary-item/category k + :ledger-mapped/amount v + :ledger-mapped/ledger-side :ledger-side/credit})))) (defn get-fees [c date] (when-let [fee (get-fee c date)] @@ -278,17 +273,17 @@ (defn sales-summaries-v2 [] (doseq [[c client-code] (dc/q '[:find ?c ?client-code - :in $ - :where [?c :client/code ?client-code]] - (dc/db conn)) - {:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)] + :in $ + :where [?c :client/code ?client-code]] + (dc/db conn)) + {:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)] (mu/with-context {:client-code client-code - :date date} - (alog/info ::updating) - (let [manual-items (->> existing-summary - :sales-summary/items - (filter :sales-summary-item/manual?)) - calculated-items (->> + :date date} + (alog/info ::updating) + (let [manual-items (->> existing-summary + :sales-summary/items + (filter :sales-summary-item/manual?)) + calculated-items (->> (get-sales c date) (concat (get-payment-items c date)) (concat (get-refund-items c date)) @@ -301,20 +296,19 @@ (map (fn [z] (assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account) :sales-summary-item/manual? false)))) - all-items (concat calculated-items manual-items) - result {:db/id id - :sales-summary/client c - :sales-summary/date date - :sales-summary/dirty false - :sales-summary/client+date [c date] - :sales-summary/items all-items}] - (if (seq (:sales-summary/items result)) - (do - (alog/info ::upserting-summaries - :category-count (count (:sales-summary/items result))) - @(dc/transact conn [[:upsert-sales-summary result]])) - @(dc/transact conn [{:db/id id :sales-summary/dirty false}])))))) - + all-items (concat calculated-items manual-items) + result {:db/id id + :sales-summary/client c + :sales-summary/date date + :sales-summary/dirty false + :sales-summary/client+date [c date] + :sales-summary/items all-items}] + (if (seq (:sales-summary/items result)) + (do + (alog/info ::upserting-summaries + :category-count (count (:sales-summary/items result))) + @(dc/transact conn [[:upsert-sales-summary result]])) + @(dc/transact conn [{:db/id id :sales-summary/dirty false}])))))) (defn reset-summaries [] @(dc/transact conn (->> (dc/q '[:find ?sos @@ -324,9 +318,6 @@ (map (fn [[sos]] [:db/retractEntity sos]))))) - - - (comment (auto-ap.datomic/transact-schema conn) @@ -336,26 +327,19 @@ (dirty-sales-summaries [:client/code "NGWH"]) - (apply mark-dirty [:client/code "NGWH"] (last-n-days 5)) (iol-ion.tx.upsert-sales-summary-ledger/summary->journal-entry (dc/db conn) 17592314245819) - (iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary (dc/db conn) {:db/id 17592314241429}) - - (mark-all-dirty 5) (delete-all) - (sales-summaries-v2) - 1 - (dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}]) :in $ :where [?sos :sales-summary/client [:client/code "NGHW"]] @@ -386,15 +370,7 @@ @(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy} {:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}]) - (auto-ap.datomic/transact-schema conn) - - - - - ) - - + (auto-ap.datomic/transact-schema conn)) (defn -main [& _] (execute "sales-summaries" sales-summaries-v2)) - \ No newline at end of file diff --git a/src/clj/auto_ap/jobs/sysco.clj b/src/clj/auto_ap/jobs/sysco.clj index bf6a9e61..c8465ce0 100644 --- a/src/clj/auto_ap/jobs/sysco.clj +++ b/src/clj/auto_ap/jobs/sysco.clj @@ -44,7 +44,6 @@ (dc/db conn) 50000)))) - (def ^:dynamic bucket-name (:data-bucket env)) (def header-keys ["TransCode" "GroupID" "Company" "CustomerNumber" "InvoiceNumber" "RecordType" "Item" "InvoiceDocument" "AccountName" "AccountDunsNo" "InvoiceDate" "AccountDate" "CustomerPONo" "PaymentTerms" "TermsDescription" "StoreNumber" "CustomerName" "AddressLine1" "AddressLine2" "City1" "State1" "Zip1" "Phone1" "Duns1" "Hin1" "Dea1" "TIDCustomer" "ChainNumber" "BidNumber" "ContractNumber" "CompanyNumber" "BriefName" "Address" "Address2" "City2" "State2" "Zip2" "Phone2" "Duns2" "Hin2" "Dea2" "Tid_OPCO" "ObligationIndicator" "Manifest" "Route" "Stop" "TermsDiscountPercent" "TermsDiscountDueDate" "TermsNetDueDate" "TermsDiscountAmount" "TermsDiscountCode" "OrderDate" "DepartmentCode"]) @@ -56,14 +55,13 @@ (defn get-sysco-vendor [] (let [db (dc/db conn)] (-> - (dc/q '[:find (pull ?v r) - :in $ r - :where [?v :vendor/name "Sysco"]] - db - d-vendors/default-read) - first - first))) - + (dc/q '[:find (pull ?v r) + :in $ r + :where [?v :vendor/name "Sysco"]] + db + d-vendors/default-read) + first + first))) (defn read-sysco-csv [k] (-> (s3/get-object {:bucket-name bucket-name @@ -73,34 +71,32 @@ csv/read-csv)) (defn check-okay-amount? [i] - (dollars= + (dollars= (:invoice/total i) (reduce + 0.0 (map :invoice-expense-account/amount (:invoice/expense-accounts i))))) (defn code-individual-items [invoice csv-rows tax] (let [items (->> csv-rows butlast - (reduce - (fn [acc row] - (update acc (get-line-account (nth row item-name-index)) - (fnil + 0.0) - (Double/parseDouble (nth row item-price-index)) - ) - ) - {}) - ) + (reduce + (fn [acc row] + (update acc (get-line-account (nth row item-name-index)) + (fnil + 0.0) + (Double/parseDouble (nth row item-price-index)))) + + {})) items-with-tax (update items (get-line-account "TAX") - (fnil + 0.0) + (fnil + 0.0) tax) - updated-invoice (assoc invoice :invoice/expense-accounts - (for [[account amount] items-with-tax] - #:invoice-expense-account {:db/id (random-tempid) - :account account - :location (:invoice/location invoice) - :amount amount}))] + updated-invoice (assoc invoice :invoice/expense-accounts + (for [[account amount] items-with-tax] + #:invoice-expense-account {:db/id (random-tempid) + :account account + :location (:invoice/location invoice) + :amount amount}))] (if (check-okay-amount? updated-invoice) updated-invoice - (do (alog/warn ::itemized-expenses-not-adding-up + (do (alog/warn ::itemized-expenses-not-adding-up :invoice updated-invoice) invoice)))) @@ -122,11 +118,11 @@ (header-row "AddressLine2") (header-row "City1") (header-row "City2")]) - + account-number (some-> account-number Long/parseLong str) matching-client (and account-number (d-clients/exact-match account-number)) - + _ (when-not matching-client (throw (ex-info "cannot find matching client" {:account-number account-number @@ -153,9 +149,9 @@ :client/locations] (:db/id matching-client)) location-hint - location-hint ) + location-hint) :date (coerce/to-date date) - :vendor (:db/id sysco-vendor ) + :vendor (:db/id sysco-vendor) :client (:db/id matching-client) :import-status :import-status/imported :status :invoice-status/unpaid @@ -180,64 +176,54 @@ (s3/delete-object {:bucket-name bucket-name :key k})) -(defn get-test-invoice-file +(defn get-test-invoice-file ([] (get-test-invoice-file 999)) - ( [i] + ([i] (nth (->> (s3/list-objects-v2 {:bucket-name "data.prod.app.integreatconsult.com" :prefix "sysco/imported"}) :object-summaries - (map :key) - ) + (map :key)) i))) - - - - -(comment - (with-bindings { #'bucket-name "data.prod.app.integreatconsult.com"} - (doall - (for [n (range 930 940 ) - :let [result (-> (get-test-invoice-file n) - read-sysco-csv - (extract-invoice-details (get-sysco-vendor)) - )] - #_#_:when (not (check-okay-amount? result))] +(comment + (with-bindings {#'bucket-name "data.prod.app.integreatconsult.com"} + (doall + (for [n (range 930 940) + :let [result (-> (get-test-invoice-file n) + read-sysco-csv + (extract-invoice-details (get-sysco-vendor)))] + #_#_:when (not (check-okay-amount? result))] result))) - (with-bindings { #'bucket-name "data.prod.app.integreatconsult.com"} - (let [result (-> "sysco/error/SYSCO050_00175962_20241010122639019.csv" + (with-bindings {#'bucket-name "data.prod.app.integreatconsult.com"} + (let [result (-> "sysco/error/SYSCO050_00175962_20241010122639019.csv" read-sysco-csv - (extract-invoice-details (get-sysco-vendor)) - )] + (extract-invoice-details (get-sysco-vendor)))] - result)) - - ) + result))) (defn import-sysco [] (let [sysco-vendor (get-sysco-vendor) keys (->> (s3/list-objects-v2 {:bucket-name bucket-name :prefix "sysco/pending"}) - :object-summaries - (map :key))] - + :object-summaries + (map :key))] (alog/info ::importing-sysco :count (count keys) :keys (pr-str keys)) - + (let [transaction (->> keys (mapcat (fn [k] - (try + (try (let [invoice-key (str "invoice-files/" (UUID/randomUUID) ".csv") ; invoice-url (str "https://" (:data-bucket env) "/" invoice-key)] (s3/copy-object {:source-bucket-name (:data-bucket env) :destination-bucket-name (:data-bucket env) :source-key k :destination-key invoice-key}) - [[:propose-invoice + [[:propose-invoice (-> k read-sysco-csv (extract-invoice-details sysco-vendor) @@ -246,7 +232,7 @@ (alog/error ::cant-load-file :file k :error e e) - (s3/copy-object {:source-bucket-name (:data-bucket env) + (s3/copy-object {:source-bucket-name (:data-bucket env) :destination-bucket-name (:data-bucket env) :source-key k :destination-key (str "sysco/error/" @@ -256,6 +242,5 @@ (doseq [k keys] (mark-key k)))) - (defn -main [& _] (execute "sysco" import-sysco)) diff --git a/src/clj/auto_ap/jobs/vendor_usages.clj b/src/clj/auto_ap/jobs/vendor_usages.clj index 3a674697..4c8bf35a 100644 --- a/src/clj/auto_ap/jobs/vendor_usages.clj +++ b/src/clj/auto_ap/jobs/vendor_usages.clj @@ -12,7 +12,7 @@ :where [?v :vendor/name] (or-join [?v ?c ?e] - (and + (and [?e :invoice/vendor ?v] [?e :invoice/client ?c]) (and diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 2f6a1f4a..7737c100 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -18,146 +18,137 @@ (t/plus (t/months -6)) (c/to-date)))) ([start-date] - (let [txes-missing-ledger-entries (->> (dc/q {:find ['?t ] + (let [txes-missing-ledger-entries (->> (dc/q {:find ['?t] :in ['$ '?sd] - :where [ - '[?t :transaction/date ?d] + :where ['[?t :transaction/date ?d] '[(>= ?d ?sd)] '(not [_ :journal-entry/original-entity ?t]) '(not [?t :transaction/amount 0.0]) '(not [?t :transaction/approval-status :transaction-approval-status/excluded]) - '(not [?t :transaction/approval-status :transaction-approval-status/suppressed]) - ]} + '(not [?t :transaction/approval-status :transaction-approval-status/suppressed])]} (dc/db conn) start-date) (map first) (mapv (fn [t] [:upsert-transaction {:db/id t}]))) - - invoices-missing-ledger-entries (->> (dc/q {:find ['?t ] - :in ['$ '?sd] - :where ['[?t :invoice/date ?d] - '[(>= ?d ?sd)] - '(not [_ :journal-entry/original-entity ?t]) - '[?t :invoice/total ?amt] - '[(not= 0.0 ?amt)] - '(not [?t :invoice/status :invoice-status/voided]) - '(not [?t :invoice/import-status :import-status/pending]) - '(not [?t :invoice/exclude-from-ledger true]) - ]} - (dc/db conn) start-date) + invoices-missing-ledger-entries (->> (dc/q {:find ['?t] + :in ['$ '?sd] + :where ['[?t :invoice/date ?d] + '[(>= ?d ?sd)] + '(not [_ :journal-entry/original-entity ?t]) + '[?t :invoice/total ?amt] + '[(not= 0.0 ?amt)] + '(not [?t :invoice/status :invoice-status/voided]) + '(not [?t :invoice/import-status :import-status/pending]) + '(not [?t :invoice/exclude-from-ledger true])]} + (dc/db conn) start-date) (map first) (mapv (fn [i] [:upsert-invoice {:db/id i}]))) - sales-summaries-missing-ledger-entries (->> (dc/q {:find ['?ss ] - :in ['$ '?sd] - :where ['[?ss :sales-summary/date ?d] - '[(>= ?d ?sd)] - '(not [_ :journal-entry/original-entity ?ss]) - '[?ss :sales-summary/items ?item] - '[?item :ledger-mapped/account] - ]} - (dc/db conn) start-date) - (map first) - (mapv (fn [ss] - [:upsert-sales-summary {:db/id ss}]))) + sales-summaries-missing-ledger-entries (->> (dc/q {:find ['?ss] + :in ['$ '?sd] + :where ['[?ss :sales-summary/date ?d] + '[(>= ?d ?sd)] + '(not [_ :journal-entry/original-entity ?ss]) + '[?ss :sales-summary/items ?item] + '[?item :ledger-mapped/account]]} + (dc/db conn) start-date) + (map first) + (mapv (fn [ss] + [:upsert-sales-summary {:db/id ss}]))) - repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries sales-summaries-missing-ledger-entries))] + repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries sales-summaries-missing-ledger-entries))] (when (seq repairs) - (mu/log ::ledger-repairs-needed - :sample (take 3 repairs) - :transaction-count (count txes-missing-ledger-entries) - :invoice-count (count invoices-missing-ledger-entries) - :sales-summary-count (count sales-summaries-missing-ledger-entries)) - @(dc/transact conn repairs))))) - + (mu/log ::ledger-repairs-needed + :sample (take 3 repairs) + :transaction-count (count txes-missing-ledger-entries) + :invoice-count (count invoices-missing-ledger-entries) + :sales-summary-count (count sales-summaries-missing-ledger-entries)) + @(dc/transact conn repairs))))) (defn touch-transaction [e] @(dc/transact conn [{:db/id "datomic.tx" - :db/doc "touching transaction to update ledger"} - [:upsert-transaction {:db/id e}]])) + :db/doc "touching transaction to update ledger"} + [:upsert-transaction {:db/id e}]])) (defn touch-invoice [e] @(dc/transact conn [{:db/id "datomic.tx" - :db/doc "touching invoice to update ledger"} - [:upsert-invoice {:db/id e}]])) - - + :db/doc "touching invoice to update ledger"} + [:upsert-invoice {:db/id e}]])) (defn recently-changed-entities [start end] (into #{} - (map first) - (dc/q '[:find ?e - :in $ - :where (or [?e :transaction/date] - [?e :invoice/date])] - (dc/since (dc/db conn) start)))) + (map first) + (dc/q '[:find ?e + :in $ + :where (or [?e :transaction/date] + [?e :invoice/date])] + (dc/since (dc/db conn) start)))) (defn mismatched-transactions ([] (mismatched-transactions (c/to-date (t/minus (t/now) (t/days 7))) - (c/to-date (t/minus (t/now) (t/hours 1)))) ) + (c/to-date (t/minus (t/now) (t/hours 1))))) ([changed-between-start changed-between-end] (mu/trace ::calculating-mismatched-transactions - [:range {:start changed-between-start - :end changed-between-end}] - (let [entities-to-consider (recently-changed-entities - changed-between-start - changed-between-end) - _ (mu/log ::checking-mismatched-transactions - :count (count entities-to-consider)) - jel-accounts (reduce - (fn [acc [e lia]] - (update acc e (fnil conj #{} ) lia)) - {} - (dc/q '[:find ?e ?lia - :in $ [?e ...] - :where - [?je :journal-entry/original-entity ?e] - [?e :transaction/date] - [?je :journal-entry/line-items ?li] - [?li :journal-entry-line/account ?lia] - [?lia :account/name]] - (dc/db conn) - entities-to-consider)) - transaction-accounts (reduce - (fn [acc [e lia]] - (update acc e (fnil conj #{} ) lia)) - {} - (dc/q '[:find ?e ?lia - :in $ [?e ...] - :where - [?e :transaction/date ?d] - [?e :transaction/accounts ?li] - (not [?e :transaction/approval-status :transaction-approval-status/excluded]) - (not [?e :transaction/approval-status :transaction-approval-status/suppressed]) - [?li :transaction-account/account ?lia] - [?lia :account/name] - [?e :transaction/amount ?amt] - [(not= ?amt 0.0)]] - (dc/db conn) - entities-to-consider))] - (->> transaction-accounts - (filter - (fn [[e accounts]] (not= accounts (get jel-accounts e)))) - (doall)))))) + [:range {:start changed-between-start + :end changed-between-end}] + (let [entities-to-consider (recently-changed-entities + changed-between-start + changed-between-end) + _ (mu/log ::checking-mismatched-transactions + :count (count entities-to-consider)) + jel-accounts (reduce + (fn [acc [e lia]] + (update acc e (fnil conj #{}) lia)) + {} + (dc/q '[:find ?e ?lia + :in $ [?e ...] + :where + [?je :journal-entry/original-entity ?e] + [?e :transaction/date] + [?je :journal-entry/line-items ?li] + [?li :journal-entry-line/account ?lia] + [?lia :account/name]] + (dc/db conn) + entities-to-consider)) + transaction-accounts (reduce + (fn [acc [e lia]] + (update acc e (fnil conj #{}) lia)) + {} + (dc/q '[:find ?e ?lia + :in $ [?e ...] + :where + [?e :transaction/date ?d] + [?e :transaction/accounts ?li] + (not [?e :transaction/approval-status :transaction-approval-status/excluded]) + (not [?e :transaction/approval-status :transaction-approval-status/suppressed]) + [?li :transaction-account/account ?lia] + [?lia :account/name] + [?e :transaction/amount ?amt] + [(not= ?amt 0.0)]] + (dc/db conn) + entities-to-consider))] + (->> transaction-accounts + (filter + (fn [[e accounts]] (not= accounts (get jel-accounts e)))) + (doall)))))) -(defn unbalanced-transactions +(defn unbalanced-transactions ([] (unbalanced-transactions (c/to-date (t/minus (t/now) (t/days 7))) (c/to-date (t/minus (t/now) (t/hours 1))))) ([changed-between-start changed-between-end] (let [entities-to-consider (recently-changed-entities changed-between-start changed-between-end)] (->> (dc/q '[:find ?je ?a (sum ?debit) (sum ?credit) - :with ?jel - :in $ [?je ...] - :where [?je :journal-entry/amount ?a] - [?je :journal-entry/line-items ?jel] - [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit] - [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit] - ] - (dc/db conn) - entities-to-consider) + :with ?jel + :in $ [?je ...] + :where [?je :journal-entry/amount ?a] + [?je :journal-entry/line-items ?jel] + [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit] + [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]] + (dc/db conn) + entities-to-consider) (filter (fn [[_ a d c]] (or (not (dollars= a d)) (not (dollars= a c))))) @@ -165,15 +156,13 @@ (map (fn [je] (pull-ref (dc/db conn) :journal-entry/original-entity je))))))) - - (defn unbalanced-invoices ([] (unbalanced-invoices (c/to-date (t/minus (t/now) (t/days 7))) (c/to-date (t/minus (t/now) (t/hours 1))))) ([changed-between-start changed-between-end] (let [entities-to-consider (recently-changed-entities - changed-between-start - changed-between-end)] + changed-between-start + changed-between-end)] (->> (dc/q '[:find ?je ?a (sum ?debit) (sum ?credit) :with ?jel :in $ [?je ...] @@ -195,19 +184,19 @@ (defn mismatched-invoices ([] (mismatched-invoices (c/to-date (t/minus (t/now) (t/days 7))) - (c/to-date (t/minus (t/now) (t/hours 1)))) ) + (c/to-date (t/minus (t/now) (t/hours 1))))) ([changed-between-start changed-between-end] (let [entities-to-consider (recently-changed-entities changed-between-start changed-between-end) jel-accounts (reduce - (fn [acc [e lia]] - (update acc e (fnil conj #{} ) lia)) - {} - (dc/q '[:find ?e ?lia + (fn [acc [e lia]] + (update acc e (fnil conj #{}) lia)) + {} + (dc/q '[:find ?e ?lia :in $ [?e ...] - :where + :where [?je :journal-entry/original-entity ?e] - [?e :invoice/date] + [?e :invoice/date] [?je :journal-entry/line-items ?li] [?li :journal-entry-line/account ?lia] (not [?lia :account/numeric-code 21000]) @@ -215,10 +204,10 @@ (dc/db conn) entities-to-consider)) invoice-accounts (reduce - (fn [acc [e lia]] - (update acc e (fnil conj #{} ) lia)) - {} - (dc/q '[:find ?e ?lia + (fn [acc [e lia]] + (update acc e (fnil conj #{}) lia)) + {} + (dc/q '[:find ?e ?lia :in $ [?e ...] :where [?e :invoice/expense-accounts ?li] @@ -230,12 +219,11 @@ (not [?e :invoice/exclude-from-ledger true]) [?e :invoice/import-status :import-status/imported]] (dc/db conn) - entities-to-consider)) - ] - (filter - (fn [[e accounts]] - (not= accounts (get jel-accounts e))) - invoice-accounts)))) + entities-to-consider))] + (filter + (fn [[e accounts]] + (not= accounts (get jel-accounts e))) + invoice-accounts)))) (defn touch-broken-ledger [] (statsd/event {:title "Reconciling Ledger" @@ -243,95 +231,91 @@ :priority :low} nil) (mu/trace ::fixing-mismatched-transactions - [] - (mu/log ::started-fixing-mismatched-transactions) - (let [mismatched-ts (mismatched-transactions)] - (if (seq mismatched-ts) - (do - (mu/log ::found-mismatched-transactions - :status "WARN" - :count (count mismatched-ts) - :sample (take 10 mismatched-ts)) - (doseq [[m] mismatched-ts] - (touch-transaction m)) - (statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions)))) - (statsd/gauge "data.mismatched_transactions" 0.0)))) + [] + (mu/log ::started-fixing-mismatched-transactions) + (let [mismatched-ts (mismatched-transactions)] + (if (seq mismatched-ts) + (do + (mu/log ::found-mismatched-transactions + :status "WARN" + :count (count mismatched-ts) + :sample (take 10 mismatched-ts)) + (doseq [[m] mismatched-ts] + (touch-transaction m)) + (statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions)))) + (statsd/gauge "data.mismatched_transactions" 0.0)))) (mu/trace ::fixing-unbalanced-transactions - [] - (mu/log ::started-fixing-unbalanced-transactions) - (let [unbalanced-ts (unbalanced-transactions)] - (if (seq unbalanced-ts) - (do - (mu/log ::found-unbalanced-transactions - :status "WARN" - :count (count unbalanced-ts) - :sample (take 10 unbalanced-ts)) - (doseq [m unbalanced-ts] - (touch-transaction m)) - (statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions)))) - (statsd/gauge "data.unbalanced_transactions" 0.0)))) + [] + (mu/log ::started-fixing-unbalanced-transactions) + (let [unbalanced-ts (unbalanced-transactions)] + (if (seq unbalanced-ts) + (do + (mu/log ::found-unbalanced-transactions + :status "WARN" + :count (count unbalanced-ts) + :sample (take 10 unbalanced-ts)) + (doseq [m unbalanced-ts] + (touch-transaction m)) + (statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions)))) + (statsd/gauge "data.unbalanced_transactions" 0.0)))) - (mu/trace ::fixing-mismatched-invoices - [] - (mu/log ::started-fixing-mismatched-invoices) - (let [mismatched-is (mismatched-invoices)] - (if (seq mismatched-is) - (do - (mu/log ::found-mismatched-invoices - :status "WARN" - :count (count mismatched-is) - :sample (take 10 mismatched-is)) - (doseq [[m] mismatched-is] - (touch-invoice m)) - (statsd/gauge "data.mismatched_invoices" (count (mismatched-invoices)))) - (statsd/gauge "data.mismatched_invoices" 0.0)))) - + [] + (mu/log ::started-fixing-mismatched-invoices) + (let [mismatched-is (mismatched-invoices)] + (if (seq mismatched-is) + (do + (mu/log ::found-mismatched-invoices + :status "WARN" + :count (count mismatched-is) + :sample (take 10 mismatched-is)) + (doseq [[m] mismatched-is] + (touch-invoice m)) + (statsd/gauge "data.mismatched_invoices" (count (mismatched-invoices)))) + (statsd/gauge "data.mismatched_invoices" 0.0)))) + (mu/trace ::fixing-unbalanced-invoices - [] - (mu/log ::started-fixing-unbalance-invoices) - (let [unbalanced-is (unbalanced-invoices)] - (if (seq unbalanced-is) - (do - (mu/log ::found-mismatched-invoices - :status "WARN" - :count (count unbalanced-is) - :sample (take 10 unbalanced-is)) - (doseq [m unbalanced-is] - (touch-invoice m)) - (statsd/gauge "data.unbalanced_invoices" (count (unbalanced-invoices)))) - (statsd/gauge "data.unbalanced_invoices" 0.0)))) + [] + (mu/log ::started-fixing-unbalance-invoices) + (let [unbalanced-is (unbalanced-invoices)] + (if (seq unbalanced-is) + (do + (mu/log ::found-mismatched-invoices + :status "WARN" + :count (count unbalanced-is) + :sample (take 10 unbalanced-is)) + (doseq [m unbalanced-is] + (touch-invoice m)) + (statsd/gauge "data.unbalanced_invoices" (count (unbalanced-invoices)))) + (statsd/gauge "data.unbalanced_invoices" 0.0)))) (statsd/event {:title "Finished Reconciling Ledger" :text "This process looks for unbalance ledger entries, or missing ledger entries" :priority :low} nil)) - (defn build-account-lookup [client-id] (let [accounts (by :db/id (map first (dc/q {:find ['(pull ?e [:db/id :account/name - :account/numeric-code + :account/numeric-code {:account/type [:db/ident] - :account/client-overrides [:account-client-override/client :account-client-override/name]} - ])] + :account/client-overrides [:account-client-override/client :account-client-override/name]}])] :in ['$] :where ['[?e :account/name]]} - (dc/db conn )))) + (dc/db conn)))) bank-accounts (by :db/id (map first (dc/q {:find ['(pull ?e [:db/id :bank-account/name :bank-account/numeric-code {:bank-account/type [:db/ident]}])] :in ['$] :where ['[?e :bank-account/name]]} - (dc/db conn)))) + (dc/db conn)))) overrides-by-client (->> accounts vals (mapcat (fn [a] (map (fn [o] [[(:db/id a) (:db/id (:account-client-override/client o))] (:account-client-override/name o)]) - (:account/client-overrides a)) - ) ) - (into {} ))] + (:account/client-overrides a)))) + (into {}))] (fn [a] {:name (or (:bank-account/name (bank-accounts a)) (overrides-by-client [a client-id]) @@ -348,7 +332,7 @@ :client_id client-id}))) (defn find-mismatch-index [] - (reduce + 0 + (reduce + 0 (for [c (map first (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn))) :let [_ (println "searching for" c) a (->> (dc/index-pull (dc/db conn) @@ -356,16 +340,15 @@ :selector [:db/id :journal-entry-line/location :journal-entry-line/account :journal-entry-line/client+account+location+date {:journal-entry/_line-items [:journal-entry/date :journal-entry/client]}] :start [:journal-entry-line/client+account+location+date [c]]}) (take-while (fn [result] - (= c (first (:journal-entry-line/client+account+location+date result))) - )) + (= c (first (:journal-entry-line/client+account+location+date result))))) (filter (fn [{index :journal-entry-line/client+account+location+date :as result}] (not= index [(-> result :journal-entry/_line-items :journal-entry/client :db/id) (-> result :journal-entry-line/account :db/id) (-> result :journal-entry-line/location) (-> result :journal-entry/_line-items :journal-entry/date)]))))]] - (do (println (count a)) - (count a))))) + (do (println (count a)) + (count a))))) (defn clients-needing-refresh [db available] (let [clients (->> @@ -383,18 +366,18 @@ (filter (comp available :db/id) clients) clients))) -#_(clients-needing-refresh (dc/db conn) #{ 17592273679867}) +#_(clients-needing-refresh (dc/db conn) #{17592273679867}) #_(comment [17592334354011 #inst "0024-08-03T07:52:58.000-00:00"] - [17592302554688 #inst "0023-07-20T07:52:58.000-00:00"] - [17592302554682 #inst "0023-07-16T07:52:58.000-00:00"] - [17592302554691 #inst "0023-07-22T07:52:58.000-00:00"] - [17592334353995 #inst "0024-08-10T07:52:58.000-00:00"] - [17592302554694 #inst "0023-07-27T07:52:58.000-00:00"] - [17592241669405 #inst "0218-08-04T07:52:58.000-00:00"] - [17592334353207 #inst "0024-07-27T07:52:58.000-00:00"] - [17592302554685 #inst "0023-07-16T07:52:58.000-00:00"] - [17592334353244 #inst "0024-07-14T07:52:58.000-00:00"]) + [17592302554688 #inst "0023-07-20T07:52:58.000-00:00"] + [17592302554682 #inst "0023-07-16T07:52:58.000-00:00"] + [17592302554691 #inst "0023-07-22T07:52:58.000-00:00"] + [17592334353995 #inst "0024-08-10T07:52:58.000-00:00"] + [17592302554694 #inst "0023-07-27T07:52:58.000-00:00"] + [17592241669405 #inst "0218-08-04T07:52:58.000-00:00"] + [17592334353207 #inst "0024-07-27T07:52:58.000-00:00"] + [17592302554685 #inst "0023-07-16T07:52:58.000-00:00"] + [17592334353244 #inst "0024-07-14T07:52:58.000-00:00"]) (defn mark-all-clients-dirty [] (auto-ap.datomic/audit-transact-batch @@ -429,17 +412,17 @@ (for [{client :db/id code :client/code bank-accounts :client/bank-accounts} clients {bank-account :db/id bac :bank-account/code} bank-accounts] (let [{[_ _ _ _ _ _ running-balance] :v} (->> (dc/index-range db :journal-entry-line/running-balance-tuple [client bank-account "A"] [client bank-account "A" #inst "2050-01-01"]) - seq - (sort-by (fn [{id :e [_ _ _ current-date] :v}] - [current-date id])) - (last)) + seq + (sort-by (fn [{id :e [_ _ _ current-date] :v}] + [current-date id])) + (last)) running-balance (or running-balance 0.0)] (alog/info ::updating-bank-account-balance :bank-account bac :balance running-balance) @(dc/transact conn [{:db/id bank-account - :bank-account/current-balance-synced (c/to-date (t/now)) - :bank-account/current-balance running-balance}]))))))) + :bank-account/current-balance-synced (c/to-date (t/now)) + :bank-account/current-balance running-balance}]))))))) ;; TODO using iol-ion query as the base, building running balance sets (defn upsert-running-balance @@ -499,98 +482,94 @@ (comment (pull-id (dc/db conn) [:client/code "SCCB"]) - - #_(do - (mu/with-context {:service "upsert-running-balance" - :source "upsert-running-balance" } - (mu/trace ::updating-balance - [:service "upsert-running-balance" - :source "upsert-running-balance" ] - (let [db (dc/db conn) - starting-at (c/to-date (t/now)) - _ (mark-client-dirty "NGA1") - clients (clients-needing-refresh db) - _ (alog/info ::clients-needing-update :clients clients :count (count clients)) - client-change-stats (atom {}) - changes (for [c clients - :let [client-id (:db/id c) - account-lookup (build-account-lookup client-id)] - running-balance-set (account-sets db client-id) - running-balance-change (->> running-balance-set - (reduce - (fn [{:keys [changes last-running-balance]} - line-item] - (if (= 0 (rand-int 1000)) - (println (.-account-id line-item) (.-debit line-item) (.-credit line-item))) - (let [delta (if (#{:account-type/asset - :account-type/dividend - :account-type/expense} (:account_type (account-lookup (.-account-id line-item)))) - (- (or (.-debit line-item) 0.0) (or (.-credit line-item) 0.0)) - (- (or (.-credit line-item) 0.0) (or (.-debit line-item) 0.0))) - correct-running-balance (+ last-running-balance delta) - running-balance-changed? (not (dollars= correct-running-balance (or (.-running-balance line-item) 0.0)))] - (when running-balance-changed? - (swap! client-change-stats update (:client/code c) (fnil inc 0))) - (cond-> {:last-account-lookup account-lookup - :last-running-balance correct-running-balance - :changes changes} - - running-balance-changed? - (update :changes conj {:db/id (.-id line-item) - :journal-entry-line/running-balance correct-running-balance})))) - {:last-running-balance 0.0}) - :changes)] - running-balance-change)] - - (mu/trace ::update-running-balance [] - (auto-ap.datomic/audit-transact-batch changes - {:user/name "running balance updater"})) - (auto-ap.datomic/audit-transact (mapv (fn [c] - {:db/id (:db/id c) - :client/last-running-balance starting-at}) - clients) - {:user/name "running balance updater"}) - (alog/info ::change-stats :stats @client-change-stats) - (refresh-bank-account-balances (map :db/id clients)) - (count changes))))) + + #_(do + (mu/with-context {:service "upsert-running-balance" + :source "upsert-running-balance"} + (mu/trace ::updating-balance + [:service "upsert-running-balance" + :source "upsert-running-balance"] + (let [db (dc/db conn) + starting-at (c/to-date (t/now)) + _ (mark-client-dirty "NGA1") + clients (clients-needing-refresh db) + _ (alog/info ::clients-needing-update :clients clients :count (count clients)) + client-change-stats (atom {}) + changes (for [c clients + :let [client-id (:db/id c) + account-lookup (build-account-lookup client-id)] + running-balance-set (account-sets db client-id) + running-balance-change (->> running-balance-set + (reduce + (fn [{:keys [changes last-running-balance]} + line-item] + (if (= 0 (rand-int 1000)) + (println (.-account-id line-item) (.-debit line-item) (.-credit line-item))) + (let [delta (if (#{:account-type/asset + :account-type/dividend + :account-type/expense} (:account_type (account-lookup (.-account-id line-item)))) + (- (or (.-debit line-item) 0.0) (or (.-credit line-item) 0.0)) + (- (or (.-credit line-item) 0.0) (or (.-debit line-item) 0.0))) + correct-running-balance (+ last-running-balance delta) + running-balance-changed? (not (dollars= correct-running-balance (or (.-running-balance line-item) 0.0)))] + (when running-balance-changed? + (swap! client-change-stats update (:client/code c) (fnil inc 0))) + (cond-> {:last-account-lookup account-lookup + :last-running-balance correct-running-balance + :changes changes} + + running-balance-changed? + (update :changes conj {:db/id (.-id line-item) + :journal-entry-line/running-balance correct-running-balance})))) + {:last-running-balance 0.0}) + :changes)] + running-balance-change)] + + (mu/trace ::update-running-balance [] + (auto-ap.datomic/audit-transact-batch changes + {:user/name "running balance updater"})) + (auto-ap.datomic/audit-transact (mapv (fn [c] + {:db/id (:db/id c) + :client/last-running-balance starting-at}) + clients) + {:user/name "running balance updater"}) + (alog/info ::change-stats :stats @client-change-stats) + (refresh-bank-account-balances (map :db/id clients)) + (count changes))))) (mark-client-dirty "NGA1") -(mark-all-clients-dirty) + (mark-all-clients-dirty) -(count (clients-needing-refresh (dc/db conn))) + (count (clients-needing-refresh (dc/db conn))) - (upsert-running-balance) + (upsert-running-balance) ;; SETUP running-balance-tuple - (doseq [[c] (dc/q '[:find ?c - :in $ - :where [?c :client/code]] - (dc/db conn))] - (println "CLIENT " c) - (auto-ap.datomic/audit-transact-batch - (for [[date client line] (->> (dc/q '[:find ?jed ?jec (pull ?jel [:journal-entry-line/debit :journal-entry-line/credit :journal-entry-line/running-balance :db/id]) - :in $ ?jec - :where [?je :journal-entry/client ?jec] - [?je :journal-entry/date ?jed] - [?je :journal-entry/line-items ?jel]] - (dc/db conn) - c))] - {:db/id (:db/id line) - :journal-entry-line/date date - :journal-entry-line/client client}) + (doseq [[c] (dc/q '[:find ?c + :in $ + :where [?c :client/code]] + (dc/db conn))] + (println "CLIENT " c) + (auto-ap.datomic/audit-transact-batch + (for [[date client line] (->> (dc/q '[:find ?jed ?jec (pull ?jel [:journal-entry-line/debit :journal-entry-line/credit :journal-entry-line/running-balance :db/id]) + :in $ ?jec + :where [?je :journal-entry/client ?jec] + [?je :journal-entry/date ?jed] + [?je :journal-entry/line-items ?jel]] + (dc/db conn) + c))] + {:db/id (:db/id line) + :journal-entry-line/date date + :journal-entry-line/client client}) + {:user/name "backfill-client and dates"}) + (println "done.")) - {:user/name "backfill-client and dates"}) - (println "done.")) - - - - #_(dc/q '[:find (pull ?je [*]) (pull ?jel [*]) - :where [?je :journal-entry/line-items ?jel] - (not [?jel :journal-entry-line/running-balance-tuple])] - (dc/db conn))) - + #_(dc/q '[:find (pull ?je [*]) (pull ?jel [*]) + :where [?je :journal-entry/line-items ?jel] + (not [?jel :journal-entry-line/running-balance-tuple])] + (dc/db conn))) ;; TODO only enable once IOL is set up in clod #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} diff --git a/src/clj/auto_ap/logging.clj b/src/clj/auto_ap/logging.clj index fd99f7ee..05bda05b 100644 --- a/src/clj/auto_ap/logging.clj +++ b/src/clj/auto_ap/logging.clj @@ -1,7 +1,6 @@ (ns auto-ap.logging (:require [com.brunobonacci.mulog :as mu])) - (defmacro with-context-as [ctx s & body] `(mu/with-context ~ctx (let [~s (mu/local-context)] @@ -12,13 +11,13 @@ ~@body)) (defmacro info [x & kvs] - `(mu/log ~x :status "INFO" ~@kvs )) + `(mu/log ~x :status "INFO" ~@kvs)) (defmacro warn [x & kvs] - `(mu/log ~x :status "WARN" ~@kvs )) + `(mu/log ~x :status "WARN" ~@kvs)) (defmacro error [x & kvs] - `(mu/log ~x :status "ERROR" ~@kvs )) + `(mu/log ~x :status "ERROR" ~@kvs)) (defn peek ([x] diff --git a/src/clj/auto_ap/parse.clj b/src/clj/auto_ap/parse.clj index a79db964..5a6957b5 100644 --- a/src/clj/auto_ap/parse.clj +++ b/src/clj/auto_ap/parse.clj @@ -16,7 +16,6 @@ (defonce last-text (atom nil)) - (defn template-applies? [text {:keys [keywords]}] (every? #(re-find % text) keywords)) @@ -24,7 +23,7 @@ ([text template] (alog/info ::template-determined :template (str template)) - + (if (:multi template) (mapcat #(extract-template % text (dissoc template :multi)) @@ -60,8 +59,6 @@ first (extract-template text))) - - (defmulti parse-file "Parses a file based on its extension. Accepts options as additional arguments. Options: @@ -74,17 +71,16 @@ :socket-timeout 120000}} {:function-name "glimpse2" :payload (json/write-str (alog/peek ::x {"url" (str "https://" "data.prod.app.integreatconsult.com" "/" f)}))})))] - (alog/info ::glimpse2-payload :payload result) (-> result - json/read-str))) + json/read-str))) (defn glimpse2 [file] - (try + (try (let [tmp-key (str "glimpse2/import/" (java.util.UUID/randomUUID) ".pdf") _ (with-open [f (io/input-stream file)] - (s3/put-object {:bucket-name "data.prod.app.integreatconsult.com" + (s3/put-object {:bucket-name "data.prod.app.integreatconsult.com" :key tmp-key :input-stream f})) is (invoke-glimpse2 tmp-key)] @@ -99,7 +95,7 @@ :total (get i "total") :invoice-number (get i "invoice_number") :template "None found - defaulting to ChatGPT"})) - + (catch Exception e (alog/warn ::glimpse2-not-work :error e) nil))) @@ -107,7 +103,7 @@ (defmethod parse-file "pdf" [file _ & {:keys [allow-glimpse?] :or {allow-glimpse? false}}] - (or + (or (-> (sh/sh "pdftotext" "-layout" file "-") :out parse) @@ -123,7 +119,6 @@ [file filename & _] (excel/parse-file file filename)) - (defmethod parse-file "xlsx" [file filename & _] @@ -157,8 +152,8 @@ client-word-match (->> clients (map (fn [{:keys [:client/matches :client/name] :as client :or {matches []}}] - (let [client-words (-> #{} - (into + (let [client-words (-> #{} + (into (mapcat (fn [match] (str/split (.toLowerCase match) #"\s")) matches)) @@ -175,13 +170,13 @@ ([clients invoice-client-name] (->> clients (filter (fn [{:keys [:client/matches :client/location-matches :client/locations :client/name] :as client :or {matches []}}] - (seq - (filter (fn [m] - (and - m - invoice-client-name - (= (.toLowerCase invoice-client-name) (.toLowerCase m)))) - (conj matches name))))) + (seq + (filter (fn [m] + (and + m + invoice-client-name + (= (.toLowerCase invoice-client-name) (.toLowerCase m)))) + (conj matches name))))) first))) (defn best-location-match [client text full-text] @@ -207,9 +202,9 @@ (defn dbg-parse [v] (println v) (map - (fn [x] (dissoc x :full-text :text)) - (parse v))) - + (fn [x] (dissoc x :full-text :text)) + (parse v))) + #_(nth (re-find #"ELECTRONICALLY.*\n\s*(.*?)\s{2,}" @last-text) - 1) + 1) diff --git a/src/clj/auto_ap/parse/csv.clj b/src/clj/auto_ap/parse/csv.clj index eb3997fd..63aca950 100644 --- a/src/clj/auto_ap/parse/csv.clj +++ b/src/clj/auto_ap/parse/csv.clj @@ -23,7 +23,6 @@ (str/includes? (str header) "Document Number") :philz - (str/includes? (str header) "DISCOUNT_MESSAGE") :wismettac @@ -35,7 +34,7 @@ (str/includes? (str header) "PARENT CUSTOMER NAME") :worldwide - + :else nil)] (alog/info ::csv-type-determined :type csv-type) @@ -44,18 +43,17 @@ (defmulti parse-csv determine :default #_{:clj-kondo/ignore [:unused-binding]} - (fn default [rows] - nil)) + (fn default [rows] + nil)) (defn parse-date-fallover [d fmts] (when-let [valid-fmt (->> fmts - (filter (fn [f] - (try - (u/parse-value :clj-time f d) - (catch Exception _ - nil)) - )) - (first))] + (filter (fn [f] + (try + (u/parse-value :clj-time f d) + (catch Exception _ + nil)))) + (first))] (u/parse-value :clj-time valid-fmt d))) (defmethod parse-csv :sysco-style-1 @@ -83,7 +81,7 @@ (defmethod parse-csv :sysco-style-2 [rows] - + (let [header (first rows)] (transduce (comp (drop 1) @@ -109,7 +107,7 @@ (map (fn [[_ po-number _ invoice-number invoice-date customer value :as row]] {:vendor-code "Mama Lu's Foods" :customer-identifier customer - :invoice-number (str po-number "-" invoice-number ) + :invoice-number (str po-number "-" invoice-number) :date (parse-date-fallover invoice-date ["M/d/yyyy HH:ss" "M/d/yyyy HH:mm:ss aa" "M/d/yyyy"]) :total (str/replace value #"," "") :text (str/join " " row) @@ -122,10 +120,10 @@ [rows] (transduce (comp (drop 1) - (map (fn [[ po-number _ invoice-number invoice-date customer value :as row]] + (map (fn [[po-number _ invoice-number invoice-date customer value :as row]] {:vendor-code "Mama Lu's Foods" :customer-identifier customer - :invoice-number (str po-number "-" invoice-number ) + :invoice-number (str po-number "-" invoice-number) :date (parse-date-fallover invoice-date ["M/d/yyyy HH:ss" "M/d/yyyy HH:mm:ss aa" "M/d/yyyy"]) :total (str/replace value #"," "") :text (str/join " " row) @@ -137,8 +135,8 @@ (defmethod parse-csv :philz [rows] (transduce - (comp - (filter (fn [[_ _ _ _ _ status _ _ _ ]] + (comp + (filter (fn [[_ _ _ _ _ status _ _ _]] (= status "Billed"))) (map (fn [[dt _ doc-number name _ _ _ _ amount :as row]] {:vendor-code "PHILZ COFFEE, INC" @@ -147,10 +145,8 @@ :date (some-> dt not-empty (parse-date-fallover ["MM/dd/yyyy"])) :total (str/replace amount #"," "") :text (str/join " " row) - :full-text (str/join " " row)})) + :full-text (str/join " " row)}))) - - ) conj [] (drop 1 rows))) @@ -158,15 +154,14 @@ (defmethod parse-csv :wismettac [rows] (transduce - (comp + (comp (map (fn [[inv_number inv_dt total :as row]] {:vendor-code "Wismettac" :invoice-number inv_number :date (some-> inv_dt not-empty (parse-date-fallover ["MM/dd/yyyy"])) :total (str/replace total #"," "") :text (str/join " " row) - :full-text (str/join " " row)})) - ) + :full-text (str/join " " row)}))) conj [] (drop 1 rows))) @@ -174,36 +169,34 @@ (defmethod parse-csv :ledyard [rows] (transduce - (comp - (map (fn [[invoice-number date due amount standard :as row]] - {:vendor-code "Performance Food Group - LEDYARD" - :invoice-number invoice-number - :date (some-> date not-empty (parse-date-fallover ["MM/dd/yy"])) - :due (some-> due not-empty (parse-date-fallover ["MM/dd/yy"])) - :total (str/replace amount #"[\$,]" "") - :text (str/join " " row) - :full-text (str/join " " row)})) - ) - conj - [] - (drop 1 rows))) + (comp + (map (fn [[invoice-number date due amount standard :as row]] + {:vendor-code "Performance Food Group - LEDYARD" + :invoice-number invoice-number + :date (some-> date not-empty (parse-date-fallover ["MM/dd/yy"])) + :due (some-> due not-empty (parse-date-fallover ["MM/dd/yy"])) + :total (str/replace amount #"[\$,]" "") + :text (str/join " " row) + :full-text (str/join " " row)}))) + conj + [] + (drop 1 rows))) (defmethod parse-csv :worldwide [rows] (transduce - (comp - (map (fn [[_ customer-name _ inv date amount :as row]] - {:vendor-code "Worldwide Produce" - :customer-identifier customer-name - :invoice-number (str/replace inv #"[=\"]" "") - :date (some-> date not-empty (parse-date-fallover ["MM/dd/yy"])) - :total (str/replace amount #"[\$,]" "") - :text (str/join " " row) - :full-text (str/join " " row)})) - ) - conj - [] - (drop 1 rows))) + (comp + (map (fn [[_ customer-name _ inv date amount :as row]] + {:vendor-code "Worldwide Produce" + :customer-identifier customer-name + :invoice-number (str/replace inv #"[=\"]" "") + :date (some-> date not-empty (parse-date-fallover ["MM/dd/yy"])) + :total (str/replace amount #"[\$,]" "") + :text (str/join " " row) + :full-text (str/join " " row)}))) + conj + [] + (drop 1 rows))) #_{:clj-kondo/ignore [:unused-binding]} (defmethod parse-csv nil diff --git a/src/clj/auto_ap/parse/excel.clj b/src/clj/auto_ap/parse/excel.clj index c296ccc2..f8305d3f 100644 --- a/src/clj/auto_ap/parse/excel.clj +++ b/src/clj/auto_ap/parse/excel.clj @@ -6,42 +6,38 @@ [clojure.data.json :as json] [config.core :refer [env]] [clojure.java.io :as io] - [amazonica.aws.s3 :as s3]) - ) - - + [amazonica.aws.s3 :as s3])) (defn template-applies? [text {:keys [keywords]}] - + (every? #(re-find % text) keywords)) (defn extract [wb {:keys [extract vendor parser]}] (if (fn? extract) (extract wb vendor) #_[(reduce-kv - (fn [invoice k [regex offset-row offset-column extract-regex]] - (assoc invoice k - (->> wb - (d/sheet-seq) - first - (d/cell-seq) - (filter (fn [cell] - (re-find regex (str (d/read-cell cell))))) - (map (fn [cell] - (let [address (.getAddress cell) - cell-value (str (d/read-cell (d/select-cell (.toString (CellAddress. (+ offset-row (.getRow address)) (+ offset-column (.getColumn address)) )) - (first (d/sheet-seq wb))))) - raw-result (if extract-regex - (second (re-find extract-regex cell-value)) - - cell-value)] - (if (get parser k) - (u/parse-value (first (get parser k) ) (second (get parser k) ) raw-result) - raw-result - )))) - first))) - {:vendor-code vendor} - extract)])) + (fn [invoice k [regex offset-row offset-column extract-regex]] + (assoc invoice k + (->> wb + (d/sheet-seq) + first + (d/cell-seq) + (filter (fn [cell] + (re-find regex (str (d/read-cell cell))))) + (map (fn [cell] + (let [address (.getAddress cell) + cell-value (str (d/read-cell (d/select-cell (.toString (CellAddress. (+ offset-row (.getRow address)) (+ offset-column (.getColumn address)))) + (first (d/sheet-seq wb))))) + raw-result (if extract-regex + (second (re-find extract-regex cell-value)) + + cell-value)] + (if (get parser k) + (u/parse-value (first (get parser k)) (second (get parser k)) raw-result) + raw-result)))) + first))) + {:vendor-code vendor} + extract)])) (defn extract-sheet-details [bucket object] (doto @@ -57,7 +53,7 @@ [file _] (let [tmp-key (str "xls-invoice/import/" (java.util.UUID/randomUUID)) _ (with-open [f (io/input-stream file)] - (s3/put-object {:bucket-name (:data-bucket env) + (s3/put-object {:bucket-name (:data-bucket env) :key tmp-key :input-stream f})) sheet (extract-sheet-details (:data-bucket env) tmp-key) @@ -67,9 +63,6 @@ first (extract sheet)))) - - - (defn xls-date->date [f] (when (not-empty f) (let [f (Double/parseDouble f) diff --git a/src/clj/auto_ap/parse/util.clj b/src/clj/auto_ap/parse/util.clj index ac22ae62..df4b7057 100644 --- a/src/clj/auto_ap/parse/util.clj +++ b/src/clj/auto_ap/parse/util.clj @@ -8,11 +8,9 @@ (defmulti parse-value (fn [method _ _] method)) - (defmethod parse-value :trim-commas [_ _ value] - (str/replace value #"," "") - ) + (str/replace value #"," "")) (defmethod parse-value :trim-commas-and-remove-dollars [_ _ value] (str/replace (str/replace value #"," "") #"\$" "")) @@ -20,7 +18,7 @@ (defmethod parse-value :trim-commas-and-remove-dollars-and-invert-parentheses [_ _ value] (let [v (str/replace (str/replace value #"," "") #"\$" "")] - (if-let [[_ a ] (re-find #"\((.*)\)" v)] + (if-let [[_ a] (re-find #"\((.*)\)" v)] (str "-" a) v))) @@ -39,13 +37,12 @@ [_ _ value] (let [format "yyyy-MM-dd" - [month day year] (str/split (-> value - (str/replace #"\s+" " ") - ) + [month day year] (str/split (-> value + (str/replace #"\s+" " ")) #"\s") - value (str "20" year "-" month "-" day) ] - (try + value (str "20" year "-" month "-" day)] + (try (time/from-time-zone (f/parse (f/formatter format) value) (time/time-zone-for-id "America/Los_Angeles")) (catch Exception e @@ -59,15 +56,14 @@ [format])] (reduce (fn [_ format] - (try + (try (reduced (time/from-time-zone (f/parse (f/formatter format) value) (time/time-zone-for-id "America/Los_Angeles"))) (catch Exception e (alog/warn ::cant-parse-date :error e :raw-value (str value)) nil))) nil - format) - )) + format))) (defmethod parse-value nil [_ _ value] diff --git a/src/clj/auto_ap/pdf/ledger.clj b/src/clj/auto_ap/pdf/ledger.clj index 567063e6..8a0d6a2c 100644 --- a/src/clj/auto_ap/pdf/ledger.clj +++ b/src/clj/auto_ap/pdf/ledger.clj @@ -23,7 +23,7 @@ (let [cell-contents (cond (and (= :dollar (:format cell)) (or (nil? (:value cell)) - (dollars-0? (:value cell)))) + (dollars-0? (:value cell)))) "-" (= :dollar (:format cell)) @@ -38,7 +38,7 @@ (cond-> {} (:border cell) (assoc :border true - :set-border (:border cell)) + :set-border (:border cell)) (:colspan cell) (assoc :colspan (:colspan cell)) (:align cell) (assoc :align (:align cell)) (= :dollar (:format cell)) (assoc :align :right) @@ -47,8 +47,7 @@ (:color cell) (assoc :color (:color cell)) (:bg-color cell) (assoc :background-color (:bg-color cell))) - cell-contents - ])) + cell-contents])) (defn cell-count [table] (let [counts (map count (:rows table))] @@ -59,9 +58,9 @@ (defn table->pdf [table widths] (let [cell-count (cell-count table)] (-> [:pdf-table {:header (mapv - (fn [header] - (map cell->pdf header)) - (:header table)) + (fn [header] + (map cell->pdf header)) + (:header table)) :cell-border false :width-percent (cond (<= cell-count 5) 50 @@ -77,15 +76,13 @@ :else 100)} - widths - ] + widths] - (into - (for [row (:rows table)] - (into [] - (for [cell (take cell-count (concat row (repeat nil)))] - (cell->pdf cell) - )))) + (into + (for [row (:rows table)] + (into [] + (for [cell (take cell-count (concat row (repeat nil)))] + (cell->pdf cell))))) (conj (take cell-count (repeat (cell->pdf {:value " "}))))))) (defn split-table [table n] @@ -95,47 +92,47 @@ (let [new-table (-> table (update :rows (fn [rows] (map - (fn [[header & rest]] - (into [header] - (take (dec n) rest))) - rows))) + (fn [[header & rest]] + (into [header] + (take (dec n) rest))) + rows))) (update :header (fn [headers] (map - (fn [[title & header]] - (first - (reduce - (fn [[so-far a] next] - (let [new-a (+ a (or (:colspan next) - 1))] - (if (<= new-a n) - [(conj so-far next) new-a] - [so-far new-a]))) + (fn [[title & header]] + (first + (reduce + (fn [[so-far a] next] + (let [new-a (+ a (or (:colspan next) + 1))] + (if (<= new-a n) + [(conj so-far next) new-a] + [so-far new-a]))) - [[title] 1] - header))) - headers)))) + [[title] 1] + header))) + headers)))) remaining (-> table (update :rows (fn [rows] (map - (fn [[header & rest]] - (into [header] - (drop (dec n) rest))) - rows))) + (fn [[header & rest]] + (into [header] + (drop (dec n) rest))) + rows))) (update :header (fn [headers] (map - (fn [[title & header]] - (first - (reduce - (fn [[so-far a] next] - (let [new-a (+ a (or (:colspan next) - 1))] - (if (> new-a n) - [(conj so-far next) new-a] - [so-far new-a]))) + (fn [[title & header]] + (first + (reduce + (fn [[so-far a] next] + (let [new-a (+ a (or (:colspan next) + 1))] + (if (> new-a n) + [(conj so-far next) new-a] + [so-far new-a]))) - [[title] 1] - header))) - headers))))] + [[title] 1] + header))) + headers))))] (into [new-table] (split-table remaining n)))))) @@ -148,12 +145,12 @@ table)) (defn make-balance-sheet [args data] - + (let [data (<-graphql data) args (<-graphql args) args (assoc args - :periods (filter identity (cond-> [(:date args)] - (:include-comparison args) (conj (:comparison-date args))))) + :periods (filter identity (cond-> [(:date args)] + (:include-comparison args) (conj (:comparison-date args))))) clients (pull-many (dc/db conn) [:client/code :client/name :db/id] (:client-ids args)) data (concat (->> (:balance-sheet-accounts data) (map (fn [b] @@ -165,22 +162,22 @@ :period (:comparison-date args)))))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) client-count (count (set (map :client-id (:data pnl-data)))) - + report (l-reports/summarize-balance-sheet pnl-data) output-stream (ByteArrayOutputStream.)] (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 - :size :letter - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Balance Sheet - " (str/join ", " (map :client/name clients)))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (conj - (table->pdf report - (cond-> (into [30 ] (repeat client-count 13)) - (:include-comparison args) (into (repeat (* 2 client-count) 13)) - (and (> client-count 1) (not (:include-comparison args))) (conj 13))))) - output-stream) + (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 + :size :letter + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Balance Sheet - " (str/join ", " (map :client/name clients)))]] + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (conj + (table->pdf report + (cond-> (into [30] (repeat client-count 13)) + (:include-comparison args) (into (repeat (* 2 client-count) 13)) + (and (> client-count 1) (not (:include-comparison args))) (conj 13))))) + output-stream) (.toByteArray output-stream))) (defn make-pnl [args data] @@ -190,42 +187,40 @@ data (->> data :periods (mapcat (fn [p1 p2] - (map - (fn [a] - (assoc a :period p1) - ) - (:accounts p2)) - ) + (map + (fn [a] + (assoc a :period p1)) + (:accounts p2))) (:periods args))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) report (l-reports/summarize-pnl pnl-data) output-stream (ByteArrayOutputStream.)] (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 - :size (cond - (and (>= (count (-> pnl-data :args :periods)) 8 ) - (-> pnl-data :args :include-deltas)) - :a2 + (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 + :size (cond + (and (>= (count (-> pnl-data :args :periods)) 8) + (-> pnl-data :args :include-deltas)) + :a2 - (>= (count (-> pnl-data :args :periods)) 4 ) - :tabloid - :else - :letter) - :orientation :landscape - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (into - (for [table (concat (:summaries report) - (:details report))] - (table->pdf table - (into [20] (take (dec (cell-count table)) - (mapcat identity - (repeat - (if (-> pnl-data :args :include-deltas) - [13 6 13] - [13 6]))))))))) + (>= (count (-> pnl-data :args :periods)) 4) + :tabloid + :else + :letter) + :orientation :landscape + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]] + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (into + (for [table (concat (:summaries report) + (:details report))] + (table->pdf table + (into [20] (take (dec (cell-count table)) + (mapcat identity + (repeat + (if (-> pnl-data :args :include-deltas) + [13 6 13] + [13 6]))))))))) output-stream) (.toByteArray output-stream))) @@ -236,42 +231,40 @@ data (->> data :periods (mapcat (fn [p1 p2] - (map - (fn [a] - (assoc a :period p1) - ) - (:accounts p2)) - ) + (map + (fn [a] + (assoc a :period p1)) + (:accounts p2))) (:periods args))) pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) report (l-reports/summarize-cash-flows pnl-data) output-stream (ByteArrayOutputStream.)] (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 - :size (cond - (and (>= (count (-> pnl-data :args :periods)) 8 ) - (-> pnl-data :args :include-deltas)) - :a2 + (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15 + :size (cond + (and (>= (count (-> pnl-data :args :periods)) 8) + (-> pnl-data :args :include-deltas)) + :a2 - (>= (count (-> pnl-data :args :periods)) 4 ) - :tabloid - :else - :letter) - :orientation :landscape - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Statement of Cash Flows - " (str/join ", " (map :client/name clients)))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (into - (for [table (concat (:summaries report) - (:details report))] - (table->pdf table - (into [20] (take (dec (cell-count table)) - (mapcat identity - (repeat - (if (-> pnl-data :args :include-deltas) - [13 6 13] - [13 6]))))))))) + (>= (count (-> pnl-data :args :periods)) 4) + :tabloid + :else + :letter) + :orientation :landscape + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Statement of Cash Flows - " (str/join ", " (map :client/name clients)))]] + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (into + (for [table (concat (:summaries report) + (:details report))] + (table->pdf table + (into [20] (take (dec (cell-count table)) + (mapcat identity + (repeat + (if (-> pnl-data :args :include-deltas) + [13 6 13] + [13 6]))))))))) output-stream) (.toByteArray output-stream))) @@ -283,55 +276,55 @@ output-stream (ByteArrayOutputStream.)] (alog/info ::make-detail-report) (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 - :size :letter - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Journal Detail Report - " (str/join ", " (map :client/name clients)))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (conj - (table->pdf report - [80 25 80 25 25 25]))) - output-stream) + (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 + :size :letter + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Journal Detail Report - " (str/join ", " (map :client/name clients)))]] + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (conj + (table->pdf report + [80 25 80 25 25 25]))) + output-stream) (.toByteArray output-stream))) (defn join-names [client-ids] - (str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_" )) + (str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_")) (defn pnl-args->name [args] (let [min-date (atime/unparse-local - (->> args :periods (map :start) first) - atime/iso-date) + (->> args :periods (map :start) first) + atime/iso-date) max-date (atime/unparse-local - (->> args :periods (map :end) last) - atime/iso-date) + (->> args :periods (map :end) last) + atime/iso-date) names (->> args :client_ids join-names)] (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names))) (defn cash-flows-args->name [args] (let [min-date (atime/unparse-local - (->> args :periods (map :start) first) - atime/iso-date) + (->> args :periods (map :start) first) + atime/iso-date) max-date (atime/unparse-local - (->> args :periods (map :end) last) - atime/iso-date) + (->> args :periods (map :end) last) + atime/iso-date) names (->> args :client_ids join-names)] (format "Cash-flows-%s-to-%s-for-%s" min-date max-date names))) (defn journal-detail-args->name [args] (let [min-date (atime/unparse-local - (->> args :date_range :start) - atime/iso-date) + (->> args :date_range :start) + atime/iso-date) max-date (atime/unparse-local - (->> args :date_range :end) - atime/iso-date) + (->> args :date_range :end) + atime/iso-date) names (->> args :client_ids join-names)] (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names))) (defn balance-sheet-args->name [args] (let [date (atime/unparse-local - (:date args) - atime/iso-date) + (:date args) + atime/iso-date) name (->> args :client_ids join-names)] (format "Balance-sheet-%s-for-%s" date name))) @@ -347,14 +340,14 @@ :metadata {:content-length (count pdf-data) :content-type "application/pdf"}) @(dc/transact conn - [{:report/name name - :report/client (:client_ids args) - :report/key key - :report/url url - :report/creator (:user user) - :report/created (java.util.Date.)}]) + [{:report/name name + :report/client (:client_ids args) + :report/key key + :report/url url + :report/creator (:user user) + :report/created (java.util.Date.)}]) {:report/name name - :report/url url })) + :report/url url})) (defn print-cash-flows [user args data] (let [uuid (str (UUID/randomUUID)) @@ -368,14 +361,14 @@ :metadata {:content-length (count pdf-data) :content-type "application/pdf"}) @(dc/transact conn - [{:report/name name - :report/client (:client_ids args) - :report/key key - :report/url url - :report/creator (:user user) - :report/created (java.util.Date.)}]) + [{:report/name name + :report/client (:client_ids args) + :report/key key + :report/url url + :report/creator (:user user) + :report/created (java.util.Date.)}]) {:report/name name - :report/url url })) + :report/url url})) (defn print-balance-sheet [user args data] (let [uuid (str (UUID/randomUUID)) @@ -389,14 +382,14 @@ :metadata {:content-length (count pdf-data) :content-type "application/pdf"}) @(dc/transact conn - [{:report/name name - :report/client (:client_ids args) - :report/key key - :report/url url - :report/creator (:user user) - :report/created (java.util.Date.)}]) + [{:report/name name + :report/client (:client_ids args) + :report/key key + :report/url url + :report/creator (:user user) + :report/created (java.util.Date.)}]) {:report/name name - :report/url url })) + :report/url url})) (defn print-journal-detail-report [user args data] (let [uuid (str (UUID/randomUUID)) @@ -410,11 +403,11 @@ :metadata {:content-length (count pdf-data) :content-type "application/pdf"}) @(dc/transact conn - [{:report/name name - :report/client (:client_ids args) - :report/key key - :report/url url - :report/creator (:user user) - :report/created (java.util.Date.)}]) + [{:report/name name + :report/client (:client_ids args) + :report/key key + :report/url url + :report/creator (:user user) + :report/created (java.util.Date.)}]) {:report/name name - :report/url url })) + :report/url url})) diff --git a/src/clj/auto_ap/plaid/core.clj b/src/clj/auto_ap/plaid/core.clj index 0439e34d..555baef1 100644 --- a/src/clj/auto_ap/plaid/core.clj +++ b/src/clj/auto_ap/plaid/core.clj @@ -13,7 +13,7 @@ (def secret-key (-> env :plaid :secret-key)) (defn get-link-token [client-code] - (-> (client/post (str base-url "/link/token/create") + (-> (client/post (str base-url "/link/token/create") {:as :json :headers {"Content-Type" "application/json"} :body (json/write-str {"client_id" client-id @@ -40,10 +40,8 @@ :body :link_token)) - - (defn exchange-public-token [public-token _] - (-> (client/post (str base-url "/item/public_token/exchange") + (-> (client/post (str base-url "/item/public_token/exchange") {:as :json :headers {"Content-Type" "application/json"} :body (json/write-str {"client_id" client-id @@ -87,10 +85,8 @@ (.getMessage (:throwable &throw-context))) json)))))) - - -(defn get-balance [access-token ] - (-> (client/post (str base-url "/accounts/balance/get") +(defn get-balance [access-token] + (-> (client/post (str base-url "/accounts/balance/get") {:as :json :headers {"Content-Type" "application/json"} :body (json/write-str {"access_token" access-token @@ -104,7 +100,6 @@ :end (str end) :acct (str account-id)) - (try+ (-> (client/post (str base-url "/transactions/get") {:as :json @@ -140,6 +135,4 @@ (clojure.pprint/pprint (get-transactions "access-production-c0e322fa-f33d-4806-bc42-5fc883fb1ba4" "VZ8Y1azZMdhoYo9MQABrfpgz4jm4kPtakyxN5" #clj-time/date-time "2024-03-15" #clj-time/date-time "2024-03-30")) - (clojure.pprint/pprint (get-accounts "access-production-c0e322fa-f33d-4806-bc42-5fc883fb1ba4")) - - ) \ No newline at end of file + (clojure.pprint/pprint (get-accounts "access-production-c0e322fa-f33d-4806-bc42-5fc883fb1ba4"))) \ No newline at end of file diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index 45fd4d14..a999cdf1 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -36,15 +36,15 @@ (.encodeToString (java.util.Base64/getEncoder) (.toByteArray raw)))) (defn gunzip [b64] - + (let [raw-bytes (.decode (java.util.Base64/getDecoder) b64) raw (java.io.ByteArrayInputStream. raw-bytes) out (java.io.ByteArrayOutputStream.)] (with-open [compressed (-> raw - (io/input-stream) - (java.util.zip.GZIPInputStream.))] + (io/input-stream) + (java.util.zip.GZIPInputStream.))] (io/copy compressed out)) - + (edn/read-string (.toString out)))) (defn user->jwt [user oauth-token] @@ -94,8 +94,8 @@ (if-let [jwt (user->jwt user token)] {:status 301 - :headers {"Location" (str (or (not-empty state) - (bidi/path-for ssr-routes/only-routes + :headers {"Location" (str (or (not-empty state) + (bidi/path-for ssr-routes/only-routes ::dashboard/page)) "?jwt=" (jwt/sign jwt (:jwt-secret env) diff --git a/src/clj/auto_ap/routes/exports.clj b/src/clj/auto_ap/routes/exports.clj index 031d2a8c..b9731d85 100644 --- a/src/clj/auto_ap/routes/exports.clj +++ b/src/clj/auto_ap/routes/exports.clj @@ -76,143 +76,137 @@ (double? v) (str v) - :else - v) - ])) + v)])) m)) (defn export-invoices [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:invoice"}}] - {:body - (list (into (list) - (map datomic-map->graphql-map) - (map first (dc/q '[:find (pull ?i [:db/id :invoice/total :invoice/outstanding-balance :invoice/invoice-number :invoice/date :invoice/original-id - { :invoice/status [:db/ident] - :invoice/payments - [:invoice-payment/amount - {:invoice-payment/payment [:payment/check-number - :payment/memo - {:payment/bank_account [:bank-account/id :bank-account/name :bank-account/number :bank-account/bank-name :bank-account/bank-code :bank-account/code]}]}] - :invoice/vendor [:vendor/name - :db/id - {:vendor/primary-contact [:contact/name] - :vendor/address [:address/street1 :address/city :address/state :address/zip]}] - :invoice/expense-accounts [:db/id - :invoice-expense-account/amount - :invoice-expense-account/id - :invoice-expense-account/location - {:invoice-expense-account/account - [:db/id :account/numeric-code :account/name]}] - :invoice/client [:client/name :db/id :client/code :client/locations]}]) - :in $ ?c - :where [?i :invoice/client ?c]] + {:body + (list (into (list) + (map datomic-map->graphql-map) + (map first (dc/q '[:find (pull ?i [:db/id :invoice/total :invoice/outstanding-balance :invoice/invoice-number :invoice/date :invoice/original-id + {:invoice/status [:db/ident] + :invoice/payments + [:invoice-payment/amount + {:invoice-payment/payment [:payment/check-number + :payment/memo + {:payment/bank_account [:bank-account/id :bank-account/name :bank-account/number :bank-account/bank-name :bank-account/bank-code :bank-account/code]}]}] + :invoice/vendor [:vendor/name + :db/id + {:vendor/primary-contact [:contact/name] + :vendor/address [:address/street1 :address/city :address/state :address/zip]}] + :invoice/expense-accounts [:db/id + :invoice-expense-account/amount + :invoice-expense-account/id + :invoice-expense-account/location + {:invoice-expense-account/account + [:db/id :account/numeric-code :account/name]}] + :invoice/client [:client/name :db/id :client/code :client/locations]}]) + :in $ ?c + :where [?i :invoice/client ?c]] - (dc/db conn) - [:client/code (query-params "client-code")]))))})) + (dc/db conn) + [:client/code (query-params "client-code")]))))})) (defn export-payments [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:payment"}}] - (let [query [[:all_payments - {:client-code (query-params "client-code") - :original-id (query-params "original")} - [:id :check-number :amount :memo :date :status :type :original-id - [:invoices [[:invoice [:id :original-id]] :amount]] - [:bank-account [:number :code :bank-name :bank-code :id]] - [:vendor [:name :id [:primary-contact [:name :email :phone]] [:default-account [:name :numeric-code :id]] [:address [:street1 :city :state :zip]]]] - [:client [:id :name :code]] - ]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}) {:clients [ [:client/code (query-params "client-code")]]})] - {:body - (list (:all-payments (:data payments)))}))) - + (let [query [[:all_payments + {:client-code (query-params "client-code") + :original-id (query-params "original")} + [:id :check-number :amount :memo :date :status :type :original-id + [:invoices [[:invoice [:id :original-id]] :amount]] + [:bank-account [:number :code :bank-name :bank-code :id]] + [:vendor [:name :id [:primary-contact [:name :email :phone]] [:default-account [:name :numeric-code :id]] [:address [:street1 :city :state :zip]]]] + [:client [:id :name :code]]]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}) {:clients [[:client/code (query-params "client-code")]]})] + {:body + (list (:all-payments (:data payments)))}))) (defn export-sales [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:sales"}}] - (let [query [[:all_sales_orders - (cond-> {:client-code (query-params "client-code")} - (query-params "after") (assoc :date-range {:start (query-params "after") - :end nil})) - [:id - :location - :external_id - :total - :tip - :tax - :discount - :returns - :service_charge - :date - [:charges [:type_name :total :tip]] - [:line_items [:item_name :total :tax :discount :category]] - [:client [:id :name :code]]]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)})) - parsedouble #(some-> % Double/parseDouble) ] - {:body - (seq (map - (fn [s] - (-> s - (assoc :utc_date (:date s)) - (update :date (fn [d] - (coerce/to-string (coerce/to-local-date-time (time/to-time-zone (coerce/to-date-time d) (time/time-zone-for-id "America/Los_Angeles")))))) - (update :total parsedouble) - (update :tax parsedouble) - (update :discount parsedouble) - (update :tip parsedouble) - (update :line-items (fn [lis] - (map - (fn [li] - (-> li - (update :tax parsedouble) - (update :discount parsedouble) - (update :total parsedouble))) - lis))) - (update :charges (fn [charges] - (map - (fn [charge] - (-> charge - (update :tip parsedouble) - (update :total parsedouble))) - charges))))) - (:all-sales-orders (:data payments))))}))) + (let [query [[:all_sales_orders + (cond-> {:client-code (query-params "client-code")} + (query-params "after") (assoc :date-range {:start (query-params "after") + :end nil})) + [:id + :location + :external_id + :total + :tip + :tax + :discount + :returns + :service_charge + :date + [:charges [:type_name :total :tip]] + [:line_items [:item_name :total :tax :discount :category]] + [:client [:id :name :code]]]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)})) + parsedouble #(some-> % Double/parseDouble)] + {:body + (seq (map + (fn [s] + (-> s + (assoc :utc_date (:date s)) + (update :date (fn [d] + (coerce/to-string (coerce/to-local-date-time (time/to-time-zone (coerce/to-date-time d) (time/time-zone-for-id "America/Los_Angeles")))))) + (update :total parsedouble) + (update :tax parsedouble) + (update :discount parsedouble) + (update :tip parsedouble) + (update :line-items (fn [lis] + (map + (fn [li] + (-> li + (update :tax parsedouble) + (update :discount parsedouble) + (update :total parsedouble))) + lis))) + (update :charges (fn [charges] + (map + (fn [charge] + (-> charge + (update :tip parsedouble) + (update :total parsedouble))) + charges))))) + (:all-sales-orders (:data payments))))}))) (defn export-expected-deposits [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:deposit"}}] - (let [query [[:all_expected_deposits - (cond-> {:client-code (query-params "client-code")} - (query-params "after") (assoc :date-range {:start (query-params "after") - :end nil})) - [:id - [:client [:id :name :code]] - :location - :external_id - :total - :fee - :date]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] - {:body - (seq (map - (fn [d] - (-> d - (update :fee #(some-> % Double/parseDouble)) - (update :total #(some-> % Double/parseDouble)))) - (:all-expected-deposits (:data payments))))}))) - + (let [query [[:all_expected_deposits + (cond-> {:client-code (query-params "client-code")} + (query-params "after") (assoc :date-range {:start (query-params "after") + :end nil})) + [:id + [:client [:id :name :code]] + :location + :external_id + :total + :fee + :date]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] + {:body + (seq (map + (fn [d] + (-> d + (update :fee #(some-> % Double/parseDouble)) + (update :total #(some-> % Double/parseDouble)))) + (:all-expected-deposits (:data payments))))}))) (generate/add-encoder org.joda.time.DateTime - (fn [c jsonGenerator] - (.writeString jsonGenerator (str c)))) + (fn [c jsonGenerator] + (.writeString jsonGenerator (str c)))) - -(defn export-clients[{:keys [identity]}] +(defn export-clients [{:keys [identity]}] (assert-admin identity) {:body (into [] (map <-graphql) @@ -221,57 +215,56 @@ (defn export-vendors [{:keys [identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{"export:vendors"}}] - {:body - (map <-graphql (->> (dc/q '[:find ?e - :in $ - :where [?e :vendor/name]] - (dc/db conn)) - (map first) - (pull-many (dc/db conn) vendor/default-read)))})) + {:body + (map <-graphql (->> (dc/q '[:find ?e + :in $ + :where [?e :vendor/name]] + (dc/db conn)) + (map first) + (pull-many (dc/db conn) vendor/default-read)))})) (defn export-company-vendors [{:keys [identity query-params]}] (statsd/time! [(str "export.time") {:tags #{"export:company-vendors"}}] - (let [client (:db/id (dc/pull (dc/db conn) [:db/id] [:client/code (get query-params "client")])) + (let [client (:db/id (dc/pull (dc/db conn) [:db/id] [:client/code (get query-params "client")])) - _ (assert-can-see-client identity client) - data (->> (dc/q '[:find (pull ?v [:vendor/name - :vendor/terms - {:vendor/default-account [:account/name :account/numeric-code - {:account/client-overrides - [:account-client-override/client - :account-client-override/name]}] - :vendor/terms-overrides [:vendor-terms-override/client - :vendor-terms-override/terms] - :vendor/account-overrides [:vendor-account-override/client - {:vendor-account-override/account [:account/numeric-code :account/name - {:account/client-overrides - [:account-client-override/client - :account-client-override/name]}]}] - :vendor/address [:address/street1 :address/city :address/state :address/zip]}]) - :in $ ?c - :where [?vu :vendor-usage/client ?c] - [?vu :vendor-usage/count ?count] - [(>= ?vu 0)] - [?vu :vendor-usage/vendor ?v] - (not [?v :vendor/hidden true])] - (dc/db conn) - client) - (map (fn [[v]] - [(-> v :vendor/name) - (-> v :vendor/address :address/street1) - (-> v :vendor/address :address/city) - (-> v :vendor/address :address/state) - (-> v :vendor/address :address/zip) - (-> v (vendor/terms-for-client-id client) ) - (-> v (vendor/account-for-client-id client) (accounts/clientize client) :account/name) - (-> v (vendor/account-for-client-id client) :account/numeric-code) - ] - )) - (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] - {:body - (into [] - data) - :headers {"content-disposition" "attachment; filename=\"vendors.csv\""}}))) + _ (assert-can-see-client identity client) + data (->> (dc/q '[:find (pull ?v [:vendor/name + :vendor/terms + {:vendor/default-account [:account/name :account/numeric-code + {:account/client-overrides + [:account-client-override/client + :account-client-override/name]}] + :vendor/terms-overrides [:vendor-terms-override/client + :vendor-terms-override/terms] + :vendor/account-overrides [:vendor-account-override/client + {:vendor-account-override/account [:account/numeric-code :account/name + {:account/client-overrides + [:account-client-override/client + :account-client-override/name]}]}] + :vendor/address [:address/street1 :address/city :address/state :address/zip]}]) + :in $ ?c + :where [?vu :vendor-usage/client ?c] + [?vu :vendor-usage/count ?count] + [(>= ?vu 0)] + [?vu :vendor-usage/vendor ?v] + (not [?v :vendor/hidden true])] + (dc/db conn) + client) + (map (fn [[v]] + [(-> v :vendor/name) + (-> v :vendor/address :address/street1) + (-> v :vendor/address :address/city) + (-> v :vendor/address :address/state) + (-> v :vendor/address :address/zip) + (-> v (vendor/terms-for-client-id client)) + (-> v (vendor/account-for-client-id client) (accounts/clientize client) :account/name) + (-> v (vendor/account-for-client-id client) :account/numeric-code)])) + + (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] + {:body + (into [] + data) + :headers {"content-disposition" "attachment; filename=\"vendors.csv\""}}))) (defn export-ledger [{:keys [identity query-params]}] (let [start-date (or (some-> (query-params "start-date") @@ -282,139 +275,138 @@ (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:ledger2"}}] - (let [results (->> (dc/q '[:find (pull ?e [:db/id - :journal-entry/external-id - :journal-entry/cleared - :journal-entry/alternate-description - :journal-entry/date - :journal-entry/note - :journal-entry/amount - :journal-entry/source - :journal-entry/cleared-against - :journal-entry/original-entity - {:journal-entry/client [:client/name :client/code :db/id] - :journal-entry/vendor [:vendor/name :db/id] - :journal-entry/line-items - [:db/id - :journal-entry-line/location - :journal-entry-line/debit - :journal-entry-line/credit - {:journal-entry-line/account - [:bank-account/include-in-reports - :bank-account/bank-name - :bank-account/numeric-code - :bank-account/code - :bank-account/visible - :bank-account/name - :bank-account/number - :account/code - :account/name - :account/numeric-code - :account/location - {:account/type [:db/ident :db/id]} - {:bank-account/type [:db/ident :db/id]}]}]}]) - :in $ ?c ?start-date - :where [?e :journal-entry/client ?c] - [?e :journal-entry/date ?date] - [(>= ?date ?start-date)]] - (dc/db conn) - [:client/code (query-params "client-code")] - (coerce/to-date start-date))) - tf-result (transduce (comp - (map first) - (filter (fn [je] - (every? - (fn [jel] - (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] - (or (nil? include-in-reports) - (true? include-in-reports)))) - (:journal-entry/line-items je)))) - (map <-graphql)) - conj - (list) - results)] - {:body - tf-result})))) + (let [results (->> (dc/q '[:find (pull ?e [:db/id + :journal-entry/external-id + :journal-entry/cleared + :journal-entry/alternate-description + :journal-entry/date + :journal-entry/note + :journal-entry/amount + :journal-entry/source + :journal-entry/cleared-against + :journal-entry/original-entity + {:journal-entry/client [:client/name :client/code :db/id] + :journal-entry/vendor [:vendor/name :db/id] + :journal-entry/line-items + [:db/id + :journal-entry-line/location + :journal-entry-line/debit + :journal-entry-line/credit + {:journal-entry-line/account + [:bank-account/include-in-reports + :bank-account/bank-name + :bank-account/numeric-code + :bank-account/code + :bank-account/visible + :bank-account/name + :bank-account/number + :account/code + :account/name + :account/numeric-code + :account/location + {:account/type [:db/ident :db/id]} + {:bank-account/type [:db/ident :db/id]}]}]}]) + :in $ ?c ?start-date + :where [?e :journal-entry/client ?c] + [?e :journal-entry/date ?date] + [(>= ?date ?start-date)]] + (dc/db conn) + [:client/code (query-params "client-code")] + (coerce/to-date start-date))) + tf-result (transduce (comp + (map first) + (filter (fn [je] + (every? + (fn [jel] + (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] + (or (nil? include-in-reports) + (true? include-in-reports)))) + (:journal-entry/line-items je)))) + (map <-graphql)) + conj + (list) + results)] + {:body + tf-result})))) (defn export-accounts [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:accounts"}}] - (let [client-id (d-clients/code->id (query-params "client-code")) - query [[:all-accounts - [:id :numeric_code :type :applicability :location :name [:client_overrides [:name [:client [:id :code :name]]]]]]] - all-accounts (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] + (let [client-id (d-clients/code->id (query-params "client-code")) + query [[:all-accounts + [:id :numeric_code :type :applicability :location :name [:client_overrides [:name [:client [:id :code :name]]]]]]] + all-accounts (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] - {:body - (list (transduce - (comp - (filter (fn [a] - (let [overriden-clients (set (map (comp :id :client) (:client-overrides a)))] - (or (nil? (:applicability a)) - (= :global (:applicability a)) - (overriden-clients (str client-id)))))) - (map (fn [a] - (let [client->name (reduce - (fn [override co] - (assoc override (str (:id (:client co))) (:name co))) - {} - (:client-overrides a))] - (-> a - (assoc :global-name (:name a)) - (assoc :client-name (client->name (str client-id) (:name a))) - (dissoc :client-overrides)))))) - conj - (list) - (:all-accounts (:data all-accounts))))}))) + {:body + (list (transduce + (comp + (filter (fn [a] + (let [overriden-clients (set (map (comp :id :client) (:client-overrides a)))] + (or (nil? (:applicability a)) + (= :global (:applicability a)) + (overriden-clients (str client-id)))))) + (map (fn [a] + (let [client->name (reduce + (fn [override co] + (assoc override (str (:id (:client co))) (:name co))) + {} + (:client-overrides a))] + (-> a + (assoc :global-name (:name a)) + (assoc :client-name (client->name (str client-id) (:name a))) + (dissoc :client-overrides)))))) + conj + (list) + (:all-accounts (:data all-accounts))))}))) (defn export-transactions2 [{:keys [query-params identity]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:transactions2"}}] - {:body (let [db (dc/db conn)] - (->> - (dc/q {:find ['?e] - :in ['$ '?client-code] - :where ['[?e :transaction/client ?client-code]]} - db [:client/code (query-params "client-code")]) - (map first) + {:body (let [db (dc/db conn)] + (->> + (dc/q {:find ['?e] + :in ['$ '?client-code] + :where ['[?e :transaction/client ?client-code]]} + db [:client/code (query-params "client-code")]) + (map first) ;; TODO - #_(map (fn [e] - (let [e (dc/entity db e) - client (:transaction/client e) - bank-account (:transaction/bank-account e)] - {:id (:db/id e) - :date (:transaction/date e) - :post_date (:transaction/post-date e) - :client { :code (:client/code client) - :id (:db/id client) - :name (:client/name client)} - :amount (:transaction/amount e) - :description_original (:transaction/description-original e) - :approval_status (:transaction/approval-status e) - :bank_account {:name (:bank-account/name bank-account) - :code (:bank-account/code bank-account) - :id (:db/id bank-account)}})))))})) + #_(map (fn [e] + (let [e (dc/entity db e) + client (:transaction/client e) + bank-account (:transaction/bank-account e)] + {:id (:db/id e) + :date (:transaction/date e) + :post_date (:transaction/post-date e) + :client {:code (:client/code client) + :id (:db/id client) + :name (:client/name client)} + :amount (:transaction/amount e) + :description_original (:transaction/description-original e) + :approval_status (:transaction/approval-status e) + :bank_account {:name (:bank-account/name bank-account) + :code (:bank-account/code bank-account) + :id (:db/id bank-account)}})))))})) (defn export-transactions [{:keys [query-params identity clients]}] (assert-admin identity) (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) "export:transactions"}}] - (let [[transactions] (d-transactions/get-graphql {:client-code (query-params "client-code") - :clients clients - #_#_:original-id (Integer/parseInt (query-params "original")) - :count Integer/MAX_VALUE})] - - - {:body (map - (comp ->graphql - (fn [i] - (cond-> i - true (update :transaction/date to-date) - true (update :transaction/post-date to-date) - (:transaction/payment i) (update-in [:transaction/payment :payment/date] to-date) - (:transaction/expected-deposit i) (update-in [:transaction/expected-deposit :expected-deposit/date] to-date)))) - transactions)}))) + (let [[transactions] (d-transactions/get-graphql {:client-code (query-params "client-code") + :clients clients + #_#_:original-id (Integer/parseInt (query-params "original")) + :count Integer/MAX_VALUE})] + + {:body (map + (comp ->graphql + (fn [i] + (cond-> i + true (update :transaction/date to-date) + true (update :transaction/post-date to-date) + (:transaction/payment i) (update-in [:transaction/payment :payment/date] to-date) + (:transaction/expected-deposit i) (update-in [:transaction/expected-deposit :expected-deposit/date] to-date)))) + transactions)}))) (defn export-trial-balance [{{:strs [client-code as-of]} :query-params}] (let [db (dc/db conn) @@ -425,44 +417,44 @@ as-of (coerce/to-date (atime/parse as-of atime/iso-date))] {:body - (->> - (dc/index-pull db {:index :avet - :selector [:db/id :journal-entry-line/debit :journal-entry-line/location :journal-entry-line/credit {:journal-entry-line/account [:db/id {:account/type [:db/ident]}]} :journal-entry-line/client+account+location+date] - :start [:journal-entry-line/client+account+location+date - [client]]}) - (take-while (fn [jel] - (let [[c _ _ _] (:journal-entry-line/client+account+location+date jel)] - (= c client)))) - (filter (fn [jel] - (let [[_ _ _ d] (:journal-entry-line/client+account+location+date jel)] - (<= (compare (or d #inst "2000-01-01") as-of) 0)))) - (reduce - (fn [acc jel] - (update acc [(:db/id (:journal-entry-line/account jel)) (:journal-entry-line/location jel)] - (fn [v] - (if (#{:account-type/asset - :account-type/dividend - :account-type/expense} (:db/ident (:account/type (:journal-entry-line/account jel)))) - (update (or v {}) :debit (fnil + 0.0) (or (:journal-entry-line/debit jel) 0.0)) - (update (or v {}) :credit (fnil + 0.0) (or (:journal-entry-line/credit jel) 0.0)))))) - {}) - (map (fn [[[a l] {:keys [debit credit]}]] - [(or (pull-attr db :account/name a) - (pull-attr db :bank-account/name a)) - (or (pull-attr db :account/numeric-code a) - (pull-attr db :bank-account/numeric-code a)) - l - (or debit 0.0) - (or credit 0.0)])) - (sort-by second)) + (->> + (dc/index-pull db {:index :avet + :selector [:db/id :journal-entry-line/debit :journal-entry-line/location :journal-entry-line/credit {:journal-entry-line/account [:db/id {:account/type [:db/ident]}]} :journal-entry-line/client+account+location+date] + :start [:journal-entry-line/client+account+location+date + [client]]}) + (take-while (fn [jel] + (let [[c _ _ _] (:journal-entry-line/client+account+location+date jel)] + (= c client)))) + (filter (fn [jel] + (let [[_ _ _ d] (:journal-entry-line/client+account+location+date jel)] + (<= (compare (or d #inst "2000-01-01") as-of) 0)))) + (reduce + (fn [acc jel] + (update acc [(:db/id (:journal-entry-line/account jel)) (:journal-entry-line/location jel)] + (fn [v] + (if (#{:account-type/asset + :account-type/dividend + :account-type/expense} (:db/ident (:account/type (:journal-entry-line/account jel)))) + (update (or v {}) :debit (fnil + 0.0) (or (:journal-entry-line/debit jel) 0.0)) + (update (or v {}) :credit (fnil + 0.0) (or (:journal-entry-line/credit jel) 0.0)))))) + {}) + (map (fn [[[a l] {:keys [debit credit]}]] + [(or (pull-attr db :account/name a) + (pull-attr db :bank-account/name a)) + (or (pull-attr db :account/numeric-code a) + (pull-attr db :bank-account/numeric-code a)) + l + (or debit 0.0) + (or credit 0.0)])) + (sort-by second)) :status 200})) (defn export-raw [{:keys [query-params identity]}] (assert-admin identity) - (alog/info ::executing-query :q (get query-params "query" )) + (alog/info ::executing-query :q (get query-params "query")) (statsd/time! [(str "export.time") {:tags #{"export:raw"}}] - {:body - (into (list) (apply dc/q (read-string (get query-params "query" )) (into [(dc/db conn)] (read-string (get query-params "args" "[]")))))})) + {:body + (into (list) (apply dc/q (read-string (get query-params "query")) (into [(dc/db conn)] (read-string (get query-params "args" "[]")))))})) (defn export-ntg-account-snapshot [_] (let [clients (->> (dc/q '[:find (pull ?e [:db/id :client/code :client/locations]) @@ -559,65 +551,58 @@ :sales-order/service-charge {:sales-order/charges [:charge/total :charge/tax :charge/tip - :charge/type-name - :charge/reference-link + :charge/type-name + :charge/reference-link {[:charge/processor :xform iol-ion.query/ident] [:db/ident]}] :sales-order/line-items [:order-line-item/item-name :order-line-item/category - :order-line-item/total]} - ] + :order-line-item/total]}] :start [:sales-order/client+date [(:db/id client) (coerce/to-date date)]] :end [:sales-order/client+date [(:db/id client) (coerce/to-date end)]] :reverse false :limit 100}) (take-while (fn matches-client [curr] - (and - (= (first (:sales-order/client+date curr)) - (:db/id client)) - (< (compare (:sales-order/date curr) - (coerce/to-date end)) - 0)) - ))) - ] + (and + (= (first (:sales-order/client+date curr)) + (:db/id client)) + (< (compare (:sales-order/date curr) + (coerce/to-date end)) + 0)))))] + entry all-entries :let [sales-columns [(-> entry :sales-order/client :client/name) - (atime/unparse-local (coerce/from-date (-> entry :sales-order/date)) atime/standard-time) - (-> entry :sales-order/total) - (-> entry :sales-order/tip) - (-> entry :sales-order/service-charge) - (-> entry :sales-order/reference-link) - ] - sales-column-count (count sales-columns) - tender-column-count 6] + (atime/unparse-local (coerce/from-date (-> entry :sales-order/date)) atime/standard-time) + (-> entry :sales-order/total) + (-> entry :sales-order/tip) + (-> entry :sales-order/service-charge) + (-> entry :sales-order/reference-link)] + sales-column-count (count sales-columns) + tender-column-count 6] row (concat [sales-columns] - (map (fn [tender] - (concat - (take sales-column-count (repeat nil)) - [ - (-> tender :charge/total) - (-> tender :charge/tax) - (-> tender :charge/tip) - (-> tender :charge/type-name) - (some-> tender :charge/processor name) - (-> tender :charge/reference-link) - ])) - (:sales-order/charges entry)) - (map (fn [tender] - (concat - (take (+ sales-column-count tender-column-count) (repeat nil)) - [ - (-> tender :order-line-item/item-name) - (-> tender :order-line-item/category) - (-> tender :order-line-item/total) - ])) - (:sales-order/line-items entry)))] + (map (fn [tender] + (concat + (take sales-column-count (repeat nil)) + [(-> tender :charge/total) + (-> tender :charge/tax) + (-> tender :charge/tip) + (-> tender :charge/type-name) + (some-> tender :charge/processor name) + (-> tender :charge/reference-link)])) + (:sales-order/charges entry)) + (map (fn [tender] + (concat + (take (+ sales-column-count tender-column-count) (repeat nil)) + [(-> tender :order-line-item/item-name) + (-> tender :order-line-item/category) + (-> tender :order-line-item/total)])) + (:sales-order/line-items entry)))] row - + #_(let [balance (account-lookup (format "%d-%d-%s-%s" (:db/id client) numeric-code l date-str)) _ (when balance (reset! last-used-value balance)) balance (or balance @last-used-value)] - [ l numeric-code (account->name a) date-str + [l numeric-code (account->name a) date-str (format "%.2f" balance)])))})) #_(export-ntg-payment-snapshot nil) @@ -628,7 +613,6 @@ (handler request) {:status 401}))) - (def routes2 {"api/" {"sales/" {"aggregated/" {#"export/?" {:get :aggregated-sales-export}} #"export/?" {:get :export-sales} "ntg-export" {:get :export-ntg-sales-snapshot}} @@ -658,12 +642,11 @@ :export-ledger (-> export-ledger wrap-json-response wrap-secure) :export-ntg-account-snapshot (-> export-ntg-account-snapshot wrap-csv-response (wrap-predetermined-api-key "fd07755a-ed4c-4c9a-ad85-fbdd8af37206")) :export-ntg-sales-snapshot (-> export-ntg-sales-snapshot wrap-csv-response - (wrap-schema-enforce :query-schema (mc/schema [:map - [:date {:required true - :decode/string #(try (atime/parse % atime/iso-date) (catch Exception _ nil))} :some]]) ) - (wrap-form-4xx-2 (fn [_] {:body "Invalid Date"})) - (wrap-predetermined-api-key "fd07755a-ed4c-4c9a-ad85-fbdd8af37206") - ) + (wrap-schema-enforce :query-schema (mc/schema [:map + [:date {:required true + :decode/string #(try (atime/parse % atime/iso-date) (catch Exception _ nil))} :some]])) + (wrap-form-4xx-2 (fn [_] {:body "Invalid Date"})) + (wrap-predetermined-api-key "fd07755a-ed4c-4c9a-ad85-fbdd8af37206")) :export-trial-balance (-> export-trial-balance wrap-csv-response wrap-secure) :export-accounts (-> export-accounts wrap-json-response wrap-secure) :export-transactions (-> export-transactions wrap-json-response wrap-secure) diff --git a/src/clj/auto_ap/routes/ezcater.clj b/src/clj/auto_ap/routes/ezcater.clj index 4d565d84..f6152c8b 100644 --- a/src/clj/auto_ap/routes/ezcater.clj +++ b/src/clj/auto_ap/routes/ezcater.clj @@ -7,12 +7,12 @@ (defn handle-ezcater [{:keys [request-method json-params] :as r}] (cond (= :get request-method) - {:status 200 + {:status 200 :headers {"Content-Type" "application/json"} :body "{}"} (= :post request-method) - (do + (do (alog/info ::ezcater-request :json-params json-params) (e/import-order json-params) @@ -23,8 +23,6 @@ :else {:status 404})) - - (def routes {"api/" {"ezcater/" {#"event/?" :ezcater-event}}}) (def match->handler {:ezcater-event (-> handle-ezcater - wrap-json-params)}) + wrap-json-params)}) diff --git a/src/clj/auto_ap/routes/ezcater_xls.clj b/src/clj/auto_ap/routes/ezcater_xls.clj index 1a99c263..3184ec72 100644 --- a/src/clj/auto_ap/routes/ezcater_xls.clj +++ b/src/clj/auto_ap/routes/ezcater_xls.clj @@ -53,54 +53,54 @@ event-date (some-> (excel/xls-date->date event-date) coerce/to-date-time atime/as-local-time - coerce/to-date )] - (cond (and event-date client-id location ) + coerce/to-date)] + (cond (and event-date client-id location) [:order #:sales-order - {:date event-date - :external-id (str "ezcater/order/" client-id "-" location "-" order-number) - :client client-id - :location location - :reference-link (str order-number) - :line-items [#:order-line-item - {:external-id (str "ezcater/order/" client-id "-" location "-" order-number "-" 0) - :item-name "EZCater Catering" - :category "EZCater Catering" - :discount (fmt-amount (or adjustments 0.0)) - :tax (fmt-amount tax) - :total (fmt-amount (+ food-total - tax))}] + {:date event-date + :external-id (str "ezcater/order/" client-id "-" location "-" order-number) + :client client-id + :location location + :reference-link (str order-number) + :line-items [#:order-line-item + {:external-id (str "ezcater/order/" client-id "-" location "-" order-number "-" 0) + :item-name "EZCater Catering" + :category "EZCater Catering" + :discount (fmt-amount (or adjustments 0.0)) + :tax (fmt-amount tax) + :total (fmt-amount (+ food-total + tax))}] - :charges [#:charge - {:type-name "CARD" - :date event-date - :client client-id - :location location - :external-id (str "ezcater/charge/" client-id "-" location "-" order-number "-" 0) - :processor :ccp-processor/ezcater - :total (fmt-amount (+ food-total - tax - tip)) - :tip (fmt-amount tip)}] - :total (fmt-amount (+ food-total - tax - (or adjustments 0.0))) - :discount (fmt-amount (or adjustments 0.0)) - :service-charge (fmt-amount (+ fee commission)) - :tax (fmt-amount tax) - :tip (fmt-amount tip) - :returns 0.0 - :vendor :vendor/ccp-ezcater}] + :charges [#:charge + {:type-name "CARD" + :date event-date + :client client-id + :location location + :external-id (str "ezcater/charge/" client-id "-" location "-" order-number "-" 0) + :processor :ccp-processor/ezcater + :total (fmt-amount (+ food-total + tax + tip)) + :tip (fmt-amount tip)}] + :total (fmt-amount (+ food-total + tax + (or adjustments 0.0))) + :discount (fmt-amount (or adjustments 0.0)) + :service-charge (fmt-amount (+ fee commission)) + :tax (fmt-amount tax) + :tip (fmt-amount tip) + :returns 0.0 + :vendor :vendor/ccp-ezcater}] - caterer-name - (do - (alog/warn ::missing-client - :order order-number - :store-name store-name - :caterer-name caterer-name) - [:missing caterer-name]) + caterer-name + (do + (alog/warn ::missing-client + :order order-number + :store-name store-name + :caterer-name caterer-name) + [:missing caterer-name]) - :else - nil))) + :else + nil))) (defn stream->sales-orders [s] (let [clients (map first (dc/q '[:find (pull ?c [:client/code @@ -115,7 +115,7 @@ object (str "/ezcater-xls/" (str (java.util.UUID/randomUUID)))] (mu/log ::writing-temp-xls :location object) - (s3/put-object {:bucket-name (:data-bucket env) + (s3/put-object {:bucket-name (:data-bucket env) :key object :input-stream s}) (into [] @@ -157,13 +157,13 @@ });")]])]) (defn upload-xls [{:keys [identity] :as request}] - + (let [file (or (get (:params request) :file) (get (:params request) "file"))] (mu/log ::uploading-file :file file) (with-open [s (io/input-stream (:tempfile file))] - (try + (try (let [parse-results (stream->sales-orders s) new-orders (->> parse-results (filter (comp #{:order} first)) @@ -181,7 +181,7 @@ [:li ml])]])])) (catch Exception e (alog/error ::import-error - :error e) + :error e) (html-response [:div (.getMessage e)])))))) (defn page [{:keys [matched-route request-method] :as request}] diff --git a/src/clj/auto_ap/routes/graphql.clj b/src/clj/auto_ap/routes/graphql.clj index 6168c884..41779a2a 100644 --- a/src/clj/auto_ap/routes/graphql.clj +++ b/src/clj/auto_ap/routes/graphql.clj @@ -11,9 +11,6 @@ [clojure.set :as set] [datomic.api :as dc])) - - - (defn handle-graphql [{:keys [request-method query-params clients] :as r}] (when (= "none" (:user/role (:identity r))) (throw-unauthorized)) @@ -22,21 +19,21 @@ (let [variables (some-> (query-params "variables") edn/read-string) body (some-> r :body slurp)] - + {:status 200 :body (pr-str (ql/query (:identity r) (if (= request-method :get) (query-params "query") body) (assoc variables :clients - clients) )) + clients))) :headers {"Content-Type" "application/edn"}}) (catch Throwable e - + (if-let [result (:result (ex-data e))] (do (alog/warn ::result-error :error e) - {:status 400 - :body (pr-str result) - :headers {"Content-Type" "application/edn"}}) - (if-let [message (:validation-error (ex-data (.getCause e)) )] - (do + {:status 400 + :body (pr-str result) + :headers {"Content-Type" "application/edn"}}) + (if-let [message (:validation-error (ex-data (.getCause e)))] + (do (alog/warn ::graphql-validation-error :message message :error e) @@ -48,6 +45,5 @@ :body (pr-str {:errors [{:message (str "Unhandled error:" (str e))}]}) :headers {"Content-Type" "application/edn"}})))))) - (def routes {"api/" {#"graphql/?" :graphql}}) (def match->handler {:graphql (wrap-secure handle-graphql)}) diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 8b1e0ebb..9accd9d3 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -29,10 +29,10 @@ {: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) + {:find ['?vendor] + :in ['$ '?vendor-name] + :where ['[?vendor :vendor/name ?vendor-name]]} + (dc/db conn) vendor-code) first first))] (when-not vendor-id @@ -40,9 +40,9 @@ {: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) + {:find [(list 'pull '?vendor-id d-vendors/default-read)] + :in ['$ '?vendor-id]} + (dc/db conn) vendor-id) first first)] matching-vendor @@ -54,16 +54,16 @@ account-number (:db/id (d-clients/exact-match account-number)) customer-identifier (:db/id (d-clients/best-match customer-identifier)) client-override (Long/parseLong client-override)) - _ (alog/info ::client-matched - :account-number account-number + _ (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))) - + :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) matching-location (or (when-not (str/blank? location-override) - 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 @@ -97,7 +97,6 @@ invoice) - (defn admin-only-if-multiple-clients [is] (let [client-count (->> is (map :invoice/client) @@ -112,8 +111,8 @@ (map #(validate-invoice % user)) admin-only-if-multiple-clients (mapv d-invoices/code-invoice) - (mapv (fn [i] [:propose-invoice i])))] - + (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 @@ -155,7 +154,7 @@ customer))) code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code {:account/applicability [:db/ident]} - :db/id])] + :db/id])] :in ['$] :where ['[?e :account/name]]} (dc/db conn)))) @@ -225,15 +224,15 @@ (defn import-transactions-cleared-against [file] (let [[_ & rows] (-> file (io/reader) csv/read-csv) txes (transduce - (comp - (filter (fn [[transaction-id _]] - (dc/pull (dc/db conn) '[:transaction/amount] (Long/parseLong transaction-id)))) - (map (fn [[transaction-id cleared-against]] - {:db/id (Long/parseLong transaction-id) - :transaction/cleared-against cleared-against}))) - conj - [] - rows)] + (comp + (filter (fn [[transaction-id _]] + (dc/pull (dc/db conn) '[:transaction/amount] (Long/parseLong transaction-id)))) + (map (fn [[transaction-id cleared-against]] + {:db/id (Long/parseLong transaction-id) + :transaction/cleared-against cleared-against}))) + conj + [] + rows)] (audit-transact txes nil))) (defn batch-upload-transactions [{{:keys [data]} :edn-params user :identity}] @@ -318,8 +317,6 @@ :data (ex-data e)}) :headers {"Content-Type" "application/edn"}})))) - - (defn bulk-account-overrides [{{files :file files-2 "file" client :client diff --git a/src/clj/auto_ap/routes/queries.clj b/src/clj/auto_ap/routes/queries.clj index ad73892f..c7b8837d 100644 --- a/src/clj/auto_ap/routes/queries.clj +++ b/src/clj/auto_ap/routes/queries.clj @@ -25,8 +25,6 @@ (csv/write-csv w %) (.toString w)))))) - - (defn execute-query [query-params params] (let [{:keys [query-id]} params] (mu/with-context {:query-id query-id} @@ -37,7 +35,6 @@ (into (list) (apply dc/q (edn/read-string query-string) (into [(dc/db conn)] (edn/read-string (get query-params "args" "[]"))))))))) - (defn put-query [guid body note & [lookup-key client]] (let [id (pull-id (dc/db conn) [:saved-query/lookup-key lookup-key]) guid (if lookup-key @@ -45,11 +42,11 @@ guid) guid)] @(dc/transact conn [[:upsert-entity {:db/id (or id (random-tempid)) - :saved-query/guid guid - :saved-query/description note - :saved-query/key (str "queries/" guid) - :saved-query/client client - :saved-query/lookup-key lookup-key}]]) + :saved-query/guid guid + :saved-query/description note + :saved-query/key (str "queries/" guid) + :saved-query/client client + :saved-query/lookup-key lookup-key}]]) (s3/put-object :bucket-name (:data-bucket env) :key (str "queries/" guid) :input-stream (io/make-input-stream (.getBytes body) {}) @@ -62,8 +59,6 @@ :csv-results-url (str "/api/queries/" guid "/results/csv") :json-results-url (str "/api/queries/" guid "/results/json")}})) - - (defn get-queries [{:keys [identity]}] (assert-admin identity) (let [obj (s3/list-objects :bucket-name (:data-bucket env) @@ -77,7 +72,7 @@ (assert-admin identity) (put-query (str (UUID/randomUUID)) (body-string request) (query-params "note"))) -(defn get-query [{:keys [identity params]} ] +(defn get-query [{:keys [identity params]}] (assert-admin identity) (let [{:keys [query-id]} params obj (s3/get-object :bucket-name (:data-bucket env) @@ -89,13 +84,13 @@ :csv-results-url (str "/api/queries/" query-id "/results/csv") :json-results-url (str "/api/queries/" query-id "/results/json")}})) -(defn update-query [{:keys [query-params identity params] :as request} ] +(defn update-query [{:keys [query-params identity params] :as request}] (assert-admin identity) (put-query (:query-id params) (body-string request) (query-params "note"))) -(defn results-json-query [{:keys [query-params params]}] +(defn results-json-query [{:keys [query-params params]}] (statsd/time! [(str "export.query.time") {:tags #{(str "query:" (:query-id params))}}] - {:body (execute-query query-params params)})) + {:body (execute-query query-params params)})) (defn raw-query [{:keys [identity params]}] (assert-admin identity) @@ -124,9 +119,9 @@ :create-query create-query :raw-query raw-query :get-query (-> get-query - wrap-json-response) + wrap-json-response) :update-query (-> update-query - wrap-json-response) + wrap-json-response) :results-json-query (-> results-json-query wrap-json-response) diff --git a/src/clj/auto_ap/routes/yodlee2.clj b/src/clj/auto_ap/routes/yodlee2.clj index 428610d4..221b8c29 100644 --- a/src/clj/auto_ap/routes/yodlee2.clj +++ b/src/clj/auto_ap/routes/yodlee2.clj @@ -11,7 +11,7 @@ (defn fastlink [{:keys [query-params identity]}] (assert-can-see-client identity (pull-attr (dc/db conn) :db/id [:client/code (get query-params "client")])) - + (let [token (if-let [client-id (get query-params "client-id")] (-> client-id Long/parseLong @@ -19,22 +19,22 @@ :client/code (yodlee/get-access-token)) (yodlee/get-access-token (get query-params "client")))] - {:status 200 + {:status 200 :headers {"Content-Type" "application/edn"} :body (pr-str {:token token - :url (:yodlee2-fastlink env)}) })) + :url (:yodlee2-fastlink env)})})) (defn refresh-provider-accounts [{:keys [identity edn-params]}] (assert-admin identity) (alog/info ::refreshing :params edn-params) - (try + (try (yodlee/refresh-provider-account (-> (:client-id edn-params) Long/parseLong d-clients/get-by-id :client/code) (:provider-account-id edn-params)) - {:status 200 + {:status 200 :headers {"Content-Type" "application/edn"} - :body "{}" } + :body "{}"} (catch Exception e (alog/error ::error :error e) {:status 400 @@ -48,9 +48,9 @@ (alog/info ::looking-up :client client :id id) - (try - - {:status 200 + (try + + {:status 200 :headers {"Content-Type" "application/edn"} :body (pr-str (yodlee/get-provider-account-detail (-> client Long/parseLong @@ -70,12 +70,12 @@ {:status 200 :headers {"Content-Type" "application/edn"} :body (pr-str (yodlee/reauthenticate-and-recache - (-> (:client-id data) - Long/parseLong - d-clients/get-by-id - :client/code) - (Long/parseLong id) - (dissoc data :client-id )))} + (-> (:client-id data) + Long/parseLong + d-clients/get-by-id + :client/code) + (Long/parseLong id) + (dissoc data :client-id)))} (catch Exception e (alog/error ::error :error e) {:status 500 @@ -85,15 +85,15 @@ (defn delete-provider-account [{:keys [edn-params identity]}] (assert-admin identity) - (try + (try (yodlee/delete-provider-account (-> (:client-id edn-params) Long/parseLong d-clients/get-by-id :client/code) (:provider-account-id edn-params)) - {:status 200 + {:status 200 :headers {"Content-Type" "application/edn"} - :body (pr-str {}) } + :body (pr-str {})} (catch Exception e (alog/error ::error :error e) {:status 400 @@ -110,11 +110,11 @@ (def routes {"api" {"/yodlee2" {"/fastlink" :fastlink "/provider-accounts/refresh/" :refresh-provider-accounts - ["/provider-accounts/" :client "/" :id ] :get-provider-account-detail - ["/reauthenticate/" :id ] :reauthenticate + ["/provider-accounts/" :client "/" :id] :get-provider-account-detail + ["/reauthenticate/" :id] :reauthenticate "/provider-accounts/delete/" :delete-provider-account}}}) (def match->handler {:fastlink (-> fastlink wrap-secure (valid-for :get)) :refresh-provider-accounts (-> refresh-provider-accounts wrap-secure (valid-for :post)) :get-provider-account-detail (-> get-provider-account-detail wrap-secure (valid-for :get)) :reauthenticate (-> reauthenticate wrap-secure (valid-for :post)) - :delete-provider-account (-> delete-provider-account wrap-secure (valid-for :post))} ) + :delete-provider-account (-> delete-provider-account wrap-secure (valid-for :post))}) diff --git a/src/clj/auto_ap/rule_matching.clj b/src/clj/auto_ap/rule_matching.clj index 74b9756e..da9cd683 100644 --- a/src/clj/auto_ap/rule_matching.clj +++ b/src/clj/auto_ap/rule_matching.clj @@ -8,12 +8,12 @@ :transaction-rule/dom-gte :transaction-rule/dom-lte :transaction-rule/amount-gte :transaction-rule/amount-lte :transaction-rule/client :transaction-rule/bank-account - :transaction-rule/yodlee-merchant]} ] + :transaction-rule/yodlee-merchant]}] (let [transaction-dom (some-> transaction - :transaction/date - .toInstant - (.atZone (java.time.ZoneId/of "US/Pacific")) - (.get java.time.temporal.ChronoField/DAY_OF_MONTH))] + :transaction/date + .toInstant + (.atZone (java.time.ZoneId/of "US/Pacific")) + (.get java.time.temporal.ChronoField/DAY_OF_MONTH))] (and (if description (re-find description (or (:transaction/description-original transaction) "")) @@ -55,14 +55,14 @@ true)))) (defn rule-priority [rule] - (or + (or (->> [[:transaction-rule/bank-account 0] [:transaction-rule/client 1] [:transaction-rule/client-group 2] [:transaction-rule/dom-lte 3] - [:transaction-rule/dom-gte 4] + [:transaction-rule/dom-gte 4] [:transaction-rule/amount-lte 4] - [:transaction-rule/amount-gte 4] + [:transaction-rule/amount-gte 4] [:transaction-rule/description 5] [:transaction-rule/yodlee-merchant 6]] (filter (fn [[key]] @@ -73,15 +73,13 @@ (defn get-matching-rules-by-priority [rules-by-priority transaction] (loop [[rule-set & rules] rules-by-priority] - (if rule-set + (if rule-set (let [matching-rules (into [] (filter #(rule-applies? transaction %) rule-set))] (if (seq matching-rules) matching-rules (recur rules))) []))) - - (defn group-rules-by-priority [rules] (->> rules (map (fn [r] (update r :transaction-rule/description #(some-> % ->pattern)))) @@ -150,7 +148,7 @@ (fn [transaction valid-locations] (if (:transaction/payment transaction) transaction - (let [matching-rules (get-matching-rules-by-priority rules-by-priority transaction )] + (let [matching-rules (get-matching-rules-by-priority rules-by-priority transaction)] (if-let [top-match (and (= (count matching-rules) 1) (first matching-rules))] (apply-rule transaction top-match valid-locations) transaction)))))) diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index c3143575..9b71483c 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -33,23 +33,22 @@ (.addShutdownHook (Runtime/getRuntime) (Thread. f))) - (defn gzip-handler [] (let [gz (GzipHandler.)] (doto gz (.setIncludedMethods (into-array ["GET" "POST" "PUT" "DELETE" "PATCH"])) - (.setIncludedMimeTypes (into-array ["text/css" - "text/*" - "text/plain" - "text/javascript" - "text/csv" - "text/html" - "text/html;charset=utf-8" - "application/javascript" - "application/csv" - "application/edn" - "application/json" - "image/svg+xml"])) + (.setIncludedMimeTypes (into-array ["text/css" + "text/*" + "text/plain" + "text/javascript" + "text/csv" + "text/html" + "text/html;charset=utf-8" + "application/javascript" + "application/csv" + "application/edn" + "application/json" + "image/svg+xml"])) (.setMinGzipSize 1024)) gz)) diff --git a/src/clj/auto_ap/solr.clj b/src/clj/auto_ap/solr.clj index 18b535f2..0feae06a 100644 --- a/src/clj/auto_ap/solr.clj +++ b/src/clj/auto_ap/solr.clj @@ -64,7 +64,7 @@ nil) (defmethod datomic->solr "journal-entry" [d] - (let [i (dc/pull (dc/db conn) '[:db/id + (let [i (dc/pull (dc/db conn) '[:db/id :journal-entry/amount :journal-entry/source {:journal-entry/client [:client/code :db/id] @@ -78,13 +78,13 @@ "date" (some-> i :journal-entry/date c/to-date-time (atime/unparse atime/iso-date) (str "T00:00:00Z")) "amount" (-> i :journal-entry/amount fmt-amount) "description" (str - (when (:journal-entry/source i) - (str (:journal-entry/source i) ": ")) - (str/join ", " (set (map - (fn [li] - (format "%s (%s)" (:account/name (:journal-entry-line/account li)) - (:account/numeric-code (:journal-entry-line/account li)))) - (:journal-entry/line-items i))))) + (when (:journal-entry/source i) + (str (:journal-entry/source i) ": ")) + (str/join ", " (set (map + (fn [li] + (format "%s (%s)" (:account/name (:journal-entry-line/account li)) + (:account/numeric-code (:journal-entry-line/account li)))) + (:journal-entry/line-items i))))) "vendor_name" (-> i :journal-entry/vendor :vendor/name) "vendor_id" (-> i :journal-entry/vendor :db/id) "type" "journal-entry"})) @@ -123,7 +123,6 @@ "vendor_id" (-> i :payment/vendor :db/id) "type" "payment"})) - (defprotocol SolrClient (index-documents-raw [this index xs]) (index-documents [this index xs]) @@ -135,46 +134,45 @@ SolrClient (index-documents-raw [this index xs] (client/post - (str (assoc (url/url solr-uri "solr" index "update") - :query {"commitWithin" 5000 - "commit" true})) - - {:headers {"Content-Type" "application/json"} - :socket-timeout 30000 - :connection-timeout 30000 - :method "POST" - :body (json/write-str xs)})) + (str (assoc (url/url solr-uri "solr" index "update") + :query {"commitWithin" 5000 + "commit" true})) + + {:headers {"Content-Type" "application/json"} + :socket-timeout 30000 + :connection-timeout 30000 + :method "POST" + :body (json/write-str xs)})) (index-documents [this index xs] (client/post - (str (assoc (url/url solr-uri "solr" index "update") - :query {"commitWithin" 5000 - "commit" true})) - {:headers {"Content-Type" "application/json"} - :socket-timeout 30000 - :connection-timeout 30000 - :method "POST" - :body (json/write-str (filter identity (map datomic->solr xs)))})) + (str (assoc (url/url solr-uri "solr" index "update") + :query {"commitWithin" 5000 + "commit" true})) + {:headers {"Content-Type" "application/json"} + :socket-timeout 30000 + :connection-timeout 30000 + :method "POST" + :body (json/write-str (filter identity (map datomic->solr xs)))})) (query [this index q] (-> (client/post (str (url/url solr-uri "solr" index "query")) - {:body (json/write-str q ) + {:body (json/write-str q) :socket-timeout 30000 :connection-timeout 30000 :headers {"Content-Type" "application/json"} - :as :json} - ) + :as :json}) :body :response :docs)) (delete [this index] (client/post - (str (assoc (url/url solr-uri "solr" index "update") - :query {"commitWithin" 15000 - "commit" true})) - {:headers {"Content-Type" "application/json"} - :method "POST" - :body (json/write-str {"delete" {"query" "*:*"}})}))) + (str (assoc (url/url solr-uri "solr" index "update") + :query {"commitWithin" 15000 + "commit" true})) + {:headers {"Content-Type" "application/json"} + :method "POST" + :body (json/write-str {"delete" {"query" "*:*"}})}))) (defrecord MockSolrClient [] SolrClient @@ -191,11 +189,7 @@ (def impl (if (= :solr (:solr-impl env)) (->RealSolrClient (:solr-uri env)) - (->MockSolrClient ))) - - - - + (->MockSolrClient))) (defn touch-with-ledger [i] (index-documents impl "invoices" [i [:journal-entry/original-entity i]])) @@ -205,7 +199,6 @@ ([i index] (index-documents impl index [i]))) - (defrecord InMemSolrClient [data-set-atom] SolrClient (index-documents [this index xs] diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index 2f192cd2..ca91c7c5 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -27,11 +27,9 @@ "Authorization" (str "Bearer " (:client/square-auth-token client)) "Content-Type" "application/json"})) - (defn ->square-date [d] (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") d)) - (def manifold-api-stream (let [stream (s/stream 100)] (->> stream @@ -42,10 +40,10 @@ (de/loop [attempt 0] (-> (de/chain (de/future-with (ex/execute-pool) #_(log/info ::request-started - :url (:url request) - :attempt attempt - :source "Square 3" - :background-job "Square 3") + :url (:url request) + :attempt attempt + :source "Square 3" + :background-job "Square 3") (try (client/request (assoc request :socket-timeout 10000 @@ -104,7 +102,6 @@ :exception error)) [])))) - (def item-cache (atom {})) (defn fetch-catalog [client i v] @@ -124,13 +121,11 @@ #(do (swap! item-cache assoc i %) %)))) - (defn fetch-catalog-cache [client i version] (if (get @item-cache i) (de/success-deferred (get @item-cache i)) (fetch-catalog client i version))) - (defn item->category-name-impl [client item version] (capture-context->lc (cond (:item_id (:item_variation_data item)) @@ -161,7 +156,6 @@ :item item) "Uncategorized")))) - (defn item-id->category-name [client i version] (capture-context->lc (-> [client i] @@ -226,7 +220,6 @@ (concat (:orders result) continued-results)))) (:orders result))))))) - (defn search ([client location start end] (capture-context->lc @@ -250,11 +243,9 @@ (concat (:orders result) continued-results)))) (:orders result)))))))) - (defn amount->money [amt] (* 0.01 (or (:amount amt) 0.0))) - ;; to get totals: (comment (reduce @@ -280,7 +271,7 @@ :reference-link (str (url/url "https://squareup.com/receipt/preview" (:id t))) :external-id (when (:id t) (str "square/charge/" (:id t))) - :processor (cond + :processor (cond (#{"OTHER" "THIRD_PARTY_CARD"} (:type t)) (condp = (some-> (:note t) str/lower-case) "doordash" :ccp-processor/doordash @@ -353,7 +344,7 @@ #:sales-order {:date (if (= "Invoices" (:name (:source order))) (when (:closed_at order) - (coerce/to-date (time/to-time-zone (coerce/to-date-time (:closed_at order)) (time/time-zone-for-id "America/Los_Angeles")))) + (coerce/to-date (time/to-time-zone (coerce/to-date-time (:closed_at order)) (time/time-zone-for-id "America/Los_Angeles")))) (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles")))) :client (:db/id client) :location (:square-location/client-location location) @@ -415,7 +406,6 @@ :client client :location location))))))) - (defn get-payment [client p] (de/chain (manifold-api-call {:url (str "https://connect.squareup.com/v2/payments/" p) @@ -424,7 +414,6 @@ :body :payment)) - (defn continue-payout-entry-list [c l poi cursor] (capture-context->lc lc (de/chain @@ -618,7 +607,6 @@ :count (count x)) @(dc/transact-async conn x)))))))) - (defn upsert-payouts ([client] (apply de/zip @@ -667,7 +655,6 @@ (log/info ::done-loading-refunds))))))) - (defn get-cash-shift [client id] (de/chain (manifold-api-call {:url (str (url/url "https://connect.squareup.com/v2/cash-drawers/shifts" id)) :method :get @@ -826,8 +813,6 @@ d1 d2)) - - (defn remove-voided-orders ([client] (apply de/zip @@ -854,7 +839,7 @@ (:sales-order/external-id o)))))) (s/map (fn [[o]] [[:db/retractEntity [:sales-order/external-id (:sales-order/external-id o)]]])) - + (s/reduce into []))) (fn [results] @@ -863,31 +848,26 @@ (log/info ::removing-orders :count (count x)) @(dc/transact-async conn x))))) - (de/catch (fn [e] - (log/warn ::couldnt-remove :error e) - nil) )))))) + (de/catch (fn [e] + (log/warn ::couldnt-remove :error e) + nil))))))) -#_(comment - (require 'auto-ap.time-reader) +#_(comment + (require 'auto-ap.time-reader) - @(let [[c [l]] (get-square-client-and-location "DBFS") ] - (log/peek :x [ c l]) - (search c l #clj-time/date-time "2026-03-28" #clj-time/date-time "2026-03-29") + @(let [[c [l]] (get-square-client-and-location "DBFS")] + (log/peek :x [c l]) + (search c l #clj-time/date-time "2026-03-28" #clj-time/date-time "2026-03-29")) - ) + @(let [[c [l]] (get-square-client-and-location "NGAK")] + (log/peek :x [c l]) - @(let [[c [l]] (get-square-client-and-location "NGAK") ] - (log/peek :x [ c l]) - - (remove-voided-orders c l #clj-time/date-time "2024-04-11" #clj-time/date-time "2024-04-15")) - (doseq [c (get-square-clients)] - (try - @(remove-voided-orders c) - (catch Exception e - nil))) - - - ) + (remove-voided-orders c l #clj-time/date-time "2024-04-11" #clj-time/date-time "2024-04-15")) + (doseq [c (get-square-clients)] + (try + @(remove-voided-orders c) + (catch Exception e + nil)))) (defn upsert-all [& clients] (capture-context->lc @@ -956,8 +936,6 @@ [:clients clients] @(apply upsert-all clients))) - - (comment (defn refunds-raw-cont ([client l cursor so-far] @@ -987,9 +965,8 @@ (->> @(let [[c [l]] (get-square-client-and-location "NGGG")] - (search c l (time/now) (time/plus (time/now) (time/days -1)))) - + (filter (fn [r] (str/starts-with? (:created_at r) "2024-03-14")))) @@ -997,7 +974,6 @@ (->> @(let [[c [l]] (get-square-client-and-location "NGGG")] - (refunds-raw-cont c l nil [])) (filter (fn [r] (str/starts-with? (:created_at r) "2024-03-14"))))) @@ -1031,13 +1007,8 @@ []))] [(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row) (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)]) :separator \tab) - - - - - ;; => - +;; => (require 'auto-ap.time-reader) @@ -1046,27 +1017,16 @@ (clojure.pprint/pprint (let [[c [l]] (get-square-client-and-location "NGVT")] l - (def z @(search c l #clj-time/date-time "2025-02-23T00:00:00-08:00" #clj-time/date-time "2025-02-28T00:00:00-08:00")) - (take 10 (map #(first (deref (order->sales-order c l %))) z))) + (take 10 (map #(first (deref (order->sales-order c l %))) z)))) - - ) - - - - - (->> z + (->> z (filter (fn [o] (seq (filter (comp #{"OTHER"} :type) (:tenders o))))) (filter #(not (:name (:source %)))) - (count) - - ) - - - + (count)) + (doseq [[code] (seq (dc/q '[:find ?code :in $ :where [?o :sales-order/date ?d] @@ -1075,32 +1035,22 @@ [?o :sales-order/client ?c] [?c :client/code ?code]] (dc/db conn))) - :let [[c [l]] (get-square-client-and-location code) - ] + :let [[c [l]] (get-square-client-and-location code)] order @(search c l #clj-time/date-time "2026-01-01T00:00:00-08:00" (time/now)) - :when (= "Invoices" (:name (:source order) )) + :when (= "Invoices" (:name (:source order))) :let [[sales-order] @(order->sales-order c l order)]] - + (when (should-import-order? order) (println "DATE IS" (:sales-order/date sales-order)) (when (some-> (:sales-order/date sales-order) coerce/to-date-time (time/after? #clj-time/date-time "2026-2-16T00:00:00-08:00")) (println "WOULD UPDATE" sales-order) - @(dc/transact auto-ap.datomic/conn [sales-order]) - ) - #_@(dc/transact ) - (println "DONE")) - - - ) + @(dc/transact auto-ap.datomic/conn [sales-order])) + #_@(dc/transact) + (println "DONE"))) #_(filter (comp #{"OTHER"} :type) (mapcat :tenders z)) - @(let [[c [l]] (get-square-client-and-location "NGRY")] #_(search c l (clj-time.coerce/from-date #inst "2025-02-28") (clj-time.coerce/from-date #inst "2025-03-01")) - (order->sales-order c l (:order (get-order c l "KdvwntmfMNTKBu8NOocbxatOs18YY" ))) - - ) - - ) + (order->sales-order c l (:order (get-order c l "KdvwntmfMNTKBu8NOocbxatOs18YY"))))) diff --git a/src/clj/auto_ap/ssr/account.clj b/src/clj/auto_ap/ssr/account.clj index d298631e..2be031c4 100644 --- a/src/clj/auto_ap/ssr/account.clj +++ b/src/clj/auto_ap/ssr/account.clj @@ -24,7 +24,7 @@ :account/invoice-allowance [:db/ident] :account/client-overrides [:db/id :account-client-override/name - {:account-client-override/client [:db/id :client/name]}]} ]) + {:account-client-override/client [:db/id :client/name]}]}]) (defn search- [id query client] (let [client-part (if (some->> client (can-see-client? id)) @@ -71,9 +71,9 @@ (valid-allowances (-> a allowance :db/ident)) (= (:db/id a) vendor-account)))) (map (fn [[n a]] - {:label (str (:account/numeric-code a) " - " (if client-id + {:label (str (:account/numeric-code a) " - " (if client-id (:account/name (d-accounts/clientize a client-id)) - n)) + n)) :value (:db/id a) :location (:account/location a) :warning (when (= :allowance/warn (-> a allowance :db/ident)) diff --git a/src/clj/auto_ap/ssr/admin.clj b/src/clj/auto_ap/ssr/admin.clj index b412afcc..687010ad 100644 --- a/src/clj/auto_ap/ssr/admin.clj +++ b/src/clj/auto_ap/ssr/admin.clj @@ -15,84 +15,77 @@ (defn hourly-changes [] (let [tx-instant-attr (:db/id (dc/pull (dc/db conn) '[:db/id] :db/txInstant)) - tx-lookup (->> - (dc/tx-range - (dc/log conn) - (coerce/to-date (time/plus (time/now) (time/hours -24))) - (coerce/to-date (time/now))) - (map (fn extract-tx-instant [tx] - (let [tx-id (->> (:data tx) - (map (fn [d] - (:tx d))) - first) - tx-instant (->> tx - :data - (filter (fn [d] - (and (= (:e d) tx-id) - (= tx-instant-attr (:a d))))) - (map :v) - first)] - - tx-instant))) - (group-by (fn hours-ago [d] - (time/in-hours (time/interval (coerce/to-date-time d) (time/now))) - )) - )] + tx-lookup (->> + (dc/tx-range + (dc/log conn) + (coerce/to-date (time/plus (time/now) (time/hours -24))) + (coerce/to-date (time/now))) + (map (fn extract-tx-instant [tx] + (let [tx-id (->> (:data tx) + (map (fn [d] + (:tx d))) + first) + tx-instant (->> tx + :data + (filter (fn [d] + (and (= (:e d) tx-id) + (= tx-instant-attr (:a d))))) + (map :v) + first)] + + tx-instant))) + (group-by (fn hours-ago [d] + (time/in-hours (time/interval (coerce/to-date-time d) (time/now))))))] + (for [h (range 24)] (count (tx-lookup h []))))) (defn page [request] - (base-page - request - (com/page {:nav com/admin-aside-nav - :client-selection (:client-selection request) - :clients (:clients request) - :client (:client request) - :identity (:identity request)} - (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)} - "Admin"]) - [:div.flex.space-x-4 - (com/content-card {:class "w-1/4"} - [:div {:class "flex flex-col px-4 py-3 space-y-3"} - [:div - [:h1.text-2xl.mb-3.font-bold "Growth in clients"] - [:div - [:div {:class "w-full h-64" - :id "client-chart" - :data-chart (hx/json { - :labels ["2 years ago" "1 year ago" "today"], - :series [(for [n [2 1 0] - :let [start (time/plus (time/now) (time/years (- n)))]] - (->> (dc/q '[:find (count ?c) - :in $ - :where [?c :client/code]] - (dc/as-of (dc/db conn) (coerce/to-date start))) - first - first))]})}] - [:script {:lang "javascript"} - (hiccup/raw - "new Chartist.Bar('#client-chart', JSON.parse(document.getElementById('client-chart').getAttribute('data-chart')))")]]]]) + (base-page + request + (com/page {:nav com/admin-aside-nav + :client-selection (:client-selection request) + :clients (:clients request) + :client (:client request) + :identity (:identity request)} + (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)} + "Admin"]) + [:div.flex.space-x-4 + (com/content-card {:class "w-1/4"} + [:div {:class "flex flex-col px-4 py-3 space-y-3"} + [:div + [:h1.text-2xl.mb-3.font-bold "Growth in clients"] + [:div + [:div {:class "w-full h-64" + :id "client-chart" + :data-chart (hx/json {:labels ["2 years ago" "1 year ago" "today"], + :series [(for [n [2 1 0] + :let [start (time/plus (time/now) (time/years (- n)))]] + (->> (dc/q '[:find (count ?c) + :in $ + :where [?c :client/code]] + (dc/as-of (dc/db conn) (coerce/to-date start))) + first + first))]})}] + [:script {:lang "javascript"} + (hiccup/raw + "new Chartist.Bar('#client-chart', JSON.parse(document.getElementById('client-chart').getAttribute('data-chart')))")]]]]) - (com/content-card {:class "w-1/2"} - [:div {:class "flex flex-col px-4 py-3 space-y-3"} - [:div - [:h1.text-2xl.mb-3.font-bold "Changes by hour"] - [:div - [:div {:class "w-full h-64" - :id "changes" - :data-chart (hx/json { - :labels (for [n (range -24 0)] - (format "%d" n)), - :series [(hourly-changes)]})}] - [:script {:lang "javascript"} - (hiccup/raw - "new Chartist.Line('#changes', JSON.parse(document.getElementById('changes').getAttribute('data-chart')))")]]]])] - ) - "Admin") - ) + (com/content-card {:class "w-1/2"} + [:div {:class "flex flex-col px-4 py-3 space-y-3"} + [:div + [:h1.text-2xl.mb-3.font-bold "Changes by hour"] + [:div + [:div {:class "w-full h-64" + :id "changes" + :data-chart (hx/json {:labels (for [n (range -24 0)] + (format "%d" n)), + :series [(hourly-changes)]})}] + [:script {:lang "javascript"} + (hiccup/raw + "new Chartist.Line('#changes', JSON.parse(document.getElementById('changes').getAttribute('data-chart')))")]]]])]) + "Admin")) (def key->handler - { - :auto-ap.routes.admin/page (wrap-client-redirect-unauthenticated (wrap-admin page)) - }) + {:auto-ap.routes.admin/page (wrap-client-redirect-unauthenticated (wrap-admin page))}) diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 3b5ac0a0..71437609 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -37,11 +37,11 @@ (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes - :admin-account-table) + :admin-account-table) "hx-target" "#entity-table" "hx-indicator" "#entity-table"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (com/field {:label "Name"} (com/text-input {:name "name" :id "name" @@ -111,19 +111,19 @@ '[(clojure.string/upper-case ?an) ?upper-an] '[(clojure.string/includes? ?upper-an ?ns)]]} :args [(str/upper-case (:name query-params))]}) - + (some->> query-params :code) (merge-query {:query {:find [] :in ['?nc] :where ['[?e :account/numeric-code ?nc]]} :args [(:code query-params)]}) - + (some->> query-params :type) (merge-query {:query {:find [] :in ['?r] - :where ['[?e :account/type ?r] ]} + :where ['[?e :account/type ?r]]} :args [(some->> query-params :type)]}) - + true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :account/numeric-code ?un] @@ -202,21 +202,20 @@ [:account/numeric-code] :form-params form-params))) _ (some->> form-params - :account/client-overrides - (group-by :account-client-override/client) - (filter (fn [[_ overrides]] - (> (count overrides) 1))) - (map first) - seq - (#(form-validation-error (format "Client(s) %s have more than one override." - (str/join ", " - (map (fn [client] - (format "'%s'" (pull-attr (dc/db conn) - :client/name - (-> client))) - ) %))) - :form-params form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form. - ) + :account/client-overrides + (group-by :account-client-override/client) + (filter (fn [[_ overrides]] + (> (count overrides) 1))) + (map first) + seq + (#(form-validation-error (format "Client(s) %s have more than one override." + (str/join ", " + (map (fn [client] + (format "'%s'" (pull-attr (dc/db conn) + :client/name + (-> client)))) %))) + :form-params form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form. + ) {:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity (:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]] (:identity request)) @@ -241,11 +240,11 @@ "client_id" (:db/id (:account-client-override/client o)) "account_client_override_id" (:db/id o)}))) (html-response - (row* identity updated-account {:flash? true}) - :headers (cond-> {"hx-trigger" "modalclose"} - (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" - "hx-reswap" "afterbegin") - (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account))))))) + (row* identity updated-account {:flash? true}) + :headers (cond-> {"hx-trigger" "modalclose"} + (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" + "hx-reswap" "afterbegin") + (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account))))))) (defn client-override* [override] (com/data-grid-row (-> {:x-ref "p" @@ -259,165 +258,161 @@ (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} (com/typeahead {:name (fc/field-name) - :placeholder "Search..." - :class "w-96" - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :value (fc/field-value) - :content-fn #(pull-attr (dc/db conn) :client/name %)})))) + :placeholder "Search..." + :class "w-96" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :content-fn #(pull-attr (dc/db conn) :client/name %)})))) (fc/with-field :account-client-override/name (com/data-grid-cell - {} - (com/validated-field {:errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-96" - :value (fc/field-value)})))) + {} + (com/validated-field {:errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-96" + :value (fc/field-value)})))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) (defn dialog* [{:keys [entity form-params form-errors]}] (fc/start-form form-params form-errors - [:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity)) - "accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity) )}) - :hx-target "this" - } - (com/modal - {} - [:form (-> {:hx-ext "response-targets" - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content" } - (assoc (if (:db/id entity) - :hx-put - :hx-post) - (str (bidi/path-for ssr-routes/only-routes - :admin-transaction-rule-edit-save)))) - [:fieldset {:class "hx-disable"} - (com/modal-card - {:class "md:h-[600px]"} - [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "accountCode"}] - " - " - [:span {:x-text "accountName"}]]] - [:div.space-y-1 - (when-let [id (:db/id entity)] - (com/hidden {:name "db/id" - :value id})) + [:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity)) + "accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity))}) + :hx-target "this"} + (com/modal + {} + [:form (-> {:hx-ext "response-targets" + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content"} + (assoc (if (:db/id entity) + :hx-put + :hx-post) + (str (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-edit-save)))) + [:fieldset {:class "hx-disable"} + (com/modal-card + {:class "md:h-[600px]"} + [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "accountCode"}] + " - " + [:span {:x-text "accountName"}]]] + [:div.space-y-1 + (when-let [id (:db/id entity)] + (com/hidden {:name "db/id" + :value id})) - (fc/with-field :account/numeric-code - (if (nil? (:db/id entity)) - (com/validated-field {:label "Numeric code" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :x-model "accountCode" - :autofocus true - :class "w-32"})) - (com/hidden {:name (fc/field-name) - :value (fc/field-value)}))) - (fc/with-field :account/name - (com/validated-field {:label "Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :x-model "accountName" + (fc/with-field :account/numeric-code + (if (nil? (:db/id entity)) + (com/validated-field {:label "Numeric code" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :x-model "accountCode" + :autofocus true + :class "w-32"})) + (com/hidden {:name (fc/field-name) + :value (fc/field-value)}))) + (fc/with-field :account/name + (com/validated-field {:label "Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :x-model "accountName" - :class "w-64" - :value (fc/field-value)}))) - (fc/with-field :account/type - (com/validated-field {:label "Account Type" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :id "type" - :value (some-> (fc/field-value) name) - :options (ref->select-options "account-type")}))) - (fc/with-field :account/location - (com/validated-field {:label "Location" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-16" - :value (fc/field-value)}))) + :class "w-64" + :value (fc/field-value)}))) + (fc/with-field :account/type + (com/validated-field {:label "Account Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :id "type" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-type")}))) + (fc/with-field :account/location + (com/validated-field {:label "Location" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-16" + :value (fc/field-value)}))) - [:div.flex.flex-wrap.gap-4 - (fc/with-field :account/invoice-allowance - (com/validated-field {:label "Invoice Allowance" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :value (some-> (fc/field-value) name) - :class "w-36" - :options (ref->select-options "allowance")}))) - (fc/with-field :account/vendor-allowance - (com/validated-field {:label "Vendor Allowance" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :value (some-> (fc/field-value) name) - :options (ref->select-options "allowance")})))] - (fc/with-field :account/applicability - (com/validated-field {:label "Applicability" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :class "w-36" - :value (some-> (fc/field-value) name) - :options (ref->select-options "account-applicability")}))) + [:div.flex.flex-wrap.gap-4 + (fc/with-field :account/invoice-allowance + (com/validated-field {:label "Invoice Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :value (some-> (fc/field-value) name) + :class "w-36" + :options (ref->select-options "allowance")}))) + (fc/with-field :account/vendor-allowance + (com/validated-field {:label "Vendor Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "allowance")})))] + (fc/with-field :account/applicability + (com/validated-field {:label "Applicability" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-applicability")}))) - (fc/with-field :account/client-overrides - - (com/field {:label "Client Overrides" :id "client-overrides"} + (fc/with-field :account/client-overrides - (com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {} "Account name") - (com/data-grid-header {})] - :id "client-override-table"} - (fc/cursor-map - #(client-override* %)) + (com/field {:label "Client Overrides" :id "client-overrides"} - (com/data-grid-new-row {:colspan 3 - :index (count (fc/field-value)) - :hx-get (bidi/path-for ssr-routes/only-routes - :admin-account-client-override-new)} - "New override"))))] - [:div - (com/form-errors {:errors (:errors fc/*form-errors*)}) - (com/validated-save-button {:errors (seq form-errors)} - "Save account")])]])])) + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Account name") + (com/data-grid-header {})] + :id "client-override-table"} + (fc/cursor-map + #(client-override* %)) -(defn new-client-override [{ {:keys [index]} :query-params}] - (html-response - (fc/start-form-with-prefix - [:account/client-overrides (or index 0)] - {:db/id (str (java.util.UUID/randomUUID)) - :new? true} - [] - (client-override* fc/*current*)))) + (com/data-grid-new-row {:colspan 3 + :index (count (fc/field-value)) + :hx-get (bidi/path-for ssr-routes/only-routes + :admin-account-client-override-new)} + "New override"))))] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors (seq form-errors)} + "Save account")])]])])) + +(defn new-client-override [{{:keys [index]} :query-params}] + (html-response + (fc/start-form-with-prefix + [:account/client-overrides (or index 0)] + {:db/id (str (java.util.UUID/randomUUID)) + :new? true} + [] + (client-override* fc/*current*)))) (def form-schema (mc/schema - [:map - [:db/id {:optional true} [:maybe entity-id]] - [:account/numeric-code {:optional true} [:maybe :int]] - [:account/name [:string {:min 1 :decode/string strip}]] - [:account/location {:optional true} [:maybe [:string {:decode/string strip}]]] - [:account/type (ref->enum-schema "account-type")] - [:account/applicability (ref->enum-schema "account-applicability")] ; - [:account/invoice-allowance (ref->enum-schema "allowance")] - [:account/vendor-allowance (ref->enum-schema "allowance")] - [:account/client-overrides {:optional true} - [:maybe - (many-entity {} - [:db/id [:or entity-id temp-id]] - [:account-client-override/client entity-id] - [:account-client-override/name [:string {:min 2 :decode/string strip}]])]]])) + [:map + [:db/id {:optional true} [:maybe entity-id]] + [:account/numeric-code {:optional true} [:maybe :int]] + [:account/name [:string {:min 1 :decode/string strip}]] + [:account/location {:optional true} [:maybe [:string {:decode/string strip}]]] + [:account/type (ref->enum-schema "account-type")] + [:account/applicability (ref->enum-schema "account-applicability")] ; + [:account/invoice-allowance (ref->enum-schema "allowance")] + [:account/vendor-allowance (ref->enum-schema "allowance")] + [:account/client-overrides {:optional true} + [:maybe + (many-entity {} + [:db/id [:or entity-id temp-id]] + [:account-client-override/client entity-id] + [:account-client-override/name [:string {:min 2 :decode/string strip}]])]]])) (defn account-dialog [{:keys [entity form-params form-errors]}] (modal-response (dialog* {:entity entity - :form-params (or (when (seq form-params) - form-params) - (when entity - (mc/decode form-schema entity main-transformer)) - {}) - :form-errors form-errors}))) - - - + :form-params (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + :form-errors form-errors}))) (def key->handler (apply-middleware-to-all-handlers diff --git a/src/clj/auto_ap/ssr/admin/background_jobs.clj b/src/clj/auto_ap/ssr/admin/background_jobs.clj index b5576aa1..34977e2a 100644 --- a/src/clj/auto_ap/ssr/admin/background_jobs.clj +++ b/src/clj/auto_ap/ssr/admin/background_jobs.clj @@ -28,14 +28,13 @@ (com.amazonaws.services.ecs.model AssignPublicIp))) (defn get-ecs-tasks [] - (->> - (concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50))) - (ecs/describe-tasks :include [] :tasks) - :tasks - (map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %))))) - (sort-by :created-at) - reverse)) - + (->> + (concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50))) + (ecs/describe-tasks :include [] :tasks) + :tasks + (map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %))))) + (sort-by :created-at) + reverse)) (defn is-background-job? "This function checks whether a given task is a background job. @@ -60,7 +59,7 @@ (defn job-exited-successfully? [task] (if (= 0 (->> task :containers - (filter (comp #{"integreat-app" } :name)) + (filter (comp #{"integreat-app"} :name)) (first) :exit-code)) true @@ -77,7 +76,7 @@ :succeeded :failed)) :name (task-definition->job-name (:task-definition task)) - :end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0))) + :end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0))) :start-date (some-> (:created-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))}) (defn fetch-page [request] @@ -85,7 +84,7 @@ (filter is-background-job?) (map ecs-task->job))] [jobs (count jobs)])) -(def query-schema (mc/schema [:map ])) +(def query-schema (mc/schema [:map])) (def grid-page (helper/build {:id "job-table" @@ -107,8 +106,7 @@ :entity-name "Job" :query-schema query-schema :route :admin-job-table - :headers [ - {:key "start" + :headers [{:key "start" :name "Start" :render #(some-> % :start-date (atime/unparse-local atime/standard-time))} {:key "end" @@ -119,7 +117,7 @@ :render (fn [e] (when (and (:start-date e) (:end-date e)) - (str (time/in-minutes (time/interval + (str (time/in-minutes (time/interval (:start-date e) (:end-date e))) " minutes")))} {:key "name" @@ -150,16 +148,16 @@ :network-configuration {:aws-vpc-configuration {:subnets ["subnet-5e675761" "subnet-8519fde2" "subnet-89bab8d4"] :security-groups ["sg-004e5855310c453a3" "sg-02d167406b1082698"] :assign-public-ip AssignPublicIp/ENABLED}}} - args (assoc-in [:overrides :container-overrides ] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}])))) + args (assoc-in [:overrides :container-overrides] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}])))) (defn job-start [{:keys [form-params]}] (if (not (get (currently-running-jobs) (:name form-params))) (let [new-job (run-task - (-> (:name form-params) - (str/replace #"-" "_") - (str/replace #":" "") - (str "_" (:dd-env env))) - (dissoc form-params :name))] + (-> (:name form-params) + (str/replace #"-" "_") + (str/replace #":" "") + (str "_" (:dd-env env))) + (dissoc form-params :name))] {:message (str "task " (str new-job) " started.")}) (form-validation-error "This job is already running" :form-params form-params))) @@ -170,107 +168,101 @@ [(fc/with-field :ledger-url (com/validated-field {:label "Url" :errors (fc/field-errors)} - [:div.flex.place-items-center.gap-2 - [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] - (com/text-input {:placeholder "ledger-data.csv" - :name (fc/field-name) - :value (fc/field-value)} )]))] + [:div.flex.place-items-center.gap-2 + [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] + (com/text-input {:placeholder "ledger-data.csv" + :name (fc/field-name) + :value (fc/field-value)})]))] (= "register-invoice-import" name) - [ - (fc/with-field :invoice-url - (com/validated-field {:label "Url" - :errors (fc/field-errors)} - [:div.flex.place-items-center.gap-2 - [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] - (com/text-input {:placeholder "invoice-data.csv" - :name (fc/field-name) - :value (fc/field-value)} )]))] + [(fc/with-field :invoice-url + (com/validated-field {:label "Url" + :errors (fc/field-errors)} + [:div.flex.place-items-center.gap-2 + [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] + (com/text-input {:placeholder "invoice-data.csv" + :name (fc/field-name) + :value (fc/field-value)})]))] (= "load-historical-sales" name) - [ - (fc/with-field :client - (com/validated-field {:label "Client" - :errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :value (fc/field-value) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search)}))) - (fc/with-field :days + [(fc/with-field :client + (com/validated-field {:label "Client" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :value (fc/field-value) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search)}))) + (fc/with-field :days (com/validated-field {:label "Days to load" :errors (fc/field-errors)} (com/text-input {:placeholder "60" :name (fc/field-name) - :value (fc/field-value)} )))] - :else nil)) + :value (fc/field-value)})))] + :else nil))) - - ) - -(defn subform [{{:keys [name]} :query-params }] +(defn subform [{{:keys [name]} :query-params}] (html-response - (fc/start-form {} nil - (subform* {:name name})))) + (fc/start-form {} nil + (subform* {:name name})))) (defn job-start-dialog [{:keys [form-errors form-params] :as request}] (fc/start-form (or form-params {}) form-errors - (modal-response - (com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas - {} - [:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start) - :class "h-full w-full"} - [:fieldset {:class "hx-disable h-full w-full"} - (com/modal-card {} - [:div.m-2 "New job"] - [:div.space-y-6 + (modal-response + (com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas + {} + [:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start) + :class "h-full w-full"} + [:fieldset {:class "hx-disable h-full w-full"} + (com/modal-card {} + [:div.m-2 "New job"] + [:div.space-y-6 - (fc/with-field :name - (com/validated-field {:label "Job" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :value (fc/field-value) - :class "w-64" - :options [["" ""] - ["yodlee2" "Yodlee Import"] - ["yodlee2-accounts" "Yodlee Account Import"] - ["intuit" "Intuit import"] - ["plaid" "Plaid import"] - ["bulk-journal-import" "Bulk Journal Import"] - ["square-import-job" "Square Import"] - ["register-invoice-import" "Register Invoice Import "] - ["ezcater-upsert" "Upsert recent ezcater orders"] - ["load-historical-sales" "Load Historical Square Sales"] - ["export-backup" "Export Backup"]] - :hx-get (bidi/path-for ssr-routes/only-routes - :admin-job-subform) - :hx-target "#sub-form" - :hx-swap "innerHTML"}))) + (fc/with-field :name + (com/validated-field {:label "Job" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :value (fc/field-value) + :class "w-64" + :options [["" ""] + ["yodlee2" "Yodlee Import"] + ["yodlee2-accounts" "Yodlee Account Import"] + ["intuit" "Intuit import"] + ["plaid" "Plaid import"] + ["bulk-journal-import" "Bulk Journal Import"] + ["square-import-job" "Square Import"] + ["register-invoice-import" "Register Invoice Import "] + ["ezcater-upsert" "Upsert recent ezcater orders"] + ["load-historical-sales" "Load Historical Square Sales"] + ["export-backup" "Export Backup"]] + :hx-get (bidi/path-for ssr-routes/only-routes + :admin-job-subform) + :hx-target "#sub-form" + :hx-swap "innerHTML"}))) - [:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]] - [:div - - (com/form-errors {:errors (:errors fc/*form-errors*)}) - (com/validated-save-button {:errors form-errors} "Run job")])]])))) + [:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))})]] + [:div + + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors form-errors} "Run job")])]])))) (def form-schema (mc/schema [:map [:name [:string {:min 1}]] [:ledger-url {:optional true} [:string {:min 1}]] [:invoice-url {:optional true} [:string {:min 1}]] [:client {:optional true} entity-id] - [:days {:optional true} [:int {:min 1 :max 120}]] - ])) + [:days {:optional true} [:int {:min 1 :max 120}]]])) (def key->handler - (apply-middleware-to-all-handlers - (->> - {:admin-jobs (helper/page-route grid-page) - :admin-job-table (helper/table-route grid-page) - :admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]])) - :admin-job-start (-> job-start - (wrap-schema-enforce :form-schema form-schema) - (wrap-nested-form-params) - (wrap-form-4xx-2 job-start-dialog)) - :admin-job-start-dialog job-start-dialog}) - (fn [h] - (-> h - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + (apply-middleware-to-all-handlers + (->> + {:admin-jobs (helper/page-route grid-page) + :admin-job-table (helper/table-route grid-page) + :admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]])) + :admin-job-start (-> job-start + (wrap-schema-enforce :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 job-start-dialog)) + :admin-job-start-dialog job-start-dialog}) + (fn [h] + (-> h + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/admin/clients.clj b/src/clj/auto_ap/ssr/admin/clients.clj index f95a10a6..93847148 100644 --- a/src/clj/auto_ap/ssr/admin/clients.clj +++ b/src/clj/auto_ap/ssr/admin/clients.clj @@ -1,9 +1,9 @@ (ns auto-ap.ssr.admin.clients (:require [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-3 - audit-transact conn merge-query pull-attr pull-id - pull-many query2]] + :refer [add-sorter-fields apply-pagination apply-sort-3 + audit-transact conn merge-query pull-attr pull-id + pull-many query2]] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.logging :as alog] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] @@ -11,7 +11,7 @@ [auto-ap.routes.indicators :as indicators] [auto-ap.routes.queries :as q] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.solr :as solr] [auto-ap.square.core3 :as square] [auto-ap.ssr-routes :as ssr-routes] @@ -26,11 +26,11 @@ [auto-ap.ssr.indicators :as i] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers entity-id - form-validation-error html-response many-entity - many-entity-custom modal-response ref->enum-schema strip - temp-id wrap-entity wrap-merge-prior-hx - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers entity-id + form-validation-error html-response many-entity + many-entity-custom modal-response ref->enum-schema strip + temp-id wrap-entity wrap-merge-prior-hx + wrap-schema-enforce]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [cheshire.core :as cheshire] @@ -47,7 +47,6 @@ (:import [java.util UUID])) - ;; TODO make more reusable malli schemas, use unions if it would be helpful ;; TODO copy save logic from graphql version ;; TODO cash drawer shift @@ -67,8 +66,6 @@ [:enum "" "all" "only-mine"]]]]])) - - (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes @@ -178,7 +175,6 @@ :where ['[?e :client/groups ?g]]} :args [(clojure.string/upper-case (:group query-params))]}) - (not (str/blank? (some-> query-params :code))) (merge-query {:query {:in ['?code] :where ['[?e :client/code ?code]]} @@ -293,8 +289,6 @@ (def row* (partial helper/row* grid-page)) - - (def bank-account-schema [:and [:map [:db/id [:or entity-id temp-id]] [:bank-account/name :string] @@ -383,10 +377,10 @@ [:maybe (many-entity-custom {} [:and [:map - + [:db/id [:or entity-id temp-id]] [:bank-account/name :string] - + [:bank-account/code :string] [:bank-account/type [:maybe (ref->enum-schema "bank-account-type")]] [:bank-account/numeric-code {:optional true} [:maybe :int]] @@ -401,15 +395,15 @@ [:bank-account/intuit-bank-account {:optional true} [:maybe entity-id]] [:bank-account/include-in-reports {:default false} [:boolean {:decode/string {:enter #(if (= % "on") true - + (boolean %))}}]] [:bank-account/visible {:default false} [:boolean {:decode/string {:enter #(if (= % "on") true - + (boolean %))}}]] [:bank-account/use-date-instead-of-post-date? {:default false} [:boolean {:decode/string {:enter #(if (= % "on") true - + (boolean %))}}]] [:bank-account/start-date {:optional true} [:maybe {:decode/arbitrary (fn [m] (if (string? m) @@ -443,10 +437,6 @@ [:client/week-b-credits {:optional true} [:maybe :double]] [:client/week-b-debits {:optional true} [:maybe :double]]])) - - - - (defn email-contact-row [email-contact-cursor] (com/data-grid-row (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? email-contact-cursor))))}) @@ -526,12 +516,10 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)))) - (defn- dialog-header [step] [:div.flex [:div.p-2 (mm/step-name step)] [:p.ml-2.rounded.bg-gray-50.p-2.dark:bg-gray-600 [:span {:x-text "clientName"}]]]) - (defrecord InfoModal [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -598,7 +586,6 @@ (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) :validation-route ::route/navigate))) - (defn match-row [_] (com/data-grid-row {:x-ref "p" @@ -644,8 +631,6 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) - - (defrecord MatchesModal [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -699,7 +684,6 @@ (step-key [_] :contact) - (edit-path [_ _] []) @@ -798,7 +782,6 @@ :to (mm/encode-step-key [:bank-account (fc/field-value (:db/id bank-account))])})} svg/pencil)]])]) - (defmulti bank-account-card (comp deref :bank-account/type)) (defmethod bank-account-card :bank-account-type/cash [bank-account] (bank-account-card-base {:bg-color "bg-green-50" @@ -821,7 +804,6 @@ :icon svg/check :bank-account bank-account})) - (defmulti bank-account-form (comp deref :bank-account/type)) (defmethod bank-account-form :bank-account-type/cash [bank-account] [:div @@ -904,8 +886,6 @@ :checked (fc/field-value)} "Visible for payment"))]]) - - (defn- plaid-account-select [client-id] (fc/with-field :bank-account/plaid-account (com/validated-field {:errors (fc/field-errors) @@ -1048,7 +1028,6 @@ [:div#days-indicator (i/days-ago* (some-> (fc/field-value)))]]) - (fc/with-field :bank-account/include-in-reports (com/checkbox {:name (fc/field-name) :value (boolean (fc/field-value)) @@ -1224,8 +1203,6 @@ (yodlee-account-select (:db/id (:snapshot fc/*form-data*))) (intuit-account-select (:db/id (:snapshot fc/*form-data*)))]) - - (defn new-bank-account-card [] [:div {:class "w-[30em]"} (com/card {:class "w-full border-dotted bg-gray-50"} @@ -1255,7 +1232,6 @@ (edit-path [_ _] []) - (step-schema [_] (mut/select-keys (mm/form-schema linear-wizard) #{})) @@ -1284,7 +1260,6 @@ :validation-route ::route/navigate)] :validation-route ::route/navigate))) - (defn square-location-table [] [:div#square-locations [:div.htmx-indicator @@ -1367,7 +1342,7 @@ :hx-include "#square-token" :hx-trigger "click" :hx-indicator "#square-locations" - :hx-target "#square-locations" } + :hx-target "#square-locations"} "Refresh")] (fc/with-field :client/square-locations @@ -1447,8 +1422,6 @@ (filterv #(not= (get-in multi-form-state [:step-params :db/id]) (:db/id %)) bank-accounts))) (mm/select-state [] nil)))) - - (defrecord CashFlowModal [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -1663,7 +1636,6 @@ #(mm/select-state % [] {}) #(assoc-in % [:snapshot :client/bank-accounts] new-bank-accounts))))))) - (def sales-summary-query "[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns) :with ?s @@ -1792,9 +1764,6 @@ [?cds :cash-drawer-shift/opened-cash ?opened-cash] [(iol-ion.query/excel-date ?date) ?d4]]") - - - (defn setup-sales-queries-impl [client-id] (let [{client-code :client/code feature-flags :client/feature-flags} (dc/pull (dc/db conn) '[:client/code :client/feature-flags] client-id) is-new-square? ((set feature-flags) "new-square")] @@ -1840,7 +1809,6 @@ (cheshire/generate-string (format (slurp (io/resource which)) url)))} children)) - (defn biweekly-sales-powerquery [request] (setup-sales-queries-impl (:db/id (:route-params request))) (modal-response @@ -1872,7 +1840,6 @@ (com/modal-footer {} [:div]))))) - (def key->handler (apply-middleware-to-all-handlers {::route/page (helper/page-route grid-page) diff --git a/src/clj/auto_ap/ssr/admin/excel_invoice.clj b/src/clj/auto_ap/ssr/admin/excel_invoice.clj index 2b0435c0..b0d7e2f3 100644 --- a/src/clj/auto_ap/ssr/admin/excel_invoice.clj +++ b/src/clj/auto_ap/ssr/admin/excel_invoice.clj @@ -6,7 +6,7 @@ [auto-ap.logging :as alog] [auto-ap.routes.admin.excel-invoices :as route] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.components.inputs :as inputs] @@ -16,8 +16,8 @@ [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers html-response wrap-form-4xx-2 - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers html-response wrap-form-4xx-2 + wrap-schema-enforce]] [auto-ap.utils :refer [by]] [bidi.bidi :as bidi] [clj-time.coerce :as coerce] @@ -38,7 +38,6 @@ invoice) - (defn reset-id [i] (update i :invoice-number (fn [n] (if (re-matches #"#+" n) @@ -85,7 +84,6 @@ (get (by (comp :db/id :vendor-schedule-payment-dom/client) :vendor-schedule-payment-dom/dom (:vendor/schedule-payment-dom vendor)) client-id)) - (defn invoice-rows->transaction [rows user] (->> rows (mapcat (fn [{:keys [vendor-id total client-id date invoice-number default-location check automatically-paid-when-due account-id]}] @@ -121,8 +119,7 @@ (let [[[bank-account]] (seq (dc/q '[:find ?ba :in $ ?c :where [?c :client/bank-accounts ?ba] - [?ba :bank-account/type :bank-account-type/cash] - ] + [?ba :bank-account/type :bank-account-type/cash]] (dc/db conn) client-id))] [:upsert-transaction #:transaction {:amount (- (:invoice/total invoice)) @@ -130,18 +127,17 @@ :client (:invoice/client invoice) :status "POSTED" :bank-account bank-account - :db/id #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) - :id #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) + :db/id #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) + :id #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) :raw-id transaction-id :vendor (:invoice/vendor invoice) :description-original "Cash payment" :date (coerce/to-date date) :approval-status :transaction-approval-status/approved - :accounts [{:db/id (str #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) "-account") + :accounts [{:db/id (str #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) "-account") :transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) :transaction-account/location "A" - :transaction-account/amount (Math/abs (:invoice/total invoice))}]}])) - ] + :transaction-account/amount (Math/abs (:invoice/total invoice))}]}]))] [[:propose-invoice (d-invoices/code-invoice (validate-invoice (remove-nils invoice)) account-id)] (some-> payment remove-nils) @@ -154,17 +150,16 @@ (map #(str/split % #"\t")) (map #(into {} (map (fn [c k] [k c]) % columns)))) vendor-name->vendor (->> - (set (map :vendor-name tabulated)) - (dc/q '[:find ?n ?v - :in $ [?n ...] - :where [?v :vendor/name ?n]] - (dc/db conn) - ) - (into {})) - all-clients (merge (into {}(dc/q '[:find ?n (pull ?v [:db/id :client/locations]) - :in $ - :where [?v :client/name ?n]] - (dc/db conn))) + (set (map :vendor-name tabulated)) + (dc/q '[:find ?n ?v + :in $ [?n ...] + :where [?v :vendor/name ?n]] + (dc/db conn)) + (into {})) + all-clients (merge (into {} (dc/q '[:find ?n (pull ?v [:db/id :client/locations]) + :in $ + :where [?v :client/name ?n]] + (dc/db conn))) (into {} (dc/q '[:find ?n (pull ?v [:db/id :client/locations]) :in $ @@ -190,16 +185,16 @@ (let [parsed-invoice-rows (parse-invoice-rows excel-rows) existing-rows (set (d-invoices/get-existing-set)) grouped-rows (group-by - (fn [i] - (cond (seq (:errors i)) - :error + (fn [i] + (cond (seq (:errors i)) + :error - (existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)]) - :exists + (existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)]) + :exists - :else - :new)) - parsed-invoice-rows) + :else + :new)) + parsed-invoice-rows) vendors-not-found (->> parsed-invoice-rows (filter #(and (nil? (:vendor-id %)) (not= "Cash" (:check %)))) @@ -208,9 +203,9 @@ (audit-transact (invoice-rows->transaction (:new grouped-rows) user) user) {:imported (count (:new grouped-rows)) - :already-imported (count (:exists grouped-rows)) - :vendors-not-found vendors-not-found - :errors (map #(dissoc % :date) (:error grouped-rows))})) + :already-imported (count (:exists grouped-rows)) + :vendors-not-found vendors-not-found + :errors (map #(dissoc % :date) (:error grouped-rows))})) (def sample "6/16/17 Acme Bread NMKT-CB 3/26/56 12:00 AM $54.00 Naschmarkt X 7/31/17 8:26 AM 8/1/17 3:57 PM 31000 6/20/17 Acme Bread NMKT-CB 3/19/58 12:00 AM $54.00 Naschmarkt X 7/31/17 8:26 AM 8/1/17 3:57 PM @@ -218,100 +213,98 @@ (defn form* [{:keys [form-params form-errors]} & children] (fc/start-form form-params form-errors - [:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import) :hx-swap "outerHTML"} - [:div {:class "flex flex-col px-4 py-3 space-y-3 w-full"} - [:h1.text-2xl.mb-3.font-bold "Import invoices from excel"] + [:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import) :hx-swap "outerHTML"} + [:div {:class "flex flex-col px-4 py-3 space-y-3 w-full"} + [:h1.text-2xl.mb-3.font-bold "Import invoices from excel"] - (fc/with-field :tsv - (com/validated-field {:label "Tab-separated invoices" - :errors (fc/field-errors)} - [:textarea {:class (hh/add-class "w-full h-96" inputs/default-input-classes) :placeholder (hiccup/raw sample) - :name (fc/field-name) - } - (fc/field-value)])) - (com/form-errors {:errors (:errors fc/*form-errors*)}) - (com/validated-save-button {:color :primary - :class "place-self-end w-32" - :errors (seq form-errors)} - "Import") - children]])) + (fc/with-field :tsv + (com/validated-field {:label "Tab-separated invoices" + :errors (fc/field-errors)} + [:textarea {:class (hh/add-class "w-full h-96" inputs/default-input-classes) :placeholder (hiccup/raw sample) + :name (fc/field-name)} + (fc/field-value)])) + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:color :primary + :class "place-self-end w-32" + :errors (seq form-errors)} + "Import") + children]])) (defn page [{:keys [form-params form-errors] :as request}] - (base-page - request - (com/page {:nav com/admin-aside-nav - :client-selection (:client-selection request) - :clients (:clients request) - :client (:client request) - :identity (:identity request) - :request request} - (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} - "Admin"]) - [:div.flex.space-x-4 - (com/content-card - {:class "w-3/4"} - (form* {:form-params {} - :form-errors []}))]) - "Admin")) + (base-page + request + (com/page {:nav com/admin-aside-nav + :client-selection (:client-selection request) + :clients (:clients request) + :client (:client request) + :identity (:identity request) + :request request} + (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} + "Admin"]) + [:div.flex.space-x-4 + (com/content-card + {:class "w-3/4"} + (form* {:form-params {} + :form-errors []}))]) + "Admin")) (defn form [{:keys [form-params form-errors] :as request}] - (html-response - (form* {:form-params (or form-params {}) - :form-errors (or form-errors [])}))) + (html-response + (form* {:form-params (or form-params {}) + :form-errors (or form-errors [])}))) (defn import [{:keys [form-params form-errors] :as request}] - (html-response - (let [result (bulk-upload-invoices (:tsv form-params) (:identity request))] - (form* {:form-params form-params - :form-errors form-errors} - [:div.flex.space-x-4 - (com/pill {:color :primary} - (format "%d imported" (:imported result))) - (com/pill {:color :secondary} - (format "%d extant" (:already-imported result))) - (when (seq (:vendors-not-found result)) - (list - (com/pill {:color :yellow - "@mouseover" "show=true" - "@mouseout" "show=false" - "x-tooltip" "{content: ()=>$refs.tooltip.innerHTML , - allowHTML: true}" } + (html-response + (let [result (bulk-upload-invoices (:tsv form-params) (:identity request))] + (form* {:form-params form-params + :form-errors form-errors} + [:div.flex.space-x-4 + (com/pill {:color :primary} + (format "%d imported" (:imported result))) + (com/pill {:color :secondary} + (format "%d extant" (:already-imported result))) + (when (seq (:vendors-not-found result)) + (list + (com/pill {:color :yellow + "@mouseover" "show=true" + "@mouseout" "show=false" + "x-tooltip" "{content: ()=>$refs.tooltip.innerHTML , + allowHTML: true}"} - (format "%d vendors not found" (count (:vendors-not-found result)))) - [:template {:x-ref "tooltip"} - [:ul - (for [n (take 5 (:vendors-not-found result))] - [:li n])]]))] + (format "%d vendors not found" (count (:vendors-not-found result)))) + [:template {:x-ref "tooltip"} + [:ul + (for [n (take 5 (:vendors-not-found result))] + [:li n])]]))] - (when (seq (:errors result)) - (com/field {:label "Errors"} - (com/data-grid - {:headers [(com/data-grid-header {} "Date") - (com/data-grid-header {} "Invoice #") - (com/data-grid-header {} "Client") - (com/data-grid-header {} "Vendor") - (com/data-grid-header {} "Amount") - (com/data-grid-header {} "Errors")]} - (for [r (:errors result)] - (com/data-grid-row - {} - (com/data-grid-cell {} (:raw-date r)) - (com/data-grid-cell {} (:invoice-number r)) - (com/data-grid-cell {} (:client-name r)) - (com/data-grid-cell {} (:vendor-name r)) - (com/data-grid-cell {} (:amount r)) - (com/data-grid-cell {} (str/join ", " (map :info (:errors r))))))))))))) + (when (seq (:errors result)) + (com/field {:label "Errors"} + (com/data-grid + {:headers [(com/data-grid-header {} "Date") + (com/data-grid-header {} "Invoice #") + (com/data-grid-header {} "Client") + (com/data-grid-header {} "Vendor") + (com/data-grid-header {} "Amount") + (com/data-grid-header {} "Errors")]} + (for [r (:errors result)] + (com/data-grid-row + {} + (com/data-grid-cell {} (:raw-date r)) + (com/data-grid-cell {} (:invoice-number r)) + (com/data-grid-cell {} (:client-name r)) + (com/data-grid-cell {} (:vendor-name r)) + (com/data-grid-cell {} (:amount r)) + (com/data-grid-cell {} (str/join ", " (map :info (:errors r))))))))))))) (def key->handler - (apply-middleware-to-all-handlers - (->> - {::route/page page - ::route/import (-> import - (wrap-schema-enforce :form-schema [:map [:tsv :string]]) - (wrap-nested-form-params) - (wrap-form-4xx-2 form)) - }) - (fn [h] - (-> h - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + (apply-middleware-to-all-handlers + (->> + {::route/page page + ::route/import (-> import + (wrap-schema-enforce :form-schema [:map [:tsv :string]]) + (wrap-nested-form-params) + (wrap-form-4xx-2 form))}) + (fn [h] + (-> h + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/admin/history.clj b/src/clj/auto_ap/ssr/admin/history.clj index 9c36b962..c5ec1f2e 100644 --- a/src/clj/auto_ap/ssr/admin/history.clj +++ b/src/clj/auto_ap/ssr/admin/history.clj @@ -13,7 +13,7 @@ [bidi.bidi :as bidi])) (defn tx-rows->changes [history] - (->> history + (->> history (group-by (fn [[a _ t]] [a t])) (map (fn [[[a t] changes]] @@ -59,7 +59,6 @@ :else (pr-str v))) - (defn inspect [{{:keys [entity-id]} :params :as request}] (alog/info ::inspect :request request) @@ -151,7 +150,7 @@ [:div.mt-4 [:form.flex.gap-2 {"hx-target" "#history-table" "hx-get" (bidi/path-for ssr-routes/only-routes - :admin-history) + :admin-history) "hx-select" "#history-table" "hx-swap" "innerHTML" "hx-push-url" "true"} @@ -187,6 +186,5 @@ (if entity-id (result-table {:entity-id entity-id}) [:div#history-table]) - [:div#inspector] - ]) + [:div#inspector]]) "History"))) diff --git a/src/clj/auto_ap/ssr/admin/import_batch.clj b/src/clj/auto_ap/ssr/admin/import_batch.clj index 7df6a478..46ab6f51 100644 --- a/src/clj/auto_ap/ssr/admin/import_batch.clj +++ b/src/clj/auto_ap/ssr/admin/import_batch.clj @@ -37,11 +37,11 @@ (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes - ::route/table) + ::route/table) "hx-target" "#entity-table" "hx-indicator" "#entity-table"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field {:value {:start (:start-date (:query-params request)) :end (:end-date (:query-params request))} :id "date-range"}) @@ -53,12 +53,12 @@ :options (ref->select-options "import-source" :allow-nil? true)})) #_(com/field {:label "Code"} - (com/text-input {:name "code" - :id "code" - :class "hot-filter" - :value (:code (:query-params request)) - :placeholder "11101" - :size :small}))]]) + (com/text-input {:name "code" + :id "code" + :class "hot-filter" + :value (:code (:query-params request)) + :placeholder "11101" + :size :small}))]]) (def default-read '[:db/id [:import-batch/date :xform clj-time.coerce/from-date] @@ -72,9 +72,9 @@ (defn fetch-ids [db request] (let [query-params (:query-params request) query (cond-> {:query {:find [] - :in '[$ ] + :in '[$] :where '[]} - :args [db ]} + :args [db]} (:sort query-params) (add-sorter-fields {"source" ['[?e :import-batch/source ?s] '[?s :db/ident ?s2] '[(name ?s2) ?sort-source]] @@ -84,8 +84,8 @@ "user" ['[?e :import-batch/user-name ?sort-user]] "date" ['[?e :import-batch/date ?sort-date]] "type" ['[?e :account/type ?t] - '[?t :db/ident ?ti] - '[(name ?ti) ?sort-type]]} + '[?t :db/ident ?ti] + '[(name ?ti) ?sort-type]]} query-params) (or (:start-date query-params) @@ -96,7 +96,7 @@ (merge-query {:query '{:in [?start-date] :where [[(>= ?d ?start-date)]]} :args [(-> query-params :start-date c/to-date)]}) - + (:end-date query-params) (merge-query {:query '{:in [?end-date] :where [[(< ?d ?end-date)]]} @@ -184,17 +184,17 @@ (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (def key->handler - (apply-middleware-to-all-handlers - (->> - {::route/page (helper/page-route grid-page) - ::route/table (helper/table-route grid-page)}) - (fn [h] - (-> h - - (wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + (apply-middleware-to-all-handlers + (->> + {::route/page (helper/page-route grid-page) + ::route/table (helper/table-route grid-page)}) + (fn [h] + (-> h + + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 5aae28cc..d163b9d4 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -1,16 +1,16 @@ (ns auto-ap.ssr.admin.transaction-rules (:require [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-3 - audit-transact conn merge-query pull-attr pull-many - query2 remove-nils]] + :refer [add-sorter-fields apply-pagination apply-sort-3 + audit-transact conn merge-query pull-attr pull-many + query2 remove-nils]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.transactions :as d-transactions] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :as query-params :refer [wrap-copy-qp-pqp]] [auto-ap.routes.admin.transaction-rules :as route] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.rule-matching :as rm] [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] @@ -23,12 +23,12 @@ [auto-ap.ssr.hx :as hx] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers - default-grid-fields-schema entity-id - field-validation-error form-validation-error - html-response many-entity modal-response money percentage - ref->enum-schema ref->radio-options regex temp-id - wrap-entity wrap-merge-prior-hx wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers + default-grid-fields-schema entity-id + field-validation-error form-validation-error + html-response many-entity modal-response money percentage + ref->enum-schema ref->radio-options regex temp-id + wrap-entity wrap-merge-prior-hx wrap-schema-enforce]] [auto-ap.time :as atime] [auto-ap.utils :refer [dollars=]] [bidi.bidi :as bidi] @@ -40,10 +40,10 @@ [malli.util :as mut])) (def query-schema (mc/schema - [:maybe - (into [:map {} - [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] ] - default-grid-fields-schema)])) + [:maybe + (into [:map {} + [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]] + default-grid-fields-schema)])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes @@ -154,7 +154,7 @@ (not (str/blank? (:client-group query-params))) (merge-query {:query {:in ['?client-group] - :where ['[?e :transaction-rule/client-group ?client-group] ]} + :where ['[?e :transaction-rule/client-group ?client-group]]} :args [(clojure.string/upper-case (:client-group query-params))]}) true @@ -288,10 +288,6 @@ [:transaction-rule/bank-account] :form-params form-params))) - - - - (def transaction-read '[{:transaction/client [:client/name] :transaction/bank-account [:bank-account/name]} :transaction/description-original @@ -369,8 +365,6 @@ '[(>= ?dom ?dom-gte)]]} :args [dom-gte]}) - - true (merge-query {:query {:where ['[?e :transaction/id]]}})) results (->> @@ -436,7 +430,7 @@ :content-fn (fn [value] (let [a (dc/pull (dc/db conn) d-accounts/default-read value)] (when value - (str + (str (:account/numeric-code a) " - " (:account/name (d-accounts/clientize a @@ -505,7 +499,6 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) - (defn all-ids-not-locked [all-ids] (->> all-ids (dc/q '[:find ?t @@ -621,18 +614,18 @@ {} (com/modal-header {} [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]) (com/modal-body {} [:form#my-form - {:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute - :db/id (:db/id entity)) - :hx-indicator "#code"} - [:div - {:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges) - :hx-trigger "change" - :hx-target "#transaction-test-results .gutter" - :hx-include "this"} - (transaction-rule-test-table* {:entity entity - :clients clients - :checkboxes? true - :only-uncoded? true})]]) + {:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute + :db/id (:db/id entity)) + :hx-indicator "#code"} + [:div + {:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges) + :hx-trigger "change" + :hx-target "#transaction-test-results .gutter" + :hx-include "this"} + (transaction-rule-test-table* {:entity entity + :clients clients + :checkboxes? true + :only-uncoded? true})]]) (com/modal-footer {} [:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")]))) :headers (-> {} (assoc "hx-trigger-after-settle" "modalnext") @@ -656,7 +649,7 @@ (edit-path [_ _] []) (step-schema [_] - (mm/form-schema linear-wizard)) + (mm/form-schema linear-wizard)) (render-step [this request] (mm/default-render-step @@ -825,11 +818,11 @@ (com/validated-field {:label "Approval status" :errors (fc/field-errors)} (com/radio-card {:options (ref->radio-options "transaction-approval-status") - :value (fc/field-value) - :name (fc/field-name) - :size :small - :orientation :horizontal})))]]]) - :footer + :value (fc/field-value) + :name (fc/field-name) + :size :small + :orientation :horizontal})))]]]) + :footer (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) :validation-route ::route/navigate))) @@ -893,25 +886,25 @@ nil))) (form-schema [_] form-schema) (submit [_ {:keys [multi-form-state request-method identity] :as request}] - - (let [transaction-rule (:snapshot multi-form-state) - _ (validate-transaction-rule transaction-rule) - entity (cond-> transaction-rule - (:transaction-rule/client-group transaction-rule) (update :transaction-rule/client-group str/upper-case) - (= :post request-method) (assoc :db/id "new") - true (assoc :transaction-rule/note (entity->note transaction-rule))) - {:keys [tempids]} (audit-transact [[:upsert-entity entity]] - (:identity request)) - updated-rule (dc/pull (dc/db conn) - default-read - (or (get tempids (:db/id entity)) (:db/id entity)))] - (html-response - (row* identity updated-rule {:flash? true}) - :headers (cond-> {"hx-trigger" "modalclose"} - (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" - "hx-reswap" "afterbegin") - (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule)) - "hx-reswap" "outerHTML")))))) + + (let [transaction-rule (:snapshot multi-form-state) + _ (validate-transaction-rule transaction-rule) + entity (cond-> transaction-rule + (:transaction-rule/client-group transaction-rule) (update :transaction-rule/client-group str/upper-case) + (= :post request-method) (assoc :db/id "new") + true (assoc :transaction-rule/note (entity->note transaction-rule))) + {:keys [tempids]} (audit-transact [[:upsert-entity entity]] + (:identity request)) + updated-rule (dc/pull (dc/db conn) + default-read + (or (get tempids (:db/id entity)) (:db/id entity)))] + (html-response + (row* identity updated-rule {:flash? true}) + :headers (cond-> {"hx-trigger" "modalclose"} + (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" + "hx-reswap" "afterbegin") + (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule)) + "hx-reswap" "outerHTML")))))) (def rule-wizard (->TransactionRuleWizard nil nil nil)) (def key->handler @@ -1003,10 +996,10 @@ {}))))}) (fn [h] (-> h -(wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) (wrap-admin) (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/admin/vendors.clj b/src/clj/auto_ap/ssr/admin/vendors.clj index 36a6404d..36c0b2ea 100644 --- a/src/clj/auto_ap/ssr/admin/vendors.clj +++ b/src/clj/auto_ap/ssr/admin/vendors.clj @@ -2,15 +2,15 @@ (:require [auto-ap.cursor :as cursor] [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-3 - audit-transact audit-transact-batch audit-transact-batch - conn merge-query pull-attr pull-many query2]] + :refer [add-sorter-fields apply-pagination apply-sort-3 + audit-transact audit-transact-batch audit-transact-batch + conn merge-query pull-attr pull-many query2]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.logging :as alog] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] [auto-ap.routes.admin.vendors :as route] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin 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 @@ -23,12 +23,12 @@ [auto-ap.ssr.hx :as hx] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers - default-grid-fields-schema entity-id - form-validation-error html-response many-entity - modal-response ref->enum-schema ref->select-options strip - temp-id wrap-entity wrap-form-4xx-2 wrap-merge-prior-hx - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers + default-grid-fields-schema entity-id + form-validation-error html-response many-entity + modal-response ref->enum-schema ref->select-options strip + temp-id wrap-entity wrap-form-4xx-2 wrap-merge-prior-hx + wrap-schema-enforce]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] @@ -41,7 +41,7 @@ (into [:map {} [:name {:optional true :default nil} [:maybe [:string {:string/decode strip}]]] #_[:role {:optional true} [:maybe (ref->enum-schema "user-role")]] - #_[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ] + #_[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]] default-grid-fields-schema)])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -60,16 +60,16 @@ :size :small})) (com/field {:label "Type"} (com/radio-card {:size :small - :name "type" - :value (:type (:query-params request)) - :options [{:value "" - :content "All"} - {:value "only-hidden" - :content "Only hidden"} - {:value "only-global" - :content "Only global"} - #_{:value "potential-duplicates" - :content "Potential duplicates"}]}))]]) + :name "type" + :value (:type (:query-params request)) + :options [{:value "" + :content "All"} + {:value "only-hidden" + :content "Only hidden"} + {:value "only-global" + :content "Only global"} + #_{:value "potential-duplicates" + :content "Potential duplicates"}]}))]]) (def default-read '[:db/id :vendor/name @@ -203,8 +203,6 @@ (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) - - (defn merge-submit [{:keys [form-params request-method identity] :as request}] (if (= (:source-vendor form-params) (:target-vendor form-params)) @@ -245,7 +243,6 @@ (= i (dec (count steps))) (assoc :last? true)) n))))) - ;; TODO add plaid merchant ;; TODO each client only used once @@ -285,7 +282,6 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))) - (defn automatically-paid-when-due-row [terms-override-cursor] (com/data-grid-row (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-cursor))))}) @@ -303,15 +299,12 @@ :value (fc/field-value) :value-fn :db/id - :content-fn #(pull-attr (dc/db conn) :client/name (:db/id %)) :size :small}))) - (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))) - (defn- account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col @@ -370,12 +363,6 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))))) - - - - - - (defn dialog* [{:keys [entity form-params form-errors] :as params}] (alog/peek ::dialog-entity form-params) (fc/start-form form-params form-errors @@ -868,7 +855,6 @@ (def vendor-wizard (->VendorWizard :info)) - (def key->handler (apply-middleware-to-all-handlers (->> @@ -921,11 +907,11 @@ (fn [cursor _] (account-override-row cursor)))}) (fn [h] (-> h -(wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) (wrap-admin) (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/auth.clj b/src/clj/auto_ap/ssr/auth.clj index aea7c985..44f9f653 100644 --- a/src/clj/auto_ap/ssr/auth.clj +++ b/src/clj/auto_ap/ssr/auth.clj @@ -14,7 +14,6 @@ :headers {"Location" "/login"} :session {}}) - (defn impersonate [request] {:status 200 :session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"]) @@ -39,23 +38,22 @@ next (assoc "state" (hu/url-encode next)))))))) (defn- page-contents [request] - [:div#app { "@notification.document" "notificationDetails=event.detail.value; showNotification=true" + [:div#app {"@notification.document" "notificationDetails=event.detail.value; showNotification=true" :x-data (hx/json {:showError false :errorDetails "" :showNotification false :notificationDetails ""}) - "@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;" - } - [:div#app-contents.flex.overflow-hidden - [:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " } + "@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"} + [:div#app-contents.flex.overflow-hidden + [:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "} [:div#notification-holder - [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification" } + [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification"} [:div.relative [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400 - { "@click" "showNotification=false"} + {"@click" "showNotification=false"} svg/filled-x]] - + [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96 {:x-show "showNotification" "x-transition:enter" "transition duration-300 transform ease-in-out" @@ -64,16 +62,16 @@ "x-transition:leave" "transition duration-300 transform ease-in-out" "x-transition:leave-start" "opacity-100 translate-y-0" "x-transition:leave-end" "opacity-0 translate-y-full"} - + [:div {:class "p-4 text-lg w-full" :role "alert"} [:div.text-sm [:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]] - [:div {:x-show "showError" + [:div {:x-show "showError" :x-init ""} [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg [:div.relative [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600 - { "@click" "showError=false"} + {"@click" "showError=false"} svg/filled-x]] [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96 @@ -81,7 +79,7 @@ "x-transition:enter" "transition duration-300" "x-transition:enter-start" "opacity-0" "x-transition:enter-end" "opacity-100"} - + [:div {:class "p-4 mb-4 text-lg w-full" :role "alert"} [:div.inline-block.w-8.h-8.mr-2 svg/alert] [:span.font-medium "Oh, drat! An unexpected error has occurred."] @@ -94,14 +92,13 @@ [:div.p-4 [:img {:src "/img/logo-big.png"}] -[:div - [:a.button.is-large.is-primary {:href (login-url (get (:query-params request) "redirect-to"))} "Login with Google"]] - "HELLO"]) - ]]] ]) + [:div + [:a.button.is-large.is-primary {:href (login-url (get (:query-params request) "redirect-to"))} "Login with Google"]] + "HELLO"])]]]]) (defn login [request] (base-page request (page-contents request) - + "Dashboard")) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/common_handlers.clj b/src/clj/auto_ap/ssr/common_handlers.clj index 2c1774e5..82b0b759 100644 --- a/src/clj/auto_ap/ssr/common_handlers.clj +++ b/src/clj/auto_ap/ssr/common_handlers.clj @@ -2,18 +2,17 @@ (:require [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.utils :refer [html-response wrap-schema-enforce]])) - (defn add-new-entity-handler ([path render-fn] (add-new-entity-handler path render-fn - (fn default-data [base _] + (fn default-data [base _] base))) ([path render-fn build-data] (-> (fn new-entity [{{:keys [index]} :query-params :as request}] (html-response (fc/start-form-with-prefix (conj path (or index 0)) (build-data {:db/id (str (java.util.UUID/randomUUID)) - :new? true} request) + :new? true} request) [] (render-fn fc/*current* request)))) (wrap-schema-enforce :query-schema [:map diff --git a/src/clj/auto_ap/ssr/company.clj b/src/clj/auto_ap/ssr/company.clj index 232cf7fa..c4b1f0b9 100644 --- a/src/clj/auto_ap/ssr/company.clj +++ b/src/clj/auto_ap/ssr/company.clj @@ -33,18 +33,17 @@ (com/content-card {:class " w-[748px]" :hx-target "this" :hx-swap "outerHTML"} - [:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible " - } + [:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible "} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} "Signature"] [:div#signature-notification.notification.block {:style {:display "none"}}] [:div {:x-data (hx/json {"signature" nil - "editing" false - "existing" (boolean signature-file)}) - :hx-put (bidi/path-for ssr-routes/only-routes - :company-update-signature) - :hx-trigger "accepted" - :hx-vals "js:{signatureData: event.detail.signatureData}"} + "editing" false + "existing" (boolean signature-file)}) + :hx-put (bidi/path-for ssr-routes/only-routes + :company-update-signature) + :hx-trigger "accepted" + :hx-vals "js:{signatureData: event.detail.signatureData}"} [:div.htmx-indicator [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} (svg/spinner {:class "w-4 h-4 text-primary-300"}) @@ -58,7 +57,6 @@ :x-show "existing && !editing"}]) [:canvas.rounded.rounded-lg.border.border-gray-300 - {:style {:width 696 :height 261} :x-init "signature= new SignaturePad($el); signature.off()" @@ -67,7 +65,6 @@ :height 261 :x-show "existing ? editing: true"}]] - [:div.flex.gap-2.justify-end (com/button {:color :primary :x-show "!editing" @@ -83,7 +80,7 @@ :x-show "editing"} "Accept")]] - [:div + [:div [:div.flex.justify-center " - or -"] [:form {:hx-post (bidi/path-for ssr-routes/only-routes :company-upload-signature) @@ -92,18 +89,17 @@ #_#_:hx-target "#signature-notification" :hx-swap "outerHTML" :id "upload" - :hx-trigger "z" - } -[:div.htmx-indicator - [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} - (svg/spinner {:class "w-4 h-4 text-primary-300"}) - [:div.ml-3 "Loading..."]]] -[:div.htmx-indicator-hidden - [: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}) - :x-dispatch:z "files" - ":class" "{'bg-blue-100': !hovering, + :hx-trigger "z"} + [:div.htmx-indicator + [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} + (svg/spinner {:class "w-4 h-4 text-primary-300"}) + [:div.ml-3 "Loading..."]]] + [:div.htmx-indicator-hidden + [: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}) + :x-dispatch:z "files" + ":class" "{'bg-blue-100': !hovering, 'border-blue-300': !hovering, 'text-blue-700': !hovering, 'bg-green-100': hovering, @@ -111,23 +107,20 @@ 'text-green-700': hovering }"} + [:input {:type "file" + :name "file" + :class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0", + :x-on:change "files = $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"})]]]] - - [:input {:type "file" - :name "file" - :class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0", - :x-on:change "files = $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 a signature file (696x261 pixels jpeg) here."]]]] ]]]))) + [:div.htmx-indicator-hidden "Drop a signature file (696x261 pixels jpeg) here."]]]]]]]))) (defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}] (let [prefix "data:image/png;base64,"] @@ -149,66 +142,66 @@ (defn upload-signature-file [{{:strs [signatureData]} :form-params client :client user :identity :as request}] (assert-can-see-client user client) - (let [{:strs [file]} (:multipart-params request) ] -(try - (let [signature-id (str (UUID/randomUUID)) ] - (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) - :key (str signature-id ".jpg") - :input-stream (io/input-stream (:tempfile file)) - :metadata {:content-type "image/jpeg" - :content-length (:length (:tempfile file))} - :canned-acl "public-read") - @(dc/transact conn [{:db/id (:db/id client) - :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}]) - (html-response - (signature request))) - (catch Exception e - (println e) - #_(-> result - (assoc :error? true) - (update :results conj {:filename filename - :response (.getMessage e) - :sample (:sample (ex-data e)) - :template (:template (ex-data e))})))) + (let [{:strs [file]} (:multipart-params request)] + (try + (let [signature-id (str (UUID/randomUUID))] + (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) + :key (str signature-id ".jpg") + :input-stream (io/input-stream (:tempfile file)) + :metadata {:content-type "image/jpeg" + :content-length (:length (:tempfile file))} + :canned-acl "public-read") + @(dc/transact conn [{:db/id (:db/id client) + :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}]) + (html-response + (signature request))) + (catch Exception e + (println e) + #_(-> result + (assoc :error? true) + (update :results conj {:filename filename + :response (.getMessage e) + :sample (:sample (ex-data e)) + :template (:template (ex-data e))})))) #_(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) + {: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 "bg-red-50 text-red-700 border-red-300"} + {: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"}))) + [: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 main-content* [{:keys [client identity] :as request}] (if-not client @@ -276,7 +269,6 @@ (def search (wrap-json-response search)) - (defn bank-account-search [{:keys [route-params query-params clients]}] (let [valid-client-ids (set (map :db/id clients)) selected-client-id (Long/parseLong (get route-params :db/id)) diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index 7a6b65d9..257c5534 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -24,32 +24,32 @@ (def query-schema (mc/schema [:maybe - (into [:map {} ] + (into [:map {}] default-grid-fields-schema)])) (def vendor-read '[:db/id - :vendor/name - {:vendor/legal-entity-1099-type [:db/ident]} - {:vendor/legal-entity-tin-type [:db/ident]} - {:vendor/address [:address/street1 - :address/city - :address/state - :address/zip]} + :vendor/name + {:vendor/legal-entity-1099-type [:db/ident]} + {:vendor/legal-entity-tin-type [:db/ident]} + {:vendor/address [:address/street1 + :address/city + :address/state + :address/zip]} {:vendor/default-account [:account/name]} - :vendor/legal-entity-tin - :vendor/legal-entity-name - :vendor/legal-entity-first-name - :vendor/legal-entity-middle-name - :vendor/legal-entity-last-name]) + :vendor/legal-entity-tin + :vendor/legal-entity-name + :vendor/legal-entity-first-name + :vendor/legal-entity-middle-name + :vendor/legal-entity-last-name]) (defn sum-for-client-vendor [client-id vendor-id] (ffirst (dc/q '[:find (sum ?a) - :with ?d + :with ?d :in $ ?c ?v :where [?p :payment/client ?c] - [?p :payment/date ?d ] + [?p :payment/date ?d] [(>= ?d #inst "2025-01-01T08:00")] [(< ?d #inst "2026-01-01T08:00")] [?p :payment/type :payment-type/check] @@ -64,11 +64,11 @@ (pull ?c [:client/code :db/id]) (pull ?v vendor-read) (sum ?a) - :with ?d + :with ?d :in $ [?c ...] vendor-read :where [?p :payment/client ?c] - [?p :payment/date ?d ] + [?p :payment/date ?d] [(>= ?d #inst "2025-01-01T08:00")] [(< ?d #inst "2026-01-01T08:00")] [?p :payment/type :payment-type/check] @@ -78,105 +78,101 @@ trimmed-clients vendor-read) all (->> results - (filter (fn [[_ _ a]] - (>= (or a 0.0) 600.0))) - (sort-by (fn [[client _ amount]] - [(:client/code client ) amount])) - (into [])) + (filter (fn [[_ _ a]] + (>= (or a 0.0) 600.0))) + (sort-by (fn [[client _ amount]] + [(:client/code client) amount])) + (into [])) paginated (apply-pagination-raw {:start (:start query-params) :per-page (:per-page query-params)} all)] [(:entries paginated) (:count paginated)])) (def grid-page - (helper/build - {:id "entity-table" - :nav com/company-aside-nav - :id-fn (comp :db/id second) - :fetch-page fetch-page - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] + (helper/build + {:id "entity-table" + :nav com/company-aside-nav + :id-fn (comp :db/id second) + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-1099)} - "1099 Vendor Info"]] - :title "1099 Vendors" - :entity-name "Vendors" - :query-schema query-schema - :route :company-1099-vendor-table - :row-buttons (fn [request e] - [(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-dialog - :vendor-id (:db/id (second e))) - {:client-id (:db/id (first e))})} - svg/pencil)]) - :headers [{:key "Client" - :name "Client" - :sort-key "client" - :render (comp :client/code first)} - {:key "vendor-name" - :name "Vendor Name" - :sort-key "vendor" - :render (fn [[_ vendor]] - [:div.flex.whitespace-nowrap.items-center.gap-4 - [:div [:div (:vendor/name vendor)] - [:div.text-sm.text-gray-400 - (or (-> vendor :vendor/legal-entity-name not-empty) - (str (-> vendor :vendor/legal-entity-first-name) " " - (-> vendor :vendor/legal-entity-middle-name) " " - (-> vendor :vendor/legal-entity-last-name)))]] - (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] - (com/pill - {:class "text-xs font-medium" - :color :primary} - (str/capitalize t99-type)) - )])} - {:key "tin" - :name "TIN" - :sort-key "tin" - :show-starting "md" - :render (fn [[_ vendor]] - [:div.flex.gap-4 - (when-let [tin (-> vendor :vendor/legal-entity-tin)] - [:span {:class "text-xs font-medium py-0.5 "} - tin]) - (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] - (com/pill {:class "text-xs font-medium" - :color :yellow} - (name tin-type)))] - )} - {:key "expense-account" - :name "Expense Account" - :show-starting "md" - :render (fn [[_ vendor]] - [:div.flex.gap-4 - (when-let [tin (-> vendor :vendor/default-account :account/name)] - [:span {:class "text-xs font-medium py-0.5 "} - tin]) ])} - {:key "address" - :name "Address" - :sort-key "address" - :show-starting "lg" - :render (fn [[_ vendor]] - (if (-> vendor :vendor/address :address/street1) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-1099)} + "1099 Vendor Info"]] + :title "1099 Vendors" + :entity-name "Vendors" + :query-schema query-schema + :route :company-1099-vendor-table + :row-buttons (fn [request e] + [(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-dialog + :vendor-id (:db/id (second e))) + {:client-id (:db/id (first e))})} + svg/pencil)]) + :headers [{:key "Client" + :name "Client" + :sort-key "client" + :render (comp :client/code first)} + {:key "vendor-name" + :name "Vendor Name" + :sort-key "vendor" + :render (fn [[_ vendor]] + [:div.flex.whitespace-nowrap.items-center.gap-4 + [:div [:div (:vendor/name vendor)] + [:div.text-sm.text-gray-400 + (or (-> vendor :vendor/legal-entity-name not-empty) + (str (-> vendor :vendor/legal-entity-first-name) " " + (-> vendor :vendor/legal-entity-middle-name) " " + (-> vendor :vendor/legal-entity-last-name)))]] + (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] + (com/pill + {:class "text-xs font-medium" + :color :primary} + (str/capitalize t99-type)))])} + {:key "tin" + :name "TIN" + :sort-key "tin" + :show-starting "md" + :render (fn [[_ vendor]] + [:div.flex.gap-4 + (when-let [tin (-> vendor :vendor/legal-entity-tin)] + [:span {:class "text-xs font-medium py-0.5 "} + tin]) + (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] + (com/pill {:class "text-xs font-medium" + :color :yellow} + (name tin-type)))])} + {:key "expense-account" + :name "Expense Account" + :show-starting "md" + :render (fn [[_ vendor]] + [:div.flex.gap-4 + (when-let [tin (-> vendor :vendor/default-account :account/name)] + [:span {:class "text-xs font-medium py-0.5 "} + tin])])} + {:key "address" + :name "Address" + :sort-key "address" + :show-starting "lg" + :render (fn [[_ vendor]] + (if (-> vendor :vendor/address :address/street1) + [:div + [:div (-> vendor :vendor/address :address/street1)] " " [:div - [:div (-> vendor :vendor/address :address/street1)] " " - [:div - (-> vendor :vendor/address :address/street2)] " " - [:div - (-> vendor :vendor/address :address/city) " " - (-> vendor :vendor/address :address/state) "," - (-> vendor :vendor/address :address/zip)]] - [:p.text-sm.italic.text-gray-400 "No address"]))} - {:key "paid" - :name "Paid" - :sort-key "paid" - :render (fn [[_ _ paid]] - (com/pill {:class "text-xs font-medium" - :color :primary} - "Paid $" (Math/round paid)))}]})) - - + (-> vendor :vendor/address :address/street2)] " " + [:div + (-> vendor :vendor/address :address/city) " " + (-> vendor :vendor/address :address/state) "," + (-> vendor :vendor/address :address/zip)]] + [:p.text-sm.italic.text-gray-400 "No address"]))} + {:key "paid" + :name "Paid" + :sort-key "paid" + :render (fn [[_ _ paid]] + (com/pill {:class "text-xs font-medium" + :color :primary} + "Paid $" (Math/round paid)))}]})) (def table* (partial helper/table* grid-page)) (def row* (partial helper/row* grid-page)) @@ -185,7 +181,6 @@ {:keys [vendor-id]} :route-params {:keys [client-id]} :query-params}] - (assert-can-see-client identity client-id) @(dc/transact conn [[:upsert-entity (-> form-params @@ -198,30 +193,28 @@ (:address/zip a) (:db/id a)) a - nil)) ))]]) - (html-response + nil))))]]) + (html-response - (row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id) - (dc/pull (dc/db conn) vendor-read vendor-id) - (sum-for-client-vendor client-id vendor-id) - ] {:flash? true}) - :headers {"hx-trigger" "modalclose" - "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)})) + (row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id) + (dc/pull (dc/db conn) vendor-read vendor-id) + (sum-for-client-vendor client-id vendor-id)] {:flash? true}) + :headers {"hx-trigger" "modalclose" + "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)})) (def default-vendor-read '[* {[:vendor/legal-entity-1099-type :xform iol-ion.query/ident] [:db/ident] [:vendor/legal-entity-tin-type :xform iol-ion.query/ident] [:db/ident]}]) - (def form-schema (mc/schema [:map [:vendor/address {:default {}} [:maybe - [:map + [:map [:db/id {:optional true} [:maybe entity-id]] [:address/street1 {:optional true} [:maybe [:string {:decode/string strip}]]] [:address/street2 {:optional true} [:maybe [:string {:decode/string strip}]]] [:address/city {:optional true} [:maybe [:string {:decode/string strip}]]] [:address/state {:optional true} [:maybe [:string {:decode/string strip}]]] - [:address/zip {:optional true} [:maybe [:re { :error/message "invalid zip" + [:address/zip {:optional true} [:maybe [:re {:error/message "invalid zip" :decode/string strip} #"^(\d{5}|)$"]]]]]] [:vendor/legal-entity-name {:optional true} [:maybe [:string {:decode/string strip}]]] [:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:decode/string strip}]]] @@ -237,131 +230,131 @@ (when entity (mc/decode form-schema entity main-transformer)) {}) - form-errors - (modal-response - (com/modal - {} - [:form {:hx-post (url (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-save - :request-method :post - :vendor-id vendor-id) - {:client-id client-id}) - :class "w-full h-full max-w-2xl" - :hx-swap "outerHTML swap:300ms"} + form-errors + (modal-response + (com/modal + {} + [:form {:hx-post (url (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-save + :request-method :post + :vendor-id vendor-id) + {:client-id client-id}) + :class "w-full h-full max-w-2xl" + :hx-swap "outerHTML swap:300ms"} - [:fieldset {:class "hx-disable w-full h-full"} - (com/modal-card - {} - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]] - [:div.grid.grid-cols-6.gap-x-4.gap-y-2 - - (fc/with-field-default :vendor/address {} - (println "ADDRESS" fc/*current*) - (list [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value (fc/field-value)})) + [:fieldset {:class "hx-disable w-full h-full"} + (com/modal-card + {} + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]] + [:div.grid.grid-cols-6.gap-x-4.gap-y-2 - (fc/with-field :address/street1 - (com/validated-field {:label "Street 1" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "1700 Pennsylvania Ave" - :autofocus true})))] - [:div.col-span-6 - (fc/with-field :address/street2 - (com/validated-field {:label "Street 2" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "Suite 200"})))] - [:div.col-span-3 - (fc/with-field :address/city - (com/validated-field {:label "City" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "Cupertino"})))] - [:div.col-span-1 - (fc/with-field :address/state - (com/validated-field {:label "State" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "CA"})))] - [:div.col-span-2 - (fc/with-field :address/zip - (com/validated-field {:label "Zip" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "98102"})))])) - - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (fc/with-field :vendor/legal-entity-name - (com/validated-field {:label "Legal Entity Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :class "w-full" - :value (fc/field-value) - :placeholder "Good Restaurant LLC"})))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-first-name - (com/validated-field {:label "First Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :placeholder "John"})))] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-middle-name - (com/validated-field {:label "Middle Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :placeholder "C."})))] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-last-name - (com/validated-field {:label "Last Name" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :placeholder "Riley"})))] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-tin - (com/validated-field {:label "TIN" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :placeholder "John"})))] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-tin-type - (com/validated-field {:label "TIN Type" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :allow-blank? true - :value (some-> (fc/field-value) name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]})))] - [:div.col-span-2 - (fc/with-field :vendor/legal-entity-1099-type - (com/validated-field {:label "1099 Type" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :allow-blank? true - :value (some-> (fc/field-value) name) - :options (ref->select-options "legal-entity-1099-type")})))]] - [:div - (com/form-errors {:errors (:errors fc/*form-errors*)}) - (com/validated-save-button {:errors form-errors} "Save vendor")])]])))) + (fc/with-field-default :vendor/address {} + (println "ADDRESS" fc/*current*) + (list [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + + (fc/with-field :address/street1 + (com/validated-field {:label "Street 1" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "1700 Pennsylvania Ave" + :autofocus true})))] + [:div.col-span-6 + (fc/with-field :address/street2 + (com/validated-field {:label "Street 2" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Suite 200"})))] + [:div.col-span-3 + (fc/with-field :address/city + (com/validated-field {:label "City" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Cupertino"})))] + [:div.col-span-1 + (fc/with-field :address/state + (com/validated-field {:label "State" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "CA"})))] + [:div.col-span-2 + (fc/with-field :address/zip + (com/validated-field {:label "Zip" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "98102"})))])) + + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (fc/with-field :vendor/legal-entity-name + (com/validated-field {:label "Legal Entity Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Good Restaurant LLC"})))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-first-name + (com/validated-field {:label "First Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "John"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-middle-name + (com/validated-field {:label "Middle Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "C."})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-last-name + (com/validated-field {:label "Last Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "Riley"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-tin + (com/validated-field {:label "TIN" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "John"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-tin-type + (com/validated-field {:label "TIN Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :allow-blank? true + :value (some-> (fc/field-value) name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-1099-type + (com/validated-field {:label "1099 Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :allow-blank? true + :value (some-> (fc/field-value) name) + :options (ref->select-options "legal-entity-1099-type")})))]] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors form-errors} "Save vendor")])]])))) (def vendor-table (helper/table-route grid-page)) (def page (helper/page-route grid-page)) diff --git a/src/clj/auto_ap/ssr/company/plaid.clj b/src/clj/auto_ap/ssr/company/plaid.clj index 984d172f..c6557a58 100644 --- a/src/clj/auto_ap/ssr/company/plaid.clj +++ b/src/clj/auto_ap/ssr/company/plaid.clj @@ -25,9 +25,9 @@ [hiccup2.core :as hiccup] [malli.core :as mc])) (def query-schema (mc/schema - [:maybe - (into [:map {} ] - default-grid-fields-schema)])) + [:maybe + (into [:map {}] + default-grid-fields-schema)])) (def default-read '[:db/id :plaid-item/external-id @@ -37,7 +37,7 @@ {:plaid-item/accounts [:db/id {:bank-account/_plaid-account [{:bank-account/integration-status - [{ [ :integration-status/state :xform iol-ion.query/ident] [:db/ident]} + [{[:integration-status/state :xform iol-ion.query/ident] [:db/ident]} :integration-status/message :integration-status/last-attempt :integration-status/last-updated]}]} @@ -66,7 +66,6 @@ true (apply-sort-3 query-params) true (apply-pagination query-params)))) - (defn hydrate-results [ids db _] (let [results (pull-many-by-id db default-read ids)] (->> ids @@ -78,15 +77,12 @@ [(hydrate-results ids-to-retrieve db request) matching-count])) - - (defn plaid-link-script [token] (format "window.plaid = Plaid.create( { token: \"%s\", onSuccess: function (x) { htmx.trigger(\"#link-account\", \"linked\", {\"public_token\": x})} })", token)) - (defn link [{{client-code "client_code" public-token "public_token"} :form-params :keys [identity] :as request}] @@ -99,24 +95,24 @@ (alog/info ::linking-plaid :id identity :client-code client-code) (assert-can-see-client identity (pull-attr (dc/db conn) :db/id [:client/code client-code])) (let [access-token (:access_token (p/exchange-public-token public-token client-code)) - account-result (p/get-accounts access-token ) + account-result (p/get-accounts access-token) item {:plaid-item/client [:client/code client-code] - :plaid-item/external-id (-> account-result :item :item_id ) + :plaid-item/external-id (-> account-result :item :item_id) :plaid-item/access-token access-token :plaid-item/status (or (some-> account-result :item :error) - "SUCCESS") + "SUCCESS") :plaid-item/last-updated (coerce/to-date (time/now)) :db/id "plaid-item"}] @(dc/transact conn (->> (:accounts account-result) - (map (fn [a] - (let [balance (some-> a :balances :current (* 0.01))] - (cond-> {:plaid-account/external-id (:account_id a) - :plaid-account/number (:mask a) - :plaid-account/name (str (:name a) " " (:mask a)) - :plaid-item/_accounts "plaid-item"} - balance (assoc :plaid-account/balance balance))))) - (into [item]))) + (map (fn [a] + (let [balance (some-> a :balances :current (* 0.01))] + (cond-> {:plaid-account/external-id (:account_id a) + :plaid-account/number (:mask a) + :plaid-account/name (str (:name a) " " (:mask a)) + :plaid-item/_accounts "plaid-item"} + balance (assoc :plaid-account/balance balance))))) + (into [item]))) (alog/info ::access-token-was :token access-token) {:headers {"Hx-redirect" (bidi/path-for ssr-routes/only-routes :company-plaid)}})) @@ -141,115 +137,110 @@ (com/button-icon {} svg/refresh) "Start relink")]))) +(def grid-page + (helper/build + {:id "plaid-table" + :nav com/company-aside-nav + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] -(def grid-page - (helper/build - {:id "plaid-table" - :nav com/company-aside-nav - :fetch-page fetch-page - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-plaid)} + "Plaid"]] + :title "Plaid Accounts" + :entity-name "Plaid accounts" + :query-schema query-schema + :route :company-plaid-table + :action-buttons (fn [request] + (when-let [client-code (:client/code (:client request))] + [[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes + :company-plaid-link + :request-method :post)) + :hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code)) + :hx-trigger "linked"} + [:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))] + (com/button {:color :primary + :id "link-account" + :onClick "window.plaid.open()"} + (com/button-icon {} svg/refresh) + (format "Link %s account" client-code))]])) + :row-buttons (fn [request e] + [[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes + :company-plaid-relink) + "?plaid-item-id=" (:db/id e)) + :color :primary + :hx-target "closest div"} + "Reauthenticate")]]) + :headers [{:key "plaid-item" + :name "Plaid Item" + :sort-key "external-id" + :render :plaid-item/external-id} + {:key "integreat-plaid-status" + :name "Integreat ↔ Plaid status" + :render (fn [e] - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-plaid)} - "Plaid"]] - :title "Plaid Accounts" - :entity-name "Plaid accounts" - :query-schema query-schema - :route :company-plaid-table - :action-buttons (fn [request] - (when-let [client-code (:client/code (:client request))] - [[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes - :company-plaid-link - :request-method :post)) - :hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code)) - :hx-trigger "linked"} - [:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))] - (com/button {:color :primary - :id "link-account" - :onClick "window.plaid.open()"} - (com/button-icon {} svg/refresh) - (format "Link %s account" client-code))]])) - :row-buttons (fn [request e] - [[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes - :company-plaid-relink) - "?plaid-item-id=" (:db/id e)) - :color :primary - :hx-target "closest div"} - "Reauthenticate")]]) - :headers [{:key "plaid-item" - :name "Plaid Item" - :sort-key "external-id" - :render :plaid-item/external-id} - {:key "integreat-plaid-status" - :name "Integreat ↔ Plaid status" - :render (fn [e] - - (let [bad-integration (->> (:plaid-item/accounts e) - (map (comp - first - :bank-account/_plaid-account)) - (filter (comp #{:integration-state/failed :integration-state/unauthorized} - :integration-status/state - :bank-account/integration-status)) - first - :bank-account/integration-status)] - [:div - - [:div.cursor-pointer (com/pill (cond-> {:color :primary} - bad-integration (assoc :color :red - :x-tooltip "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}")) + (let [bad-integration (->> (:plaid-item/accounts e) + (map (comp + first + :bank-account/_plaid-account)) + (filter (comp #{:integration-state/failed :integration-state/unauthorized} + :integration-status/state + :bank-account/integration-status)) + first + :bank-account/integration-status)] + [:div - [:div.inline-flex.gap-2 - (or - (some-> bad-integration - :integration-status/state - name - str/capitalize) - "Success") - (when bad-integration - " (detail)") - - - (when bad-integration - [:template {:x-ref "tooltip"} - [:div.text-red-700 - (:integration-status/message bad-integration)]])])] - [:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink - [:div "Attempted: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-attempt e)) atime/normal-date)] - [:div "Last Updated: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-updated e)) atime/normal-date)]]]))} - {:key "plaid-bank-status" - :name "Plaid ↔ Bank Status" - :sort-key "plaid-bank-status" - :render (fn [e] - (when-let [status (:plaid-item/status e)] - [:div [:div (com/pill {:color :primary} - status)] - [:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated e)) atime/normal-date)]]))} - - {:key "accounts" - :name "Accounts" - :show-starting "md" - :render (fn [e] - [:ul - (for [a (:plaid-item/accounts e)] - [:li [:svg.inline {:data-jdenticon-value (:db/id a) :width "24" :height "24"}] (:plaid-account/name a) " - " (:plaid-account/number a) " - updated " - (atime/unparse-local (:plaid-account/last-synced a) atime/normal-date)])])}]})) + [:div.cursor-pointer (com/pill (cond-> {:color :primary} + bad-integration (assoc :color :red + :x-tooltip "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}")) + [:div.inline-flex.gap-2 + (or + (some-> bad-integration + :integration-status/state + name + str/capitalize) + "Success") + (when bad-integration + " (detail)") + + (when bad-integration + [:template {:x-ref "tooltip"} + [:div.text-red-700 + (:integration-status/message bad-integration)]])])] + [:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink + [:div "Attempted: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-attempt e)) atime/normal-date)] + [:div "Last Updated: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-updated e)) atime/normal-date)]]]))} + {:key "plaid-bank-status" + :name "Plaid ↔ Bank Status" + :sort-key "plaid-bank-status" + :render (fn [e] + (when-let [status (:plaid-item/status e)] + [:div [:div (com/pill {:color :primary} + status)] + [:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated e)) atime/normal-date)]]))} + + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:plaid-item/accounts e)] + [:li [:svg.inline {:data-jdenticon-value (:db/id a) :width "24" :height "24"}] (:plaid-account/name a) " - " (:plaid-account/number a) " - updated " + (atime/unparse-local (:plaid-account/last-synced a) atime/normal-date)])])}]})) (def page (helper/page-route grid-page)) (def table (helper/table-route grid-page)) -(def key->handler - (apply-middleware-to-all-handlers - { - :company-plaid page +(def key->handler + (apply-middleware-to-all-handlers + {:company-plaid page :company-plaid-table table :company-plaid-link link - :company-plaid-relink relink - - } + :company-plaid-relink relink} + (fn [h] (-> h (wrap-copy-qp-pqp) diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj index 416a1d15..4652b295 100644 --- a/src/clj/auto_ap/ssr/company/reports.clj +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -27,13 +27,12 @@ (def query-schema (mc/schema [:maybe (into [:map {:date-range [:date-range :start-date :end-date]} - + [:start-date {:optional true} [:maybe clj-date-schema]] [:end-date {:optional true} [:maybe clj-date-schema]] - [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] - ] + [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]] default-grid-fields-schema)])) (def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator]) @@ -43,22 +42,20 @@ query (cond-> {:query {:find [] :in '[$ [?c ...]] :where '[[?e :report/client ?c]]} - :args [db (:trimmed-clients request)]} + :args [db (:trimmed-clients request)]} - (:sort query-params) (add-sorter-fields {"client" ['[?e :report/client ?c] - '[?c :client/name ?sort-client]] - "created" ['[?e :report/created ?sort-created]] - "creator" ['[?e :report/creator ?sort-creator]] - "name" ['[?e :report/name ?sort-name] - ]} - query-params) + '[?c :client/name ?sort-client]] + "created" ['[?e :report/created ?sort-created]] + "creator" ['[?e :report/creator ?sort-creator]] + "name" ['[?e :report/name ?sort-name]]} + query-params) true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :report/created ?sort-default]]}}))] (->> (query2 query) - (apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true})) - (apply-pagination query-params)))) + (apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true})) + (apply-pagination query-params)))) (defn hydrate-results [ids db request] (let [results (->> (pull-many db default-read ids) @@ -67,7 +64,7 @@ (->> ids (map results) (filter identity) - + (map first) (filter (fn [r] (let [used-clients (set (map :db/id (:report/client r)))] @@ -78,7 +75,7 @@ (defn fetch-page [args] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (fetch-ids db args)] - + [(->> (hydrate-results ids-to-retrieve db args)) matching-count])) @@ -115,7 +112,7 @@ :sort-key "creator" :render (fn [report] (when (:report/creator report) - (com/pill {:color :primary } + (com/pill {:color :primary} (:report/creator report))))} {:key "created" :name "Created" @@ -129,7 +126,7 @@ (def page (helper/page-route grid-page)) (defn delete-report [{:keys [form-params identity]}] - + (let [[id-to-delete key] (first (dc/q '[:find ?i ?k :in $ ?i :where [?i :report/key ?k]] @@ -137,29 +134,28 @@ (some-> (get form-params "id") not-empty Long/parseLong))) report (dc/pull (dc/db conn) default-read id-to-delete)] (assert-can-see-client identity (:report/client report)) - (when id-to-delete + (when id-to-delete (s3/delete-object :bucket-name (:data-bucket env) :key key) @(dc/transact conn [[:db/retractEntity id-to-delete]])) - (html-response - (row* identity - report - {:flash? true - :delete-after-settle? true})))) - + (html-response + (row* identity + report + {:flash? true + :delete-after-settle? true})))) (def key->handler (apply-middleware-to-all-handlers (->> - (into - {:company-reports page - :company-reports-table table - :company-reports-delete delete-report} - company-expense-report/key->handler) + (into + {:company-reports page + :company-reports-table table + :company-reports-delete delete-report} + company-expense-report/key->handler) (into company-reconciliation-report/key->handler)) (fn [h] (-> h -(wrap-copy-qp-pqp) + (wrap-copy-qp-pqp) (wrap-apply-sort grid-page) (wrap-merge-prior-hx) (wrap-schema-enforce :query-schema query-schema) diff --git a/src/clj/auto_ap/ssr/company/reports/expense.clj b/src/clj/auto_ap/ssr/company/reports/expense.clj index 0dad1a22..f98c410a 100644 --- a/src/clj/auto_ap/ssr/company/reports/expense.clj +++ b/src/clj/auto_ap/ssr/company/reports/expense.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.company.reports.expense +(ns auto-ap.ssr.company.reports.expense (:require [auto-ap.datomic :refer [conn merge-query]] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.logging :as alog] @@ -20,73 +20,71 @@ [hiccup2.core :as hiccup])) (defn lookup-breakdown-data [request] - (let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)] - :with [?e] - :in [$ [?clients ?start ?end]] - :where - [[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] - (not [?e :invoice/status :invoice-status/voided]) - [?e :invoice/date ?d] - [?e :invoice/client ?c] - [?e :invoice/expense-accounts ?iea] - [?iea :invoice-expense-account/amount ?amt] - [?c :client/name ?cn] - [(clj-time.coerce/to-date-time ?d) ?user-date]]} - :args - [(dc/db conn) - [(extract-client-ids (:clients request) - (:client-id request) - (when (:client-code request) - [:client/code (:client-code request)])) - (some-> (time/plus (time/now) (time/days -65)) coerce/to-date) - (some-> (time/now) coerce/to-date)]]} + (let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)] + :with [?e] + :in [$ [?clients ?start ?end]] + :where + [[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] + (not [?e :invoice/status :invoice-status/voided]) + [?e :invoice/date ?d] + [?e :invoice/client ?c] + [?e :invoice/expense-accounts ?iea] + [?iea :invoice-expense-account/amount ?amt] + [?c :client/name ?cn] + [(clj-time.coerce/to-date-time ?d) ?user-date]]} + :args + [(dc/db conn) + [(extract-client-ids (:clients request) + (:client-id request) + (when (:client-code request) + [:client/code (:client-code request)])) + (some-> (time/plus (time/now) (time/days -65)) coerce/to-date) + (some-> (time/now) coerce/to-date)]]} - (:vendor-id (:query-params request)) - (merge-query {:query '{:in [?v] - :where [ [?e :invoice/vendor ?v]]} - :args [ (:db/id (:vendor-id (:query-params request)))]}) - - (:account-id (:query-params request)) - (merge-query {:query '{:in [?a] - :where [ [?iea :invoice-expense-account/account ?a]]} - :args [ (:db/id (:account-id (:query-params request)))]}))] - - (dc/query query))) + (:vendor-id (:query-params request)) + (merge-query {:query '{:in [?v] + :where [[?e :invoice/vendor ?v]]} + :args [(:db/id (:vendor-id (:query-params request)))]}) + + (:account-id (:query-params request)) + (merge-query {:query '{:in [?a] + :where [[?iea :invoice-expense-account/account ?a]]} + :args [(:db/id (:account-id (:query-params request)))]}))] + + (dc/query query))) (defn lookup-invoice-total-data [request] - (let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30))) - end (:end-date (:query-params request) (time/now)) - query (cond-> {:query '{:find [?cn ?vn (sum ?t)] - :with [ ?e] - :in [$ [?clients ?start ?end]] - :where - [[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] - (not [?e :invoice/status :invoice-status/voided]) - [?e :invoice/client ?c] - [?e :invoice/total ?t] - [?e :invoice/vendor ?v] - [?v :vendor/name ?vn] - [?c :client/name ?cn] - ]} - :args - [(dc/db conn) - [(extract-client-ids (:clients request) - (:client-id request) - (when (:client-code request) - [:client/code (:client-code request)])) - (some-> start coerce/to-date) - (some-> end coerce/to-date)]]})] - - (dc/query query))) + (let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30))) + end (:end-date (:query-params request) (time/now)) + query (cond-> {:query '{:find [?cn ?vn (sum ?t)] + :with [?e] + :in [$ [?clients ?start ?end]] + :where + [[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] + (not [?e :invoice/status :invoice-status/voided]) + [?e :invoice/client ?c] + [?e :invoice/total ?t] + [?e :invoice/vendor ?v] + [?v :vendor/name ?vn] + [?c :client/name ?cn]]} + :args + [(dc/db conn) + [(extract-client-ids (:clients request) + (:client-id request) + (when (:client-code request) + [:client/code (:client-code request)])) + (some-> start coerce/to-date) + (some-> end coerce/to-date)]]})] -(defn week-seq + (dc/query query))) + +(defn week-seq ([c] (week-seq c (atime/last-monday))) ([c starting] (reverse (for [n (range c) :let [start (time/minus starting (time/weeks n)) end (time/minus starting (time/weeks (dec n)))]] [(atime/as-local-time (coerce/to-date-time start)) (atime/as-local-time (coerce/to-date-time end))])))) - (defn- best-week [d weeks] (reduce (fn [acc [start end]] @@ -97,11 +95,10 @@ nil weeks)) - (defn expense-breakdown-card* [request] (com/card {:class "w-full h-full" :id "expense-breakdown-report"} [:div {:class "flex flex-col px-8 py-8 space-y-3 w-full h-full"} - + [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card) :hx-trigger "change" :hx-target "#expense-breakdown-report" @@ -157,14 +154,14 @@ (for [d weeks] (get-in lookup [ea d] 0)))] [:canvas {:x-data (hx/json {:chart nil - :labels x-axis - :datasets (map (fn [s a] {:label a - :data s - :borderWidth 1}) - series - distinct-accounts)}) - :x-init - "new Chart($el, { + :labels x-axis + :datasets (map (fn [s a] {:label a + :data s + :borderWidth 1}) + series + distinct-accounts)}) + :x-init + "new Chart($el, { type: 'bar', data: { labels: labels, @@ -186,7 +183,7 @@ [:div {:class "flex flex-col px-8 py-8 space-y-3"} [:div [:h1.text-2xl.mb-3.font-bold "Invoice totals by vendor"] - [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card ) + [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card) :hx-trigger "change" :hx-target "#invoice-totals-report" :hx-swap "outerHTML"} @@ -201,7 +198,7 @@ (com/date-input {:name (fc/field-name) :class "w-64" :value (some-> (fc/field-value) - (atime/unparse-local atime/normal-date)) })])) + (atime/unparse-local atime/normal-date))})])) (fc/with-field :end-date (com/validated-field {:label "End" :errors (fc/field-errors)} @@ -209,13 +206,12 @@ (com/date-input {:name (fc/field-name) :class "w-64" :value (some-> (fc/field-value) - (atime/unparse-local atime/normal-date)) })]))])] + (atime/unparse-local atime/normal-date))})]))])] [:div {:class "overflow-scroll min-w-full max-h-[700px]"} (let [data (lookup-invoice-total-data request) companies (sort (set (map first data))) vendors (sort (set (map second data))) - result (by (juxt first second) last data) - ] + result (by (juxt first second) last data)] (com/data-grid {:headers (into [(com/data-grid-header {:class "sticky left-0 z-60 bg-gray-100"} "Vendor")] @@ -231,7 +227,7 @@ (com/data-grid-cell {} (or (some->> (get result [company vendor]) - (format "$%,.2f" )) + (format "$%,.2f")) [:span.text-gray-200 "-"])))))))]]])) (defn page [request] diff --git a/src/clj/auto_ap/ssr/company/reports/reconciliation.clj b/src/clj/auto_ap/ssr/company/reports/reconciliation.clj index b1e5d059..fce29a9a 100644 --- a/src/clj/auto_ap/ssr/company/reports/reconciliation.clj +++ b/src/clj/auto_ap/ssr/company/reports/reconciliation.clj @@ -53,7 +53,7 @@ (com/data-grid-cell {:class class} (when (> (count (:missing-transactions row)) 0) [:div - (com/button { :x-tooltip.on.click "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}" } + (com/button {:x-tooltip.on.click "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}"} [:div.flex.gap-2.items-center (count (:missing-transactions row)) [:div.w-4.h-4 svg/question]]) @@ -67,13 +67,12 @@ (com/data-grid-cell {} (format "$%,.2f" (:transaction/amount r))))))]]))))))]) - (defn reconciliation-card* [{:keys [request report]}] (com/content-card {:class "w-full" :id "reconciliation-report"} [:div {:class "flex flex-col px-8 py-8 space-y-3"} [:div [:h1.text-2xl.mb-3.font-bold "Bank Reconciliation Report"] - + [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-reconciliation-report-card) :hx-target "#reconciliation-report" :hx-swap "outerHTML"} @@ -88,7 +87,7 @@ (com/date-input {:name (fc/field-name) :class "w-64" :value (some-> (fc/field-value) - (atime/unparse-local atime/normal-date)) })])) + (atime/unparse-local atime/normal-date))})])) (fc/with-field :end-date (com/validated-field {:label "End" :errors (fc/field-errors)} @@ -96,12 +95,11 @@ (com/date-input {:name (fc/field-name) :class "w-64" :value (some-> (fc/field-value) - (atime/unparse-local atime/normal-date)) })])) + (atime/unparse-local atime/normal-date))})])) (com/button {:color :primary :class "self-center w-24"} "Run")])] -(if report + (if report (report* {:request request :report report}) - [:div "Please choose a time range to run the report"]) - ]])) + [:div "Please choose a time range to run the report"])]])) (defn page [request] (base-page @@ -134,7 +132,7 @@ url/map->query)) (defn get-report-data [start-date end-date client-ids] - (let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn ) client-ids))] + (let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn) client-ids))] (for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes)) :let [raw-transactions (get-transactions (atime/unparse-local start-date atime/iso-date) (atime/unparse-local end-date atime/iso-date) @@ -169,11 +167,11 @@ :requires-feedback-count (:transaction-approval-status/requires-feedback found-transactions 0) :missing-transactions missing-transactions}))) -(defn card [{ {:keys [start-date end-date]} :query-params :as request}] +(defn card [{{:keys [start-date end-date]} :query-params :as request}] (let [client-ids (extract-client-ids (:clients request) - (:client-id request) - (when (:client-code request) - [:client/code (:client-code request)])) + (:client-id request) + (when (:client-code request) + [:client/code (:client-code request)])) report (get-report-data start-date end-date client-ids)] (html-response (reconciliation-card* {:request request @@ -182,7 +180,7 @@ (def key->handler (apply-middleware-to-all-handlers - {:company-reconciliation-report page + {:company-reconciliation-report page :company-reconciliation-report-card card} (fn [h] (-> h @@ -191,4 +189,4 @@ [:start-date {:optional true} [:maybe clj-date-schema]] [:end-date {:optional true} - [:maybe clj-date-schema]] ]))))) \ No newline at end of file + [:maybe clj-date-schema]]]))))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/company/yodlee.clj b/src/clj/auto_ap/ssr/company/yodlee.clj index f2e65d0d..163893af 100644 --- a/src/clj/auto_ap/ssr/company/yodlee.clj +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -33,36 +33,34 @@ :yodlee-provider-account/client [:client/code]}]) (def query-schema (mc/schema - [:maybe - (into [:map {} - [:client-id {:optional true} [:maybe entity-id]] ] - default-grid-fields-schema)])) + [:maybe + (into [:map {} + [:client-id {:optional true} [:maybe entity-id]]] + default-grid-fields-schema)])) (defn fetch-ids [db request] (let [query-params (:query-params request) query (cond-> {:query {:find [] - :in ['$ '[?xx ...]] - :where ['[?e :yodlee-provider-account/id] - '[?e :yodlee-provider-account/client ?xx]]} - :args [db (:trimmed-clients request)]} + :in ['$ '[?xx ...]] + :where ['[?e :yodlee-provider-account/id] + '[?e :yodlee-provider-account/client ?xx]]} + :args [db (:trimmed-clients request)]} - - (:sort query-params) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]] - "client" ['[?e :yodlee-provider-account/client ?c] - '[?c :client/code ?sort-client]] - "provider-account" ['[?e :yodlee-provider-account/id ?sort-provider-account]] - "last-updated" ['[?e :yodlee-provider-account/last-updated ?sort-last-updated]]} - query-params) - true - (merge-query {:query {:find ['?e ] - :where ['[?e :yodlee-provider-account/id]]}}))] + (:sort query-params) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]] + "client" ['[?e :yodlee-provider-account/client ?c] + '[?c :client/code ?sort-client]] + "provider-account" ['[?e :yodlee-provider-account/id ?sort-provider-account]] + "last-updated" ['[?e :yodlee-provider-account/last-updated ?sort-last-updated]]} + query-params) + true + (merge-query {:query {:find ['?e] + :where ['[?e :yodlee-provider-account/id]]}}))] (->> query (query2) (apply-sort-3 query-params) (apply-pagination query-params)))) - (defn hydrate-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id))] @@ -70,26 +68,24 @@ (map results) (map first)))) - (defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) - (defn fastlink-dialog [{:keys [client]}] (modal-response - (com/modal - {} - (com/modal-card - {} - [:div.flex [:div.p-2 "Yodlee Fastlink"] ] - [:div - [:div#fa-spot] - [:script {:lang "text/javascript"} - (hiccup/raw - (format " + (com/modal + {} + (com/modal-card + {} + [:div.flex [:div.p-2 "Yodlee Fastlink"]] + [:div + [:div#fa-spot] + [:script {:lang "text/javascript"} + (hiccup/raw + (format " fastlink.open({fastLinkURL: '%s', accessToken: '%s', params: {'configName': 'Aggregation'}, @@ -100,25 +96,24 @@ fastlink.open({fastLinkURL: '%s', }}, 'fa-spot'); -" (:yodlee2-fastlink env) (yodlee/get-access-token (:client/code client))))] - ] - [:div])))) +" (:yodlee2-fastlink env) (yodlee/get-access-token (:client/code client))))]] + [:div])))) (defn reauthenticate [{:keys [form-params identity]}] (assert-can-see-client identity (-> (dc/pull (dc/db conn) '[{:yodlee-provider-account/client [:db/id]}] (Long/parseLong (get form-params "id"))) :yodlee-provider-account/client :db/id)) (html-response - (com/modal - {} - (com/modal-card - {} - [:div.flex [:div.p-2 "Yodlee Fastlink"] ] - [:div - [:div#fa-spot] - [:script {:lang "text/javascript"} - (hiccup/raw - (format " + (com/modal + {} + (com/modal-card + {} + [:div.flex [:div.p-2 "Yodlee Fastlink"]] + [:div + [:div#fa-spot] + [:script {:lang "text/javascript"} + (hiccup/raw + (format " fastlink.open({fastLinkURL: '%s', accessToken: '%s', params: {'configName': 'Aggregation', @@ -127,94 +122,93 @@ fastlink.open({fastLinkURL: '%s', 'fa-spot'); " - (:yodlee2-fastlink env) - (yodlee/get-access-token (-> (dc/pull (dc/db conn) - [{:yodlee-provider-account/client [:client/code]}] - (Long/parseLong (get form-params "id"))) - :yodlee-provider-account/client - :client/code)) - (pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]] - [:div])))) + (:yodlee2-fastlink env) + (yodlee/get-access-token (-> (dc/pull (dc/db conn) + [{:yodlee-provider-account/client [:client/code]}] + (Long/parseLong (get form-params "id"))) + :yodlee-provider-account/client + :client/code)) + (pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]] + [:div])))) (def grid-page - (helper/build - {:id "yodlee-table" - :nav com/company-aside-nav - :id-fn :db/id - :fetch-page fetch-page - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-yodlee)} - "Yodlee"]] - :title "Yodlee Accounts" - :entity-name "Yodlee accounts" - :query-schema query-schema - :route :company-yodlee-table - :action-buttons (fn [request] - [[:div.flex.flex-col.flex-shrink - [:div.flex-shrink - (com/button {:color :primary - :on-click "openFastlink()" - :disabled (if (:client request) - false - true) - :hx-get (bidi/path-for ssr-routes/only-routes - :company-yodlee-fastlink-dialog) - :hx-target "#modal-holder"} - (com/button-icon {} svg/refresh) - "Link new account")] - (when-not (:client request) - [:div.text-xs "Note: please select a specific customer to link a new account."])]]) - :row-buttons (fn [request _] - [ - (com/button {:hx-put (bidi/path-for ssr-routes/only-routes - :company-yodlee-provider-account-reauthenticate) - :color :primary - :hx-target "#modal-holder"} - "Reauthenticate") - (when (is-admin? (:identity request)) - (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes - :company-yodlee-provider-account-refresh) - :hx-target "closest tr"} - svg/refresh))]) - :headers [{:key "client" - :name "Client" - :sort-key "client" - :hide? (fn [args] - (= (count (:clients args)) 1)) - :render #(-> % :yodlee-provider-account/client :client/code)} - {:key "provider-account" - :name "Provider Account" - :sort-key "provider-account" - :render :yodlee-provider-account/id} - {:key "status" - :name "Status" - :sort-key "status" - :render #(when-let [status (:yodlee-provider-account/status %)] - (com/pill {:color (if (not= status "SUCCESS") - :yellow - :primary) } - status))} - {:key "detailed-status" - :name "Detailed Status" - :sort-key "detailed-status" - :render #(when-let [status (:yodlee-provider-account/detailed-status %)] - status)} + (helper/build + {:id "yodlee-table" + :nav com/company-aside-nav + :id-fn :db/id + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} + "Yodlee"]] + :title "Yodlee Accounts" + :entity-name "Yodlee accounts" + :query-schema query-schema + :route :company-yodlee-table + :action-buttons (fn [request] + [[:div.flex.flex-col.flex-shrink + [:div.flex-shrink + (com/button {:color :primary + :on-click "openFastlink()" + :disabled (if (:client request) + false + true) + :hx-get (bidi/path-for ssr-routes/only-routes + :company-yodlee-fastlink-dialog) + :hx-target "#modal-holder"} + (com/button-icon {} svg/refresh) + "Link new account")] + (when-not (:client request) + [:div.text-xs "Note: please select a specific customer to link a new account."])]]) + :row-buttons (fn [request _] + [(com/button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-reauthenticate) + :color :primary + :hx-target "#modal-holder"} + "Reauthenticate") + (when (is-admin? (:identity request)) + (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-refresh) + :hx-target "closest tr"} + svg/refresh))]) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :yodlee-provider-account/client :client/code)} + {:key "provider-account" + :name "Provider Account" + :sort-key "provider-account" + :render :yodlee-provider-account/id} + {:key "status" + :name "Status" + :sort-key "status" + :render #(when-let [status (:yodlee-provider-account/status %)] + (com/pill {:color (if (not= status "SUCCESS") + :yellow + :primary)} + status))} + {:key "detailed-status" + :name "Detailed Status" + :sort-key "detailed-status" + :render #(when-let [status (:yodlee-provider-account/detailed-status %)] + status)} - {:key "last-updated" - :name "Last Updated" - :sort-key "last-updated" - :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) - atime/normal-date)} - {:key "accounts" - :name "Accounts" - :show-starting "md" - :render (fn [e] - [:ul - (for [a (:yodlee-provider-account/accounts e)] - [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]})) + {:key "last-updated" + :name "Last Updated" + :sort-key "last-updated" + :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) + atime/normal-date)} + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:yodlee-provider-account/accounts e)] + [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]})) (def page (helper/page-route grid-page)) (def table (helper/table-route grid-page)) @@ -224,26 +218,23 @@ fastlink.open({fastLinkURL: '%s', (yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account)) (:yodlee-provider-account/id provider-account)) (html-response - (helper/row* - grid-page - identity - provider-account - {:flash? true})))) + (helper/row* + grid-page + identity + provider-account + {:flash? true})))) - -(def key->handler - (apply-middleware-to-all-handlers - { - :company-yodlee page +(def key->handler + (apply-middleware-to-all-handlers + {:company-yodlee page :company-yodlee-table table - :company-yodlee-fastlink-dialog fastlink-dialog - } + :company-yodlee-fastlink-dialog fastlink-dialog} (fn [h] - (-> h - (wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) - (wrap-client-redirect-unauthenticated) - (wrap-secure))))) \ No newline at end of file + (-> h + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) + (wrap-client-redirect-unauthenticated) + (wrap-secure))))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 7b9bd2b2..e4f0ba4e 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -15,7 +15,6 @@ [hiccup2.core :as hiccup] [iol-ion.query :refer [can-see-client?]])) - (defn dropdown-search-results* [{:keys [options]}] [:ul (for [{:keys [id name group]} options] @@ -44,9 +43,6 @@ :hx-trigger "click"} name])]])]) - - - (defn get-clients [identity query] (let [raw-query (not-empty (strip-special query)) cleansed-query (not-empty (cleanse-query query)) @@ -89,11 +85,11 @@ "localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n" "localStorage.setItem(\"last-selected-clients\", " (json/write-str (json/write-str client-selection)) #_(cond (:group client-selection) - (:group client-selection) - (:selected client-selection) - (:selected client-selection) - :else - client-selection) ")")] + (:group client-selection) + (:selected client-selection) + (:selected client-selection) + :else + client-selection) ")")] [:div [:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" "x-tooltip.on.click" "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', onMount(i) { htmx.process(i.popper); }, allowHTML: true, interactive:true}" @@ -103,11 +99,11 @@ "My Companies" (= :all client-selection) "All Companies" - + (and client (= 1 (count clients))) (:client/name client) - + :else (str (count clients) " Companies")) [:div.w-4.h-4.ml-2 diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 22c15de3..b3c2f2fe 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -55,8 +55,8 @@ ":style" (format "selected == '%s' ? 'max-height: ' + $el.scrollHeight + 'px' : ''" (:selector params)))) (for [c children] [:li - (update-in c [1 1 :class ] (fn [c] - (hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])]) + (update-in c [1 1 :class] (fn [c] + (hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])]) (defn left-aside- [{:keys [nav page-specific]} & _] [:aside {:id "left-nav", @@ -83,7 +83,6 @@ [:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"} nav - (when page-specific [:div {:class " pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"} page-specific])]]) @@ -94,7 +93,7 @@ "invoices" (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts ::ss-routes/page} (:matched-route request)) - "sales" + "sales" (#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request)) "payments" (#{::transaction-routes/page ::transaction-routes/approved-page ::transaction-routes/unapproved-page ::transaction-routes/requires-feedback-page :transaction-insights} (:matched-route request)) @@ -108,7 +107,7 @@ [:li (menu-button- {:icon svg/pie - :href (bidi/path-for ssr-routes/only-routes + :href (bidi/path-for ssr-routes/only-routes ::dashboard/page)} "Dashboard")] @@ -147,7 +146,6 @@ :hx-boost "true"} "Voided") - (when (can? (:identity request) {:subject :invoice :activity :import}) @@ -156,7 +154,6 @@ :active? (= ::invoice-route/import-page (:matched-route request)) :hx-boost "true"} "Import")) - #_(when (can? (:identity request) {:subject :invoice :activity :import}) @@ -168,7 +165,6 @@ "Glimpse" (tags/pill- {:color :secondary} "Beta")])) - (when (can? (:identity request) {:subject :ar-invoice :activity :read}) @@ -213,12 +209,12 @@ :hx-boost "true"} "Refunds") - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-cash-drawer-shifts) - "?date-range=week") - :active? (= :pos-cash-drawer-shifts (:matched-route request)) - :hx-boost "true"} - "Cash drawer shifts") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shifts) + "?date-range=week") + :active? (= :pos-cash-drawer-shifts (:matched-route request)) + :hx-boost "true"} + "Cash drawer shifts") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes ::ss-routes/page) "?date-range=week") @@ -288,7 +284,6 @@ (menu-button- {:href (bidi/path-for ssr-routes/only-routes :transaction-insights)} "Insights")))] - (when (can? (:identity request) {:subject :ledger-page}) (list @@ -314,7 +309,7 @@ [:div.flex.gap-2 "External Register" (tags/pill- {:color :secondary} "WIP")])) - + (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/profit-and-loss)) :active? (= ::ledger-routes/profit-and-loss (:matched-route request)) @@ -322,7 +317,7 @@ [:div.flex.gap-2 "Profit and loss" (tags/pill- {:color :secondary} "WIP")]) - + (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/cash-flows)) :active? (= ::ledger-routes/cash-flows (:matched-route request)) @@ -330,7 +325,7 @@ [:div.flex.gap-2 "Cash flows" (tags/pill- {:color :secondary} "WIP")]) - + (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/balance-sheet)) :active? (= ::ledger-routes/balance-sheet (:matched-route request)) @@ -338,8 +333,7 @@ [:div.flex.gap-2 "Balance Sheet" (tags/pill- {:color :secondary} "WIP")]) - - + (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/external-import-page) {:date-range "month"}) @@ -349,7 +343,6 @@ "External Import" (tags/pill- {:color :secondary} "WIP")]))))])) - (defn company-aside-nav- [request] [:ul {:class "space-y-2" :hx-boost "true"} [:li @@ -465,7 +458,6 @@ :hx-boost true} "Background Jobs")] - (menu-button- {:icon svg/arrow-in "@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "} "Import") diff --git a/src/clj/auto_ap/ssr/components/bank_account_icon.clj b/src/clj/auto_ap/ssr/components/bank_account_icon.clj index 90cf3906..84e31626 100644 --- a/src/clj/auto_ap/ssr/components/bank_account_icon.clj +++ b/src/clj/auto_ap/ssr/components/bank_account_icon.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.components.bank-account-icon +(ns auto-ap.ssr.components.bank-account-icon (:require [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.svg :as svg])) diff --git a/src/clj/auto_ap/ssr/components/breadcrumbs.clj b/src/clj/auto_ap/ssr/components/breadcrumbs.clj index 3cbaf6aa..286c7f85 100644 --- a/src/clj/auto_ap/ssr/components/breadcrumbs.clj +++ b/src/clj/auto_ap/ssr/components/breadcrumbs.clj @@ -8,15 +8,15 @@ [:a {:href "#", :class "inline-flex w-4 h-4 mr-2 items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"} [:div.w-4.h-4 svg/home]]] (for [p steps] - [:li + [:li [:div {:class "flex items-center"} - - [:div {:class "w-6 h-6 text-gray-400",} + + [:div {:class "w-6 h-6 text-gray-400"} svg/breadcrumb-component] - + (update-in p [1 :class] str " ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 dark:text-gray-400 dark:hover:text-white")]]) #_[:li {:aria-current "page"} - [:div {:class "flex items-center"} - [:svg {:aria-hidden "true", :class "w-6 h-6 text-gray-400", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]] - [:span {:class "ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400"} "Flowbite"]]]]]) + [:div {:class "flex items-center"} + [:svg {:aria-hidden "true", :class "w-6 h-6 text-gray-400", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} + [:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]] + [:span {:class "ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400"} "Flowbite"]]]]]) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index b5a3814f..1412043f 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -113,9 +113,9 @@ (= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") (= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ") (= :secondary-light (:color params)) (str " text-blue-800 bg-white-200 border-gray-100 border hover:bg-blue-100 focus:ring-blue-100 dark:bg-blue-400 dark:hover:bg-blue-800 ") - + (not (nil? (:color params))) -(str " text-white " (bg-colors (:color params) (:disabled params))) + (str " text-white " (bg-colors (:color params) (:disabled params))) (nil? (:color params)) (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))) @@ -126,7 +126,7 @@ (svg/spinner {:class "inline w-4 h-4 text-white"}) [:div.ml-3 "Loading..."]]) (into [:div.inline-flex.gap-2.items-center.justify-center {:class (when (:indicator? params true) - "htmx-indicator-hidden")}] + "htmx-indicator-hidden")}] children)]) (defn icon-button- [params & children] @@ -162,8 +162,6 @@ [:div.ml-3 "Loading..."]] (into [:div.htmx-indicator-hidden] children)]) - - (defn group-button- [{:keys [size] :or {size :normal} :as params} & children] (into [:button (cond-> params true (assoc :type (or (:type params) "button")) @@ -191,7 +189,7 @@ (defn navigation-button- [{:keys [class next-arrow?] :or {next-arrow? true} :as params} & children] [:button (-> params - (update :class (fnil hh/add-class "") + (update :class (fnil hh/add-class "") "p-4 text-green-700 border border-gray-300 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-green-800 dark:text-green-400 focus:ring-green-400 focus:ring-2 @@ -211,7 +209,7 @@ {:class "space-y-4 w-72"} (for [n children] [:li n]) - + #_[:li [:div {:class @@ -231,7 +229,6 @@ [:span {:class "sr-only"} "Confirmation"] [:h3 {:class "font-medium"} "5. Confirmation"]]]]]) - (defn validated-save-button- [{:keys [errors class] :as params} & children] (button- (-> {:color (or (:color params) :primary) :type "submit" :class (cond-> (or class "") diff --git a/src/clj/auto_ap/ssr/components/card.clj b/src/clj/auto_ap/ssr/components/card.clj index 2c8f7a28..f13242ad 100644 --- a/src/clj/auto_ap/ssr/components/card.clj +++ b/src/clj/auto_ap/ssr/components/card.clj @@ -4,7 +4,7 @@ [clojure.string :as str])) (defn card- [params & children] - (into [:div (update params :class + (into [:div (update params :class #(cond-> (or % "") (not (str/includes? (or % "") "bg-")) (hh/add-class "dark:bg-gray-800 bg-white ") true (hh/add-class "shadow-md sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 overflow-hidden")))] @@ -13,6 +13,6 @@ (defn content-card- [params & children] [:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))}) [:div {:class (:max-w params "max-w-screen-2xl")} - (into - [:div {:class "relative overflow-scroll shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}] - children)]]) + (into + [:div {:class "relative overflow-scroll shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}] + children)]]) diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index 63625824..e9bd9f3b 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -16,13 +16,13 @@ "@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params)) :style (:style params)}] (if (:sort-key params) - [(into [:a {:href "#"} ] rest)] + [(into [:a {:href "#"}] rest)] rest))) (defn sort-header- [params & rest] [:th.px-4.py-3 {:scope "col" :class (:class params) - "@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params)) } - (into [:a {:href "#"} ] rest)]) + "@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params))} + (into [:a {:href "#"}] rest)]) (defn row- [params & rest] (into [:tr (update params @@ -31,11 +31,11 @@ (defn cell- [params & rest] (into [:td.px-4.py-2 (update params :class #(str (-> "" - (hh/add-class (or % ""))))) ] + (hh/add-class (or % "")))))] rest)) (defn right-stack-cell- [params & rest] - (cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between + (cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between rest]))) (defn checkbox-header- [params & rest] @@ -46,17 +46,17 @@ (defn data-grid- [{:keys [headers thead-params id] :as params} & rest] - [:div.shrink.overflow-y-scroll + [:div.shrink.overflow-y-scroll [:table (merge {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink"} (dissoc params :headers :thead-params)) [:thead (update thead-params :class #(-> "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:z-10 group-[.raw]:top-0" (hh/add-class (or % "")))) - (into - [:tr] - headers)] - (into - [:tbody {}] - rest)]]) + (into + [:tr] + headers)] + (into + [:tbody {}] + rest)]]) ;; needed for tailwind ;; lg:table-cell md:table-cell @@ -81,35 +81,35 @@ rows] :as params} & children] (let [card (if raw? raw-table-card content-card-)] (card - (cond-> { :id id :class (cond-> "group" raw? (hh/add-class "raw h-full flex flex-col overflow-hidden")) } - root-params (merge root-params) - route (assoc - :hx-get (bidi/path-for ssr-routes/only-routes - route - :request-method :get) - :hx-trigger "clientSelected from:body, invalidated from:body" - :hx-swap "outerHTML swap:300ms")) - [:div {:class " flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-baseline lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} - [:h1.text-2xl.mb-3.font-bold title] - [:div {:class "flex items-center flex-1 space-x-4"} - [:h5 - (when subtitle - [:span subtitle])]] - (into [:div {:class "group-[.raw]:hidden flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}] - action-buttons)] - [:div {:class "overflow-x-auto contents"} - (data-grid- {:headers headers - :thead-params thead-params} - rows)] + (cond-> {:id id :class (cond-> "group" raw? (hh/add-class "raw h-full flex flex-col overflow-hidden"))} + root-params (merge root-params) + route (assoc + :hx-get (bidi/path-for ssr-routes/only-routes + route + :request-method :get) + :hx-trigger "clientSelected from:body, invalidated from:body" + :hx-swap "outerHTML swap:300ms")) + [:div {:class " flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-baseline lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} + [:h1.text-2xl.mb-3.font-bold title] + [:div {:class "flex items-center flex-1 space-x-4"} + [:h5 + (when subtitle + [:span subtitle])]] + (into [:div {:class "group-[.raw]:hidden flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}] + action-buttons)] + [:div {:class "overflow-x-auto contents"} + (data-grid- {:headers headers + :thead-params thead-params} + rows)] - (when (or paginate? - (nil? paginate?)) - [:div {:class "contents group-[.raw]:block"} - (paginator- {:start start - :end (Math/min (+ start per-page) total) - :per-page per-page - :total total - :a-params (fn [page] + (when (or paginate? + (nil? paginate?)) + [:div {:class "contents group-[.raw]:block"} + (paginator- {:start start + :end (Math/min (+ start per-page) total) + :per-page per-page + :total total + :a-params (fn [page] ;; TODO it might be good to have a more global form defined in the specific page ;; with elements that are part of item ;; that way this is not deeply coupled @@ -117,44 +117,43 @@ ;; TODO the other way to think about this is that we want the request to include ;; all of the correct parameters, not parameters to merge with the current ones. ;; think sorting, filters, pagination - {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes - route - :request-method :get) - {:start (* page per-page)}) - :hx-target (str "#" id) - :hx-swap "outerHTML show:#app:top" - :hx-indicator (str "#" id)}) - :per-page-params {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes + {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes route - :request-method :get)) - :hx-trigger "change" - :hx-include "this" - :hx-target (str "#" id) ; + :request-method :get) + {:start (* page per-page)}) + :hx-target (str "#" id) :hx-swap "outerHTML show:#app:top" - :hx-indicator (str "#" id)}})]) - children - [:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"} - [:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" } - [:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]]))) + :hx-indicator (str "#" id)}) + :per-page-params {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes + route + :request-method :get)) + :hx-trigger "change" + :hx-include "this" + :hx-target (str "#" id) ; + :hx-swap "outerHTML show:#app:top" + :hx-indicator (str "#" id)}})]) + children + [:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"} + [:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50"} + [:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]]))) (defn new-row- [{:keys [index colspan tr-params row-offset] :as params} & content] (row- - (merge {:class "new-row" -"x-on:htmx:after-settle.camel" "let options=$el.parentNode.querySelectorAll('tr'); let target=options[options.length-2]; $nextTick(() => $focus.within(target).first())" + (merge {:class "new-row" + "x-on:htmx:after-settle.camel" "let options=$el.parentNode.querySelectorAll('tr'); let target=options[options.length-2]; $nextTick(() => $focus.within(target).first())" - :x-data (hx/json {:newRowIndex index - :offset (or row-offset 0)}) } - tr-params) - (cell- {:colspan colspan - :class "bg-gray-100"} - [:div.flex.justify-center - (a-button- (merge - (dissoc params :index :colspan) - {"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})" - :color :secondary - :hx-trigger "newRow" - :hx-vals (hiccup/raw "js:{index: event.detail.index }") - :hx-target "closest .new-row" - :hx-swap "beforebegin" - }) - content)]))) + :x-data (hx/json {:newRowIndex index + :offset (or row-offset 0)})} + tr-params) + (cell- {:colspan colspan + :class "bg-gray-100"} + [:div.flex.justify-center + (a-button- (merge + (dissoc params :index :colspan) + {"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})" + :color :secondary + :hx-trigger "newRow" + :hx-vals (hiccup/raw "js:{index: event.detail.index }") + :hx-target "closest .new-row" + :hx-swap "beforebegin"}) + content)]))) diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index a27ea8fc..e951ff40 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -5,7 +5,6 @@ [auto-ap.ssr.hx :as hx] [auto-ap.ssr.svg :as svg])) - (defn modal- "This modal function is used to create a modal window with a stack that allows for transitioning between modals. @@ -26,16 +25,16 @@ :class (fn [c] (-> c (or "") (hh/add-class "w-full p-4 modal-card flex max-h-[inherit]")))) - [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-full overflow-hidden" } + [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-full overflow-hidden"} [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] [:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"} - + content] (when footer [:div {:class "p-4"} [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"}) [:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]] - (when (:error params ) - [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex { :class "dark:bg-red-900 dark:text-red-300"} + (when (:error params) + [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex {:class "dark:bg-red-900 dark:text-red-300"} [:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 (:error params)]]) [:div {:class "shrink-0"} @@ -45,7 +44,6 @@ [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} children]) - (defn modal-header-attachment- [params & children] [:div {:class "flex items-start justify-between p-4 border-b shrink-0"} children]) @@ -56,7 +54,7 @@ (defn modal-footer- [params & children] [:div {:class "p-4 border-t"} - [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex + [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"}) (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"}) [:span {:class "w-2 h-2 bg-red-500 rounded-full"}] @@ -66,7 +64,7 @@ (defn modal-card-advanced- [params & children] [:div (merge params - {:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params "")) }) + {:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params ""))}) children]) (defn success-modal- [{:keys [title]} & children] diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index a2cd3ac6..d7e9dddc 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -8,7 +8,6 @@ [clojure.string :as str] [hiccup2.core :as hiccup])) - (def default-input-classes ["bg-gray-50" "border" "text-sm" "rounded-lg" "" "block" "p-2.5" "border-gray-300" "text-gray-900" "focus:ring-blue-500" "focus:border-blue-500" @@ -149,7 +148,6 @@ [:li {":style" "index == 0 && 'border: 0 !important;'"} [:label {:class "p-3 group rounded flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 [&.implied]:text-gray-500 text-gray-800 dark:text-gray-100 cursor-pointer" - :href "#" ":class" (hx/json {"active" (hx/js-fn "active==index") "implied" (hx/js-fn "all_selected && index != 0")}) @@ -178,7 +176,6 @@ :x-show "value.size > 0"} svg/x]]) - (defn multi-typeahead- [params] [:div.relative {:x-data (hx/json {:baseUrl (str (:url params)) :reset_elements (js-fn "function(e) { @@ -268,21 +265,17 @@ :x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"} (tags/badge- {:class "peer"} "!") - [:div {:x-show "value.warning" :x-ref "warning_pop" :class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4" :x-text "value.warning"}]]] (multi-typeahead-dropdown- params)])]) - (defn use-size [size] (if (= :small size) (str " " "text-xs p-2") (str " " "text-sm p-2.25"))) - - (defn text-input- [{:keys [size error?] :as params}] [:input (-> params @@ -415,8 +408,6 @@ (update :class #(str % (use-size size) " w-full")) (dissoc :size :name :x-model :x-modelable))]])) - - (defn field-errors- [{:keys [source key]} & rest] (let [errors (:errors (cond-> (meta source) key (get key)))] @@ -469,8 +460,6 @@ (defn hidden- [{:keys [name value] :as params}] [:input (merge {:type "hidden" :value value :name name} params)]) - - (defn toggle- [params & children] [:label {:class "inline-flex items-center cursor-pointer"} [:input (merge {:type "checkbox", :class "sr-only peer"} params)] diff --git a/src/clj/auto_ap/ssr/components/link_dropdown.clj b/src/clj/auto_ap/ssr/components/link_dropdown.clj index 395ca085..d2d88dd8 100644 --- a/src/clj/auto_ap/ssr/components/link_dropdown.clj +++ b/src/clj/auto_ap/ssr/components/link_dropdown.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.components.link-dropdown +(ns auto-ap.ssr.components.link-dropdown (:require [auto-ap.ssr.components :as com] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.svg :as svg])) @@ -8,8 +8,7 @@ [:div {:x-data (hx/json {})} (com/a-icon-button {:class "relative" - "@click.prevent" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true, interactive:true, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}})" - } + "@click.prevent" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true, interactive:true, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}})"} svg/paperclip (com/badge {:color "blue"} (count links))) [:template {:x-ref "tooltip"} diff --git a/src/clj/auto_ap/ssr/components/multi_modal.clj b/src/clj/auto_ap/ssr/components/multi_modal.clj index b6295ad4..a42d6ac7 100644 --- a/src/clj/auto_ap/ssr/components/multi_modal.clj +++ b/src/clj/auto_ap/ssr/components/multi_modal.clj @@ -15,12 +15,11 @@ [malli.core :as mc] [malli.core :as m])) - (def default-form-props {:hx-ext "response-targets" :hx-swap "outerHTML" :hx-target-400 "#form-errors .error-content" :hx-trigger "submit" - :hx-target "this" }) + :hx-target "this"}) (defprotocol ModalWizardStep (step-key [this]) @@ -57,7 +56,6 @@ (or (get-in (:snapshot multi-form-state) edit-path) default))) - (defn merge-multi-form-state [{:keys [snapshot edit-path step-params] :as multi-form-state}] (let [cursor (cursor/cursor (or snapshot {})) ;; this hack makes sure that, in the event of a missing vector entry, will make sure to add it first @@ -87,8 +85,6 @@ (fn encode-step-key [sk] (mc/encode step-key-schema sk main-transformer)))) - - (defn render-timeline [linear-wizard current-step validation-route] (let [step-names (map #(step-name (get-step linear-wizard %)) (steps linear-wizard)) active-index (.indexOf step-names (step-name current-step))] @@ -148,9 +144,9 @@ next-button-content]}] [:div.flex.justify-end [:div.flex.items-baseline.gap-x-4 - (let [step-errors (:step-params fc/*form-errors*)] - (com/form-errors {:errors (or (:errors step-errors) - (when (sequential? step-errors) step-errors))})) + (let [step-errors (:step-params fc/*form-errors*)] + (com/form-errors {:errors (or (:errors step-errors) + (when (sequential? step-errors) step-errors))})) (when (not= (first (steps linear-wizard)) (step-key step)) (when validation-route @@ -172,7 +168,7 @@ (com/modal-card-advanced {"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}" :class (str - (or width-height-class " md:w-[750px] md:h-[600px] ") + (or width-height-class " md:w-[750px] md:h-[600px] ") " w-full h-full group-[.forward]/transition:htmx-swapping:opacity-0 group-[.forward]/transition:htmx-swapping:-translate-x-1/4 @@ -261,7 +257,7 @@ :oob (or oob [])))) (def next-handler - + (-> (fn [{:keys [wizard] :as request}] (let [current-step (get-current-step wizard)] (if (satisfies? CustomNext current-step) @@ -361,8 +357,6 @@ (render-wizard wizard request)]) (get query-params :replace-modal) (assoc-in [:headers "hx-trigger"] "modalswap"))) - - (defn wrap-init-multi-form-state [handler get-multi-form-state] (-> (fn init-multi-form [request] diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index ba2efea1..b7057b66 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -6,16 +6,15 @@ [config.core :refer [env]] [hiccup2.core :as hiccup])) -(defn page- [{:keys [nav page-specific client clients client-selection identity app-params request] :or {app-params {}} } & children] - [:div#app { "@notification.document" "notificationDetails=event.detail.value; showNotification=true" +(defn page- [{:keys [nav page-specific client clients client-selection identity app-params request] :or {app-params {}}} & children] + [:div#app {"@notification.document" "notificationDetails=event.detail.value; showNotification=true" :x-data (hx/json {:leftNavShow true :showError false :errorDetails "" :showNotification false :notificationDetails ""}) - "@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;" - } + "@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"} (navbar- {:client-selection client-selection :clients clients :client client @@ -29,33 +28,32 @@ :page-specific page-specific}) [:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content lg:pl-64" ":class" "leftNavShow ? 'lg:pl-64' : ''" - :x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')" - } + :x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')"} [:div#notification-holder - [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification" } + [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification"} [:div.relative [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400 - { "@click" "showNotification=false"} + {"@click" "showNotification=false"} svg/filled-x]] - + [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96 {:x-show "showNotification" - "x-transition:enter" "transition duration-300 transform ease-in-out" - "x-transition:enter-start" "opacity-0 translate-y-full" - "x-transition:enter-end" "opacity-100 translate-y-0" - "x-transition:leave" "transition duration-300 transform ease-in-out" - "x-transition:leave-start" "opacity-100 translate-y-0" - "x-transition:leave-end" "opacity-0 translate-y-full"} - + "x-transition:enter" "transition duration-300 transform ease-in-out" + "x-transition:enter-start" "opacity-0 translate-y-full" + "x-transition:enter-end" "opacity-100 translate-y-0" + "x-transition:leave" "transition duration-300 transform ease-in-out" + "x-transition:leave-start" "opacity-100 translate-y-0" + "x-transition:leave-end" "opacity-0 translate-y-full"} + [:div {:class "p-4 text-lg w-full" :role "alert"} [:div.text-sm [:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]] - [:div {:x-show "showError" - :x-init ""} + [:div {:x-show "showError" + :x-init ""} [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg [:div.relative [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600 - { "@click" "showError=false"} + {"@click" "showError=false"} svg/filled-x]] [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96 @@ -63,7 +61,7 @@ "x-transition:enter" "transition duration-300" "x-transition:enter-start" "opacity-0" "x-transition:enter-end" "opacity-100"} - + [:div {:class "p-4 mb-4 text-lg w-full" :role "alert"} [:div.inline-block.w-8.h-8.mr-2 svg/alert] [:span.font-medium "Oh, drat! An unexpected error has occurred."] @@ -73,6 +71,4 @@ [:pre#error-details.text-xs {:x-show "expandError" :x-text "errorDetails"}]]]]]] (into [:div.p-4] - children)]] - - ]) + children)]]]) diff --git a/src/clj/auto_ap/ssr/components/paginator.clj b/src/clj/auto_ap/ssr/components/paginator.clj index 21dfe445..cf2c341c 100644 --- a/src/clj/auto_ap/ssr/components/paginator.clj +++ b/src/clj/auto_ap/ssr/components/paginator.clj @@ -10,7 +10,7 @@ x (> y z) z - :else + :else y)) (def elipsis-button @@ -24,42 +24,41 @@ current-page (long (Math/floor (/ start per-page))) first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) all-buttons (into [] (for [x (range total-pages)] - [:li + [:li [:a (-> (a-params x) - (update - :class #(cond-> % - true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") + (update + :class #(cond-> % + true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") - (= current-page x) - (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") + (= current-page x) + (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") - (not= current-page x) - (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) + (not= current-page x) + (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) (assoc :href "#")) [:div.htmx-indicator.flex.items-center (svg/spinner {:class "inline w-4 h-4 text-black"})] - [:div.htmx-indicator-hidden + [:div.htmx-indicator-hidden (inc x)]]])) - last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) extended-last-page-button (when (not= last-page-button total-pages) (list - elipsis-button - (last all-buttons))) + elipsis-button + (last all-buttons))) extended-first-page-button (when (not= first-page-button 0) (list - (first all-buttons) - elipsis-button))] + (first all-buttons) + elipsis-button))] [:nav.flex.items-center.space-x-3 [:span.text-sm.text-gray-500 "Per page"] (inputs/select- (merge per-page-params {:options [[25 "25"] - [50 "50"] - [100 "100"] - [200 "200"]] + [50 "50"] + [100 "100"] + [200 "200"]] :value per-page :name "per-page"})) [:ul {:class "inline-flex items-stretch -space-x-px"} diff --git a/src/clj/auto_ap/ssr/components/periods.clj b/src/clj/auto_ap/ssr/components/periods.clj index d7aa4217..fa725462 100644 --- a/src/clj/auto_ap/ssr/components/periods.clj +++ b/src/clj/auto_ap/ssr/components/periods.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.components.periods +(ns auto-ap.ssr.components.periods (:require [auto-ap.ssr.components.buttons :as buttons] [auto-ap.ssr.components.inputs :as inputs] @@ -19,7 +19,7 @@ (atime/unparse-local atime/normal-date))}) :x-effect "console.log('periods are', periods)" - :x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)" } + :x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)"} [:template {:x-for "(v,n) in periods" ":key" "n"} [:div [:input {:type "hidden" @@ -29,62 +29,61 @@ ":name" "'periods[' + n + '][end]'" :x-model "v.end"}]]] (buttons/a-button- {"x-tooltip.on.click.theme.dropdown.placement.bottom.interactive" "{content: ()=> $refs.tooltip.innerHTML, allowHTML: true, appendTo: $root}" - :indicator? false} - [:template {:x-if "periods.length == 0"} - [:span.text-left.text-gray-400 "None selected"]] - [:template {:x-if "periods.length < 3 && periods.length > 0"} - [:span.inline-flex.gap-2 - [:template {:x-for "p in periods"} - (tags/pill- {:color :secondary} - [:span {:x-text "p.start"}] - " - " - [:span {:x-text "p.end"}])]]] - [:template {:x-if "periods.length >= 3"} - (tags/pill- {:color :secondary} - [:span {:x-text "periods.length"}] - " periods selected")] - [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"} - svg/drop-down]) + :indicator? false} + [:template {:x-if "periods.length == 0"} + [:span.text-left.text-gray-400 "None selected"]] + [:template {:x-if "periods.length < 3 && periods.length > 0"} + [:span.inline-flex.gap-2 + [:template {:x-for "p in periods"} + (tags/pill- {:color :secondary} + [:span {:x-text "p.start"}] + " - " + [:span {:x-text "p.end"}])]]] + [:template {:x-if "periods.length >= 3"} + (tags/pill- {:color :secondary} + [:span {:x-text "periods.length"}] + " periods selected")] + [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"} + svg/drop-down]) [:template {:x-ref "tooltip"} [:div.p-4.gap-2 {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4 w-[700px] "} [:div.flex.flex-col.gap-2 (tabs/tabs- - {:tabs [{:name "Quick" - :content [:div.flex.flex.gap-2 - (inputs/calendar-input- {:placeholder "12/21/2020" :x-model "source_date"}) - [:div.flex.flex-col.gap-2 - (buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending " - [:span {:x-text "source_date"}]]) - (buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year (" - [:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}] - ")"]) - (buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending " - [:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]]) - [:hr {:class "h-px my-1 bg-gray-200 border-0 dark:bg-gray-700"} ] - (buttons/a-button- {"@click" "periods=getLastMonthPeriods()"} "Last Month") - (buttons/a-button- {"@click" "periods=getMonthToDatePeriods()"} "Month to date") - (buttons/a-button- {"@click" "periods=getYearToDatePeriods()"} "Year to date") - (buttons/a-button- {"@click" "periods=[]"} "Clear")]]} - {:name "Advanced" - :content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]" - :x-data (hx/json {:calendarTarget "0" - :calendarWhich "start"}) - "@change-date.camel" "$el.querySelectorAll('.text-inputs.' + calendarWhich)[calendarTarget].focus()"} - (inputs/calendar-input- {:x-model "periods[calendarTarget][calendarWhich]"}) - [:div.flex.flex-col.gap-4.p-2 - [:div.overflow-y-scroll.flex.flex-col.gap-4 - [:template {:x-for "(p, i) in periods" ":key" "i"} - [:div.flex.gap-4. - (inputs/text-input- { :class "text-inputs start" :x-model "periods[i].start" "@focus" "calendarTarget =i; calendarWhich='start'" }) - (inputs/text-input- { :class "text-inputs end" :x-model "periods[i].end" "@focus" "calendarTarget =i; calendarWhich='end'"}) - (buttons/a-icon-button- {"@click.prevent.stop" "periods=periods.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)] - #_(com/pill {:color :secondary} - [:span {:x-text "p.start"}] - " - " - [:span {:x-text "p.end"}])]] - (buttons/button- {"@click.prevent.stop" "periods.push({start: '', end: ''}); calendarTarget=0" :class "w-32"} "Add new period")] - ]}] - :active "Quick"}) ]]]]) + {:tabs [{:name "Quick" + :content [:div.flex.flex.gap-2 + (inputs/calendar-input- {:placeholder "12/21/2020" :x-model "source_date"}) + [:div.flex.flex-col.gap-2 + (buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending " + [:span {:x-text "source_date"}]]) + (buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year (" + [:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}] + ")"]) + (buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending " + [:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]]) + [:hr {:class "h-px my-1 bg-gray-200 border-0 dark:bg-gray-700"}] + (buttons/a-button- {"@click" "periods=getLastMonthPeriods()"} "Last Month") + (buttons/a-button- {"@click" "periods=getMonthToDatePeriods()"} "Month to date") + (buttons/a-button- {"@click" "periods=getYearToDatePeriods()"} "Year to date") + (buttons/a-button- {"@click" "periods=[]"} "Clear")]]} + {:name "Advanced" + :content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]" + :x-data (hx/json {:calendarTarget "0" + :calendarWhich "start"}) + "@change-date.camel" "$el.querySelectorAll('.text-inputs.' + calendarWhich)[calendarTarget].focus()"} + (inputs/calendar-input- {:x-model "periods[calendarTarget][calendarWhich]"}) + [:div.flex.flex-col.gap-4.p-2 + [:div.overflow-y-scroll.flex.flex-col.gap-4 + [:template {:x-for "(p, i) in periods" ":key" "i"} + [:div.flex.gap-4. + (inputs/text-input- {:class "text-inputs start" :x-model "periods[i].start" "@focus" "calendarTarget =i; calendarWhich='start'"}) + (inputs/text-input- {:class "text-inputs end" :x-model "periods[i].end" "@focus" "calendarTarget =i; calendarWhich='end'"}) + (buttons/a-icon-button- {"@click.prevent.stop" "periods=periods.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)] + #_(com/pill {:color :secondary} + [:span {:x-text "p.start"}] + " - " + [:span {:x-text "p.end"}])]] + (buttons/button- {"@click.prevent.stop" "periods.push({start: '', end: ''}); calendarTarget=0" :class "w-32"} "Add new period")]]}] + :active "Quick"})]]]]) (defn dates-dropdown- [{:keys [value name]}] [:div {:x-data (hx/json {:dates (map #(atime/unparse-local % atime/normal-date) value)})} @@ -122,16 +121,16 @@ (buttons/a-button- {"@click" "dates=[]"} "Clear")]]} {:name "Advanced oooo" :content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]" - :x-data (hx/json {:calendarTarget "0" }) + :x-data (hx/json {:calendarTarget "0"}) "@change-date.camel" "$el.querySelectorAll('.text-inputs')[calendarTarget].focus();"} - (inputs/calendar-input- {:x-model "dates[calendarTarget]" }) + (inputs/calendar-input- {:x-model "dates[calendarTarget]"}) [:div.flex.flex-col.gap-4.p-2 [:div.overflow-y-scroll.flex.flex-col.gap-4 [:template {:x-for "(p, i) in dates" ":key" "i"} [:div.flex.gap-4. - (inputs/text-input- {:x-model "dates[i]" + (inputs/text-input- {:x-model "dates[i]" "@focus" "calendarTarget =i; " :class "text-inputs"}) - (buttons/a-icon-button- {"@click.prevent.stop" "dates=dates.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)] ]] + (buttons/a-icon-button- {"@click.prevent.stop" "dates=dates.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)]]] (buttons/button- {"@click.prevent.stop" "dates.push(null); calendarTarget=0" :class "w-32"} "Add new period")]]}] :active "Quick"})]]]]) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/components/tabs.clj b/src/clj/auto_ap/ssr/components/tabs.clj index 116206f6..03fe2315 100644 --- a/src/clj/auto_ap/ssr/components/tabs.clj +++ b/src/clj/auto_ap/ssr/components/tabs.clj @@ -1,28 +1,26 @@ -(ns auto-ap.ssr.components.tabs +(ns auto-ap.ssr.components.tabs (:require [auto-ap.ssr.hx :as hx])) (defn tabs- [{:keys [tabs active]}] [:div.flex.flex-col.gap-2 {:x-data (hx/json {:activeTab active})} - [:div {:class "text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700" } + [:div {:class "text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700"} [:ul {:class "flex flex-wrap -mb-px"} (for [tab tabs] [:li {:class "me-2"} [:a {:href "#" :x-data (hx/json {:tabName (:name tab)}) ":data-active" (format "activeTab==tabName") - "@click" (format "activeTab=tabName" ) - :class "inline-block data-[active]:text-blue-600 data-[active]:border-blue-600 p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"} + "@click" (format "activeTab=tabName") + :class "inline-block data-[active]:text-blue-600 data-[active]:border-blue-600 p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"} (:name tab)]]) - - - + #_[:li [:a {:class "inline-block p-4 text-gray-400 rounded-t-lg cursor-not-allowed dark:text-gray-500"} "Disabled"]]]] (for [tab tabs] [:div {:x-data (hx/json {:tabName (:name tab)}) - :x-show (format "activeTab==tabName") + :x-show (format "activeTab==tabName") "x-transition:enter" "transition-opacity duration-300" - "x-transition:enter-start" "opacity-0" - "x-transition:enter-end" "opacity-100"} - (:content tab) ])]) \ No newline at end of file + "x-transition:enter-start" "opacity-0" + "x-transition:enter-end" "opacity-100"} + (:content tab)])]) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/components/tags.clj b/src/clj/auto_ap/ssr/components/tags.clj index 7d1c0119..ee40071b 100644 --- a/src/clj/auto_ap/ssr/components/tags.clj +++ b/src/clj/auto_ap/ssr/components/tags.clj @@ -1,7 +1,6 @@ (ns auto-ap.ssr.components.tags (:require [auto-ap.ssr.hiccup-helper :as hh])) - (defn pill- [params & children] (into [:span (cond-> params @@ -23,7 +22,6 @@ (defn badge- [params & children] [:div (merge params {:class (-> (hh/add-class "absolute inline-flex items-center z-10 justify-center w-6 h-6 text-xs font-black text-white border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" - (:class params) - ) + (:class params)) (hh/add-class (or (some-> (:color params) (#(str "bg-" % "-300"))) "bg-red-300")))}) children]) diff --git a/src/clj/auto_ap/ssr/components/timeline.clj b/src/clj/auto_ap/ssr/components/timeline.clj index 1e78553f..a12e5265 100644 --- a/src/clj/auto_ap/ssr/components/timeline.clj +++ b/src/clj/auto_ap/ssr/components/timeline.clj @@ -30,14 +30,14 @@ (if active? [:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"} [:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}] - children ] + children] [:li {:class (cond-> "flex items-center" (not visited?) (hh/add-class "text-gray-400"))} [:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"} (when visited? [:svg {:class "w-3 h-3 text-primary-600 dark:text-primary-500", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 16 12"} [:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M1 5.917 5.724 10.5 15 1.5"}]])] - children ])) + children])) (defn vertical-timeline [params & children] [:ol {:class (hh/add-class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2" diff --git a/src/clj/auto_ap/ssr/components/user_dropdown.clj b/src/clj/auto_ap/ssr/components/user_dropdown.clj index 8b18e533..881a7022 100644 --- a/src/clj/auto_ap/ssr/components/user_dropdown.clj +++ b/src/clj/auto_ap/ssr/components/user_dropdown.clj @@ -10,15 +10,14 @@ [:div {:class "flex items-center ml-3 mr-10"} [:div [:button#user-menu-button {:type "button", :class "flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600", :aria-expanded "false" - "@click" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: $store.darkMode.on ? 'dark' : 'light', allowHTML: true, interactive:true})" - } + "@click" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: $store.darkMode.on ? 'dark' : 'light', allowHTML: true, interactive:true})"} [:span {:class "sr-only"} "Open user menu"] [:img {:class "w-8 h-8 rounded-full", :src (pull-attr (dc/db conn) :user/profile-image-url (:db/id identity)) :alt "user photo" :referrerpolicy "no-referrer"}]]] [:template {:class "" - :x-ref "tooltip"} + :x-ref "tooltip"} [:div {:class "px-4 py-3", :role "none"} [:p {:class "text-sm text-gray-900 dark:text-white", :role "none"} (:user/name identity)] - [:p {:class "text-sm font-medium text-gray-900 truncate dark:text-gray-300", :role "none"} (pull-attr (dc/db conn) :user/email (:db/id identity))] ] + [:p {:class "text-sm font-medium text-gray-900 truncate dark:text-gray-300", :role "none"} (pull-attr (dc/db conn) :user/email (:db/id identity))]] [:ul {:class "py-1", :role "none"} [:li [:a {:href (bidi/path-for ssr-routes/only-routes :company), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "My Company"]] @@ -27,7 +26,7 @@ :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"]) [:li [:a {:href "#", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem" - "@click.prevent" "$store.darkMode.toggle()" } + "@click.prevent" "$store.darkMode.toggle()"} "Night Mode"]] [:li - [:a {:href "/logout", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Sign out"]]]] ]) + [:a {:href "/logout", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Sign out"]]]]]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 70e993a2..e8b39ef1 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -2,7 +2,7 @@ (:require [auto-ap.permissions :refer [wrap-must]] [auto-ap.routes.ezcater-xls :as ezcater-xls] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]] + :refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]] [auto-ap.ssr.account :as account] [auto-ap.ssr.admin :as admin] [auto-ap.ssr.not-found :as not-found] @@ -43,7 +43,6 @@ ;; from auto-ap.ssr-routes, because they're shared - (def key->handler (-> {:logout auth/logout :login auth/login @@ -86,17 +85,17 @@ (into company-1099/key->handler) (into invoice/key->handler) (into import-batch/key->handler) - (into pos-sales/key->handler) - (into pos-expected-deposits/key->handler) - (into pos-tenders/key->handler) - (into pos-cash-drawer-shifts/key->handler) - (into pos-refunds/key->handler) - (into pos-sales-summaries/key->handler) - (into users/key->handler) - (into admin-accounts/key->handler) - (into admin-excel-invoices/key->handler) - (into admin/key->handler) - (into admin-jobs/key->handler) + (into pos-sales/key->handler) + (into pos-expected-deposits/key->handler) + (into pos-tenders/key->handler) + (into pos-cash-drawer-shifts/key->handler) + (into pos-refunds/key->handler) + (into pos-sales-summaries/key->handler) + (into users/key->handler) + (into admin-accounts/key->handler) + (into admin-excel-invoices/key->handler) + (into admin/key->handler) + (into admin-jobs/key->handler) (into admin-vendors/key->handler) (into admin-clients/key->handler) (into admin-rules/key->handler) diff --git a/src/clj/auto_ap/ssr/dashboard.clj b/src/clj/auto_ap/ssr/dashboard.clj index 4dc55315..13101c6e 100644 --- a/src/clj/auto_ap/ssr/dashboard.clj +++ b/src/clj/auto_ap/ssr/dashboard.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.dashboard +(ns auto-ap.ssr.dashboard (:require [auto-ap.datomic :refer [conn]] [auto-ap.graphql.ledger :refer [get-profit-and-loss-raw]] @@ -25,11 +25,11 @@ [hiccup.util :as hu])) (defn bank-accounts-card [request] - (html-response + (html-response (com/card {:class "h-full"} [:div.p-4.h-full [:h1.text-2xl.font-bold "Bank Accounts"] - [:div (hx/htmx-transition-appear {:class "h-full overflow-scroll" }) + [:div (hx/htmx-transition-appear {:class "h-full overflow-scroll"}) (for [c (:valid-trimmed-client-ids request) b (:client/bank-accounts (dc/pull (dc/db conn) '[{:client/bank-accounts @@ -58,43 +58,42 @@ (#(str "Synced " %)))] #_(when-let [n (cond (-> b :bank-account/intuit-bank-account) - "Intuit" - (-> b :bank-account/yodlee-account) - "Yodlee" - (-> b :bank-account/plaid-account) - "Plaid" - :else - nil)] - (list - [:div (str n " Balance")] - [:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance) - (-> b :bank-account/yodlee-account :yodlee-account/available-balance) - (-> b :bank-account/plaid-account :plaid-account/balance) - 0.0))] - + "Intuit" + (-> b :bank-account/yodlee-account) + "Yodlee" + (-> b :bank-account/plaid-account) + "Plaid" + :else + nil)] + (list + [:div (str n " Balance")] + [:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance) + (-> b :bank-account/yodlee-account :yodlee-account/available-balance) + (-> b :bank-account/plaid-account :plaid-account/balance) + 0.0))] - [:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b) - (:intuit-bank-account/last-synced) - (atime/unparse-local atime/standard-time) - (#(str "Synced " %))) - (some-> (:bank-account/yodlee-account b) - (:yodlee-account/last-synced) - (atime/unparse-local atime/standard-time) - (#(str "Synced " %))) - (some-> (:bank-account/plaid-account b) - (:plaid-account/last-synced) - (atime/unparse-local atime/standard-time) - (#(str "Synced " %))))] - (when-let [pending-balance (-> b :bank-account/yodlee-account :yodlee-account/pending-balance)] - (list - [:div (str n " Pending Txs")] - [:div.text-right (format "$%,.2f" pending-balance)])) - [:div.inline-flex.justify-end.text-xs.text-gray-400.it])) + [:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b) + (:intuit-bank-account/last-synced) + (atime/unparse-local atime/standard-time) + (#(str "Synced " %))) + (some-> (:bank-account/yodlee-account b) + (:yodlee-account/last-synced) + (atime/unparse-local atime/standard-time) + (#(str "Synced " %))) + (some-> (:bank-account/plaid-account b) + (:plaid-account/last-synced) + (atime/unparse-local atime/standard-time) + (#(str "Synced " %))))] + (when-let [pending-balance (-> b :bank-account/yodlee-account :yodlee-account/pending-balance)] + (list + [:div (str n " Pending Txs")] + [:div.text-right (format "$%,.2f" pending-balance)])) + [:div.inline-flex.justify-end.text-xs.text-gray-400.it])) #_[:div.inline-flex.justify-between.items-baseline]]])]]))) (defn sales-chart-card [request] - (html-response - (let [ totals + (html-response + (let [totals (->> (dc/q '[:find ?sd (sum ?total) :with ?e :in $ [?clients ?start-date ?end-date] @@ -113,7 +112,7 @@ [:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil :labels (map first totals) :data (map second totals)}) - :x-init + :x-init "new Chart($el, { type: 'bar', data: { @@ -136,8 +135,8 @@ });"}]])))) (defn expense-pie-card [request] - (html-response - (let [ totals + (html-response + (let [totals (->> (dc/q '[:find ?an (sum ?amt) :with ?iea :in $ [?clients ?start-date ?end-date] @@ -179,19 +178,18 @@ });"}]])))) (defn pnl-card [request] - (html-response - (com/card {:class "w-full h-full p-4"} + (html-response + (com/card {:class "w-full h-full p-4"} [:h1.text-2xl.font-bold.text-gray-700 - "Profit and Loss, last month" ] - (let [ data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request) - [{:start (time/plus (time/now) (time/months -1)) - :end (time/now)}])) + "Profit and Loss, last month"] + (let [data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request) + [{:start (time/plus (time/now) (time/months -1)) + :end (time/now)}])) data (r/->PNLData {} (:accounts (first (:periods data))) {}) - sales (r/aggregate-accounts (r/filter-categories data [ :sales])) - expenses (r/aggregate-accounts (r/filter-categories data [ :cogs :payroll :controllable :fixed-overhead :ownership-controllable ]))] - (list - #_(when (not= (count all-clients) (count clients)) - ) + sales (r/aggregate-accounts (r/filter-categories data [:sales])) + expenses (r/aggregate-accounts (r/filter-categories data [:cogs :payroll :controllable :fixed-overhead :ownership-controllable]))] + (list + #_(when (not= (count all-clients) (count clients))) [:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil :labels [(format "Income $%,.2f" sales) (format "Expenses $%,.2f" expenses)] :data [sales expenses]}) @@ -217,12 +215,12 @@ } } });"}] - [:div + [:div "Income: " (format "$%,.2f" sales)] - [:div + [:div "Expenses: " (format "$%,.2f" expenses)]))))) (defn tasks-card [request] - (html-response + (html-response (com/card {:class "w-full h-full p-4"} [:h1.text-2xl.font-bold.text-gray-700 "Tasks"] @@ -237,7 +235,7 @@ [(:valid-trimmed-client-ids request) (coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1)))) nil])) - + [uncategorized-transaction-count uncategorized-transaction-amount] (first (dc/q '[:find (count ?e) (sum ?am) :in $ [?clients ?start-date ?end-date] @@ -248,25 +246,23 @@ [(:valid-trimmed-client-ids request) (coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1)))) nil]))] - (list + (list (when (not= 0 (or unpaid-invoice-count 0)) [:div.bg-gray-50.rounded.p-4 - [:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". " ] - + [:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". "] + (com/link {:href (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::i-routes/unpaid-page) - {:date-range "year"}) - } - - "Pay now") - ]) + {:date-range "year"})} + + "Pay now")]) (when (not= 0 (or uncategorized-transaction-count 0)) [:div.bg-gray-50.rounded.p-4 - [:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. " ] - + [:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. "] + (com/link {:href (str (bidi.bidi/path-for ssr-routes/only-routes ::transaction-routes/requires-feedback-page) "?date-range=" - (url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)}))) } - + (url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)})))} + "Review now")])))]))) (defn stub-card [params & children] @@ -280,35 +276,33 @@ [:div.htmx-indicator (svg/spinner {:class "inline w-32 h-32 text-green-500"})]])) (defn- page-contents [request] - [:div.mb-8 - [:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"} - [:div.h-96 (stub-card {:title "Expenses" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card) - :hx-trigger "load"} )] - [:div.h-96 - (stub-card {:title "Tasks" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card) - :hx-trigger "load"} )] - [:div {:class " row-span-2 h-[49rem]"} -(stub-card {:title "Bank Accounts" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card) - :hx-trigger "load"} ) - ] - - [:div.h-96 - (stub-card {:title "Gross Sales, last 14 days" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card) - :hx-trigger "load"}) - ] - [:div.h-96 - (stub-card {:title "Profit and Loss, last month" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card) - :hx-trigger "load"}) ] - [:div.col-span-2.h-96 - (stub-card {:title "Expense breakdown" - :hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card) - :hx-trigger "load"} )] - [:div]] ]) + [:div.mb-8 + [:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"} + [:div.h-96 (stub-card {:title "Expenses" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card) + :hx-trigger "load"})] + [:div.h-96 + (stub-card {:title "Tasks" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card) + :hx-trigger "load"})] + [:div {:class " row-span-2 h-[49rem]"} + (stub-card {:title "Bank Accounts" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card) + :hx-trigger "load"})] + + [:div.h-96 + (stub-card {:title "Gross Sales, last 14 days" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card) + :hx-trigger "load"})] + [:div.h-96 + (stub-card {:title "Profit and Loss, last month" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card) + :hx-trigger "load"})] + [:div.col-span-2.h-96 + (stub-card {:title "Expense breakdown" + :hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card) + :hx-trigger "load"})] + [:div]]]) (defn page [request] (base-page @@ -334,12 +328,12 @@ "Dashboard")) (def key->handler - ( apply-middleware-to-all-handlers - {::d-routes/page page - ::d-routes/expense-card expense-pie-card - ::d-routes/pnl-card pnl-card - ::d-routes/sales-card sales-chart-card - ::d-routes/bank-accounts-card bank-accounts-card - ::d-routes/tasks-card tasks-card } - (fn [h] - (wrap-client-redirect-unauthenticated (wrap-admin h))))) \ No newline at end of file + (apply-middleware-to-all-handlers + {::d-routes/page page + ::d-routes/expense-card expense-pie-card + ::d-routes/pnl-card pnl-card + ::d-routes/sales-card sales-chart-card + ::d-routes/bank-accounts-card bank-accounts-card + ::d-routes/tasks-card tasks-card} + (fn [h] + (wrap-client-redirect-unauthenticated (wrap-admin h))))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/form_cursor.clj b/src/clj/auto_ap/ssr/form_cursor.clj index 36b4f2e1..82f4d785 100644 --- a/src/clj/auto_ap/ssr/form_cursor.clj +++ b/src/clj/auto_ap/ssr/form_cursor.clj @@ -8,8 +8,6 @@ (def ^:dynamic *prev-cursor* nil) (def ^:dynamic *current* nil) - - (defmacro start-form [form-data errors & rest] `(binding [*form-data* ~form-data *form-errors* (or ~errors {})] @@ -37,13 +35,13 @@ (defmacro with-field-default [field default & rest] `(let [new-cursor# (get *current* ~field ~default) new-cursor2# (if (not (deref new-cursor#)) - (do - (cursor/transact! *current* - (fn [c#] - (assoc c# ~field ~default))) - (get *current* ~field ~default)) - - new-cursor#)] + (do + (cursor/transact! *current* + (fn [c#] + (assoc c# ~field ~default))) + (get *current* ~field ~default)) + + new-cursor#)] (with-cursor new-cursor2# ~@rest))) @@ -71,7 +69,6 @@ (and (sequential? errors) (every? string? errors))))) - (defn cursor-map ([f] (cursor-map *current* f)) ([cursor f] diff --git a/src/clj/auto_ap/ssr/grid_page_helper.clj b/src/clj/auto_ap/ssr/grid_page_helper.clj index 5c68ed3a..0af5b704 100644 --- a/src/clj/auto_ap/ssr/grid_page_helper.clj +++ b/src/clj/auto_ap/ssr/grid_page_helper.clj @@ -1,39 +1,37 @@ -(ns auto-ap.ssr.grid-page-helper - (:require - [auto-ap.graphql.utils :refer [extract-client-ids]] - [auto-ap.logging :as alog] - [auto-ap.query-params :as query-params] - [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated wrap-secure]] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.hiccup-helper :as hh] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.ui :refer [base-page]] - [auto-ap.ssr.utils :refer [html-response main-transformer]] - [auto-ap.time :as atime] - [bidi.bidi :as bidi] - [cemerick.url :as url] - [clojure.string :as str] - [hiccup.util :as hu] - [malli.core :as m] - [malli.transform :as mt2] - [taoensso.encore :refer [filter-vals]] - [clojure.java.io :as io] - [clojure.data.csv :as csv])) - - +(ns auto-ap.ssr.grid-page-helper + (:require + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.logging :as alog] + [auto-ap.query-params :as query-params] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.hiccup-helper :as hh] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.ui :refer [base-page]] + [auto-ap.ssr.utils :refer [html-response main-transformer]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [cemerick.url :as url] + [clojure.string :as str] + [hiccup.util :as hu] + [malli.core :as m] + [malli.transform :as mt2] + [taoensso.encore :refer [filter-vals]] + [clojure.java.io :as io] + [clojure.data.csv :as csv])) (defn row* [{:keys [check-box-warning? check-boxes?] :as gridspec} user entity {:keys [flash? delete-after-settle? request class] :as options}] (let [cells (if check-boxes? - [(com/data-grid-cell {:class "relative"} + [(com/data-grid-cell {:class "relative"} (let [cb (com/checkbox {:name "id" :value ((:id-fn gridspec) entity) :x-model "selected"})] - (if (and check-box-warning? (check-box-warning? entity)) - (do - [:div.bg-yellow-100.absolute.inset-0.flex.items-center.px-4.py-2 - [:div {:class "absolute inset-0 bg-yellow-50 z-0", + (if (and check-box-warning? (check-box-warning? entity)) + (do + [:div.bg-yellow-100.absolute.inset-0.flex.items-center.px-4.py-2 + [:div {:class "absolute inset-0 bg-yellow-50 z-0", :style "background-image: linear-gradient(135deg, rgba(0, 0, 0, 0.1) 12.5%, transparent 12.5%, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 62.5%, transparent 62.5%, transparent);\n background-size: 10px 10px;"}] @@ -63,7 +61,7 @@ (cond-> {:class (cond-> (or class "") flash? (hh/add-class "live-added group")) :data-id ((:id-fn gridspec) entity)} - delete-after-settle? + delete-after-settle? (assoc "@htmx:after-settle.camel" "setTimeout(() => $el.remove(), 400)")) cells))) @@ -89,7 +87,7 @@ [:div.h-4.w-4 svg/x]]]])) "default sort")) -(defn create-break-table-fn [break-table grid-spec ] +(defn create-break-table-fn [break-table grid-spec] (let [last (atom nil)] (fn [request entity] (let [break-table-value (break-table request entity)] @@ -106,7 +104,6 @@ "desc"))) s))) - (defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}] (alog/info ::TABLE-QP :qp (:query-params request) @@ -123,7 +120,7 @@ :raw? (:raw? grid-spec) :title [:div.flex.gap-2 (if (string? (:title grid-spec)) (:title grid-spec) - ((:title grid-spec) request)) ] + ((:title grid-spec) request))] :route (:route grid-spec) :root-params {:x-data (hx/json {:sort (sort->query sort)}) "x-hx-val:sort" "sort"} @@ -154,11 +151,11 @@ "selected" "all-selected")) :color :secondary-light} [:div.w-4.h-4 svg/download]))) - :rows - (let [break-table-fn (some-> grid-spec :break-table ( create-break-table-fn grid-spec))] + :rows + (let [break-table-fn (some-> grid-spec :break-table (create-break-table-fn grid-spec))] (for [entity entities row (if-let [break-table-row (when break-table-fn (break-table-fn request entity))] - + [break-table-row (row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request})] [(row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request})])] row)) @@ -203,9 +200,6 @@ []))) (com/data-grid-header {}))}))) - - - (defn wrap-trim-client-ids [handler] (fn trim-client-ids [request] (let [valid-clients (extract-client-ids (:clients request) @@ -218,8 +212,7 @@ set)] (handler (assoc request :trimmed-clients valid-clients))))) - -(defn table-route [grid-spec & {:keys [push-url?] :or { push-url? true}}] +(defn table-route [grid-spec & {:keys [push-url?] :or {push-url? true}}] (cond-> (fn table [{:keys [identity] :as request}] (html-response (table* @@ -247,7 +240,7 @@ ;; make it so that it rerenders the date range component, along with a hx-trigger change header (defn csv-route [{:keys [fetch-page headers page->csv-entities]} & {:keys []}] (cond-> (fn csv-route [{:keys [identity] :as request}] - + (let [page-results (fetch-page (assoc-in request [:query-params :per-page] Long/MAX_VALUE)) csv-entities ((or page->csv-entities (fn [[entities]] entities)) page-results) csv-content (with-open [i (java.io.StringWriter.)] @@ -255,13 +248,13 @@ (into [(for [h headers :when ((:render-for h #{:html :csv}) :csv)] (:name h))] - (for [e csv-entities ] + (for [e csv-entities] (for [h headers :when ((:render-for h #{:html :csv}) :csv)] ((or (:render-csv h) (comp str (:render h))) e))))) (.toString i))] - + {:headers {"Content-Type" "text/csv"} :body csv-content})) true (wrap-trim-client-ids) @@ -285,7 +278,7 @@ :request request} (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) (when (:above-grid grid-spec) - ( (:above-grid grid-spec) request)) + ((:above-grid grid-spec) request)) [:div {:x-data (hx/json {:selected [] :all_selected false :type (:entity-name grid-spec)}) "x-on:copy" "if (selected.length > 0) {$clipboard(JSON.stringify({'type': type, 'selected': selected}))}" "x-on:client-selected.document" "selected=[]; all_selected=false" diff --git a/src/clj/auto_ap/ssr/hiccup_helper.clj b/src/clj/auto_ap/ssr/hiccup_helper.clj index 812ffb63..5b74fbd2 100644 --- a/src/clj/auto_ap/ssr/hiccup_helper.clj +++ b/src/clj/auto_ap/ssr/hiccup_helper.clj @@ -4,7 +4,6 @@ [hiccup.util :as hu] [clojure.set :as set])) - (defprotocol ClassHelper (add-class [this add]) (remove-class [this remove]) @@ -70,7 +69,6 @@ (replace-tw (string->class-list this) tw))) - (str (hiccup/html [:div {:class (-> "hello bryce hello-1 hello-2" (replace-wildcard ["hello-" "b"] ["hi" "there"]))}])) (str (hiccup/html [:div {:class (-> "p-1.5 " diff --git a/src/clj/auto_ap/ssr/hx.clj b/src/clj/auto_ap/ssr/hx.clj index 08a88a72..30df886c 100644 --- a/src/clj/auto_ap/ssr/hx.clj +++ b/src/clj/auto_ap/ssr/hx.clj @@ -4,7 +4,6 @@ [cheshire.generate :refer [add-encoder]] [clojure.string :as str])) - (defn vals [m] (cheshire/generate-string m)) @@ -15,18 +14,17 @@ (add-encoder jsfn jsf) - (defn json [m] (let [starting-point (cheshire/generate-string m)] (if (map? m) - (reduce - (fn [starting-point [k v]] - (if (instance? jsfn v) - (-> (str/replace starting-point (re-pattern (str "(?s)\"__" (.-name v) "__(.*?)__end__\"")) "$1" ) - (str/replace "\\n" "\n")) - starting-point)) - starting-point - m) + (reduce + (fn [starting-point [k v]] + (if (instance? jsfn v) + (-> (str/replace starting-point (re-pattern (str "(?s)\"__" (.-name v) "__(.*?)__end__\"")) "$1") + (str/replace "\\n" "\n")) + starting-point)) + starting-point + m) starting-point))) (defn random-alpha-string [] @@ -67,7 +65,7 @@ alpine-disappear) (dissoc params :data-key))) -(defn alpine-mount-then-disappear [{:keys [data-key] :as params :or {data-key "show"}} ] +(defn alpine-mount-then-disappear [{:keys [data-key] :as params :or {data-key "show"}}] (merge (-> {:x-data (json {data-key true}) :x-init (format "$nextTick(() => %s=false)" (name data-key)) :x-show (name data-key)} @@ -85,13 +83,12 @@ (format "\"%s\": $data.%s || ''" field alpine-field)) field->alpine-field))))) - + (defn trigger-click-or-enter [m] (assoc m :hx-trigger "click, keyup[keyCode==13]")) (defn htmx-transition-appear [params] - (-> params + (-> params (update :class (fn [c] (-> (or c "") - (hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300"))))) - ) + (hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300")))))) diff --git a/src/clj/auto_ap/ssr/indicators.clj b/src/clj/auto_ap/ssr/indicators.clj index 9b6dea16..1976630c 100644 --- a/src/clj/auto_ap/ssr/indicators.clj +++ b/src/clj/auto_ap/ssr/indicators.clj @@ -7,7 +7,7 @@ [clj-time.core :as t])) (defn days-ago* [date] - (if date + (if date (let [start (c/to-date-time date) today (t/now)] @@ -33,4 +33,4 @@ {::route/days-ago (wrap-schema-enforce days-ago :query-schema [:map [:date {:optional false} - clj-date-schema ]])}) \ No newline at end of file + clj-date-schema]])}) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/invoice/common.clj b/src/clj/auto_ap/ssr/invoice/common.clj index 3081deb0..e56549f6 100644 --- a/src/clj/auto_ap/ssr/invoice/common.clj +++ b/src/clj/auto_ap/ssr/invoice/common.clj @@ -3,7 +3,7 @@ (def default-read '[:db/id :invoice/invoice-number :invoice/total - { :invoice/uploader [:user/name]} + {:invoice/uploader [:user/name]} :invoice/outstanding-balance :invoice/source-url :invoice/location diff --git a/src/clj/auto_ap/ssr/invoice/glimpse.clj b/src/clj/auto_ap/ssr/invoice/glimpse.clj index 85572188..83576d2a 100644 --- a/src/clj/auto_ap/ssr/invoice/glimpse.clj +++ b/src/clj/auto_ap/ssr/invoice/glimpse.clj @@ -32,9 +32,9 @@ (def bucket-name (:data-bucket env)) (defn lookup [tx] - (->> (:expense-documents tx) + (->> (:expense-documents tx) (mapcat :summary-fields) - (concat (->> tx :expense-documents )) + (concat (->> tx :expense-documents)) (map (fn [sf] (-> sf (update :label-detection dissoc :geometry) @@ -53,22 +53,22 @@ (clojure.string/replace c #"\W+" " ")) (defn deduplicate [xs] - (first - (reduce - (fn [[so-far seen-parsed?] [raw parsed]] - (if (seen-parsed? parsed) - [so-far seen-parsed?] - [(conj so-far [raw parsed]) - (conj seen-parsed? parsed)])) - [[] #{}] - xs))) + (first + (reduce + (fn [[so-far seen-parsed?] [raw parsed]] + (if (seen-parsed? parsed) + [so-far seen-parsed?] + [(conj so-far [raw parsed]) + (conj seen-parsed? parsed)])) + [[] #{}] + xs))) (defn textract->textract-invoice [request id tx] (let [lookup (lookup tx) valid-client-ids (extract-client-ids (:clients request) - (:client-id request) - (when (:client-code request) - [:client/code (:client-code request)])) + (:client-id request) + (when (:client-code request) + [:client/code (:client-code request)])) total-options (->> (stack-rank #{"AMOUNT_DUE"} lookup) (map (fn [t] [t (some->> t @@ -103,10 +103,10 @@ [(pull-attr (dc/db conn) :client/name c) c])))) vendor-name-options (->> (stack-rank #{"VENDOR_NAME"} lookup) (mapcat (fn [t] - (for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"}) - (filter (fn [d] (> (:score d) 2.0))) - (map (comp #(Long/parseLong %) :id)))] - [t m]))) + (for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"}) + (filter (fn [d] (> (:score d) 2.0))) + (map (comp #(Long/parseLong %) :id)))] + [t m]))) (deduplicate)) date-options (->> (stack-rank #{"INVOICE_RECEIPT_DATE" "ORDER_DATE" "DELIVERY_DATE"} lookup) (map (fn [t] @@ -120,22 +120,22 @@ [t t])) (deduplicate))] #:textract-invoice - {:db/id id - :textract-status "SUCCEEDED" - :total (first total-options) - :total-options (seq total-options) - :customer-identifier (first customer-identifier-options) - :customer-identifier-options (seq customer-identifier-options) - :location [nil ""] - :vendor-name (first vendor-name-options) - :vendor-name-options (seq vendor-name-options) - :date (first date-options) - :date-options (seq date-options) - :invoice-number (first invoice-number-options) - :invoice-number-options (seq invoice-number-options)})) + {:db/id id + :textract-status "SUCCEEDED" + :total (first total-options) + :total-options (seq total-options) + :customer-identifier (first customer-identifier-options) + :customer-identifier-options (seq customer-identifier-options) + :location [nil ""] + :vendor-name (first vendor-name-options) + :vendor-name-options (seq vendor-name-options) + :date (first date-options) + :date-options (seq date-options) + :invoice-number (first invoice-number-options) + :invoice-number-options (seq invoice-number-options)})) (defn upload-form* [] - [:div + [:div [:form.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer {:action (bidi/path-for ssr-routes/only-routes :invoice-glimpse-upload) @@ -144,7 +144,7 @@ "Drop an invoice here"] [:script (hiccup/raw - " + " invoice_dropzone = new Dropzone(\"#invoice\", { success: function(file, response) { window.location.href = file.xhr.responseURL; @@ -154,14 +154,14 @@ invoice_dropzone = new Dropzone(\"#invoice\", { }); ")]]) (defn customer-identifier-id->customer-identifier-client [[ci client]] - (when client + (when client (let [real-client (dc/pull (dc/db conn) [:client/name :db/id] client)] [ci [(:db/id real-client) (:client/name real-client)]]))) (defn vendor-name-tuple->vendor-tuple [[vn vendor]] - (when vendor + (when vendor (let [real-vendor (dc/pull (dc/db conn) [:vendor/name :db/id] vendor)] @@ -170,9 +170,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (defn get-job [id] (-> (dc/pull (dc/db conn) '[*] id) (update :textract-invoice/customer-identifier customer-identifier-id->customer-identifier-client) - (update :textract-invoice/customer-identifier-options #(map customer-identifier-id->customer-identifier-client %) ) + (update :textract-invoice/customer-identifier-options #(map customer-identifier-id->customer-identifier-client %)) (update :textract-invoice/vendor-name vendor-name-tuple->vendor-tuple) - (update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %) ))) + (update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %)))) (defn refresh-job [request id] (let [{:keys [:db/id :textract-invoice/job-id :textract-invoice/textract-status]} (get-job id)] @@ -185,7 +185,6 @@ invoice_dropzone = new Dropzone(\"#invoice\", { @(dc/transact conn [{:db/id id :textract-invoice/textract-status new-status}])))) (get-job id))) - (defn pill-list* [{:keys [selected options class ->text ->value id field]}] (let [options (->> options (filter (complement #{selected})) @@ -194,7 +193,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (com/pill {:color :secondary} (com/link {:hx-patch (str (bidi/path-for ssr-routes/only-routes :invoice-glimpse-update-textract-invoice :textract-invoice-id id) "?" (url/map->query {field (if ->value (->value x) (->text x))})) :hx-target "closest form" - :href "#"} (->text x)))]) ))] + :href "#"} (->text x)))])))] (when (seq options) [:div.col-span-6.col-start-1.text-xs "Alternates: " @@ -224,21 +223,21 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (format "%s (%s)" client-name customer-identifier)) :->value (fn [[client-identifier [id client-name]]] id)}) - -[:div.col-span-2.col-start-1 + + [:div.col-span-2.col-start-1 (com/field {:label "Location (blank will use default location)"} (com/text-input {:name "location" - :value (-> textract-invoice - :textract-invoice/location - second) - :placeholder "Location"}))] + :value (-> textract-invoice + :textract-invoice/location + second) + :placeholder "Location"}))] #_(pill-list* {:selected (:textract-invoice/location textract-invoice) - :options (:textract-invoice/location-options textract-invoice) - :id (:db/id textract-invoice) - :field "location" - :->text (fn [[_ amount]] - (str amount))}) - + :options (:textract-invoice/location-options textract-invoice) + :id (:db/id textract-invoice) + :field "location" + :->text (fn [[_ amount]] + (str amount))}) + [:div.col-span-6 (com/field {:label "Vendor"} (com/text-input {:name (path->name [:invoice/vendor]) @@ -269,9 +268,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", { :id (:db/id textract-invoice) :field "date" :->text (fn [[_ date]] - (-> date - (coerce/to-date-time) - (atime/unparse-local atime/normal-date)))}) + (-> date + (coerce/to-date-time) + (atime/unparse-local atime/normal-date)))}) [:div.col-span-2.col-start-1 (com/field {:label "Total"} (com/money-input {:name "total" @@ -284,7 +283,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { :id (:db/id textract-invoice) :field "total" :->text (fn [[_ amount]] - (str amount))}) + (str amount))}) [:div.col-span-2.col-start-1 (com/field {:label "Invoice Number"} (com/text-input {:name "invoice-number" @@ -311,11 +310,11 @@ invoice_dropzone = new Dropzone(\"#invoice\", { :hx-trigger "load delay:5s" :hx-swap "outerHTML"} "Analyzing job " (some-> textract-invoice - :textract-invoice/job-id - (subs 0 8)) "..."] + :textract-invoice/job-id + (subs 0 8)) "..."] (= "SUCCEEDED" (:textract-invoice/textract-status textract-invoice)) [:div.px-4 - + [:div.flex.flex-row.space-x-4 [:div {:style {:width "805"}} (com/card {} @@ -335,15 +334,15 @@ invoice_dropzone = new Dropzone(\"#invoice\", { :invoice-glimpse)} (com/button {:color :secondary} "New glimpse")]])] [:p.text-sm.italic "Import your invoices with the power of AI. Please only use PDFs with a single invoice in them."] - - (when id + + (when id (job-progress* request id)) - (when-not id + (when-not id (upload-form*))])]) (defn begin-textract-file [s3-location] (let [tempid (random-tempid) - + id (get-in @(dc/transact conn [{:db/id tempid :textract-invoice/textract-status "IN_PROGRESS" :textract-invoice/pdf-url (str "https://" bucket-name "/" s3-location)}]) @@ -364,7 +363,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { [_ invoice-number] (:textract-invoice/invoice-number textract-invoice) vendor (dc/pull (dc/db conn) d-vendors/default-read vendor-id) location (when (and client-id) - (or location + (or location (->> (dc/pull (dc/db conn) '[:client/locations] client-id) :client/locations first))) @@ -374,7 +373,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { due)] (alog/peek ::temp-textract-invoice textract-invoice) (when (and client-id date invoice-number vendor-id total) - (alog/peek ::TEMP-invoice + (alog/peek ::TEMP-invoice (cond-> {:db/id (random-tempid) :invoice/client client-id :invoice/client-identifier (first (:textract-invoice/customer-identifier textract-invoice)) @@ -382,7 +381,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { :invoice/invoice-number invoice-number :invoice/total total :invoice/date date - + :invoice/location location :invoice/import-status :import-status/imported :invoice/outstanding-balance total @@ -408,14 +407,14 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (get (:params request) "file"))] (mu/log ::uploading-file :file file) - (try + (try (let [s3-location (str "textract-files/" (UUID/randomUUID) "." (last (str/split (:filename file) #"[\\.]"))) _ (with-open [stream (io/input-stream (:tempfile file))] - (s3/put-object (:data-bucket env) - s3-location - stream - {:content-type "application/pdf" - :content-length (.length (:tempfile file))})) + (s3/put-object (:data-bucket env) + s3-location + stream + {:content-type "application/pdf" + :content-length (.length (:tempfile file))})) textract-invoice (begin-textract-file s3-location)] {:headers {"Location" (str (bidi/path-for ssr-routes/only-routes @@ -437,7 +436,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", { new-invoice-id (get-in @(dc/transact conn [[:propose-invoice new-invoice]]) [:tempids (:db/id new-invoice)]) _ (when new-invoice-id @(dc/transact conn [{:db/id (:db/id current-job) - :textract-invoice/invoice new-invoice-id}]))] + :textract-invoice/invoice new-invoice-id}]))] (if new-invoice-id (html-response (page* request nil) :headers {"hx-push-url" (bidi/path-for ssr-routes/only-routes :invoice-glimpse) @@ -456,36 +455,36 @@ invoice_dropzone = new Dropzone(\"#invoice\", { (mu/log ::method :method request-method) (base-page - request - (com/page {:nav com/main-aside-nav - :client-selection (:client-selection request) - :client (:client request) - :clients (:clients request) - :request request - :identity (:identity request) - :app-params {:hx-get (bidi/path-for ssr-routes/only-routes - :invoice-glimpse) - :hx-trigger "clientSelected from:body" - :hx-select "#app-contents" - :hx-swap "outerHTML swap:300ms"}} - (com/breadcrumbs {} - [:a {:href (bidi/path-for ssr-routes/only-routes - :admin)} - "Invoice"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :invoice-glimpse)} - "Glimpse"]) - (page* request (some-> request - :route-params - :textract-invoice-id - Long/parseLong))) + request + (com/page {:nav com/main-aside-nav + :client-selection (:client-selection request) + :client (:client request) + :clients (:clients request) + :request request + :identity (:identity request) + :app-params {:hx-get (bidi/path-for ssr-routes/only-routes + :invoice-glimpse) + :hx-trigger "clientSelected from:body" + :hx-select "#app-contents" + :hx-swap "outerHTML swap:300ms"}} + (com/breadcrumbs {} + [:a {:href (bidi/path-for ssr-routes/only-routes + :admin)} + "Invoice"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :invoice-glimpse)} + "Glimpse"]) + (page* request (some-> request + :route-params + :textract-invoice-id + Long/parseLong))) - "Invoice Glimpse")) + "Invoice Glimpse")) (defn textract-invoice [request] (if (get-in request [:headers "hx-request"]) (html-response (job-progress* request (some-> request - :route-params - :textract-invoice-id - Long/parseLong))) + :route-params + :textract-invoice-id + Long/parseLong))) (page request))) diff --git a/src/clj/auto_ap/ssr/invoice/import.clj b/src/clj/auto_ap/ssr/invoice/import.clj index b44a2f47..dfabd346 100644 --- a/src/clj/auto_ap/ssr/invoice/import.clj +++ b/src/clj/auto_ap/ssr/invoice/import.clj @@ -2,9 +2,9 @@ (: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]] + :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] @@ -24,24 +24,23 @@ [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]) + :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]) (: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"} @@ -108,7 +107,6 @@ :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) @@ -131,8 +129,6 @@ (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]]} @@ -144,7 +140,6 @@ '[?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)]]} @@ -155,7 +150,6 @@ '[(<= ?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]]} @@ -232,7 +226,6 @@ (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)) @@ -307,7 +300,6 @@ (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) - :else selected) ids (->> (dc/q '[:find ?i @@ -318,29 +310,27 @@ (map first))] ids)) -(def upload-schema - [:map +(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 + [:force-chatgpt {:optional true :default false} + [:maybe [:boolean {:decode/string {:enter #(if (= % "on") true - (boolean %))}}]]] + (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.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]] [:div#page-notification.notification.block {:style {:display "none"}}] - - + [:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import-file) @@ -351,7 +341,7 @@ (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)} @@ -366,7 +356,7 @@ (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}))) @@ -382,15 +372,15 @@ :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) + (com/validated-field {:errors (fc/field-errors) :label " "} (com/checkbox {:name (fc/field-name) - :error? (fc/error?) } + :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}) + {:x-data (hx/json {"files" nil + "hovering" false}) ":class" "{'bg-blue-100': !hovering, 'border-blue-300': !hovering, 'text-blue-700': !hovering, @@ -399,8 +389,6 @@ 'text-green-700': hovering }" :x-ref "box"} - - [:input {:type "file" :name "file" @@ -410,13 +398,12 @@ :x-on:dragover "hovering = true", :x-on:dragleave "hovering = false", :x-on:drop "hovering = false"}] - [:div.flex.flex-col.space-2 - [:div + [: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"}) ] - ]]] - + [: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")]])) @@ -435,14 +422,12 @@ :query-schema query-schema :action-buttons (fn [request] (let [[_ _ outstanding total] (:page-results request)] - [ - (when (can? (:identity request) {:subject :invoice :activity :import}) + [(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 " - } + ":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)) @@ -463,8 +448,8 @@ (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))} + ::route/approve + :db/id (:db/id entity))} svg/thumbs-up))]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} @@ -515,11 +500,11 @@ (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)) + (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 @@ -528,13 +513,13 @@ (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)) + (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) + + (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"}) @@ -542,51 +527,49 @@ (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) ) + 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." + :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 + (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 [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." + :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)) - ] - )) + 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 + (if vendor-search (throw (ex-info (format "No vendor found. Searched for '%s'. Please supply an forced vendor." vendor-search) {:vendor-code vendor-code})) @@ -594,10 +577,10 @@ {: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) + {:find ['?vendor] + :in ['$ '?vendor-name] + :where ['[?vendor :vendor/name ?vendor-name]]} + (dc/db conn) vendor-code) first first))] (when-not vendor-id @@ -605,9 +588,9 @@ {: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) + {:find [(list 'pull '?vendor-id d-vendors/default-read)] + :in ['$ '?vendor-id]} + (dc/db conn) vendor-id) first first)] matching-vendor @@ -617,7 +600,7 @@ (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 + (when-not date (throw (Exception. "Couldn't parse date from file."))) (let [matching-client (cond client-override client-override @@ -629,9 +612,9 @@ :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) @@ -658,22 +641,20 @@ (defn validate-invoice [invoice user] (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 + 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."))) (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.") - ) - + (do + (alog/warn ::cant-see-client :invoice invoice) + (assoc invoice :error-message "No access for the client in this file.")) + (seq missing-keys) - (do + (do (alog/warn ::mising-keys :keys missing-keys) (assoc invoice :error-message (str "Missing the key " missing-keys))) :else @@ -686,33 +667,31 @@ 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 - ) - errored-invoices (->> potential-invoices + admin-only-if-multiple-clients) + 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])))] - + (mapv (fn [i] [:propose-invoice 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))}))) + :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 :errored-invoices errored-invoices :successful-invoices successful-invoices @@ -730,7 +709,7 @@ "text/csv" "application/pdf") :content-length (.length tempfile)}) - imports (->> (if force-chatgpt + imports (->> (if force-chatgpt (parse/glimpse2 (.getPath tempfile)) (parse/parse-file (.getPath tempfile) filename :allow-glimpse? true)) (map #(assoc % @@ -740,16 +719,16 @@ :source-url (str "https://" (:data-bucket env) "/" s3-location))))] - (try - + (try + (import-uploaded-invoice identity imports) (catch Exception e (alog/warn ::couldnt-import-upload :error e - :template (:template ( first imports))) + :template (:template (first imports))) (throw (ex-info (ex-message e) - {:template (:template ( first imports)) + {:template (:template (first imports)) :sample (first imports)} e))))) (catch Exception e @@ -767,23 +746,21 @@ (fn [result {:keys [filename tempfile]}] (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)) ) - - (-> result - (update :error? #(or % + (alog/info ::failure-error-count :count (count (:errored-invoices i))) + + (-> 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 + :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)]) ]] + [:li (:error-message e)])]] :template (:template (first (:imports i)))}))) (catch Exception e (-> result @@ -793,11 +770,10 @@ :response (.getMessage e) :sample (:sample (ex-data e)) :template (:template (ex-data e))}))))) - {:error? false - :files [] - } + {:error? false + :files []} file)] - + (html-response [:div#page-notification.p-4.rounded-lg [:table [:thead @@ -835,34 +811,34 @@ {"hx-trigger" "invalidated"}))) #_(defn wrap-test [handler] - (fn [request] - (clojure.pprint/pprint (:multipart-params request)) - (handler request ))) + (fn [request] + (clojure.pprint/pprint (:multipart-params request)) + (handler request))) (def key->handler - (apply-middleware-to-all-handlers + (apply-middleware-to-all-handlers {::route/import-page (-> (helper/page-route grid-page) (wrap-implied-route-param :status nil)) - ::route/import-table + ::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]])) + (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)) + (wrap-schema-enforce :form-schema query-schema)) ::route/import-file (-> import-file (wrap-schema-enforce :multipart-schema upload-schema))} (fn [a] - (-> a + (-> a (wrap-must {:subject :invoice :activity :import}))))) 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 c5c8859a..d7ed7829 100644 --- a/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj +++ b/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj @@ -1,7 +1,7 @@ (ns auto-ap.ssr.invoice.new-invoice-wizard (:require [auto-ap.datomic - :refer [audit-transact conn pull-attr]] + :refer [audit-transact conn pull-attr]] [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 @@ -9,7 +9,7 @@ [auto-ap.logging :as alog] [auto-ap.routes.invoice :as route] [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated]] + :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]] @@ -21,10 +21,10 @@ [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [->db-id apply-middleware-to-all-handlers check-allowance - check-location-belongs clj-date-schema entity-id - form-validation-error html-response money strip - wrap-schema-enforce]] + :refer [->db-id apply-middleware-to-all-handlers check-allowance + check-location-belongs clj-date-schema 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] @@ -48,10 +48,6 @@ [:vendor-terms-override/client :vendor-terms-override/terms]}] vendor-id)) - - - - (defn check-vendor-default-account [vendor-id] (some? (:vendor/default-account (get-vendor vendor-id)))) @@ -83,7 +79,7 @@ [:invoice-expense-account/location :string] [:invoice-expense-account/amount :double]] [:fn {:error/fn (fn [r x] (:type r)) - :error/path [:invoice-expense-account/location]} + :error/path [:invoice-expense-account/location]} (fn [iea] (check-location-belongs (:invoice-expense-account/location iea) (:invoice-expense-account/account iea)))]]]]]) @@ -95,7 +91,6 @@ true (and invoice-number vendor client)))]]) - (defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id] (if (nil? vendor) nil @@ -142,7 +137,6 @@ {:value (name :customize) :content [:div "Customize accounts"]}])})))) - (defrecord BasicDetailsStep [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -181,7 +175,6 @@ (com/hidden {:name (fc/field-name) :value (fc/field-value)}))) - (fc/with-field :customize-due-and-scheduled? (com/hidden {:name (fc/field-name) :value (fc/field-value) @@ -220,11 +213,10 @@ [:div.mb-4 ;; TODO DO NOT MERGE UNTIL THIS IS FIXED #_[:span.text-sm.text-gray-500 "Can't find the vendor? " - (com/link {:href ... - :target "new"} - "Add new vendor") - " in a new window, then return here."]] - + (com/link {:href ... + :target "new"} + "Add new vendor") + " in a new window, then return here."]] [:div.flex.items-center.gap-2 (fc/with-field :invoice/date @@ -333,7 +325,6 @@ #_(mm/navigate-handler {:request request :to-step :next-steps})))) - (defn location-select* [{:keys [name account-location client-locations value]}] (let [options (into (cond account-location @@ -433,8 +424,8 @@ (filter number?) (reduce + 0.0)) balance (- - (-> request :multi-form-state :snapshot :invoice/total) - total)] + (-> request :multi-form-state :snapshot :invoice/total) + total)] [:span {:class (when-not (dollars= 0.0 balance) "text-red-300")} (format "$%,.2f" balance)])) @@ -569,7 +560,6 @@ nil :validation-route ::route/new-wizard-navigate))) - (defn assert-no-conflicting [{:invoice/keys [invoice-number client vendor] :db/keys [id]}] (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice-number :invoice/vendor (->db-id vendor) @@ -577,13 +567,11 @@ :db/id id})) (form-validation-error (str "Invoice '" invoice-number "' already exists.")))) - (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 ")"))))) - (defn- calculate-spread "Helper function to calculate the amount to be assigned to each location" [shared-amount total-locations] @@ -592,7 +580,6 @@ {:base-amount base-amount :remainder remainder})) - (defn- spread-expense-account "Spreads the expense account amount across the given locations" [locations expense-account] @@ -628,7 +615,6 @@ (update first-eas :invoice-expense-account/amount #(+ % leftover)) rest)))) - (defn maybe-spread-locations "Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client" ([invoice] @@ -643,8 +629,6 @@ (apply-total-delta-to-account ($->cents (:invoice/total invoice))) (map (fn [ea] (update ea :invoice-expense-account/amount cents->$)))))))) - - (defrecord NewWizard2 [_ current-step] mm/LinearModalWizard (hydrate-from-request @@ -728,13 +712,12 @@ (exception->4xx #(assert-not-locked client-id (:invoice/date invoice))) (let [transaction-result (audit-transact [transaction] (:identity request))] - (try + (try (solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"])) (catch Exception e - (alog/error ::cant-save-solr - :error e - )) - ) + (alog/error ::cant-save-solr + :error e))) + (if extant? (html-response @@ -750,7 +733,6 @@ (def new-wizard (->NewWizard2 nil nil)) - (defn initial-new-wizard-state [request] (mm/->MultiStepFormState {:invoice/date (time/now) :customize-accounts :default} @@ -758,9 +740,6 @@ {:invoice/date (time/now) :customize-accounts :default})) - - - (defn initial-edit-wizard-state [request] (let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request))) entity (select-keys entity (mut/keys new-form-schema))] @@ -779,7 +758,6 @@ :client-locations (some->> client-id (pull-attr (dc/db conn) :client/locations))}))) - (defn due-date [{:keys [multi-form-state]}] (let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state))) (->db-id (:invoice/client (:step-params multi-form-state)))) @@ -816,7 +794,6 @@ :error? false :placeholder "1/1/2024"})))) - (defn account-prediction [{:keys [multi-form-state form-errors] :as request}] (html-response (account-prediction* request))) diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index ce68ed83..6c8e06da 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -1,9 +1,9 @@ (ns auto-ap.ssr.invoices (:require [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-3 - audit-transact audit-transact-batch conn merge-query - observable-query pull-many]] + :refer [add-sorter-fields apply-pagination apply-sort-3 + audit-transact audit-transact-batch conn merge-query + observable-query pull-many]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.bank-accounts :as d-bank-accounts] [auto-ap.datomic.clients :as d-clients] @@ -23,7 +23,7 @@ [auto-ap.routes.payments :as payment-route] [auto-ap.routes.transactions :as transaction-routes] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.rule-matching :as rm] [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] @@ -41,13 +41,13 @@ [auto-ap.ssr.components.date-range :as dr] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers assert-schema - clj-date-schema dissoc-nil-transformer entity-id - form-validation-error html-response main-transformer - many-entity modal-response money percentage - ref->enum-schema round-money strip wrap-entity - wrap-implied-route-param wrap-merge-prior-hx - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers assert-schema + clj-date-schema dissoc-nil-transformer entity-id + form-validation-error html-response main-transformer + many-entity modal-response money percentage + ref->enum-schema round-money strip wrap-entity + wrap-implied-route-param wrap-merge-prior-hx + wrap-schema-enforce]] [auto-ap.time :as atime] [auto-ap.utils :refer [by dollars-0? dollars=]] [bidi.bidi :as bidi] @@ -63,7 +63,6 @@ [malli.util :as mut] [slingshot.slingshot :refer [try+]])) - (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"} @@ -105,9 +104,9 @@ (:db/id (:client request)))) :class "filter-trigger"})) (dr/date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range" - :apply-button? true}) + :end (:end-date (:query-params request))} + :id "date-range" + :apply-button? true}) (com/field {:label "Check #"} (com/text-input {:name "check-number" :id "check-number" @@ -122,7 +121,7 @@ :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" @@ -143,7 +142,6 @@ :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) @@ -165,7 +163,6 @@ (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]]} @@ -177,7 +174,6 @@ '[?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)]]} @@ -188,14 +184,13 @@ '[(<= ?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)]}) (not (:import-status query-params)) - (merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} }) + (merge-query {:query {:where ['[?e :invoice/import-status :import-status/imported]]}}) (:status route-params) (merge-query {:query {:in ['?status] @@ -269,7 +264,6 @@ (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)) @@ -279,31 +273,30 @@ refunds)) (defn sum-outstanding [ids] - - (->> - (dc/q {:find ['?id '?o] - :in ['$ '[?id ...]] - :where ['[?id :invoice/outstanding-balance ?o]]} - (dc/db conn) - 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))) + + (->> + (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) @@ -351,7 +344,6 @@ (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) - :else selected)] ids)) @@ -369,21 +361,20 @@ (defn can-undo-autopayment [invoice] (try+ - (assert-can-undo-autopayment invoice) + (assert-can-undo-autopayment invoice) true (catch [:type :warning] {} - false))) - + false))) (defn pay-button* [params] (let [ids (:ids params) - ids (if (seq ids) + ids (if (seq ids) (map first - (dc/q '[:find ?i - :in $ [?i ...] - :where (not [?i :invoice/scheduled-payment])] - (dc/db conn) - ids)) + (dc/q '[:find ?i + :in $ [?i ...] + :where (not [?i :invoice/scheduled-payment])] + (dc/db conn) + ids)) ids) selected-client-count (if (seq ids) (ffirst @@ -417,18 +408,17 @@ outstanding-balances) total (reduce + 0.0 vendor-totals) paying-credit? (and (> (count ids) 1) - (= 1 (count vendor-totals)) - at-least-one-positive-payment - (dollars-0? total))] - + (= 1 (count vendor-totals)) + at-least-one-positive-payment + (dollars-0? total))] [:div (cond-> {:hx-target "this" - + :hx-trigger "click from:#pay-button" :x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}"} paying-credit? (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/pay-using-credit)) - (not paying-credit? ) (assoc :hx-get (bidi/path-for ssr-routes/only-routes - ::route/pay-wizard))) + (not paying-credit?) (assoc :hx-get (bidi/path-for ssr-routes/only-routes + ::route/pay-wizard))) (com/button {:color :primary :id "pay-button" :disabled (or (= (count (:ids params)) 0) @@ -445,14 +435,13 @@ (cond paying-credit? "Pay invoices using credit" - + (> (count ids) 0) - + (format "Pay %d invoices ($%,.2f)" (count ids) (or total 0.0)) - - + (or (= 0 (count ids)) (> selected-client-count 1)) (list "Pay " (com/badge {} "!")) @@ -474,13 +463,11 @@ :else [:div "Click to choose a bank account"])]])) - (defn pay-button [request] (html-response (pay-button* {:ids (selected->ids request (:query-params request))}))) - ;; TODO test as a real user (def grid-page (helper/build {:id "entity-table" @@ -493,9 +480,9 @@ :oob-render (fn [request] [(assoc-in (dr/date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range" - :apply-button? true}) [1 :hx-swap-oob] true) + :end (:end-date (:query-params request))} + :id "date-range" + :apply-button? true}) [1 :hx-swap-oob] true) (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) :query-schema query-schema :parse-query-params (fn [p] @@ -547,12 +534,11 @@ :db/id (:db/id entity))} svg/undo)) (when (and (can? (:identity request) {:subject :invoice :activity :edit}) - (can-undo-autopayment entity) - ) + (can-undo-autopayment entity)) (com/button {:hx-put (bidi/path-for ssr-routes/only-routes - ::route/undo-autopay - :db/id (:db/id entity))} - "Undo autopay"))]) + ::route/undo-autopay + :db/id (:db/id entity))} + "Undo autopay"))]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} "Invoices"]] @@ -573,7 +559,7 @@ (= 1 (count (:client/locations (:client args)))))) :render (fn [x] [:div.flex.items-center.gap-2 (-> x :invoice/client :client/name) (map #(com/pill {:color :primary} (-> % :invoice-expense-account/location)) - (:invoice/expense-accounts x)) ])} + (:invoice/expense-accounts x))])} {:key "vendor" :name "Vendor" :sort-key "vendor" @@ -593,13 +579,12 @@ :name "Due" :show-starting "xl" ;; xl:table-cell :render (fn [{:invoice/keys [due]}] - (if-let [due-date (some-> due (atime/unparse-local atime/normal-date)) ] - (let [ - today (time/now) + (if-let [due-date (some-> due (atime/unparse-local atime/normal-date))] + (let [today (time/now) [start end] (if (time/before? due today) [due today] [today due]) - i (time/interval start end ) + i (time/interval start end) days (if (time/before? due today) (- (time/in-days i)) (time/in-days i))] @@ -607,23 +592,23 @@ [:div.text-primary-700 "today"] (> days 0) [:div.text-primary-700 (format "in %d days", days)] - :else - [:div.text-red-700 (format "%d days ago", (- days))]))))} + :else + [:div.text-red-700 (format "%d days ago", (- days))]))))} {:key "status" :name "Status" :render (fn [{:invoice/keys [status scheduled-payment]}] (cond (= status :invoice-status/paid) - (com/pill {:color :primary} "Paid") - (= status :invoice-status/voided) - (com/pill {:color :red} "Voided") - - scheduled-payment - (com/pill {:color :yellow} "Scheduled") + (com/pill {:color :primary} "Paid") + (= status :invoice-status/voided) + (com/pill {:color :red} "Voided") - (= status :invoice-status/unpaid) - (com/pill {:color :secondary} "Unpaid") - :else - ""))} + scheduled-payment + (com/pill {:color :yellow} "Scheduled") + + (= status :invoice-status/unpaid) + (com/pill {:color :secondary} "Unpaid") + :else + ""))} {:key "accounts" :name "Account" :show-starting "lg" @@ -656,32 +641,32 @@ :class "w-8" :render (fn [i] (link-dropdown - (into [] - (concat (->> i - :invoice/payments - (map :invoice-payment/payment) - (filter (fn [p] - (not= :payment-status/voided - (:payment/status p)))) - (mapcat (fn [p] - (cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes - ::payment-route/all-page) - {:exact-match-id (:db/id p)}) - :content (str (format "$%,.2f" (:payment/amount p)) - (some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}] - (:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page) - {:exact-match-id (:db/id (first (:payment/transaction p)))}) - :color :secondary - :content "Transaction"}))))) - (when (:invoice/journal-entry i) - [{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page) - {:exact-match-id (:db/id (first (:invoice/journal-entry i)))}) - :color :yellow - :content "Ledger entry"}]) - (when (:invoice/source-url i) - [{:link (:invoice/source-url i) - :color :secondary - :content "File"}])))))}]})) + (into [] + (concat (->> i + :invoice/payments + (map :invoice-payment/payment) + (filter (fn [p] + (not= :payment-status/voided + (:payment/status p)))) + (mapcat (fn [p] + (cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes + ::payment-route/all-page) + {:exact-match-id (:db/id p)}) + :content (str (format "$%,.2f" (:payment/amount p)) + (some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}] + (:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page) + {:exact-match-id (:db/id (first (:payment/transaction p)))}) + :color :secondary + :content "Transaction"}))))) + (when (:invoice/journal-entry i) + [{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page) + {:exact-match-id (:db/id (first (:invoice/journal-entry i)))}) + :color :yellow + :content "Ledger entry"}]) + (when (:invoice/source-url i) + [{:link (:invoice/source-url i) + :color :secondary + :content "File"}])))))}]})) (def row* (partial helper/row* grid-page)) @@ -722,18 +707,16 @@ (defn undo-autopay [{:as request :keys [identity entity]}] (let [invoice entity id (:db/id entity) - _ (assert-can-see-client identity (:db/id (:invoice/client invoice))) - ] + _ (assert-can-see-client identity (:db/id (:invoice/client invoice)))] (alog/info ::undoing-autopay :transaction :tx) (assert-can-undo-autopayment invoice) - (audit-transact - [[:upsert-invoice {:db/id id - :invoice/status :invoice-status/unpaid - :invoice/outstanding-balance (:invoice/total entity) - :invoice/scheduled-payment nil}]] + (audit-transact + [[:upsert-invoice {:db/id id + :invoice/status :invoice-status/unpaid + :invoice/outstanding-balance (:invoice/total entity) + :invoice/scheduled-payment nil}]] identity) - (html-response (row* identity (dc/pull (dc/db conn) default-read id) {:flash? true :request request}) @@ -847,8 +830,6 @@ id) (count all-ids))) - - (defn bulk-delete-dialog-confirm [request] (alog/peek (:form-params request)) (let [ids (selected->ids request (:form-params request)) @@ -861,8 +842,7 @@ (count ids))})}))) #_(defn pay-invoices-from-balance [context {invoices :invoices - client-id :client_id} _] - ) + client-id :client_id} _]) (defn pay-using-credit [request] (alog/peek (:form-params request)) @@ -896,7 +876,6 @@ 0.001)) invoices) - total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid)) _ (when (<= total-to-pay 0.001) (throw (ex-info "Select some invoices that need to be paid" {:type :form-validation}))) @@ -926,8 +905,6 @@ [total-to-pay []]))) (into {})) - - vendor-id (:db/id (:invoice/vendor (first invoices))) payment {:db/id (str vendor-id) :payment/amount total-to-pay @@ -949,7 +926,6 @@ :notification (format "Successfully paid %d invoices." (count invoices))})}))) - (defn does-amount-exceed-outstanding? [amount outstanding-balance] (let [outstanding-balance (round-money outstanding-balance) amount (round-money amount)] @@ -1018,12 +994,11 @@ :to (mm/encode-step-key :payment-details)})} "Credit") (com/button {:x-ref "button" - "@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})" } + "@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})"} "Pay")) - [:template { :x-ref "tooltip"} - [:div.flex.flex-col.gap-2 { - :data-key "vis" - :class "p-4 w-max" } + [:template {:x-ref "tooltip"} + [:div.flex.flex-col.gap-2 {:data-key "vis" + :class "p-4 w-max"} (when (= :bank-account-type/check (:bank-account/type bank-account)) (com/button {:color :primary @@ -1094,7 +1069,6 @@ :can-handwrite? can-handwrite? :credit-only? credit-only?})) - (defn can-handwrite? [invoices] (let [selected-vendors (set (map (comp :db/id :invoice/vendor) invoices))] (and @@ -1110,7 +1084,6 @@ (reduce + 0.0 (map :invoice/outstanding-balance is)))) (every? #(<= % 0.0)))) - (defrecord ChoosePaymentMethodModal [linear-wizard] mm/ModalWizardStep (step-name [_] @@ -1195,7 +1168,7 @@ (format "Pay in full ($%,.2f)" total)))} {:value "advanced" :content "Customize payments"}]}) - + [:div.space-y-4 (fc/with-field :invoices (com/validated-field @@ -1345,7 +1318,6 @@ (:snapshot multi-form-state) mt/strip-extra-keys-transformer) - _ (assert-schema payment-form-schema snapshot) _ (exception->4xx @@ -1354,7 +1326,7 @@ (= "" (:check-number snapshot))) (throw (Exception. "Check number is required"))) true)) - + result (exception->4xx #(do (when (:handwritten-date snapshot) @@ -1378,7 +1350,7 @@ :payment-type/credit :else :payment-type/debit) identity - (:handwritten-date snapshot)) + (:handwritten-date snapshot)) (catch Exception e (println e))))))] (modal-response @@ -1455,11 +1427,10 @@ (defn redirect-handler [target-route] (fn handle [request] {:status 302 - :headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes + :headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes target-route) (:query-params request)))}})) - (defn initial-bulk-edit-state [request] (mm/->MultiStepFormState {:search-params (:query-params request) :expense-accounts [{:db/id "123" @@ -1479,7 +1450,7 @@ (com/typeahead {:name name :placeholder "Search..." :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) - { :purpose "invoice"}) + {:purpose "invoice"}) :id name :x-model x-model :value value @@ -1487,8 +1458,6 @@ (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) client-id)))})]) - - ;; TODO clientize (defn all-ids-not-locked [all-ids] (->> all-ids @@ -1527,7 +1496,7 @@ (com/validated-field {:errors (fc/field-errors) :x-hx-val:account-id "accountId" - :hx-vals (hx/json {:name (fc/field-name) }) + :hx-vals (hx/json {:name (fc/field-name)}) :x-dispatch:changed "accountId" :hx-trigger "changed" :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) @@ -1536,7 +1505,7 @@ (location-select* {:name (fc/field-name) :account-location (:account/location (cond->> (:account @value) (nat-int? (:account @value)) (dc/pull (dc/db conn) - '[:account/location]))) + '[:account/location]))) :value (fc/field-value)})))) (fc/with-field :percentage (com/data-grid-cell @@ -1544,10 +1513,10 @@ (com/validated-field {:errors (fc/field-errors)} (com/money-input {:name (fc/field-name) - :class "w-16 amount-field" - :value (some-> (fc/field-value) - (* 100) - (long))})))) + :class "w-16 amount-field" + :value (some-> (fc/field-value) + (* 100) + (long))})))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) @@ -1587,7 +1556,7 @@ :hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-new-account) :row-offset 0 - :index (count (fc/field-value)) } + :index (count (fc/field-value))} "New account") (com/data-grid-row {} (com/data-grid-cell {}) @@ -1617,7 +1586,6 @@ :next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save")) :validation-route ::route/new-wizard-navigate)))) - (defn maybe-code-accounts [invoice account-rules valid-locations] (with-precision 2 (let [accounts (vec (mapcat @@ -1667,9 +1635,9 @@ (navigate [this step-key] (assoc this :current-step step-key)) (get-current-step [this] - (if current-step - (mm/get-step this current-step) - (mm/get-step this :accounts))) + (if current-step + (mm/get-step this current-step) + (mm/get-step this :accounts))) (render-wizard [this {:keys [multi-form-state] :as request}] (mm/default-render-wizard this request @@ -1683,44 +1651,43 @@ (get-step [this step-key] (let [step-key-result (mc/parse mm/step-key-schema step-key) [step-key-type step-key] step-key-result] - (get {:accounts (->AccountsStep this) } + (get {:accounts (->AccountsStep this)} step-key))) (form-schema [_] (mc/schema [:map [:expense-accounts - (many-entity {:min 1} - [:account entity-id] - [:location [:string {:min 1 :error/message "required"}]] - [:percentage percentage])]])) + (many-entity {:min 1} + [:account entity-id] + [:location [:string {:min 1 :error/message "required"}]] + [:percentage percentage])]])) (submit [this {:keys [multi-form-state request-method identity] :as request}] - (let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state))) - all-ids (all-ids-not-locked selected-ids) - invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids)) ] -(assert-percentages-add-up (:snapshot multi-form-state)) - - (doseq [a (-> multi-form-state :snapshot :expense-accounts) - :let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]] - (when (and location (not= location (:location a))) - (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] - (throw (ex-info err {:validation-error err}))))) - (alog/info ::bulk-code :count (count all-ids)) - (audit-transact-batch - (map (fn [i] - [:upsert-invoice {:db/id (:db/id i) - :invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}]) - invoices) - (:identity request)) - - (html-response - [:div] - :headers (cond-> {"hx-trigger" (hx/json { "modalclose" "" - "invalidated" "" - "notification" (str "Successfully coded " (count all-ids) " invoices.")}) - "hx-reswap" "outerHTML"}))))) + (let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state))) + all-ids (all-ids-not-locked selected-ids) + invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids))] + (assert-percentages-add-up (:snapshot multi-form-state)) + + (doseq [a (-> multi-form-state :snapshot :expense-accounts) + :let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]] + (when (and location (not= location (:location a))) + (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)] + (throw (ex-info err {:validation-error err}))))) + (alog/info ::bulk-code :count (count all-ids)) + (audit-transact-batch + (map (fn [i] + [:upsert-invoice {:db/id (:db/id i) + :invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}]) + invoices) + (:identity request)) + + (html-response + [:div] + :headers (cond-> {"hx-trigger" (hx/json {"modalclose" "" + "invalidated" "" + "notification" (str "Successfully coded " (count all-ids) " invoices.")}) + "hx-reswap" "outerHTML"}))))) (def bulk-edit-wizard (->BulkEditWizard nil nil)) - (defn bulk-edit-total* [request] (let [total (->> (-> request :multi-form-state @@ -1740,7 +1707,7 @@ (filter number?) (reduce + 0.0)) balance (- 100.0 - (* 100.0 total))] + (* 100.0 total))] [:span {:class (when-not (dollars= 0.0 balance) "text-red-300")} (format "%.1f%%" balance)])) @@ -1769,31 +1736,31 @@ ::route/legacy-voided-invoices (redirect-handler ::route/voided-page) ::route/legacy-new-invoice (redirect-handler ::route/new-wizard) ::route/bulk-edit (-> mm/open-wizard-handler - (mm/wrap-wizard bulk-edit-wizard) - (mm/wrap-init-multi-form-state initial-bulk-edit-state)) -::route/bulk-edit-submit (-> mm/submit-handler - (mm/wrap-wizard bulk-edit-wizard) - (mm/wrap-decode-multi-form-state) - (wrap-must {:subject :invoice :activity :bulk-edit})) -::route/bulk-edit-total (-> bulk-edit-total - (mm/wrap-wizard bulk-edit-wizard) - (mm/wrap-decode-multi-form-state) - (wrap-must {:subject :invoice :activity :bulk-edit})) -::route/bulk-edit-balance (-> bulk-edit-balance - - (mm/wrap-wizard bulk-edit-wizard) - (mm/wrap-decode-multi-form-state) - (wrap-must {:subject :invoice :activity :bulk-edit})) -::route/bulk-edit-new-account (-> - (add-new-entity-handler [:step-params :expense-accounts] - (fn render [cursor request] - (bulk-edit-account-row* - {:value cursor })) - (fn build-new-row [base _] - (assoc base :invoice-expense-account/location "Shared"))) - (wrap-schema-enforce :query-schema [:map - [:client-id {:optional true} - [:maybe entity-id]]])) + (mm/wrap-wizard bulk-edit-wizard) + (mm/wrap-init-multi-form-state initial-bulk-edit-state)) + ::route/bulk-edit-submit (-> mm/submit-handler + (mm/wrap-wizard bulk-edit-wizard) + (mm/wrap-decode-multi-form-state) + (wrap-must {:subject :invoice :activity :bulk-edit})) + ::route/bulk-edit-total (-> bulk-edit-total + (mm/wrap-wizard bulk-edit-wizard) + (mm/wrap-decode-multi-form-state) + (wrap-must {:subject :invoice :activity :bulk-edit})) + ::route/bulk-edit-balance (-> bulk-edit-balance + + (mm/wrap-wizard bulk-edit-wizard) + (mm/wrap-decode-multi-form-state) + (wrap-must {:subject :invoice :activity :bulk-edit})) + ::route/bulk-edit-new-account (-> + (add-new-entity-handler [:step-params :expense-accounts] + (fn render [cursor request] + (bulk-edit-account-row* + {:value cursor})) + (fn build-new-row [base _] + (assoc base :invoice-expense-account/location "Shared"))) + (wrap-schema-enforce :query-schema [:map + [:client-id {:optional true} + [:maybe entity-id]]])) ::route/undo-autopay (-> undo-autopay (wrap-entity [:route-params :db/id] default-read) diff --git a/src/clj/auto_ap/ssr/ledger.clj b/src/clj/auto_ap/ssr/ledger.clj index f89ac592..4a24f42d 100644 --- a/src/clj/auto_ap/ssr/ledger.clj +++ b/src/clj/auto_ap/ssr/ledger.clj @@ -1,8 +1,8 @@ (ns auto-ap.ssr.ledger (:require [auto-ap.datomic - :refer [audit-transact audit-transact-batch conn pull-many - remove-nils]] + :refer [audit-transact audit-transact-batch conn pull-many + remove-nils]] [auto-ap.datomic.accounts :as a] [auto-ap.graphql.utils :refer [assert-admin assert-can-see-client exception->notification notify-if-locked]] @@ -11,7 +11,7 @@ [auto-ap.query-params :refer [wrap-copy-qp-pqp]] [auto-ap.routes.ledger :as route] [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated]] + :refer [wrap-client-redirect-unauthenticated]] [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] @@ -30,11 +30,11 @@ [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers clj-date-schema - html-response main-transformer money strip - wrap-form-4xx-2 wrap-implied-route-param - wrap-merge-prior-hx wrap-schema-decode - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers clj-date-schema + html-response main-transformer money strip + wrap-form-4xx-2 wrap-implied-route-param + wrap-merge-prior-hx wrap-schema-decode + wrap-schema-enforce]] [auto-ap.time :as atime] [auto-ap.utils :refer [dollars=]] [bidi.bidi :as bidi] @@ -50,8 +50,6 @@ [malli.core :as mc] [slingshot.slingshot :refer [throw+]])) - - (comment (mc/decode query-schema {:start " "} @@ -67,14 +65,10 @@ (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) - :else selected)] ids)) - - - (defn delete [{invoice :entity :as request identity :identity}] (exception->notification #(when-not (= :invoice-status/unpaid (:invoice/status invoice)) @@ -101,10 +95,9 @@ identity) (html-response (ledger.common/row* (:identity request) (dc/pull (dc/db conn) default-read (:db/id invoice)) - {:class "live-removed"}) + {:class "live-removed"}) :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))})) - (defn wrap-ensure-bank-account-belongs [handler] (fn [{:keys [query-params client] :as request}] (let [bank-account-belongs? (get (set (map :db/id (:client/bank-accounts client))) @@ -131,7 +124,7 @@ (clojure.pprint/pprint (fc/field-errors)) (when (seq (fc/field-value)) - [:div {:x-data (hx/json { "showTable" false})} + [:div {:x-data (hx/json {"showTable" false})} [:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-import) :autocomplete "off"} (when (:just-parsed? request) @@ -140,7 +133,7 @@ [:div.inline-flex.gap-2 (->> (:form-errors request) :table - ( #(if (map? %) ( vals %) %)) + (#(if (map? %) (vals %) %)) (mapcat identity) (group-by last) (map (fn [[k v]] @@ -148,12 +141,12 @@ (com/pill {:color :yellow} (format "%d warnings" (count v))) (com/pill {:color :red} - (format "%d errors" (count v)))))))] ]) + (format "%d errors" (count v)))))))]]) [:div.flex.gap-4.items-center (com/checkbox {"@click" "showTable=!showTable"} "Show table") (com/button {:color :primary} "Import")] - [:div { :x-show "showTable"} + [:div {:x-show "showTable"} (com/data-grid-card {:id "ledger-import-data" :route nil :title "Data to import" @@ -230,11 +223,11 @@ (let [errors (seq (fc/field-errors))] (cond errors [:div - { "x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"} + {"x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"} [:div.w-8.h-8.rounded-full.p-2.flex.items-start {:class (if (seq (filter (fn [[_ status]] - + (= :error status)) errors)) "bg-red-50 text-red-300" @@ -246,29 +239,29 @@ [:li m])]]] :else nil))]))))} - + [:div.flex.m-4.flex-row-reverse (com/button {:color :primary} "Import")])]]])))]) (defn external-import-text-form* [request] - (fc/start-form - (or (:form-params request) {}) (:form-errors request) - [:form#parse-form {:x-data (hx/json {"clipboard" nil}) - :hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse) - :hx-swap "outerHTML" - :hx-trigger "pasted"} - (fc/with-field :table - [:div - (com/errors {:errors (fc/field-errors)}) - (com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})]) - (com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))" - "x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"} - "Load from clipboard")])) - + (fc/start-form + (or (:form-params request) {}) (:form-errors request) + [:form#parse-form {:x-data (hx/json {"clipboard" nil}) + :hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse) + :hx-swap "outerHTML" + :hx-trigger "pasted"} + (fc/with-field :table + [:div + (com/errors {:errors (fc/field-errors)}) + (com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})]) + (com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))" + "x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"} + "Load from clipboard")])) + (defn external-import-form* [request] [:div#forms {:hx-target "this" :hx-swap "outerHTML"} - (when (and (not (:just-parsed? request)) + (when (and (not (:just-parsed? request)) (seq (->> (:form-errors request) :table vals @@ -289,14 +282,14 @@ :client (:client request) :identity (:identity request) :request request} - (com/breadcrumbs {} + (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/all-page)} "Ledger"] [:a {:href (bidi/path-for ssr-routes/only-routes ::route/external-import-page)} "Import"]) #_(when (:above-grid grid-spec) - ( (:above-grid grid-spec) request)) - + ((:above-grid grid-spec) request)) + [:script (hiccup/raw "async function getclpboard() { @@ -306,7 +299,7 @@ console.log(r) return await r.text() }")] - + (external-import-form* request) [:div #_{:x-data (hx/json {:selected [] :all_selected false :type (:entity-name grid-spec)}) "x-on:copy" "if (selected.length > 0) {$clipboard(JSON.stringify({'type': type, 'selected': selected}))}" @@ -322,23 +315,21 @@ #_(if (string? (:title grid-spec)) (:title grid-spec) ((:title grid-spec) request)))) - - (defn trim-header [t] (if (->> t - first - (map clojure.string/lower-case) - (filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"}) - seq) - (drop 1 t) - t)) + first + (map clojure.string/lower-case) + (filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"}) + seq) + (drop 1 t) + t)) (defn tsv->import-data [data] (if (string? data) (with-open [r (io/reader (char-array data))] (into [] (filter (fn filter-row [r] - (seq (filter (comp not-empty #(str/replace % #"\s+" "")) r)))) + (seq (filter (comp not-empty #(str/replace % #"\s+" "")) r)))) (trim-header (csv/read-csv r :separator \tab)))) data)) @@ -347,52 +338,45 @@ [:bank-account [:string]]])) - - (def parse-form-schema (mc/schema - [:map + [:map [:table {:min 1 :error/message "Clipboard should contain rows to import" - :decode/string tsv->import-data} + :decode/string tsv->import-data} [:vector {:coerce? true} - [:map { :decode/arbitrary (fn [t] + [:map {:decode/arbitrary (fn [t] (if (vector? t) (into {} (map vector [:external-id :client-code :source :vendor-name :date :account-code :location :debit :credit] t)) t))} - [:external-id [:string {:title "external id" - :min 1 - :decode/string strip}]] - [:client-code [:string {:title "client code" - :min 1 - :decode/string strip}]] - [:source [:string {:title "source" - :min 1 - :decode/string strip}]] - [:vendor-name [:string {:min 1 :decode/string strip}]] - [:date [:and clj-date-schema - [:any {:title "date"}]]] - [:account-code account-schema] - - [:location [:string { :min 1 - :max 2 - :decode/string strip}]] - [:debit [:maybe money]] - [:credit [:maybe money]]]] - + [:external-id [:string {:title "external id" + :min 1 + :decode/string strip}]] + [:client-code [:string {:title "client code" + :min 1 + :decode/string strip}]] + [:source [:string {:title "source" + :min 1 + :decode/string strip}]] + [:vendor-name [:string {:min 1 :decode/string strip}]] + [:date [:and clj-date-schema + [:any {:title "date"}]]] + [:account-code account-schema] + + [:location [:string {:min 1 + :max 2 + :decode/string strip}]] + [:debit [:maybe money]] + [:credit [:maybe money]]]] + #_[:string {:decode/string tsv->import-data :error/message "Clipboard should contain rows to import"}]]])) - - - - (defn external-import-parse [request] - (html-response - ( external-import-form* (assoc request :just-parsed? true)))) + (html-response + (external-import-form* (assoc request :just-parsed? true)))) (defn line->id [{:keys [source external-id client-code]}] (str client-code "-" source "-" external-id)) - (defn add-errors [entry all-vendors all-accounts client-locked-lookup all-client-bank-accounts all-client-locations] (let [vendor (all-vendors (:vendor-name entry)) locked-until (client-locked-lookup (:client-code entry)) @@ -413,29 +397,29 @@ entry (cond (not locked-until) (all-row-error (str "Client '" (:client-code entry) "' not found.")) - + (not vendor) (all-row-error (str "Vendor '" (:vendor-name entry) "' not found.")) - + (and locked-until (and (not (t/after? (:date entry) (coerce/to-date-time locked-until))) (not (t/equal? (:date entry) (coerce/to-date-time locked-until))))) (all-row-error (str "Client's data is locked until " locked-until)) - (not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry))) - (reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry))))) - (all-row-error (str "Debits '" - (reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry))) - "' and credits '" - (reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry))) - "' do not add up.")) - (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry))) - 0.0) - (all-row-error (str "Cannot have ledger entries that total $0.00") :warn) - - :else - entry)] + (not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry))) + (reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry))))) + (all-row-error (str "Debits '" + (reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry))) + "' and credits '" + (reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry))) + "' do not add up.")) + (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry))) + 0.0) + (all-row-error (str "Cannot have ledger entries that total $0.00") :warn) + + :else + entry)] (update entry :line-items @@ -466,7 +450,6 @@ (:account-code ea)))) (row-error ea (str "Bank Account '" (:account-code ea) "' not found.")) - (and matching-account (:account/location matching-account) (not= (:account/location matching-account) @@ -494,12 +477,11 @@ (let [lines-with-indexes (for [[i l] (map vector (range) table)] (assoc l :index i))] (into [] - (for [ - [_ lines] (group-by line->id lines-with-indexes) - :let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]] + (for [[_ lines] (group-by line->id lines-with-indexes) + :let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]] (add-errors {:source source :indices (map :index lines) - :external-id (line->id line) + :external-id (line->id line) :client-code client-code :date date :note note @@ -515,9 +497,9 @@ :debit debit :credit credit}) lines)} - all-vendors - all-accounts - client-locked-lookup + all-vendors + all-accounts + client-locked-lookup all-client-bank-accounts all-client-locations))))) @@ -645,7 +627,7 @@ good-entries (filter (fn [e] (and (not (:error (entry-error-types e))) (not (:warn (entry-error-types e))))) entries) bad-entries (filter (fn [e] (:error (entry-error-types e))) entries) form-errors (reduce (fn [acc [path m status]] - (update-in acc path conj [ m status])) + (update-in acc path conj [m status])) {} errors) _ (when (seq bad-entries) @@ -654,7 +636,7 @@ {:type :field-validation :form-errors form-errors :form-params form-params}))) - + retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external-id x)]]) good-entries) ignore-retraction (->> ignored-entries @@ -696,21 +678,20 @@ (defn external-import-import [request] (let [result (import-ledger request)] - (html-response - [:div + (html-response + [:div (external-import-form* (assoc request :form-errors (:form-errors result)))] - :headers {"hx-trigger" (hx/json { "notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})}))) - + :headers {"hx-trigger" (hx/json {"notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})}))) (def key->handler - (merge + (merge (apply-middleware-to-all-handlers (-> {::route/all-page (-> (helper/page-route grid-page) (wrap-implied-route-param :external? false)) ::route/external-page (-> (helper/page-route grid-page) (wrap-implied-route-param :external? true)) - + ::route/table (helper/table-route grid-page) ::route/csv (helper/csv-route grid-page) ::route/external-import-page external-import-page @@ -739,4 +720,4 @@ profit-and-loss/key->handler cash-flows/key->handler investigate/key->handler - new/key->handler)) \ No newline at end of file + new/key->handler)) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/ledger/balance_sheet.clj b/src/clj/auto_ap/ssr/ledger/balance_sheet.clj index 491fbfec..45f6b56d 100644 --- a/src/clj/auto_ap/ssr/ledger/balance_sheet.clj +++ b/src/clj/auto_ap/ssr/ledger/balance_sheet.clj @@ -2,7 +2,7 @@ (:require [amazonica.aws.s3 :as s3] [auto-ap.datomic - :refer [conn pull-many]] + :refer [conn pull-many]] [auto-ap.graphql.utils :refer [assert-can-see-client]] [auto-ap.ledger :refer [build-account-lookup upsert-running-balance]] [auto-ap.ledger.reports :as l-reports] @@ -11,7 +11,7 @@ [auto-ap.permissions :refer [wrap-must]] [auto-ap.routes.ledger :as route] [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated]] + :refer [wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.form-cursor :as fc] @@ -20,9 +20,9 @@ [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers clj-date-schema - html-response modal-response wrap-form-4xx-2 - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers clj-date-schema + html-response modal-response wrap-form-4xx-2 + wrap-schema-enforce]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-pdf.core :as pdf] @@ -38,42 +38,38 @@ [java.util UUID] [org.apache.commons.io.output ByteArrayOutputStream])) - - - - (def query-schema (mc/schema [:maybe [:map - [:client {:unspecified/value :all} - [:or - [:enum :all] - [:vector {:coerce? true :min 1} - [:entity-map {:pull [:db/id :client/name]}]]]] + [:client {:unspecified/value :all} + [:or + [:enum :all] + [:vector {:coerce? true :min 1} + [:entity-map {:pull [:db/id :client/name]}]]]] [:include-deltas {:default false} - [:boolean {:decode/string {:enter #(if (= % "on") true - - (boolean %))}}]] - [:date {:unspecified/fn (fn [] [(atime/local-now)])} - [:vector {:coerce? true - :decode/string (fn [s] (if (string? s) (str/split s #", ") - s))} - clj-date-schema]] ]])) + [:boolean {:decode/string {:enter #(if (= % "on") true + + (boolean %))}}]] + [:date {:unspecified/fn (fn [] [(atime/local-now)])} + [:vector {:coerce? true + :decode/string (fn [s] (if (string? s) (str/split s #", ") + s))} + clj-date-schema]]]])) ;; TODO ;; 1. Rerender form when running ;; 2. Don't throw crazy errors when missing a field ;; 3. General cleanup of the patterns in run-balance-sheet ;; 4. Review ledger dialog -(defn get-report [{ {:keys [date client] :as qp} :query-params :as request}] +(defn get-report [{{:keys [date client] :as qp} :query-params :as request}] (when (and date client) (let [client (if (= :all client) (take 5 (:clients request)) client) - date (reverse (sort date )) + date (reverse (sort date)) client-ids (map :db/id client) _ (doseq [client-id client-ids] (assert-can-see-client (:identity request) client-id)) - + _ (upsert-running-balance (into #{} client-ids)) - + lookup-account (->> client-ids (map (fn build-lookup [client-id] [client-id (build-account-lookup client-id)])) @@ -97,50 +93,50 @@ args (assoc (:query-params request) :periods (map coerce/to-date (filter identity date))) clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids) - + pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) - report (l-reports/summarize-balance-sheet pnl-data) ] + report (l-reports/summarize-balance-sheet pnl-data)] (alog/info ::balance-sheet :params args) {:data report :report report}))) -(defn maybe-trim-clients [request client ] +(defn maybe-trim-clients [request client] (if (= :all client) (cond-> {:client (take 20 (:clients request))} (> (count (:clients request)) 20) (assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20.")) {:client client})) -(defn balance-sheet* [{ {:keys [date client] } :query-params :as request}] - [:div#report +(defn balance-sheet* [{{:keys [date client]} :query-params :as request}] + [:div#report (when (and date client) (let [{:keys [client warning]} (maybe-trim-clients request client) {:keys [data report]} (get-report (assoc-in request [:query-params :client] client)) - client-count (count (set (map :client-id (:data data)))) ] - (list - [:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ] - (rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count)) - (> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date))))) - (and (> client-count 1) (= (count date) 1)) (conj 13)) - :investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate) - :table report - :warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))} ))))]) + client-count (count (set (map :client-id (:data data))))] + (list + [:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client)))] + (rtable/table {:widths (cond-> (into [30] (repeat 13 client-count)) + (> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date))))) + (and (> client-count 1) (= (count date) 1)) (conj 13)) + :investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate) + :table report + :warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))}))))]) (defn form* [request & children] (let [params (or (:query-params request) {})] (fc/start-form - params + params (:form-errors request) [:div#balance-sheet-form.flex.flex-col.gap-4.mt-4 - [:div.flex.gap-8 - [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/run-balance-sheet) + [:div.flex.gap-8 + [:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/run-balance-sheet) :hx-target "#balance-sheet-form" :hx-swap "outerHTML" :hx-disabled-elt "find fieldset"} - [:fieldset + [:fieldset [:div.flex.gap-8 {:x-data (hx/json {})} (fc/with-field :client - (com/validated-inline-field + (com/validated-inline-field {:label "Customers" :errors (fc/field-errors)} (com/multi-typeahead {:name (fc/field-name) :placeholder "Search for companies..." @@ -152,7 +148,7 @@ :content-fn :client/name}))) (fc/with-field :date (com/validated-inline-field {:label "Date" - :errors (fc/field-errors)} + :errors (fc/field-errors)} (com/dates-dropdown {:value (fc/field-value) :name (fc/field-name)}))) (fc/with-field :include-deltas @@ -161,7 +157,7 @@ "Include Deltas")) (com/button {:color :primary :class "w-32"} "Run") - (com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-balance-sheet) } "Export PDF")]]] ] + (com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-balance-sheet)} "Export PDF")]]]] children]))) (defn form [request] @@ -174,47 +170,47 @@ (base-page request (com/page {:nav com/main-aside-nav - + :client-selection (:client-selection request) :clients (:clients request) :client (:client request) :identity (:identity request) :request request} (apply com/breadcrumbs {} [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} - "Ledger"]]) + "Ledger"]]) (form* request)) "Balance Sheet")) (defn make-balance-sheet-pdf [request report] - + (let [output-stream (ByteArrayOutputStream.) client-count (count (or (seq (:client (:query-params request))) (seq (:client (:form-params request))))) - date (:date (:query-params request)) ] + date (:date (:query-params request))] (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 - :size :letter - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Balance Sheet - " (str/join ", " (map :client/name (or (seq (:client (:query-params request))) - (seq (:client (:form-params request)))))))]] + (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 + :size :letter + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Balance Sheet - " (str/join ", " (map :client/name (or (seq (:client (:query-params request))) + (seq (:client (:form-params request)))))))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (conj - (table->pdf report - (cond-> (into [30 ] (repeat client-count 13)) - (> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13 )) - (and (> client-count 1) (= (count date) 1)) (conj 13))))) - output-stream) + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (conj + (table->pdf report + (cond-> (into [30] (repeat client-count 13)) + (> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13)) + (and (> client-count 1) (= (count date) 1)) (conj 13))))) + output-stream) (.toByteArray output-stream))) (defn join-names [client-ids] - (str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_" )) + (str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_")) (defn balance-sheet-args->name [request] (let [date (atime/unparse-local - (:date (:query-params request)) - atime/iso-date) + (:date (:query-params request)) + atime/iso-date) name (->> request :query-params :client (map :db/id) join-names)] (format "Balance-sheet-%s-for-%s" date name))) @@ -258,8 +254,7 @@ [:span.text-gray-800 "Click " (com/link {:href (:report/url bs)} "here") - " to download"] - ])) + " to download"]])) nil)) :headers (-> {} (assoc "hx-retarget" ".modal-stack") @@ -269,15 +264,14 @@ (apply-middleware-to-all-handlers (-> {::route/balance-sheet (-> balance-sheet - (wrap-schema-enforce :query-schema query-schema) - (wrap-form-4xx-2 balance-sheet)) + (wrap-schema-enforce :query-schema query-schema) + (wrap-form-4xx-2 balance-sheet)) ::route/run-balance-sheet (-> form (wrap-schema-enforce :query-schema query-schema) (wrap-form-4xx-2 form)) ::route/export-balance-sheet (-> export - (wrap-schema-enforce :query-schema query-schema) - (wrap-form-4xx-2 form))} - ) + (wrap-schema-enforce :query-schema query-schema) + (wrap-form-4xx-2 form))}) (fn [h] (-> h #_(wrap-merge-prior-hx) diff --git a/src/clj/auto_ap/ssr/ledger/cash_flows.clj b/src/clj/auto_ap/ssr/ledger/cash_flows.clj index 53fc1e2c..c5ad8ed3 100644 --- a/src/clj/auto_ap/ssr/ledger/cash_flows.clj +++ b/src/clj/auto_ap/ssr/ledger/cash_flows.clj @@ -38,39 +38,37 @@ [java.util UUID] [org.apache.commons.io.output ByteArrayOutputStream])) - (def query-schema (mc/schema [:maybe [:map - [:client {:unspecified/value :all} - [:or - [:enum :all] - [:vector {:coerce? true :min 1} - [:entity-map {:pull [:db/id :client/name]}]]]] - - [:periods {:unspecified/fn (fn [] (let [now (atime/local-now)] - [{:start (atime/as-local-time (time/date-time (time/year now) - 1 - 1)) - :end (atime/local-now)}]) + [:client {:unspecified/value :all} + [:or + [:enum :all] + [:vector {:coerce? true :min 1} + [:entity-map {:pull [:db/id :client/name]}]]]] - )} - [:vector {:coerce? true} - [:map - [:start clj-date-schema] - [:end clj-date-schema]]]]]])) + [:periods {:unspecified/fn (fn [] (let [now (atime/local-now)] + [{:start (atime/as-local-time (time/date-time (time/year now) + 1 + 1)) + :end (atime/local-now)}]))} + + [:vector {:coerce? true} + [:map + [:start clj-date-schema] + [:end clj-date-schema]]]]]])) ;; TODO ;; 1. Rerender form when running ;; 2. Don't throw crazy errors when missing a field ;; 3. General cleanup of the patterns in run-balance-sheet ;; 4. Review ledger dialog -(defn get-report [{ {:keys [periods client] :as qp} :form-params :as request}] +(defn get-report [{{:keys [periods client] :as qp} :form-params :as request}] (when (and (seq periods) client) (let [client (if (= :all client) (take 5 (:clients request)) client) client-ids (map :db/id client) _ (doseq [client-id client-ids] (assert-can-see-client (:identity request) client-id)) - + lookup-account (->> client-ids (map (fn build-lookup [client-id] [client-id (build-account-lookup client-id)])) @@ -90,11 +88,11 @@ :account-type (:account_type account) :numeric-code (:numeric_code account) :name (:name account) - :period {:start ( coerce/to-date (:start p)) :end ( coerce/to-date (:end p))}})) + :period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}})) args (assoc (:form-params request) - :periods (map (fn [d] {:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods)) + :periods (map (fn [d] {:start (coerce/to-date (:start d)) :end (coerce/to-date (:end d))}) periods)) clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids) - + pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) report (l-reports/summarize-cash-flows pnl-data)] (alog/info ::cash-flows :params args) @@ -108,14 +106,14 @@ (assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20.")) {:client client})) -(defn cash-flows* [{ {:keys [periods client] } :form-params :as request}] - [:div#report +(defn cash-flows* [{{:keys [periods client]} :form-params :as request}] + [:div#report (when (and periods client) (let [{:keys [client warning]} (maybe-trim-clients request client) {:keys [data report]} (get-report (assoc-in request [:form-params :client] client)) - client-count (count (set (map :client-id (:data data)))) + client-count (count (set (map :client-id (:data data)))) table-contents (concat-tables (:details report))] - (list + (list [:div.text-2xl.font-bold.text-gray-600 (str "Cash flows - " (str/join ", " (map :client/name client)))] (table {:widths (into [20] (take (dec (cell-count table-contents)) (mapcat identity @@ -128,18 +126,18 @@ (defn form* [request & children] (let [params (or (:query-params request) {})] (fc/start-form - params + params (:form-errors request) [:div#cash-flows-form.flex.flex-col.gap-4.mt-4 - [:div.flex.gap-8 - [:form {:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/run-cash-flows) + [:div.flex.gap-8 + [:form {:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/run-cash-flows) :hx-target "#cash-flows-form" :hx-swap "outerHTML" :hx-disabled-elt "find fieldset"} - [:fieldset + [:fieldset [:div.flex.gap-8 {:x-data (hx/json {})} (fc/with-field :client - (com/validated-inline-field + (com/validated-inline-field {:label "Customers" :errors (fc/field-errors)} (com/multi-typeahead {:name (fc/field-name) :placeholder "Search for companies..." @@ -151,12 +149,12 @@ :content-fn :client/name}))) (fc/with-field :periods (com/validated-inline-field {:label "Periods" - :errors (fc/field-errors)} + :errors (fc/field-errors)} (com/periods-dropdown {:value (fc/field-value)}))) - + (com/button {:color :primary :class "w-32"} "Run") - (com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-cash-flows) } "Export PDF")]]]] + (com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-cash-flows)} "Export PDF")]]]] children]))) (defn form [request] @@ -169,7 +167,7 @@ (base-page request (com/page {:nav com/main-aside-nav - + :client-selection (:client-selection request) :clients (:clients request) :client (:client request) @@ -181,20 +179,20 @@ "Cash Flows")) (defn make-cash-flows-pdf [request report] - (let [ output-stream (ByteArrayOutputStream.) + (let [output-stream (ByteArrayOutputStream.) date (:periods (:form-params request))] (pdf/pdf - (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 - :size :letter - :font {:size 6 - :ttf-name "fonts/calibri-light.ttf"}} - [:heading (str "Balance Sheet - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]] + (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15 + :size :letter + :font {:size 6 + :ttf-name "fonts/calibri-light.ttf"}} + [:heading (str "Balance Sheet - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]] - (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) - (conj - (table->pdf (concat-tables (:details report)) - (into [20 ] (mapcat identity (repeat (count date) [ 13 13 13])))))) - output-stream) + (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)]) + (conj + (table->pdf (concat-tables (:details report)) + (into [20] (mapcat identity (repeat (count date) [13 13 13])))))) + output-stream) (.toByteArray output-stream))) (defn join-names [client-ids] @@ -202,8 +200,8 @@ (defn cash-flows-args->name [request] (let [date (atime/unparse-local - (:date (:query-params request)) - atime/iso-date) + (:date (:query-params request)) + atime/iso-date) name (->> request :query-params :client (map :db/id) join-names)] (format "Balance-sheet-%s-for-%s" date name))) @@ -248,7 +246,7 @@ "Click " (com/link {:href (:report/url bs)} "here") " to download"]])) - + nil)) :headers (-> {} (assoc "hx-retarget" ".modal-stack") @@ -258,15 +256,15 @@ (apply-middleware-to-all-handlers (-> {::route/cash-flows (-> cash-flows - (wrap-schema-enforce :query-schema query-schema) - (wrap-form-4xx-2 cash-flows)) + (wrap-schema-enforce :query-schema query-schema) + (wrap-form-4xx-2 cash-flows)) ::route/run-cash-flows (-> form - (wrap-schema-enforce :form-schema query-schema) - (wrap-form-4xx-2 form)) + (wrap-schema-enforce :form-schema query-schema) + (wrap-form-4xx-2 form)) ::route/export-cash-flows (-> export (wrap-schema-enforce :form-schema query-schema) (wrap-form-4xx-2 form))}) - + (fn [h] (-> h #_(wrap-merge-prior-hx) diff --git a/src/clj/auto_ap/ssr/ledger/common.clj b/src/clj/auto_ap/ssr/ledger/common.clj index 3e284966..5209f6fe 100644 --- a/src/clj/auto_ap/ssr/ledger/common.clj +++ b/src/clj/auto_ap/ssr/ledger/common.clj @@ -1,8 +1,8 @@ -(ns auto-ap.ssr.ledger.common +(ns auto-ap.ssr.ledger.common (:require [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-4 conn - merge-query observable-query pull-many]] + :refer [add-sorter-fields apply-pagination apply-sort-4 conn + merge-query observable-query pull-many]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.permissions :refer [can?]] @@ -17,8 +17,8 @@ [auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils - :refer [clj-date-schema entity-id html-response ref->enum-schema - strip]] + :refer [clj-date-schema entity-id html-response ref->enum-schema + strip]] [auto-ap.time :as atime] [auto-ap.utils :refer [dollars-0?]] [bidi.bidi :as bidi] @@ -68,10 +68,10 @@ (defn filters [request] [:form#ledger-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" - "hx-get" (bidi/path-for ssr-routes/only-routes - ::route/table) - "hx-target" "#entity-table" - "hx-indicator" "#entity-table"} + "hx-get" (bidi/path-for ssr-routes/only-routes + ::route/table) + "hx-target" "#entity-table" + "hx-indicator" "#entity-table"} (com/hidden {:name "status" :value (some-> (:status (:query-params request)) name)}) @@ -102,7 +102,7 @@ :value (:invoice-number (:query-params request)) :placeholder "e.g., ABC-456" :size :small})) - + (com/field {:label "Account Code"} [:div.flex.space-x-4.items-baseline (com/int-input {:name "numeric-code-gte" @@ -140,10 +140,10 @@ :value (:amount-lte (:query-params request)) :placeholder "9999.34" :size :small})]) - [:div.mt-4 {:x-data (hx/json { :onlyUnbalanced (:only-unbalanced (:query-params request))})} + [:div.mt-4 {:x-data (hx/json {:onlyUnbalanced (:only-unbalanced (:query-params request))})} (com/hidden {:name "only-unbalanced" ":value" "onlyUnbalanced ? 'on' : ''"}) - (com/checkbox {:value (:only-unbalanced (:query-params request)) + (com/checkbox {:value (:only-unbalanced (:query-params request)) :x-model "onlyUnbalanced"} "Show unbalanced")] (exact-match-id* request)]]) @@ -163,8 +163,8 @@ (filter (fn [[debits credits]] (not (dollars= debits credits)))) (map last) - (into #{})) ] - (for [ result results + (into #{}))] + (for [result results :when (get unbalanced-ids (last result))] result)) results)) @@ -245,20 +245,19 @@ :args [(or (:numeric-code-gte args) 0) (or (:numeric-code-lte args) 99999)]}) (seq (:numeric-code args)) - (merge-query {:query {:in '[ [ [?from-numeric-code ?to-numeric-code] ...]] + (merge-query {:query {:in '[[[?from-numeric-code ?to-numeric-code] ...]] :where ['[?li :journal-entry-line/account ?a] '(or-join [?a ?c] [?a :account/numeric-code ?c] [?a :bank-account/numeric-code ?c]) '[(>= ?c ?from-numeric-code)] '[(<= ?c ?to-numeric-code)]]} - :args [ (map (juxt :from :to ) (:numeric-code args))]}) + :args [(map (juxt :from :to) (:numeric-code args))]}) (seq (:account args)) (merge-query {:query {:in ['?a3] :where ['[?li :journal-entry-line/account ?a3]]} :args [(:db/id (:account args))]}) - (:amount-gte args) (merge-query {:query {:in ['?amount-gte] :where ['[?e :journal-entry/amount ?a] @@ -317,17 +316,15 @@ (apply-only-unbalanced query-params) (apply-pagination query-params)))) - -#_(dc/q '{:find [ ?sort-vendor (count ?e)], -:in [$ [?clients ?start ?end]], -:where [[(iol-ion.query/scan-ledger $ ?clients ?start ?end) - [[?e _ ?sort-default] ...]] - #_(not [?e :journal-entry/vendor]) - [(missing? $ ?e :journal-entry/vendor)] - [(ground "ih") ?sort-vendor]]} -(dc/db conn) -args -) +#_(dc/q '{:find [?sort-vendor (count ?e)], + :in [$ [?clients ?start ?end]], + :where [[(iol-ion.query/scan-ledger $ ?clients ?start ?end) + [[?e _ ?sort-default] ...]] + #_(not [?e :journal-entry/vendor]) + [(missing? $ ?e :journal-entry/vendor)] + [(ground "ih") ?sort-vendor]]} + (dc/db conn) + args) (def default-read '[:journal-entry/amount @@ -338,7 +335,7 @@ args :db/id [:journal-entry/date :xform clj-time.coerce/from-date] {:journal-entry/vendor [:vendor/name :db/id] - :journal-entry/original-entity [:invoice/invoice-number + :journal-entry/original-entity [:invoice/invoice-number :invoice/source-url :transaction/description-original :db/id] :journal-entry/client [:client/name :client/code :db/id] @@ -363,32 +360,30 @@ args refunds)) (defn sum-outstanding [ids] - - (->> - (dc/q {:find ['?id '?o] - :in ['$ '[?id ...]] - :where ['[?id :invoice/outstanding-balance ?o]]} - (dc/db conn) - 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))) + (->> + (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) @@ -413,12 +408,12 @@ args (list (if account-name - [:div { :x-tooltip (hx/json (str "Running Balance: " (some->> (:journal-entry-line/running-balance jel) - (format "$%,.2f"))))} + [:div {:x-tooltip (hx/json (str "Running Balance: " (some->> (:journal-entry-line/running-balance jel) + (format "$%,.2f"))))} [:div.text-left.underline.cursor-pointer {:x-ref "source"} (:journal-entry-line/location jel) ": " (or (:account/numeric-code account) (:bank-account/numeric-code account)) - " - " account-name] ] + " - " account-name]] [:div.text-left (com/pill {:color :yellow} "Unassigned")]) [:div.text-right.text-underline (format "$%,.2f" (key jel))])) @@ -436,12 +431,12 @@ args [:amount-gte {:optional true} [:maybe :double]] [:amount-lte {:optional true} [:maybe :double]] [:client-id {:optional true} [:maybe entity-id]] - [:only-unbalanced {:optional true } + [:only-unbalanced {:optional true} [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true - (= % "") false - :else - - (boolean %))} + (= % "") false + :else + + (boolean %))} :encode/string {:enter #(if % "on" "")}}]]] [:numeric-code {:optional true :decode/string clojure.edn/read-string} [:maybe [:vector [:map [:from nat-int?] @@ -527,103 +522,101 @@ args jel (:journal-entry/line-items je)] (merge jel je))) :headers [{:key "id" - :name "Id" - :render-csv :db/id - :render-for #{:csv}} - {: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 :journal-entry/client :client/name)]) - :render-csv (fn [x] (-> x :journal-entry/client :client/name))} + :name "Id" + :render-csv :db/id + :render-for #{:csv}} + {: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 :journal-entry/client :client/name)]) + :render-csv (fn [x] (-> x :journal-entry/client :client/name))} - {:key "vendor" - :name "Vendor" - :sort-key "vendor" - :render (fn [e] (or (-> e :journal-entry/vendor :vendor/name) - [:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)])) - :render-csv (fn [e] (or (-> e :journal-entry/vendor :vendor/name) - (-> e :journal-entry/alternate-description)))} - {:key "source" - :name "Source" - :sort-key "source" - :hide? (fn [args] - (not (:external? (:route-params args)))) - :render :journal-entry/source - :render-csv :journal-entry/source} - {:key "external-id" - :name "External Id" - :sort-key "external-id" - :class "max-w-[12rem]" - :hide? (fn [args] - (not (:external? (:route-params args)))) - :render (fn [x] [:p.truncate (:journal-entry/external-id x)]) - :render-csv :journal-entry/external-id} - {:key "date" - :sort-key "date" - :name "Date" - :show-starting "lg" - :render (fn [{:journal-entry/keys [date]}] - (some-> date (atime/unparse-local atime/normal-date)))} - {:key "amount" - :sort-key "amount" - :name "Amount" - :show-starting "lg" - :render (fn [{:journal-entry/keys [amount]}] - (some->> amount - (format "$%,.2f")))} - {:key "account" - :name "Account" - :sort-key "account" - :class "text-right" - :render-csv #(or (-> % :journal-entry-line/account :account/name) - (-> % :journal-entry-line/account :bank-account/name)) - :render-for #{:csv}} - {:key "debit" - :name "Debit" - :class "text-right" - :render (partial render-lines :journal-entry-line/debit) - :render-csv :journal-entry-line/debit} + {:key "vendor" + :name "Vendor" + :sort-key "vendor" + :render (fn [e] (or (-> e :journal-entry/vendor :vendor/name) + [:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)])) + :render-csv (fn [e] (or (-> e :journal-entry/vendor :vendor/name) + (-> e :journal-entry/alternate-description)))} + {:key "source" + :name "Source" + :sort-key "source" + :hide? (fn [args] + (not (:external? (:route-params args)))) + :render :journal-entry/source + :render-csv :journal-entry/source} + {:key "external-id" + :name "External Id" + :sort-key "external-id" + :class "max-w-[12rem]" + :hide? (fn [args] + (not (:external? (:route-params args)))) + :render (fn [x] [:p.truncate (:journal-entry/external-id x)]) + :render-csv :journal-entry/external-id} + {:key "date" + :sort-key "date" + :name "Date" + :show-starting "lg" + :render (fn [{:journal-entry/keys [date]}] + (some-> date (atime/unparse-local atime/normal-date)))} + {:key "amount" + :sort-key "amount" + :name "Amount" + :show-starting "lg" + :render (fn [{:journal-entry/keys [amount]}] + (some->> amount + (format "$%,.2f")))} + {:key "account" + :name "Account" + :sort-key "account" + :class "text-right" + :render-csv #(or (-> % :journal-entry-line/account :account/name) + (-> % :journal-entry-line/account :bank-account/name)) + :render-for #{:csv}} + {:key "debit" + :name "Debit" + :class "text-right" + :render (partial render-lines :journal-entry-line/debit) + :render-csv :journal-entry-line/debit} + {:key "credit" + :name "Credit" + :class "text-right" + :render (partial render-lines :journal-entry-line/credit) + :render-csv :journal-entry-line/credit} + {:key "links" + :name "Links" + :show-starting "lg" + :class "w-8" + :render (fn [i] + (link-dropdown + (cond-> [] + (-> i :journal-entry/original-entity :invoice/invoice-number) + (conj + {:link (hu/url (bidi/path-for ssr-routes/only-routes + ::invoice-route/all-page) + {:exact-match-id (:db/id (:journal-entry/original-entity i))}) + :color :primary + :content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))}) + (-> i :journal-entry/original-entity :invoice/source-url) + {:link (-> i :journal-entry/original-entity :invoice/source-url) + :color :secondary + :content (str "File")} - {:key "credit" - :name "Credit" - :class "text-right" - :render (partial render-lines :journal-entry-line/credit) - :render-csv :journal-entry-line/credit} - - {:key "links" - :name "Links" - :show-starting "lg" - :class "w-8" - :render (fn [i] - (link-dropdown - (cond-> [] - (-> i :journal-entry/original-entity :invoice/invoice-number) - (conj - {:link (hu/url (bidi/path-for ssr-routes/only-routes - ::invoice-route/all-page) - {:exact-match-id (:db/id (:journal-entry/original-entity i))}) - :color :primary - :content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))}) - (-> i :journal-entry/original-entity :invoice/source-url) - {:link (-> i :journal-entry/original-entity :invoice/source-url) - :color :secondary - :content (str "File")} - - (-> i :journal-entry/original-entity :transaction/description-original) - (conj - {:link (hu/url (bidi/path-for ssr-routes/only-routes - ::transaction-routes/all-page) - {:exact-match-id (:db/id (:journal-entry/original-entity i))}) - :color :primary - :content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}) - (-> i :journal-entry/memo) - (conj {:color :secondary - :content (str "Memo: " (:journal-entry/memo i))})))) - :render-for #{:html}}]})) + (-> i :journal-entry/original-entity :transaction/description-original) + (conj + {:link (hu/url (bidi/path-for ssr-routes/only-routes + ::transaction-routes/all-page) + {:exact-match-id (:db/id (:journal-entry/original-entity i))}) + :color :primary + :content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}) + (-> i :journal-entry/memo) + (conj {:color :secondary + :content (str "Memo: " (:journal-entry/memo i))})))) + :render-for #{:html}}]})) (def row* (partial helper/row* grid-page)) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/ledger/investigate.clj b/src/clj/auto_ap/ssr/ledger/investigate.clj index cbdefde6..9639013c 100644 --- a/src/clj/auto_ap/ssr/ledger/investigate.clj +++ b/src/clj/auto_ap/ssr/ledger/investigate.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.ledger.investigate +(ns auto-ap.ssr.ledger.investigate (:require [auto-ap.permissions :refer [wrap-must]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] @@ -14,37 +14,33 @@ wrap-schema-enforce]] [auto-ap.time :as atime])) - -(def altered-grid-page - (assoc grid-page +(def altered-grid-page + (assoc grid-page :id "yoho" - :raw? true + :raw? true :check-boxes? false :route ::route/investigate-results)) - (defn investigate [request] - (modal-response + (modal-response (com/modal {:class "max-h-[600px]"} - (com/modal-card {:hx-vals (hx/json (cond-> (:query-params request) - true (update :numeric-code pr-str) - (:start-date (:query-params request)) (update :start-date #(some-> (atime/unparse-local % atime/normal-date))) - (:end-date (:query-params request)) (update :end-date #(some-> (atime/unparse-local % atime/normal-date))))) } - [:div "Ledger entries"] - (table* - altered-grid-page - identity - request - #_(assoc-in request [:query-params :sort] [{:sort-key "date" :asc? false :name "Date"}])) - nil) - ))) + (com/modal-card {:hx-vals (hx/json (cond-> (:query-params request) + true (update :numeric-code pr-str) + (:start-date (:query-params request)) (update :start-date #(some-> (atime/unparse-local % atime/normal-date))) + (:end-date (:query-params request)) (update :end-date #(some-> (atime/unparse-local % atime/normal-date)))))} + [:div "Ledger entries"] + (table* + altered-grid-page + identity + request + #_(assoc-in request [:query-params :sort] [{:sort-key "date" :asc? false :name "Date"}])) + nil)))) (def key->handler (apply-middleware-to-all-handlers (-> - {::route/investigate investigate - ::route/investigate-results (helper/table-route altered-grid-page :push-url? false)} - ) + {::route/investigate investigate + ::route/investigate-results (helper/table-route altered-grid-page :push-url? false)}) (fn [h] (-> h (wrap-apply-sort grid-page) diff --git a/src/clj/auto_ap/ssr/ledger/new.clj b/src/clj/auto_ap/ssr/ledger/new.clj index c1f8bc63..8740e750 100644 --- a/src/clj/auto_ap/ssr/ledger/new.clj +++ b/src/clj/auto_ap/ssr/ledger/new.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.ledger.new +(ns auto-ap.ssr.ledger.new (:require [auto-ap.datomic :refer [audit-transact conn pull-attr]] [auto-ap.datomic.accounts :as d-accounts] @@ -26,25 +26,25 @@ [datomic.api :as dc] [iol-ion.query :refer [dollars=]] [iol-ion.utils :refer [remove-nils]]) - (:import - [java.util UUID])) + (:import + [java.util UUID])) (def new-ledger-schema [:and [:map [:db/id {:optional true} [:maybe entity-id]] - [:journal-entry/client {:optional false} [:entity-map {:pull [:db/id :client/name :client/locations] }]] + [:journal-entry/client {:optional false} [:entity-map {:pull [:db/id :client/name :client/locations]}]] [:journal-entry/date clj-date-schema] - [:journal-entry/memo {:optional true} [:maybe [ :string {:decode/string strip}]]] + [:journal-entry/memo {:optional true} [:maybe [:string {:decode/string strip}]]] [:journal-entry/vendor {:optional false :default nil} - [:entity-map {:pull [:db/id :vendor/name] }]] + [:entity-map {:pull [:db/id :vendor/name]}]] [:journal-entry/amount {:min 0.01} money] [:journal-entry/line-items [:vector {:coerce? true} [:and [:map - [:journal-entry-line/account [:and [:entity-map {:pull a/default-read }] + [:journal-entry-line/account [:and [:entity-map {:pull a/default-read}] [:fn {:error/message "Not an allowed account."} (fn check-allow [x] (check-allowance (:db/id x) :account/default-allowance))]]] @@ -81,18 +81,18 @@ :value value :content-fn (fn [value] (when value - (str - (:account/numeric-code value) - " - " - (:account/name (d-accounts/clientize value - client-id)))))})]) + (str + (:account/numeric-code value) + " - " + (:account/name (d-accounts/clientize value + client-id)))))})]) (defn- location-select* [{:keys [name account-location client-locations value]}] (com/select {:options (into [["" ""]] (cond account-location [[account-location account-location]] - + :else (for [c (seq client-locations)] [c c]))) @@ -198,19 +198,19 @@ (com/hidden {:name (fc/field-name) :value (:db/id (:client request))}) [:div.w-96 - (com/validated-field - {:label "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) - :value-fn :db/id - :content-fn :client/name - :x-model "clientId"})])])) + (com/validated-field + {:label "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) + :value-fn :db/id + :content-fn :client/name + :x-model "clientId"})])])) (fc/with-field :journal-entry/date (com/validated-field {:label "Date" @@ -245,18 +245,18 @@ :class "w-24" :error? (fc/field-errors) :placeholder "212.44"})])) - (fc/with-field :journal-entry/memo + (fc/with-field :journal-entry/memo [:div.w-96 (com/validated-field {:label "Memo" :errors (fc/field-errors)} [:div.w-96 (com/text-input {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" - :placeholder "A custom note" - :url (bidi/path-for ssr-routes/only-routes :company-search) - :value (fc/field-value) })])]) + :error? (fc/error?) + :class "w-96" + :placeholder "A custom note" + :url (bidi/path-for ssr-routes/only-routes :company-search) + :value (fc/field-value)})])]) (fc/with-field :journal-entry/line-items (com/validated-field {:errors (fc/field-errors)} @@ -273,7 +273,6 @@ :tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})} "New account"))))]))) - (defn new [request] (modal-response (com/modal {:hx-target "this" @@ -282,7 +281,7 @@ ::route/new-submit)} (com/modal-card {:class "md:h-[800px] md:w-[750px] flex-col relative" :error (when (vector? (:form-errors request)) - (str/join ", "(:form-errors request) ))} + (str/join ", " (:form-errors request)))} [:div "New ledger entry"] [:div.overflow-y-scroll.relative (form* request)] [:div (com/button {:color :primary} "Save")])]))) @@ -296,10 +295,10 @@ (update :journal-entry/line-items (fn [lis] (mapv - #(remove-nils (-> % - (update :journal-entry-line/account :db/id) - (assoc :journal-entry-line/client (-> request :form-params :journal-entry/client :db/id) - :journal-entry-line/date (-> request :form-params :journal-entry/date coerce/to-date)))) + #(remove-nils (-> % + (update :journal-entry-line/account :db/id) + (assoc :journal-entry-line/client (-> request :form-params :journal-entry/client :db/id) + :journal-entry-line/date (-> request :form-params :journal-entry/date coerce/to-date)))) lis))) (assoc :journal-entry/external-id (str "manual-" (UUID/randomUUID)))) (= :post (:request-method request)) (assoc :db/id "new")) @@ -308,9 +307,9 @@ :client/ledger-last-change (iol-ion.tx.upsert-ledger/current-date (dc/db conn))}] (:identity request)) updated-entity (dc/pull (dc/db conn) - ledger.common/default-read - (or (get tempids (:db/id entity)) (:db/id entity)))] - + ledger.common/default-read + (or (get tempids (:db/id entity)) (:db/id entity)))] + (html-response (ledger.common/row* identity updated-entity {:flash? true @@ -323,7 +322,6 @@ (assoc "hx-retarget" "#entity-table tbody" "hx-reswap" "afterbegin"))))) - (def key->handler (apply-middleware-to-all-handlers (-> diff --git a/src/clj/auto_ap/ssr/ledger/profit_and_loss.clj b/src/clj/auto_ap/ssr/ledger/profit_and_loss.clj index e52a5e10..eedf9a4c 100644 --- a/src/clj/auto_ap/ssr/ledger/profit_and_loss.clj +++ b/src/clj/auto_ap/ssr/ledger/profit_and_loss.clj @@ -38,7 +38,6 @@ [java.util UUID] [org.apache.commons.io.output ByteArrayOutputStream])) - (def query-schema (mc/schema [:maybe [:map [:client {:unspecified/value :all} @@ -71,14 +70,14 @@ ;; 4. Review ledger dialog ;; 5. pagination and filtering within dialog. looks weird with the full screen refresh -(defn get-report [{ {:keys [periods client] :as qp} :form-params :as request}] +(defn get-report [{{:keys [periods client] :as qp} :form-params :as request}] (when (and (seq periods) client) (let [client (if (= :all client) (take 5 (:clients request)) client) client-ids (map :db/id client) _ (upsert-running-balance (into #{} client-ids)) _ (doseq [client-id client-ids] (assert-can-see-client (:identity request) client-id)) - + lookup-account (->> client-ids (map (fn build-lookup [client-id] [client-id (build-account-lookup client-id)])) @@ -103,13 +102,13 @@ :numeric-code (:numeric_code account) :name (:name account) :sample sample - :period {:start ( coerce/to-date (:start p)) :end (coerce/to-date (:end p))}})) - + :period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}})) + args (assoc (:form-params request) - :periods (map (fn [d] - {:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods)) + :periods (map (fn [d] + {:start (coerce/to-date (:start d)) :end (coerce/to-date (:end d))}) periods)) clients (pull-many (dc/db conn) [:client/code :client/name :db/id :client/feature-flags] client-ids) - + pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients)) #_#__ (clojure.pprint/pprint pnl-data) report (l-reports/summarize-pnl pnl-data)] @@ -124,14 +123,14 @@ (assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20.")) {:client client})) -(defn profit-and-loss* [{ {:keys [periods client] } :form-params :as request}] - [:div#report +(defn profit-and-loss* [{{:keys [periods client]} :form-params :as request}] + [:div#report (when (and periods client) (let [{:keys [client warning]} (maybe-trim-clients request client) {:keys [data report]} (get-report (assoc-in request [:form-params :client] client)) - client-count (count (set (map :client-id (:data data)))) + client-count (count (set (map :client-id (:data data)))) table-contents (concat-tables (concat (:summaries report) (:details report)))] - (list + (list [:div.text-2xl.font-bold.text-gray-600 (str "Profit and loss - " (str/join ", " (map :client/name client)))] (table {:widths (into [20] (take (dec (cell-count table-contents)) (mapcat identity @@ -148,14 +147,11 @@ {:subject :history :activity :view}) (for [n (:invalid-ids report)] - [:div + [:div (com/link {:href (str (bidi/path-for ssr-routes/only-routes :admin-history) "/" n)} - "Sample")]))] - }))))]) - - + "Sample")]))]}))))]) (defn form* [request & children] (let [params (or (:query-params request) {})] @@ -209,7 +205,7 @@ (base-page request (com/page {:nav com/main-aside-nav - + :client-selection (:client-selection request) :clients (:clients request) :client (:client request) @@ -259,8 +255,8 @@ (defn profit-and-loss-args->name [request] (let [date (atime/unparse-local - (:date (:query-params request)) - atime/iso-date) + (:date (:query-params request)) + atime/iso-date) name (->> request :query-params :client (map :db/id) join-names)] (format "Profit-and-loss-%s-for-%s" date name))) @@ -268,7 +264,7 @@ (let [uuid (str (UUID/randomUUID)) {:keys [client warning]} (maybe-trim-clients request (:client (:form-params request))) request (assoc-in request [:form-params :client] client) - pdf-data (binding [*report-pedantic* (boolean ((set (:client/feature-flags (first client))) + pdf-data (binding [*report-pedantic* (boolean ((set (:client/feature-flags (first client))) "report-pedantic"))] (make-profit-and-loss-pdf request (:report (get-report request)))) name (profit-and-loss-args->name request) key (str "reports/profit-and-loss/" uuid "/" name ".pdf") @@ -306,7 +302,7 @@ "Click " (com/link {:href (:report/url bs)} "here") " to download"]])) - + nil)) :headers (-> {} (assoc "hx-retarget" ".modal-stack") @@ -316,15 +312,15 @@ (apply-middleware-to-all-handlers (-> {::route/profit-and-loss (-> profit-and-loss - (wrap-schema-enforce :query-schema query-schema) - (wrap-form-4xx-2 profit-and-loss)) + (wrap-schema-enforce :query-schema query-schema) + (wrap-form-4xx-2 profit-and-loss)) ::route/run-profit-and-loss (-> form - (wrap-schema-enforce :form-schema query-schema) - (wrap-form-4xx-2 form)) + (wrap-schema-enforce :form-schema query-schema) + (wrap-form-4xx-2 form)) ::route/export-profit-and-loss (-> export - (wrap-schema-enforce :form-schema query-schema) - (wrap-form-4xx-2 form))}) - + (wrap-schema-enforce :form-schema query-schema) + (wrap-form-4xx-2 form))}) + (fn [h] (-> h #_(wrap-merge-prior-hx) diff --git a/src/clj/auto_ap/ssr/ledger/report_table.clj b/src/clj/auto_ap/ssr/ledger/report_table.clj index 0c6d556b..22a50efc 100644 --- a/src/clj/auto_ap/ssr/ledger/report_table.clj +++ b/src/clj/auto_ap/ssr/ledger/report_table.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.ledger.report-table +(ns auto-ap.ssr.ledger.report-table (:require [auto-ap.ssr.components :as com] [auto-ap.time :as atime] @@ -7,22 +7,19 @@ [hiccup.util :as hu] [iol-ion.query :as query])) - - (defn cell [{:keys [width investigate-url other-style]} c] (let [cell-contents (cond - + (= :dollar (:format c)) (format "$%,.2f" (if (query/dollars-0? (:value c)) 0.0 (:value c))) - - + (= :percent (:format c)) (format "%%%.1f" (if (query/dollars-0? (:value c)) 0.0 (* 100.0 (or (:value c) 0.0)))) - + :else (str (:value c))) cell-contents (if (:filters c) @@ -32,8 +29,7 @@ (inst? (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:date-range (:filters c))) atime/normal-date)) (:end (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:end (:date-range (:filters c)))) atime/normal-date)) (:start (:date-range (:filters c))) (assoc :start-date (atime/unparse-local (coerce/to-date-time (:start (:date-range (:filters c)))) atime/normal-date)) - (:client-id (:filters c)) (assoc :client-id (:client-id (:filters c)))) - )} + (:client-id (:filters c)) (assoc :client-id (:client-id (:filters c)))))} cell-contents) cell-contents)] [:td.px-4.py-2 @@ -44,10 +40,9 @@ (fn [s] (->> (:border c) (map - (fn [b] - [(keyword (str "border-" (name b))) "1px solid black"]) - ) - (into s)))) + (fn [b] + [(keyword (str "border-" (name b))) "1px solid black"])) + (into s)))) (:colspan c) (assoc :colspan (:colspan c)) (:align c) (assoc :align (:align c)) (= :dollar (:format c)) (assoc :align :right) @@ -57,10 +52,10 @@ (str/join "," (:color c)) ")")) - true (assoc-in [:style :background-color] (str "rgb(" - (str/join "," - (or (:bg-color c) [255 255 255])) - ")"))) + true (assoc-in [:style :background-color] (str "rgb(" + (str/join "," + (or (:bg-color c) [255 255 255])) + ")"))) cell-contents])) @@ -72,47 +67,47 @@ (defn table [{:keys [table widths investigate-url warning]}] (let [cell-count (cell-count table)] - (com/content-card {:class "inline-block overflow-scroll"} - [:div {:class "overflow-scroll h-[70vh] m-4 inline-block"} - (when warning [:div.rounded.bg-red-50.text-red-800.p-4.m-2 - warning]) - (-> [:table {:class "text-sm text-left text-gray-500 dark:text-gray-400"} - [:thead {:class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 font-bold"} - (map - (fn [header-row header] - (into - [:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}] - (map - (fn [w header i] - (cell {:width w - :investigate-url investigate-url - :other-style {:position "sticky" - :top (* header-row (+ 22 18))}} header)) - widths - header - (range)))) - (range) - (:header table))]] - - (conj - (-> [:tbody {:style {}}] - (into - (for [[i row] (map vector (range) (:rows table))] - - [:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"} - (for [[i c] (map vector (range) (take cell-count - (reduce - (fn [[acc cnt] cur] - (if (>= (+ cnt (:colspan cur 1)) cell-count) - (reduced (conj acc cur)) - [(conj acc cur) (+ cnt (:colspan cur 1))])) - [[] 0] - (concat row (repeat nil)))))] - - (cell {:investigate-url investigate-url} c))])) - (conj [:tr (for [i (range cell-count)] - - (cell {:investigate-url investigate-url} {:value " "}))]))))]))) + (com/content-card {:class "inline-block overflow-scroll"} + [:div {:class "overflow-scroll h-[70vh] m-4 inline-block"} + (when warning [:div.rounded.bg-red-50.text-red-800.p-4.m-2 + warning]) + (-> [:table {:class "text-sm text-left text-gray-500 dark:text-gray-400"} + [:thead {:class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 font-bold"} + (map + (fn [header-row header] + (into + [:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}] + (map + (fn [w header i] + (cell {:width w + :investigate-url investigate-url + :other-style {:position "sticky" + :top (* header-row (+ 22 18))}} header)) + widths + header + (range)))) + (range) + (:header table))]] + + (conj + (-> [:tbody {:style {}}] + (into + (for [[i row] (map vector (range) (:rows table))] + + [:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"} + (for [[i c] (map vector (range) (take cell-count + (reduce + (fn [[acc cnt] cur] + (if (>= (+ cnt (:colspan cur 1)) cell-count) + (reduced (conj acc cur)) + [(conj acc cur) (+ cnt (:colspan cur 1))])) + [[] 0] + (concat row (repeat nil)))))] + + (cell {:investigate-url investigate-url} c))])) + (conj [:tr (for [i (range cell-count)] + + (cell {:investigate-url investigate-url} {:value " "}))]))))]))) (defn concat-tables [tables] (let [[first & rest] tables] @@ -120,8 +115,8 @@ :rows (concat (:rows first) [[]] (mapcat - (fn [table] - (-> (:header table) - (into (:rows table)) - (conj []))) - rest))})) + (fn [table] + (-> (:header table) + (into (:rows table)) + (conj []))) + rest))})) diff --git a/src/clj/auto_ap/ssr/nested_form_params.clj b/src/clj/auto_ap/ssr/nested_form_params.clj index 1fa4648e..be15274e 100644 --- a/src/clj/auto_ap/ssr/nested_form_params.clj +++ b/src/clj/auto_ap/ssr/nested_form_params.clj @@ -39,11 +39,11 @@ "Return a list of name-value pairs for a parameter map." [params] (mapcat - (fn [[name value]] - (if (and (sequential? value) (not (coll? (first value)))) - (for [v value] [name v]) - [[name value]])) - params)) + (fn [[name value]] + (if (and (sequential? value) (not (coll? (first value)))) + (for [v value] [name v]) + [[name value]])) + params)) (defn- nest-params "Takes a flat map of parameters and turns it into a nested map of @@ -51,10 +51,10 @@ into keys." [params parse] (reduce - (fn [m [k v]] - (assoc-nested m (parse k) v)) - {} - (param-pairs params))) + (fn [m [k v]] + (assoc-nested m (parse k) v)) + {} + (param-pairs params))) (defn nested-params-request "Converts a request with a flat map of parameters to a nested map. diff --git a/src/clj/auto_ap/ssr/not_found.clj b/src/clj/auto_ap/ssr/not_found.clj index 4c75c895..b33c5eff 100644 --- a/src/clj/auto_ap/ssr/not_found.clj +++ b/src/clj/auto_ap/ssr/not_found.clj @@ -2,21 +2,20 @@ (:require [auto-ap.ssr.components :as com] [auto-ap.ssr.ui :refer [base-page]])) - (defn page [{:keys [identity matched-route] :as request}] (base-page request - (com/page { :request request + (com/page {:request request :client (:client request) :clients (:clients request) :identity (:identity request) :app-params {}} #_(com/breadcrumbs {} - [:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"]) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"]) [:div.flex.items-center.justify-center.flex-col - + [:div.text-2xl.font-bold.text-gray-600 "Page not found"] [:p.text-gray-500 "Sorry, we can't find the page you're looking for. Try going " (com/link {:href "/"} "home") " and try again."]]) "Not found")) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/outgoing_invoice/new.clj b/src/clj/auto_ap/ssr/outgoing_invoice/new.clj index 16e8249b..1360d1d1 100644 --- a/src/clj/auto_ap/ssr/outgoing_invoice/new.clj +++ b/src/clj/auto_ap/ssr/outgoing_invoice/new.clj @@ -120,8 +120,6 @@ :value (-> (fc/field-value) (atime/unparse-local atime/normal-date))})])) - - (fc/with-field-default :outgoing-invoice/line-items [{:db/id "first"}] (com/validated-field {:errors (fc/field-errors) :label "Line items"} @@ -280,6 +278,6 @@ (add-new-entity-handler [:outgoing-invoice/line-items] (fn render [cursor request] (line-item - {:value cursor })) + {:value cursor})) (fn build-new-row [base _] base)))}) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/payments.clj b/src/clj/auto_ap/ssr/payments.clj index f2978cdd..65d4d587 100644 --- a/src/clj/auto_ap/ssr/payments.clj +++ b/src/clj/auto_ap/ssr/payments.clj @@ -1,9 +1,9 @@ (ns auto-ap.ssr.payments (:require [auto-ap.datomic - :refer [add-sorter-fields apply-pagination apply-sort-3 - audit-transact conn merge-query observable-query - pull-many]] + :refer [add-sorter-fields apply-pagination apply-sort-3 + audit-transact conn merge-query observable-query + pull-many]] [auto-ap.graphql.utils :refer [assert-can-see-client exception->notification extract-client-ids notify-if-locked]] @@ -14,7 +14,7 @@ [auto-ap.routes.payments :as route] [auto-ap.routes.transactions :as transaction-routes] [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.components.bank-account-icon :as bank-account-icon] @@ -24,11 +24,11 @@ [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 - dissoc-nil-transformer entity-id html-response - main-transformer modal-response ref->enum-schema strip - wrap-entity wrap-implied-route-param wrap-merge-prior-hx - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers clj-date-schema + dissoc-nil-transformer entity-id html-response + main-transformer modal-response ref->enum-schema strip + wrap-entity wrap-implied-route-param wrap-merge-prior-hx + wrap-schema-enforce]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-time.coerce :as coerce] @@ -105,19 +105,18 @@ :size :small})]) (com/field {:label "Payment Type"} (com/radio-card {:size :small - :name "payment-type" - :value (:payment-type (:query-params request)) - :options [{:value "" - :content "All"} - {:value "cash" - :content "Cash"} - {:value "check" - :content "Check"} - {:value "debit" - :content "Debit"}]})) + :name "payment-type" + :value (:payment-type (:query-params request)) + :options [{:value "" + :content "All"} + {:value "cash" + :content "Cash"} + {:value "check" + :content "Check"} + {:value "debit" + :content "Debit"}]})) (exact-match-id* request)]]) - (def default-read '[* [:payment/date :xform clj-time.coerce/from-date] {:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]} @@ -213,7 +212,6 @@ '[(iol-ion.query/dollars= ?transaction-amount ?amount)]]} :args [(:amount query-params)]}) - (:status route-params) (merge-query {:query {:in ['?status] :where ['[?e :payment/status ?status]]} @@ -244,30 +242,30 @@ refunds)) (defn sum-visible-pending [ids] - (->> - (dc/q {:find ['?id '?o] - :in ['$ '[?id ...]] - :where ['[?id :payment/amount ?o] - '[?id :payment/status :payment-status/pending]]} - (dc/db conn) - ids) + (->> + (dc/q {:find ['?id '?o] + :in ['$ '[?id ...]] + :where ['[?id :payment/amount ?o] + '[?id :payment/status :payment-status/pending]]} + (dc/db conn) + ids) (map last) (reduce + 0.0))) (defn sum-client-pending [clients] - (->> - (dc/q {:find '[?e ?a] - :in '[$ [?clients ?start ?end]] - :where '[[(iol-ion.query/scan-payments $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] - [?e :payment/status :payment-status/pending] - [?e :payment/amount ?a]]} - (dc/db conn) - [clients - nil - nil]) - + (->> + (dc/q {:find '[?e ?a] + :in '[$ [?clients ?start ?end]] + :where '[[(iol-ion.query/scan-payments $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] + [?e :payment/status :payment-status/pending] + [?e :payment/amount ?a]]} + (dc/db conn) + [clients + nil + nil]) + (map last) (reduce + @@ -278,16 +276,14 @@ {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-visible-pending all-ids) (sum-client-pending (extract-client-ids (:clients request) - (:client request) - (:client-id (:query-params request)) - (when (:client-code (:query-params request)) - [:client/code (:client-code (:query-params request))]))) - ])) + (:client request) + (:client-id (:query-params request)) + (when (:client-code (:query-params request)) + [:client/code (:client-code (:query-params request))])))])) (def query-schema (mc/schema [:maybe [:map {:date-range [:date-range :start-date :end-date]} @@ -328,7 +324,7 @@ (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) :query-schema query-schema :action-buttons (fn [request] - (let [[_ _ visible-in-float total-in-float ] (:page-results request)] + (let [[_ _ visible-in-float total-in-float] (:page-results request)] [(com/pill {:color :primary} " Visible in float " (format "$%,.2f" visible-in-float)) (com/pill {:color :secondary} " Total in float " @@ -355,7 +351,7 @@ (= (-> request :query-params :sort first :name) "Bank account") (-> entity :payment/bank-account :bank-account/name) - + :else nil)) :title (fn [r] (str @@ -410,7 +406,7 @@ :render (fn [{:payment/keys [date]}] (some-> date (atime/unparse-local atime/normal-date)))} {:key "amount" - :sort-key "amount" + :sort-key "amount" :name "Amount" :render (fn [{:payment/keys [amount]}] (some->> amount (format "$%.2f")))} @@ -422,10 +418,10 @@ (map :invoice-payment/invoice) (filter identity) (map (fn [invoice] - {:link (hu/url (bidi/path-for ssr-routes/only-routes - ::invoice-route/all-page) - {:exact-match-id (:db/id invoice)}) - :content (str "Inv. " (:invoice/invoice-number invoice))}))) + {:link (hu/url (bidi/path-for ssr-routes/only-routes + ::invoice-route/all-page) + {:exact-match-id (:db/id invoice)}) + :content (str "Inv. " (:invoice/invoice-number invoice))}))) (some-> p :transaction/_payment ((fn [t] [{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page) {:exact-match-id (:db/id (first t))}) @@ -434,8 +430,6 @@ (def row* (partial helper/row* grid-page)) - - (comment (mc/decode query-schema {"exact-match-id" "123"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)) (mc/decode query-schema {} (mt/transformer main-transformer mt/strip-extra-keys-transformer)) @@ -445,7 +439,6 @@ (mc/decode query-schema {"payment-type" "food"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)) (mc/decode query-schema {"vendor" "87"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)) - (mc/decode query-schema {"start-date" #inst "2023-12-21T08:00:00.000-00:00"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))) (defn delete [{check :entity :as request identity :identity}] @@ -459,7 +452,7 @@ #(assert-can-see-client identity (:db/id (:payment/client check)))) (notify-if-locked (:db/id (:payment/client check)) (:payment/date check)) - (let [ removing-payments (mapcat (fn [x] + (let [removing-payments (mapcat (fn [x] (let [invoice (:invoice-payment/invoice x) new-balance (+ (:invoice/outstanding-balance invoice) (:invoice-payment/amount x))] @@ -475,9 +468,9 @@ :payment/status :payment-status/voided}] (audit-transact (cond-> removing-payments true (conj updated-payment) - (:transaction/_payment check) (conj [:db/retract (:db/id (first (:transaction/_payment check))) + (:transaction/_payment check) (conj [:db/retract (:db/id (first (:transaction/_payment check))) :transaction/payment - (:db/id check)])) + (:db/id check)])) identity) (html-response (row* (:identity request) updated-payment {:delete-after-settle? true :class "live-removed" @@ -578,7 +571,6 @@ (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) - :else selected) updated-count (void-payments-internal ids (:identity request))] @@ -591,7 +583,7 @@ (defn wrap-status-from-source [handler] (fn [{:keys [matched-current-page-route] :as request}] - (let [ request (cond-> request + (let [request (cond-> request (= ::route/cleared-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/cleared) (= ::route/pending-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/pending) (= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/voided) @@ -605,7 +597,7 @@ ::route/pending-page (-> (helper/page-route grid-page) (wrap-implied-route-param :status :payment-status/pending)) ::route/voided-page (-> (helper/page-route grid-page) - (wrap-implied-route-param :status :payment-status/voided)) + (wrap-implied-route-param :status :payment-status/voided)) ::route/all-page (-> (helper/page-route grid-page) (wrap-implied-route-param :status nil)) @@ -618,7 +610,6 @@ ::route/bulk-delete (-> bulk-delete-dialog (wrap-admin)) - ::route/table (helper/table-route grid-page)} (fn [h] (-> h diff --git a/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj index 4dc52eed..e880a135 100644 --- a/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj +++ b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj @@ -25,7 +25,7 @@ [:maybe clj-date-schema]] [:end-date {:optional true} [:maybe clj-date-schema]] - [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ] + [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]] default-grid-fields-schema)])) (defn filters [params] @@ -36,7 +36,7 @@ "hx-indicator" "#cash-drawer-shift-table" #_#_:hx-disabled-elt "find fieldset"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field* params) (total-field* params)]]) @@ -52,15 +52,14 @@ :where '[[(iol-ion.query/scan-cash-drawer-shifts $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} :args [db [(:trimmed-clients request) (some-> (:start-date query-params) c/to-date) - (some-> (:end-date query-params) c/to-date )]]} + (some-> (:end-date query-params) c/to-date)]]} (:sort query-params) (add-sorter-fields {"client" ['[?e :cash-drawer-shift/client ?c] '[?c :client/name ?sort-client]] "date" ['[?e :cash-drawer-shift/date ?sort-date]] "paid-in" ['[?e :cash-drawer-shift/paid-in ?sort-paid-in]] "paid-out" ['[?e :cash-drawer-shift/paid-out ?sort-paid-out]] "expected-cash" ['[?e :cash-drawer-shift/expected-cash ?sort-expected-cash]] - "opened-cash" ['[?e :cash-drawer-shift/opened-cash ?sort-opened-cash]] - } + "opened-cash" ['[?e :cash-drawer-shift/opened-cash ?sort-opened-cash]]} query-params) (:exact-match-id query-params) @@ -71,7 +70,7 @@ true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :cash-drawer-shift/date ?sort-default]]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -80,8 +79,8 @@ (let [results (->> (pull-many db default-read ids) (group-by :db/id)) cash-drawer-shifts (->> ids - (map results) - (map first))] + (map results) + (map first))] cash-drawer-shifts)) (defn fetch-page [request] @@ -109,7 +108,7 @@ "Cash Drawer Shifts"]] :title "Cash drawer shifts" :entity-name "Cash drawer shift" - :query-schema query-schema + :query-schema query-schema :route :pos-cash-drawer-shift-table :headers [{:key "client" :name "Client" @@ -138,12 +137,11 @@ :sort-key "opened-cash" :render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))}]})) - (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (def key->handler - (apply-middleware-to-all-handlers + (apply-middleware-to-all-handlers {:pos-cash-drawer-shifts (helper/page-route grid-page) :pos-cash-drawer-shift-table (helper/table-route grid-page)} (fn [h] diff --git a/src/clj/auto_ap/ssr/pos/common.clj b/src/clj/auto_ap/ssr/pos/common.clj index 57ef83d5..fff74367 100644 --- a/src/clj/auto_ap/ssr/pos/common.clj +++ b/src/clj/auto_ap/ssr/pos/common.clj @@ -5,30 +5,30 @@ [auto-ap.ssr.svg :as svg])) (defn date-range-field* [request] (dr/date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range"})) + :end (:end-date (:query-params request))} + :id "date-range"})) (defn processor-field* [request] (com/field {:label "Processor"} (com/radio-card {:size :small - :name "processor" - :value (:processor (:query-params request)) - :options [{:value "" - :content "All"} - {:value "square" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/square.png" :style {:width "16px" :height "16px"}}] [:div "Square"]]} - {:value "doordash" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/doordash.png" :style {:width "16px" :height "16px"}}] [:div "Doordash"]]} - {:value "uber-eats" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ubereats.png" :style {:width "16px" :height "16px"}}] [:div "Uber eats"]]} - {:value "grubhub" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/grubhub.png" :style {:width "16px" :height "16px"}}] [:div "Grubhub"]]} - {:value "koala" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/koala.png" :style {:width "16px" :height "16px"}}] [:div "Koala"]]} - {:value "ezcater" - :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ezcater.png" :style {:width "16px" :height "16px"}}] [:div "EZCater"]]} - {:value "na" - :content "No Processor"}]}))) + :name "processor" + :value (:processor (:query-params request)) + :options [{:value "" + :content "All"} + {:value "square" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/square.png" :style {:width "16px" :height "16px"}}] [:div "Square"]]} + {:value "doordash" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/doordash.png" :style {:width "16px" :height "16px"}}] [:div "Doordash"]]} + {:value "uber-eats" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ubereats.png" :style {:width "16px" :height "16px"}}] [:div "Uber eats"]]} + {:value "grubhub" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/grubhub.png" :style {:width "16px" :height "16px"}}] [:div "Grubhub"]]} + {:value "koala" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/koala.png" :style {:width "16px" :height "16px"}}] [:div "Koala"]]} + {:value "ezcater" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ezcater.png" :style {:width "16px" :height "16px"}}] [:div "EZCater"]]} + {:value "na" + :content "No Processor"}]}))) (defn total-field* [request] (com/field {:label "Total"} @@ -40,7 +40,7 @@ :value (:total-gte (:query-params request)) :placeholder "0.01" :size :small}) - [:div.align-baseline + [:div.align-baseline "to"] (com/money-input {:name "total-lte" :hx-preserve "true" @@ -52,7 +52,7 @@ (defn exact-match-id-field* [request] (when-let [exact-match-id (:exact-match-id (:query-params request))] - [:div + [:div (com/field {:label "Exact match"} (com/pill {:color :primary} [:span.inline-flex.gap-2 diff --git a/src/clj/auto_ap/ssr/pos/expected_deposits.clj b/src/clj/auto_ap/ssr/pos/expected_deposits.clj index 546aebdb..c664a90d 100644 --- a/src/clj/auto_ap/ssr/pos/expected_deposits.clj +++ b/src/clj/auto_ap/ssr/pos/expected_deposits.clj @@ -33,7 +33,6 @@ [:processor {:optional true} [:maybe (ref->enum-schema "ccp-processor")]]] default-grid-fields-schema)])) - (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes @@ -42,7 +41,7 @@ "hx-indicator" "#expected-deposit-table" #_#_:hx-disabled-elt "find fieldset"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field* request) (exact-match-id-field* request)]]) @@ -70,26 +69,25 @@ (some-> (:start-date query-params) c/to-date) (some-> (:end-date query-params) c/to-date)]]} (:sort query-params) (add-sorter-fields {"client" ['[?e :expected-deposit/client ?c] - '[?c :client/name ?sort-client]] - "location" ['[?e :expected-deposit/location ?sort-location]] - "date" ['[?e :expected-deposit/date ?sort-date]] - "total" ['[?e :expected-deposit/total ?sort-total]] - "fee" ['[?e :expected-deposit/fee ?sort-fee]]} - query-params) + '[?c :client/name ?sort-client]] + "location" ['[?e :expected-deposit/location ?sort-location]] + "date" ['[?e :expected-deposit/date ?sort-date]] + "total" ['[?e :expected-deposit/total ?sort-total]] + "fee" ['[?e :expected-deposit/fee ?sort-fee]]} + query-params) (:exact-match-id query-params) (merge-query {:query {:in ['?e] :where []} :args [(:exact-match-id query-params)]}) - - (:total-gte query-params) + (:total-gte query-params) (merge-query {:query {:in ['?total-gte] :where ['[?e :expected-deposit/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte query-params)]}) - (:total-lte query-params) + (:total-lte query-params) (merge-query {:query {:in ['?total-lte] :where ['[?e :expected-deposit/total ?a] '[(<= ?a ?total-lte)]]} @@ -104,7 +102,7 @@ true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :expected-deposit/date ?sort-default]]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -113,25 +111,25 @@ (let [results (->> (pull-many db default-read ids) (group-by :db/id)) payments (->> ids - (map results) - (map first) - (map (fn get-totals [ed] - (assoc ed :totals - (->> (dc/q '[:find ?d4 (count ?c) (sum ?a) - :in $ ?ed - :where [?ed :expected-deposit/charges ?c] - [?c :charge/total ?a] - [?o :sales-order/charges ?c] - [?o :sales-order/date ?d] - [(clj-time.coerce/from-date ?d) ?d2] - [(auto-ap.time/localize ?d2) ?d3] - [(clj-time.coerce/to-local-date ?d3) ?d4]] - (dc/db conn) - (:db/id ed)) - (map (fn [[date count amount]] - {:date (c/to-date-time date) - :count count - :amount amount})))))))] + (map results) + (map first) + (map (fn get-totals [ed] + (assoc ed :totals + (->> (dc/q '[:find ?d4 (count ?c) (sum ?a) + :in $ ?ed + :where [?ed :expected-deposit/charges ?c] + [?c :charge/total ?a] + [?o :sales-order/charges ?c] + [?o :sales-order/date ?d] + [(clj-time.coerce/from-date ?d) ?d2] + [(auto-ap.time/localize ?d2) ?d3] + [(clj-time.coerce/to-local-date ?d3) ?d4]] + (dc/db conn) + (:db/id ed)) + (map (fn [[date count amount]] + {:date (c/to-date-time date) + :count count + :amount amount})))))))] payments)) (defn fetch-page [args] @@ -142,66 +140,64 @@ matching-count])) (def grid-page - (helper/build - {:id "expected-deposit-table" - :nav com/main-aside-nav - :page-specific-nav filters - :fetch-page fetch-page - :oob-render - (fn [request] - [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "POS"] + (helper/build + {:id "expected-deposit-table" + :nav com/main-aside-nav + :page-specific-nav filters + :fetch-page fetch-page + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "POS"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :pos-expected-deposits)} - "Expected deposits"]] - :title "Expected deposits" - :entity-name "Expected deposit" - :query-schema query-schema - :route :pos-expected-deposit-table - :row-buttons (fn [_ e] - [ - (when (:expected-deposit/reference-link e) - (com/a-icon-button {:href (:expected-deposit/reference-link e)} - svg/external-link)) - (when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)] - (com/a-button {:href (str (bidi/path-for ssr-routes/only-routes - ::transaction-routes/all-page) - "?exact-match-id=" - transaction-id)} "Transaction"))]) - :headers [{:key "client" - :name "Client" - :sort-key "client" - :hide? (fn [args] - (= (count (:clients args)) 1)) - :render #(-> % :expected-deposit/client :client/code)} - {:key "date" - :name "Date" - :sort-key "date" - :render #(atime/unparse-local (:expected-deposit/date %) atime/standard-time)} - {:key "sales-date" - :name "Sales Date" - :sort-key "sales-date" - :render #(atime/unparse-local (:expected-deposit/sales-date %) atime/standard-time)} - {:key "total" - :name "Total" - :sort-key "total" - :render #(some->> % :expected-deposit/total (format "$%.2f"))} - {:key "fee" - :name "Fee" - :sort-key "fee" - :render #(some->> % :expected-deposit/fee (format "$%.2f"))}]})) + [:a {:href (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits)} + "Expected deposits"]] + :title "Expected deposits" + :entity-name "Expected deposit" + :query-schema query-schema + :route :pos-expected-deposit-table + :row-buttons (fn [_ e] + [(when (:expected-deposit/reference-link e) + (com/a-icon-button {:href (:expected-deposit/reference-link e)} + svg/external-link)) + (when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)] + (com/a-button {:href (str (bidi/path-for ssr-routes/only-routes + ::transaction-routes/all-page) + "?exact-match-id=" + transaction-id)} "Transaction"))]) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :expected-deposit/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:expected-deposit/date %) atime/standard-time)} + {:key "sales-date" + :name "Sales Date" + :sort-key "sales-date" + :render #(atime/unparse-local (:expected-deposit/sales-date %) atime/standard-time)} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :expected-deposit/total (format "$%.2f"))} + {:key "fee" + :name "Fee" + :sort-key "fee" + :render #(some->> % :expected-deposit/fee (format "$%.2f"))}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) - (def key->handler (apply-middleware-to-all-handlers {:pos-expected-deposits (helper/page-route grid-page) - :pos-expected-deposit-table (helper/table-route grid-page)} + :pos-expected-deposit-table (helper/table-route grid-page)} (fn [h] (-> h (wrap-copy-qp-pqp) diff --git a/src/clj/auto_ap/ssr/pos/refunds.clj b/src/clj/auto_ap/ssr/pos/refunds.clj index c33b6784..617d46f7 100644 --- a/src/clj/auto_ap/ssr/pos/refunds.clj +++ b/src/clj/auto_ap/ssr/pos/refunds.clj @@ -36,7 +36,7 @@ "hx-indicator" "#refund-table" #_#_:hx-disabled-elt "find fieldset"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field* request) (total-field* request)]]) @@ -54,7 +54,7 @@ :where '[[(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} :args [db [(:trimmed-clients request) (some-> query-params :start-date c/to-date) - (some-> query-params :end-date c/to-date )]]} + (some-> query-params :end-date c/to-date)]]} (:sort query-params) (add-sorter-fields {"client" ['[?e :sales-refund/client ?c] '[?c :client/name ?sort-client]] "date" ['[?e :sales-refund/date ?sort-date]] @@ -68,13 +68,13 @@ :where []} :args [(:exact-match-id query-params)]}) - (:total-gte query-params) + (:total-gte query-params) (merge-query {:query {:in ['?total-gte] :where ['[?e :sales-refund/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte query-params)]}) - (:total-lte query-params) + (:total-lte query-params) (merge-query {:query {:in ['?total-lte] :where ['[?e :sales-refund/total ?a] '[(<= ?a ?total-lte)]]} @@ -83,7 +83,7 @@ true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :sales-refund/date ?sort-default]]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -149,13 +149,13 @@ (def table* (partial helper/table* grid-page)) (def key->handler - (apply-middleware-to-all-handlers + (apply-middleware-to-all-handlers {:pos-refunds (helper/page-route grid-page) :pos-refund-table (helper/table-route grid-page)} (fn [h] - (-> h - (wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema))))) + (-> h + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema))))) diff --git a/src/clj/auto_ap/ssr/pos/sales_orders.clj b/src/clj/auto_ap/ssr/pos/sales_orders.clj index 8fb5ec31..6e5b4be2 100644 --- a/src/clj/auto_ap/ssr/pos/sales_orders.clj +++ b/src/clj/auto_ap/ssr/pos/sales_orders.clj @@ -36,29 +36,28 @@ (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes - :pos-sales-table) + :pos-sales-table) "hx-target" "#sales-table" "hx-indicator" "#sales-table" #_#_:hx-disabled-elt "find fieldset"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field* request) (total-field* request) [:div (com/field {:label "Payment Method"} (com/radio-card {:size :small - :name "payment-method" - :options [{:value "" - :content "All"} - {:value "CASH" - :content "Cash"} - {:value "CARD" - :content "Card"} - {:value "SQUARE_GIFT_CARD" - :content "Gift Card"} - {:value "OTHER" - :content "Other"} - ]}))] + :name "payment-method" + :options [{:value "" + :content "All"} + {:value "CASH" + :content "Cash"} + {:value "CARD" + :content "Card"} + {:value "SQUARE_GIFT_CARD" + :content "Gift Card"} + {:value "OTHER" + :content "Other"}]}))] [:div (processor-field* request)] @@ -87,8 +86,7 @@ :sales-order/source, :sales-order/reference-link, {:sales-order/client [:client/name :db/id :client/code] - :sales-order/charges [ - :charge/type-name, + :sales-order/charges [:charge/type-name, :charge/total, :charge/tax, :charge/tip, @@ -125,13 +123,13 @@ :where []} :args [(:exact-match-id query-params)]}) - (:total-gte query-params) + (:total-gte query-params) (merge-query {:query {:in ['?total-gte] :where ['[?e :sales-order/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte query-params)]}) - (:total-lte query-params) + (:total-lte query-params) (merge-query {:query {:in ['?total-lte] :where ['[?e :sales-order/total ?a] '[(<= ?a ?total-lte)]]} @@ -155,7 +153,6 @@ '[?chg :charge/processor ?processor]]} :args [(:processor query-params)]}) - true (merge-query {:query {:find ['?sort-default '?e]}}))] (clojure.pprint/pprint (update-in query [:args] #(drop 1 %))) @@ -178,7 +175,6 @@ [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) - (def grid-page (helper/build {:id "sales-table" @@ -255,7 +251,6 @@ "OTHER" "other" nil)))])}]})) - (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) diff --git a/src/clj/auto_ap/ssr/pos/sales_summaries.clj b/src/clj/auto_ap/ssr/pos/sales_summaries.clj index 45988b07..315e6beb 100644 --- a/src/clj/auto_ap/ssr/pos/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/pos/sales_summaries.clj @@ -158,13 +158,13 @@ [:span.text-sm account-name] (com/pill {:color :red} "Missing acct")) (com/a-icon-button {:class "p-1" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-vals (hx/json {:item-index (or (:item-index item) 0) - :client-id client-id - :current-account-id (or account-id "")})} - svg/pencil)])) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-vals (hx/json {:item-index (or (:item-index item) 0) + :client-id client-id + :current-account-id (or account-id "")})} + svg/pencil)])) (defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}] (let [account-input-name (str field-name-prefix "[ledger-mapped/account]")] @@ -172,23 +172,23 @@ (account-typeahead* {:name account-input-name :value current-account-id :client-id client-id}) - [:div.flex.gap-1 - (com/a-icon-button {:class "p-1" - :hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-include "closest .account-cell" - :hx-vals (hx/json {:field-name-prefix field-name-prefix - :client-id client-id})} - svg/check) - (com/a-icon-button {:class "p-1" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-vals (hx/json {:field-name-prefix field-name-prefix - :client-id client-id - :current-account-id (or current-account-id "")})} - svg/x)]])) + [:div.flex.gap-1 + (com/a-icon-button {:class "p-1" + :hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-include "closest .account-cell" + :hx-vals (hx/json {:field-name-prefix field-name-prefix + :client-id client-id})} + svg/check) + (com/a-icon-button {:class "p-1" + :hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-vals (hx/json {:field-name-prefix field-name-prefix + :client-id client-id + :current-account-id (or current-account-id "")})} + svg/x)]])) (def grid-page (helper/build {:id "entity-table" @@ -576,8 +576,8 @@ [:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)] (account-display-cell {:item (assoc item :item-index actual-idx) :field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]") - :client-id client-id}) - [:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]])) + :client-id client-id}) + [:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]])) [:div.h-6]))] [:div.mt-2.border-t.pt-1 (summary-total-display request) @@ -619,13 +619,13 @@ [:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)] (account-display-cell {:item (assoc item :item-index actual-idx) :field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]") - :client-id client-id}) - [:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]])) + :client-id client-id}) + [:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]])) [:div.h-6]))] [:div.mt-2.border-t.pt-1 (summary-total-display request) (unbalanced-display request)]]] - [:div.mt-4.border-t.pt-2 + [:div.mt-4.border-t.pt-2 (fc/with-field :sales-summary/items (com/data-grid-new-row {:colspan 2 :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) @@ -761,16 +761,16 @@ ::route/edit-wizard-navigate (-> mm/next-handler (mm/wrap-wizard edit-wizard) (mm/wrap-decode-multi-form-state)) - ::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items] - (fn render [cursor request] - (sales-summary-item-row* - {:value cursor - :client-id (:client-id (:query-params request))})) - (fn build-new-row [base _] - (assoc base :sales-summary-item/manual? true))) - (wrap-schema-enforce :query-schema [:map - [:client-id {:optional true} - [:maybe entity-id]]])) + ::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items] + (fn render [cursor request] + (sales-summary-item-row* + {:value cursor + :client-id (:client-id (:query-params request))})) + (fn build-new-row [base _] + (assoc base :sales-summary-item/manual? true))) + (wrap-schema-enforce :query-schema [:map + [:client-id {:optional true} + [:maybe entity-id]]])) ::route/edit-item-account (-> edit-item-account (wrap-schema-enforce :query-schema [:map [:item-index nat-int?] diff --git a/src/clj/auto_ap/ssr/pos/tenders.clj b/src/clj/auto_ap/ssr/pos/tenders.clj index 4b42ec4f..664cbf57 100644 --- a/src/clj/auto_ap/ssr/pos/tenders.clj +++ b/src/clj/auto_ap/ssr/pos/tenders.clj @@ -29,7 +29,7 @@ "hx-indicator" "#tender-table" #_#_:hx-disabled-elt "find fieldset"} - [:fieldset.space-y-6 + [:fieldset.space-y-6 (date-range-field* request) (processor-field* request) (total-field* request)]]) @@ -79,13 +79,13 @@ :where []} :args [(:exact-match-id query-params)]}) - (:total-gte query-params) + (:total-gte query-params) (merge-query {:query {:in ['?total-gte] :where ['[?e :charge/total ?a] '[(>= ?a ?total-gte)]]} :args [(:total-gte query-params)]}) - (:total-lte query-params) + (:total-lte query-params) (merge-query {:query {:in ['?total-lte] :where ['[?e :charge/total ?a] '[(<= ?a ?total-lte)]]} @@ -96,10 +96,9 @@ :where ['[?e :charge/processor ?processor]]} :args [(:processor query-params)]}) - true (merge-query {:query {:find ['?sort-default '?e]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -121,63 +120,62 @@ (def grid-page (helper/build - {:id "tender-table" - :nav com/main-aside-nav - :page-specific-nav filters - :fetch-page fetch-page - :oob-render - (fn [request] - [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "POS"] - - [:a {:href (bidi/path-for ssr-routes/only-routes - :pos-tenders)} - "Tenders"]] - :title "Tenders" - :entity-name "Tender" - :query-schema query-schema - :route :pos-tender-table - :row-buttons (fn [request e] - (when (:charge/reference-link e) - [(com/a-icon-button {:href (:charge/reference-link e)} - svg/external-link)])) - :headers [{:key "client" - :name "Client" - :sort-key "client" - :hide? (fn [request] - (= (count (:clients request)) 1)) - :render #(-> % :charge/client :client/code)} - {:key "date" - :name "Date" - :sort-key "date" - :render #(atime/unparse-local (:charge/date %) atime/standard-time)} - {:key "total" - :name "Total" - :sort-key "total" - :render #(some->> % :charge/total (format "$%.2f"))} - {:key "processor" - :name "Processor" - :sort-key "processor" - :render (fn [sales-order] - (when (:charge/processor sales-order) - (com/pill {:color :primary } - (name (:charge/processor sales-order)))))} - {:key "tip" - :name "Tip" - :sort-key "tip" - :render #(some->> % :charge/tip (format "$%.2f"))} - {:key "links" - :name "Links" - :render (fn [entity] - (when-let [expected-deposit-id (some->> entity :expected-deposit/_charges first :db/id)] - [:a {:href (str (bidi/path-for ssr-routes/only-routes - :pos-expected-deposits) - "?exact-match-id=" expected-deposit-id) - :hx-boost "true"} - (com/pill {:color :secondary} "expected deposit")]))}]})) + {:id "tender-table" + :nav com/main-aside-nav + :page-specific-nav filters + :fetch-page fetch-page + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "POS"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :pos-tenders)} + "Tenders"]] + :title "Tenders" + :entity-name "Tender" + :query-schema query-schema + :route :pos-tender-table + :row-buttons (fn [request e] + (when (:charge/reference-link e) + [(com/a-icon-button {:href (:charge/reference-link e)} + svg/external-link)])) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [request] + (= (count (:clients request)) 1)) + :render #(-> % :charge/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:charge/date %) atime/standard-time)} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :charge/total (format "$%.2f"))} + {:key "processor" + :name "Processor" + :sort-key "processor" + :render (fn [sales-order] + (when (:charge/processor sales-order) + (com/pill {:color :primary} + (name (:charge/processor sales-order)))))} + {:key "tip" + :name "Tip" + :sort-key "tip" + :render #(some->> % :charge/tip (format "$%.2f"))} + {:key "links" + :name "Links" + :render (fn [entity] + (when-let [expected-deposit-id (some->> entity :expected-deposit/_charges first :db/id)] + [:a {:href (str (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits) + "?exact-match-id=" expected-deposit-id) + :hx-boost "true"} + (com/pill {:color :secondary} "expected deposit")]))}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) diff --git a/src/clj/auto_ap/ssr/search.clj b/src/clj/auto_ap/ssr/search.clj index 92cd9e06..15fc181b 100644 --- a/src/clj/auto_ap/ssr/search.clj +++ b/src/clj/auto_ap/ssr/search.clj @@ -11,15 +11,15 @@ (defn try-cleanse-date [d] (try - (or - (some-> (atime/parse-utc d atime/normal-date) (atime/unparse atime/solr-date)) - (some-> (atime/parse-utc d atime/iso-date) (atime/unparse atime/solr-date)) - d) + (or + (some-> (atime/parse-utc d atime/normal-date) (atime/unparse atime/solr-date)) + (some-> (atime/parse-utc d atime/iso-date) (atime/unparse atime/solr-date)) + d) (catch Exception _ d))) (defn try-parse-number [n] - (if (re-find #"^[\-]?\d+\.\d+$" n ) + (if (re-find #"^[\-]?\d+\.\d+$" n) (str (with-precision 2 (some-> n (Double/parseDouble) @@ -28,7 +28,6 @@ (double)))) n)) - (defn q->solr-q [q] (let [matches (re-seq #"(?:\".*?\"|\S)+" q)] (str/join " AND " @@ -46,10 +45,8 @@ (= "journal-entry" m) "type:journal-entry" - :else - (str "_text_:\"" (try-parse-number (try-cleanse-date m)) ""\")))))))) - + (str "_text_:\"" (try-parse-number (try-cleanse-date m)) "" \")))))))) (defn search-results [q id] (into [] @@ -58,96 +55,89 @@ (solr/query solr/impl "invoices" {"query" (q->solr-q q) "fields" "id, date, amount, type, description, number, client_code, client_id, vendor_name"}))) - (defn search-results* [q id] - (let [results (search-results q id)] - [:div + (let [results (search-results q id)] + [:div (if (seq results) [:div.flex.gap-8.flex-col (for [doc results] (com/card {} - [:div.flex.flex-col.gap-4 - [:div.flex.items-center.p-2.gap-4.bg-gray-50.dark:bg-gray-800 - [:div.h-8.w-8.p-2 - (cond (= "transaction" (:type doc)) - svg/bank - + [:div.flex.flex-col.gap-4 + [:div.flex.items-center.p-2.gap-4.bg-gray-50.dark:bg-gray-800 + [:div.h-8.w-8.p-2 + (cond (= "transaction" (:type doc)) + svg/bank - (= "invoice" (:type doc)) - svg/accounting-invoice-mail - + (= "invoice" (:type doc)) + svg/accounting-invoice-mail - (= "payment" (:type doc)) - svg/payments + (= "payment" (:type doc)) + svg/payments - (= "journal-entry" (:type doc)) - svg/receipt + (= "journal-entry" (:type doc)) + svg/receipt - :else - nil)] - (clojure.string/capitalize (:type doc)) - (com/pill {:color :primary} - "client: " (:client_code doc)) - (com/pill {:color :secondary} - "amount: $" (first (:amount doc))) - (when-let [vendor-name (first (:vendor_name doc))] - (com/pill {:color :yellow} - "vendor: " vendor-name)) - [:div - (com/link {:href (str "/" (cond (= "invoice" - (:type doc)) - "invoices" + :else + nil)] + (clojure.string/capitalize (:type doc)) + (com/pill {:color :primary} + "client: " (:client_code doc)) + (com/pill {:color :secondary} + "amount: $" (first (:amount doc))) + (when-let [vendor-name (first (:vendor_name doc))] + (com/pill {:color :yellow} + "vendor: " vendor-name)) + [:div + (com/link {:href (str "/" (cond (= "invoice" + (:type doc)) + "invoices" - (= "transaction" - (:type doc)) - "transactions" + (= "transaction" + (:type doc)) + "transactions" - (= "journal-entry" - (:type doc)) - "ledger" + (= "journal-entry" + (:type doc)) + "ledger" - :else - "payments") "/?exact-match-id=" (:id doc)) - :target "_blank"} - [:div.h-8.w-8.p-2 - svg/external-link])] - ] - - - [:div.px-4.pb-2 - [:span - - [:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)] - ": " - (str (or (first (:description doc)) - (first (:number doc))))]]]) - )] + :else + "payments") "/?exact-match-id=" (:id doc)) + :target "_blank"} + [:div.h-8.w-8.p-2 + svg/external-link])]] + + [:div.px-4.pb-2 + [:span + + [:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)] + ": " + (str (or (first (:description doc)) + (first (:number doc))))]]]))] [:div.block "No results found."])])) (defn dialog-contents [request] (if-let [q (get (:form-params request) "q")] (html-response (search-results* q (:identity request))) (modal-response - (com/modal {} - (com/modal-card {:class "w-full h-full"} - [:div.p-2 "Search"] - [:div#search.overflow-auto.space-y-6.p-2.w-full - - (com/text-input {:id "search-input" - :type "search" - :placeholder "5/5/2034 Magheritas" - :name "q" - :hx-post "/search" - :hx-trigger "keyup changed delay:300ms, search" - :hx-target "#search-results" - :hx-indicator "#search" - :value (:q (:params request)) - :autofocus true}) - [:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."] - #_[:style - ".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"] - [:div#search-results - ] - [:div.loader.is-loading.big.htmx-indicator ]] - nil))))) + (com/modal {} + (com/modal-card {:class "w-full h-full"} + [:div.p-2 "Search"] + [:div#search.overflow-auto.space-y-6.p-2.w-full + + (com/text-input {:id "search-input" + :type "search" + :placeholder "5/5/2034 Magheritas" + :name "q" + :hx-post "/search" + :hx-trigger "keyup changed delay:300ms, search" + :hx-target "#search-results" + :hx-indicator "#search" + :value (:q (:params request)) + :autofocus true}) + [:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."] + #_[:style + ".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"] + [:div#search-results] + [:div.loader.is-loading.big.htmx-indicator]] + nil))))) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index c1f2e9bf..5a95d601 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -8,7 +8,6 @@ [:path {:d "M10.5,14.25v-9a9,9,0,1,0,5.561,16.077Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] [:path {:d "M22.5,12.75h-9l5.561,7.077A8.986,8.986,0,0,0,22.5,12.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]) - (def accounting-invoice-mail [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] @@ -91,7 +90,6 @@ [:svg {:id "theme-toggle-light-icon", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [:path {:d "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z", :fill-rule "evenodd", :clip-rule "evenodd"}]]) - (def home [:svg {:fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [:path {:d "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"}]]) @@ -112,7 +110,6 @@ [:svg {:xmlns "http://www.w3.org/2000/svg", :aria-hidden "true", :fill "none", :viewbox "0 0 24 24", :stroke-width "1.5", :stroke "currentColor"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"}]]) - (def upload [:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24", :stroke-width "2", :stroke "currentColor", :aria-hidden "true"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"}]]) @@ -315,7 +312,6 @@ [:line {:x1 "7", :y1 "7", :x2 "17", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] [:line {:x1 "17", :y1 "7", :x2 "7", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) - (def filled-x [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:circle {:cx "12", :cy "12", :r "11.5", :fill "#FFF", :stroke-linecap "round", :stroke-linejoin "round"}] @@ -384,7 +380,6 @@ [:circle {:cx "12", :cy "6.75", :r "5.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] [:path {:d "M7.261,3.958A9.124,9.124,0,0,0,13.833,6.75a9.138,9.138,0,0,0,3.617-.744", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) - (def accounts [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] @@ -404,7 +399,6 @@ [:line {:x1 "15.504", :y1 "5.5", :x2 "15.504", :y2 "12.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] [:line {:x1 "15.504", :y1 "0.5", :x2 "15.504", :y2 "3.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) - (def cog [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] @@ -432,7 +426,6 @@ [:path {:stroke "currentColor", :d "M20.458 13.742C20.3206 13.742 20.2092 13.6305 20.2092 13.4931C20.2092 13.3557 20.3206 13.2443 20.458 13.2443"}] [:path {:stroke "currentColor", :d "M20.458 13.742C20.5955 13.742 20.7069 13.6305 20.7069 13.4931C20.7069 13.3557 20.5955 13.2443 20.458 13.2443"}]]) - (def arrow-in [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] @@ -528,4 +521,4 @@ [:path {:d "M11 15a1 1 0 1 0 2 0 1 1 0 1 0 -2 0", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] [:path {:d "m12 16 0 3", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] [:path {:d "M4.5 9.5h15s1 0 1 1v12s0 1 -1 1h-15s-1 0 -1 -1v-12s0 -1 1 -1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] - [:path {:d "M6.5 6a5.5 5.5 0 0 1 11 0v3.5h-11Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]]) + [:path {:d "M6.5 6a5.5 5.5 0 0 1 11 0v3.5h-11Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]]) diff --git a/src/clj/auto_ap/ssr/transaction.clj b/src/clj/auto_ap/ssr/transaction.clj index d21606dd..521a6f62 100644 --- a/src/clj/auto_ap/ssr/transaction.clj +++ b/src/clj/auto_ap/ssr/transaction.clj @@ -1,8 +1,8 @@ (ns auto-ap.ssr.transaction (:require [auto-ap.datomic - :refer [audit-transact audit-transact-batch conn pull-attr - pull-many]] + :refer [audit-transact audit-transact-batch conn pull-attr + pull-many]] [auto-ap.logging :as alog] [auto-ap.permissions :refer [wrap-must]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] @@ -20,10 +20,10 @@ wrap-status-from-source]] [auto-ap.ssr.transaction.edit :as edit :refer [transaction-account-row*]] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers entity-id html-response - many-entity modal-response percentage ref->enum-schema - wrap-implied-route-param wrap-merge-prior-hx - wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers entity-id html-response + many-entity modal-response percentage ref->enum-schema + wrap-implied-route-param wrap-merge-prior-hx + wrap-schema-enforce]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] @@ -39,8 +39,6 @@ (def page (helper/page-route grid-page)) - - (def table (helper/table-route grid-page)) (def csv (helper/csv-route grid-page)) @@ -60,29 +58,29 @@ selected) all-ids (all-ids-not-locked ids) db (dc/db conn)] - + (alog/info ::bulk-delete-transactions :count (count all-ids) :sample (take 3 all-ids)) - + ;; First retract journal entries and handle payment relationships - (audit-transact - (mapcat (fn [i] - (let [transaction (dc/pull db [:transaction/payment - :transaction/expected-deposit - :db/id] i) - payment-id (-> transaction :transaction/payment :db/id) - expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] - (cond->> [[:db/retractEntity [:journal-entry/original-entity i]]] - payment-id (into [{:db/id payment-id - :payment/status :payment-status/pending} - [:db/retract (:db/id transaction) :transaction/payment payment-id]]) - expected-deposit-id (into [{:db/id expected-deposit-id + (audit-transact + (mapcat (fn [i] + (let [transaction (dc/pull db [:transaction/payment + :transaction/expected-deposit + :db/id] i) + payment-id (-> transaction :transaction/payment :db/id) + expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)] + (cond->> [[:db/retractEntity [:journal-entry/original-entity i]]] + payment-id (into [{:db/id payment-id + :payment/status :payment-status/pending} + [:db/retract (:db/id transaction) :transaction/payment payment-id]]) + expected-deposit-id (into [{:db/id expected-deposit-id :expected-deposit/status :expected-deposit-status/pending} [:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]])))) - all-ids) - (:identity request)) - + all-ids) + (:identity request)) + ;; Then retract or suppress the transactions (audit-transact (mapcat (fn [i] @@ -94,14 +92,12 @@ [:db/retractEntity [:journal-entry/original-entity i]]])) all-ids) (:identity request)) - - (html-response + + (html-response (com/success-modal {:title "Transactions Updated"} [:p (str "Successfully " (if suppress "suppressed" "deleted") " " (count all-ids) " transactions.")]) :headers {"hx-trigger" "invalidated"}))) - - (def key->handler (merge edit/key->handler bulk-code/key->handler diff --git a/src/clj/auto_ap/ssr/transaction/bulk_code.clj b/src/clj/auto_ap/ssr/transaction/bulk_code.clj index 5cc2f268..53d0e802 100644 --- a/src/clj/auto_ap/ssr/transaction/bulk_code.clj +++ b/src/clj/auto_ap/ssr/transaction/bulk_code.clj @@ -1,7 +1,7 @@ (ns auto-ap.ssr.transaction.bulk-code (:require [auto-ap.datomic - :refer [audit-transact-batch conn pull-attr pull-many]] + :refer [audit-transact-batch conn pull-attr pull-many]] [auto-ap.logging :as alog] [auto-ap.permissions :refer [wrap-must]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] @@ -23,9 +23,9 @@ [auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead* location-select*]] [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers entity-id - form-validation-error html-response percentage - ref->enum-schema wrap-merge-prior-hx wrap-schema-enforce]] + :refer [apply-middleware-to-all-handlers entity-id + form-validation-error html-response percentage + ref->enum-schema wrap-merge-prior-hx wrap-schema-enforce]] [bidi.bidi :as bidi] [datomic.api :as dc] [iol-ion.query :refer [dollars=]] @@ -34,52 +34,52 @@ (defn transaction-account-row* [{:keys [value client-id]}] (com/data-grid-row - (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value)))) - :accountId (fc/field-value (:account value))}) + (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value)))) + :accountId (fc/field-value (:account value))}) :data-key "show" - :x-ref "p"} + :x-ref "p"} hx/alpine-mount-then-appear) (fc/with-field :db/id - (com/hidden {:name (fc/field-name) + (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} - (account-typeahead* {:value (fc/field-value) + (account-typeahead* {:value (fc/field-value) :client-id client-id - :name (fc/field-name) - :x-model "accountId"})))) + :name (fc/field-name) + :x-model "accountId"})))) (fc/with-field :location (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors) :x-hx-val:account-id "accountId" - :hx-vals (hx/json (cond-> {:name (fc/field-name) } + :hx-vals (hx/json (cond-> {:name (fc/field-name)} client-id (assoc :client-id client-id))) :x-dispatch:changed "accountId" :hx-trigger "changed" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) :hx-target "find *" :hx-swap "outerHTML"} - (location-select* {:name (fc/field-name) - :account-location (:account/location (cond->> (:account @value) - (nat-int? (:account @value)) (dc/pull (dc/db conn) - '[:account/location]))) + (location-select* {:name (fc/field-name) + :account-location (let [account-id (:account @value)] + (when (nat-int? account-id) + (:account/location (dc/pull (dc/db conn) '[:account/location] account-id)))) :client-locations (pull-attr (dc/db conn) :client/locations client-id) - :value (fc/field-value)})))) + :value (fc/field-value)})))) (fc/with-field :percentage (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} - (com/money-input {:name (fc/field-name) + (com/money-input {:name (fc/field-name) :class "w-16" :value (some-> (fc/field-value) - (* 100) - (long))})))) + (* 100) + (long))})))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) @@ -90,64 +90,58 @@ {:search-params (:query-params request) :accounts []})) - (defn all-ids-not-locked "Filters transaction IDs to only those that aren't locked (client locked date earlier than transaction date)" [all-ids] (->> all-ids (dc/q '[:find ?t - :in $ [?t ...] - :where - [?t :transaction/client ?c] - [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] - [?t :transaction/date ?d] - [(>= ?d ?lu)]] - (dc/db conn)) + :in $ [?t ...] + :where + [?t :transaction/client ?c] + [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] + [?t :transaction/date ?d] + [(>= ?d ?lu)]] + (dc/db conn)) (map first))) - - - - -(def bulk-code-schema +(def bulk-code-schema (mc/schema [:map [:vendor {:optional true} [:maybe entity-id]] - [:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")] ] + [:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]] [:accounts {:optional true} - [:maybe + [:maybe [:vector {:coerce? true} [:map [:account entity-id] [:location [:string {:min 1 :error/message "required"}]] - [:percentage percentage]]]] ]])) - + [:percentage percentage]]]]]])) (defn maybe-code-accounts [transaction account-rules valid-locations] (with-precision 2 (let [accounts (vec (mapcat - (fn [ar] - (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) - (:transaction/amount transaction) - 100))))] - (if (= "Shared" (:location ar)) - (->> valid-locations - (map - (fn [cents location] - {:db/id (random-tempid) - :transaction-account/account (:account ar) - :transaction-account/amount (* 0.01 cents) - :transaction-account/location location}) - (rm/spread-cents cents-to-distribute (count valid-locations)))) - [(cond-> {:db/id (random-tempid) - :transaction-account/account (:account ar) - :transaction-account/amount (* 0.01 cents-to-distribute)} - (:location ar) (assoc :transaction-account/location (:location ar)))]))) - account-rules)) + (fn [ar] + (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar) + (:transaction/amount transaction) + 100))))] + (if (= "Shared" (:location ar)) + (->> valid-locations + (map + (fn [cents location] + {:db/id (random-tempid) + :transaction-account/account (:account ar) + :transaction-account/amount (* 0.01 cents) + :transaction-account/location location}) + (rm/spread-cents cents-to-distribute (count valid-locations)))) + [(cond-> {:db/id (random-tempid) + :transaction-account/account (:account ar) + :transaction-account/amount (* 0.01 cents-to-distribute)} + (:location ar) (assoc :transaction-account/location (:location ar)))]))) + account-rules)) accounts (mapv - (fn [a] - (update a :transaction-account/amount - #(with-precision 2 - (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) - accounts) + (fn [a] + (update a :transaction-account/amount + #(with-precision 2 + (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) + accounts) leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction)) (Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts))))) *math-context*)) @@ -167,74 +161,76 @@ []) (step-schema [_] - (mm/form-schema linear-wizard)) + (mm/form-schema linear-wizard)) (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}] - (let [_ (alog/peek ::SEARCH_PARAMS (:search-params snapshot)) - selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot)) - all-ids (all-ids-not-locked selected-ids)] - (mm/default-render-step - linear-wizard this - :head [:div.p-2 "Bulk editing " (count all-ids) " transactions"] - :body (mm/default-step-body - {} - [:div - #_(com/hidden {:name "ids" :value (pr-str ids)}) - - [:div.space-y-4.p-4 - [:div.grid.grid-cols-2.gap-4 - - ;; Vendor field - [:div - (fc/with-field :vendor - (com/validated-field {:label "Vendor" - :errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :placeholder "Search for vendor..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))] - - ;; Status field - [:div - (fc/with-field :approval-status - (com/validated-field {:label "Status" - :errors (fc/field-errors)} - (com/select {:name (fc/field-name) - :options [["" "No Change"] - ["approved" "Approved"] - ["unapproved" "Unapproved"] - ["suppressed" "Suppressed"] - ["requires_feedback" "Requires Feedback"]]})))] - - ;; Accounts section - [:div.col-span-2.pt-4 - [:h3.text-lg.font-medium.mb-3 "Expense Accounts"] - - [:div#account-entries.space-y-3 - (fc/with-field :accounts - (com/validated-field - {:errors (fc/field-errors)} - (com/data-grid {:headers [(com/data-grid-header {} "Account") - (com/data-grid-header {:class "w-32"} "Location") - (com/data-grid-header {:class "w-16"} "$") - (com/data-grid-header {:class "w-16"})]} - (fc/cursor-map #(transaction-account-row* {:value %})) - - (com/data-grid-new-row {:colspan 4 - :hx-get (bidi/path-for ssr-routes/only-routes - ::route/bulk-code-new-account) - :row-offset 0 - :index (count (fc/field-value))} - "New account") - ))) - - ;; Button to add more accounts - ]]]]]) - :footer - (mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate - :next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save")) - :validation-route ::route/new-wizard-navigate)))) + (let [_ (alog/peek ::SEARCH_PARAMS (:search-params snapshot)) + selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot)) + all-ids (all-ids-not-locked selected-ids)] + (mm/default-render-step + linear-wizard this + :head [:div.p-2 "Bulk editing " (count all-ids) " transactions"] + :body (mm/default-step-body + {} + [:div + #_(com/hidden {:name "ids" :value (pr-str ids)}) + [:div.space-y-4.p-4 + [:div.grid.grid-cols-2.gap-4 + + ;; Vendor field + [:div {:hx-trigger "change" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-vendor-changed) + :hx-target "#account-entries" + :hx-swap "innerHTML" + :hx-include "closest form"} + (fc/with-field :vendor + (com/validated-field {:label "Vendor" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :placeholder "Search for vendor..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))] + + ;; Status field + [:div + (fc/with-field :approval-status + (com/validated-field {:label "Status" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :options [["" "No Change"] + ["approved" "Approved"] + ["unapproved" "Unapproved"] + ["suppressed" "Suppressed"] + ["requires_feedback" "Requires Feedback"]]})))] + + ;; Accounts section + [:div.col-span-2.pt-4 + [:h3.text-lg.font-medium.mb-3 "Expense Accounts"] + + [:div#account-entries.space-y-3 + (fc/with-field :accounts + (com/validated-field + {:errors (fc/field-errors)} + (com/data-grid {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} "$") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(transaction-account-row* {:value %})) + + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/bulk-code-new-account) + :row-offset 0 + :index (count (fc/field-value))} + "New account"))))]]]]]) + +;; Button to add more accounts + + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate + :next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save")) + :validation-route ::route/new-wizard-navigate)))) (defn assert-percentages-add-up [{:keys [accounts]}] (let [account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts))] @@ -263,78 +259,128 @@ (steps [_] [:accounts]) (get-step [this step-key] - (let [step-key-result (mc/parse mm/step-key-schema step-key) + (let [step-key-result (mc/parse mm/step-key-schema step-key) [step-key-type step-key] step-key-result] (get {:accounts (->AccountsStep this)} step-key))) (form-schema [_] bulk-code-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] - (let [ ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state))) - all-ids (all-ids-not-locked ids) - vendor (-> request :multi-form-state :snapshot :vendor) - approval-status (-> request :multi-form-state :snapshot :approval-status) - accounts (-> request :multi-form-state :snapshot :accounts) ] - (when (seq accounts) - (assert-percentages-add-up (:snapshot multi-form-state))) - (alog/peek ::ACCOUNTS (-> request :multi-form-state :snapshot)) - + (let [ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state))) + all-ids (all-ids-not-locked ids) + vendor (-> request :multi-form-state :snapshot :vendor) + approval-status (-> request :multi-form-state :snapshot :approval-status) + accounts (-> request :multi-form-state :snapshot :accounts)] + (when (seq accounts) + (assert-percentages-add-up (:snapshot multi-form-state))) + (alog/peek ::ACCOUNTS (-> request :multi-form-state :snapshot)) + ;; Get transactions and filter for locked ones - (let [db (dc/db conn) - transactions (pull-many db [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids)) + (let [db (dc/db conn) + transactions (pull-many db [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids)) ;; Get client locations - client->locations (->> (map (comp :db/id :transaction/client) transactions) - (distinct) - (dc/q '[:find (pull ?e [:db/id :client/locations]) - :in $ [?e ...]] - db) - (map (fn [[client]] - [(:db/id client) (:client/locations client)])) - (into {}))] + client->locations (->> (map (comp :db/id :transaction/client) transactions) + (distinct) + (dc/q '[:find (pull ?e [:db/id :client/locations]) + :in $ [?e ...]] + db) + (map (fn [[client]] + [(:db/id client) (:client/locations client)])) + (into {}))] ;; Validate account locations - (doseq [a accounts - :let [{:keys [:account/location :account/name]} (dc/pull db - [:account/location :account/name] - (:account a))]] - (when (and location (not= location (:location a))) - (form-validation-error (str "Account " name " uses location " (:location a) ", but is supposed to be " location))) - (doseq [[_ locations] client->locations] - (when (and (not location) - (not (get (into #{"Shared"} locations) - (:location a)))) - (form-validation-error (str "Account " name " uses location " (:location a) ", but doesn't belong to the client."))))) + (doseq [a accounts + :let [{:keys [:account/location :account/name]} (dc/pull db + [:account/location :account/name] + (:account a))]] + (when (and location (not= location (:location a))) + (form-validation-error (str "Account " name " uses location " (:location a) ", but is supposed to be " location))) + (doseq [[_ locations] client->locations] + (when (and (not location) + (not (get (into #{"Shared"} locations) + (:location a)))) + (form-validation-error (str "Account " name " uses location " (:location a) ", but doesn't belong to the client."))))) - (audit-transact-batch - (map (fn [t] - (let [locations (client->locations (-> t :transaction/client :db/id))] - [:upsert-transaction (cond-> t - approval-status - (assoc :transaction/approval-status approval-status) + (audit-transact-batch + (map (fn [t] + (let [locations (client->locations (-> t :transaction/client :db/id))] + [:upsert-transaction (cond-> t + approval-status + (assoc :transaction/approval-status approval-status) - vendor - (assoc :transaction/vendor vendor) + vendor + (assoc :transaction/vendor vendor) - (seq accounts) - (assoc :transaction/accounts - (maybe-code-accounts t accounts locations)))])) - transactions) - (:identity request)) + (seq accounts) + (assoc :transaction/accounts + (maybe-code-accounts t accounts locations)))])) + transactions) + (:identity request)) ;; Return success modal - (html-response - (com/success-modal {:title "Transactions Coded"} - [:p (str "Successfully coded " (count all-ids) " transactions.")]) - :headers {"hx-trigger" "refreshTable"}))))) + (html-response + (com/success-modal {:title "Transactions Coded"} + [:p (str "Successfully coded " (count all-ids) " transactions.")]) + :headers {"hx-trigger" "refreshTable"}))))) + +(defn- get-client-id [request] + (-> request :clients first :db/id)) + +(defn- vendor-default-account [vendor-id client-id] + (when vendor-id + (let [vendor (edit/get-vendor vendor-id) + clientized (edit/clientize-vendor vendor client-id)] + (:vendor/default-account clientized)))) + +(defn- build-default-account-row [account] + {:db/id (str (java.util.UUID/randomUUID)) + :account (:db/id account) + :location (or (:account/location account) "Shared") + :percentage 1.0}) + +(defn- render-accounts-section [request] + (let [step-params (:step-params (:multi-form-state request))] + (html-response + [:div + (fc/start-form step-params + (when (:form-errors request) {:step-params (:form-errors request)}) + (fc/with-field :accounts + (com/validated-field + {:errors (fc/field-errors)} + (com/data-grid {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} "$") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(transaction-account-row* {:value %})) + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/bulk-code-new-account) + :row-offset 0 + :index (count (fc/field-value))} + "New account")))))]))) + +(defn vendor-changed-handler [request] + (let [snapshot (:snapshot (:multi-form-state request)) + step-params (:step-params (:multi-form-state request)) + client-id (get-client-id request) + vendor-id (or (:vendor step-params) (:vendor snapshot)) + updated-step-params (if (and (empty? (:accounts step-params)) + vendor-id + client-id) + (if-let [default-account (vendor-default-account vendor-id client-id)] + (assoc step-params :accounts [(build-default-account-row default-account)]) + step-params) + step-params)] + (render-accounts-section (assoc-in request [:multi-form-state :step-params] updated-step-params)))) (def bulk-code-wizard (->BulkCodeWizard nil nil)) (def key->handler (apply-middleware-to-all-handlers - {::route/bulk-code (-> mm/open-wizard-handler - (mm/wrap-wizard bulk-code-wizard) - (mm/wrap-init-multi-form-state initial-bulk-edit-state)) + {::route/bulk-code (-> mm/open-wizard-handler + (mm/wrap-wizard bulk-code-wizard) + (mm/wrap-init-multi-form-state initial-bulk-edit-state)) ::route/bulk-code-new-account (-> (add-new-entity-handler [:step-params :accounts] (fn render [cursor request] @@ -345,9 +391,12 @@ (wrap-schema-enforce :query-schema [:map [:client-id {:optional true} [:maybe entity-id]]])) - ::route/bulk-code-submit (-> mm/submit-handler - (wrap-wizard bulk-code-wizard) - (mm/wrap-decode-multi-form-state))} + ::route/bulk-code-vendor-changed (-> vendor-changed-handler + (mm/wrap-wizard bulk-code-wizard) + (mm/wrap-decode-multi-form-state)) + ::route/bulk-code-submit (-> mm/submit-handler + (wrap-wizard bulk-code-wizard) + (mm/wrap-decode-multi-form-state))} (fn [h] (-> h (wrap-copy-qp-pqp) diff --git a/src/clj/auto_ap/ssr/transaction/common.clj b/src/clj/auto_ap/ssr/transaction/common.clj index 5e303410..ae066f46 100644 --- a/src/clj/auto_ap/ssr/transaction/common.clj +++ b/src/clj/auto_ap/ssr/transaction/common.clj @@ -1,9 +1,9 @@ -(ns auto-ap.ssr.transaction.common +(ns auto-ap.ssr.transaction.common (:require - [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4 - conn merge-query observable-query pull-many]] - [auto-ap.datomic.accounts :as d-accounts] - [auto-ap.graphql.utils :refer [extract-client-ids is-admin?]] + [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4 + conn merge-query observable-query pull-many]] + [auto-ap.datomic.accounts :as d-accounts] + [auto-ap.graphql.utils :refer [extract-client-ids is-admin?]] [auto-ap.routes.invoice :as invoice-routes] [auto-ap.routes.ledger :as ledger-routes] [auto-ap.routes.payments :as payment-routes] @@ -33,26 +33,26 @@ [:amount-gte {:optional true} [:maybe :double]] [:amount-lte {:optional true} [:maybe :double]] [:client-id {:optional true} [:maybe entity-id]] - [:import-batch-id {:optional true} [:maybe entity-id]] - [:unresolved {:optional true} - [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true - (= % "") false - :else - (boolean %))}}]]] - [:description {:optional true} [:maybe [:string {:decode/string strip}]]] - [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] - [:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]] - [:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]] - [:linked-to {:optional true} - [:maybe [:enum {:decode/string {:enter #(if (seq %) % nil)}} - "payment" "expected-deposit" "invoice" "none"]]] - [:location {:optional true} [:maybe [:string {:decode/string strip}]]] - [:potential-duplicates {:optional true} - [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true - (= % "") false - :else - (boolean %))}}]]] - #_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]] + [:import-batch-id {:optional true} [:maybe entity-id]] + [:unresolved {:optional true} + [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true + (= % "") false + :else + (boolean %))}}]]] + [:description {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] + [:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]] + [:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]] + [:linked-to {:optional true} + [:maybe [:enum {:decode/string {:enter #(if (seq %) % nil)}} + "payment" "expected-deposit" "invoice" "none"]]] + [:location {:optional true} [:maybe [:string {:decode/string strip}]]] + [:potential-duplicates {:optional true} + [:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true + (= % "") false + :else + (boolean %))}}]]] + #_[:status {:optional true} [:maybe (ref->enum-schema "transaction-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} @@ -66,14 +66,14 @@ '[:transaction/amount :transaction/description-original :transaction/description-simple - [ :transaction/date :xform clj-time.coerce/from-date] - [ :transaction/post-date :xform clj-time.coerce/from-date] + [:transaction/date :xform clj-time.coerce/from-date] + [:transaction/post-date :xform clj-time.coerce/from-date] :transaction/type :transaction/status :transaction/client-overrides :db/id {:transaction/vendor [:vendor/name :db/id] - :transaction/client [:client/name :client/code :db/id [ :client/locked-until :xform clj-time.coerce/from-date]] + :transaction/client [:client/name :client/code :db/id [:client/locked-until :xform clj-time.coerce/from-date]] :transaction/bank-account [:bank-account/numeric-code :bank-account/name] :transaction/accounts [{:transaction-account/account [:account/name :db/id]} :transaction-account/location @@ -104,13 +104,13 @@ [all-ids] (->> all-ids (dc/q '[:find ?t - :in $ [?t ...] - :where - [?t :transaction/client ?c] - [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] - [?t :transaction/date ?d] - [(>= ?d ?lu)]] - (dc/db conn)) + :in $ [?t ...] + :where + [?t :transaction/client ?c] + [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] + [?t :transaction/date ?d] + [(>= ?d ?lu)]] + (dc/db conn)) (map first))) (defn fetch-ids [db {:keys [query-params route-params] :as request}] @@ -161,75 +161,75 @@ :where ['[?e :transaction/bank-account ?ba]]} :args [(:db/id (:bank-account args))]}) - (:vendor args) - (merge-query {:query {:in ['?vendor-id] - :where ['[?e :transaction/vendor ?vendor-id]]} - :args [(:db/id (:vendor args))]}) + (:vendor args) + (merge-query {:query {:in ['?vendor-id] + :where ['[?e :transaction/vendor ?vendor-id]]} + :args [(:db/id (:vendor args))]}) - (:db/id (:account args)) - (merge-query {:query {:in ['?account-id] - :where ['[?e :transaction/accounts ?tas] - '[?tas :transaction-account/account ?account-id]]} - :args [(:db/id (:account args))]}) - (:import-batch-id args) - (merge-query {:query {:in ['?import-batch-id] - :where ['[?import-batch-id :import-batch/entry ?e]]} - :args [(:import-batch-id args)]}) + (:db/id (:account args)) + (merge-query {:query {:in ['?account-id] + :where ['[?e :transaction/accounts ?tas] + '[?tas :transaction-account/account ?account-id]]} + :args [(:db/id (:account args))]}) + (:import-batch-id args) + (merge-query {:query {:in ['?import-batch-id] + :where ['[?import-batch-id :import-batch/entry ?e]]} + :args [(:import-batch-id args)]}) - (:unresolved args) - (merge-query {:query {:where ['[?e :transaction/date] - '(or-join [?e] - (not [?e :transaction/accounts]) - (and [?e :transaction/accounts ?tas] - (not [?tas :transaction-account/account]))) ]}}) + (:unresolved args) + (merge-query {:query {:where ['[?e :transaction/date] + '(or-join [?e] + (not [?e :transaction/accounts]) + (and [?e :transaction/accounts ?tas] + (not [?tas :transaction-account/account])))]}}) - (seq (:location args)) - (merge-query {:query {:in ['?location] - :where ['[?e :transaction/accounts ?tas] - '[?tas :transaction-account/location ?location]]} - :args [(:location args)]}) + (seq (:location args)) + (merge-query {:query {:in ['?location] + :where ['[?e :transaction/accounts ?tas] + '[?tas :transaction-account/location ?location]]} + :args [(:location args)]}) - (= (:linked-to args) "payment") - (merge-query {:query {:where ['[?e :transaction/payment]]}}) + (= (:linked-to args) "payment") + (merge-query {:query {:where ['[?e :transaction/payment]]}}) - (= (:linked-to args) "expected-deposit") - (merge-query {:query {:where ['[?e :transaction/expected-deposit]]}}) + (= (:linked-to args) "expected-deposit") + (merge-query {:query {:where ['[?e :transaction/expected-deposit]]}}) - (= (:linked-to args) "invoice") - (merge-query {:query {:where ['[?e :transaction/payment ?p] - '[_ :invoice-payment/payment ?p]]}}) + (= (:linked-to args) "invoice") + (merge-query {:query {:where ['[?e :transaction/payment ?p] + '[_ :invoice-payment/payment ?p]]}}) - (= (:linked-to args) "none") - (merge-query {:query {:where ['(not [?e :transaction/payment]) - '(not [?e :transaction/expected-deposit])]}}) + (= (:linked-to args) "none") + (merge-query {:query {:where ['(not [?e :transaction/payment]) + '(not [?e :transaction/expected-deposit])]}}) - (:potential-duplicates args) - (merge-query (let [bank-account-id (:db/id (:bank-account args)) - _ (when-not bank-account-id - (throw (ex-info "In order to select potential duplicates, you must choose a bank account." - {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) - duplicate-ids (->> (dc/q '[:find ?tx ?amount ?date - :in $ ?ba - :where - [?tx :transaction/bank-account ?ba] - [?tx :transaction/amount ?amount] - [?tx :transaction/date ?date] - (not [?tx :transaction/approval-status :transaction-approval-status/suppressed])] - db - bank-account-id) - (group-by (fn [[_ amount date]] - [amount date])) - (filter (fn [[_ txes]] - (> (count txes) 1))) - (vals) - (mapcat identity) - (map first) - set)] - {:query {:in '[[?e ...]] - :where []} - :args [duplicate-ids]})) + (:potential-duplicates args) + (merge-query (let [bank-account-id (:db/id (:bank-account args)) + _ (when-not bank-account-id + (throw (ex-info "In order to select potential duplicates, you must choose a bank account." + {:validation-error "In order to select potential duplicates, you must choose a bank account."}))) + duplicate-ids (->> (dc/q '[:find ?tx ?amount ?date + :in $ ?ba + :where + [?tx :transaction/bank-account ?ba] + [?tx :transaction/amount ?amount] + [?tx :transaction/date ?date] + (not [?tx :transaction/approval-status :transaction-approval-status/suppressed])] + db + bank-account-id) + (group-by (fn [[_ amount date]] + [amount date])) + (filter (fn [[_ txes]] + (> (count txes) 1))) + (vals) + (mapcat identity) + (map first) + set)] + {:query {:in '[[?e ...]] + :where []} + :args [duplicate-ids]})) - (:status route-params) + (:status route-params) (merge-query {:query {:in ['?status] :where ['[?e :transaction/approval-status ?status]]} :args [(:status route-params)]}) @@ -253,8 +253,8 @@ (->> (observable-query query) (apply-sort-4 (assoc query-params :default-asc? true)) (apply-pagination query-params)))) - - (defn fetch-page [request] + +(defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count all-ids :all-ids} (fetch-ids db request)] @@ -263,8 +263,6 @@ matching-count (sum-amount all-ids)])) - - (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"} @@ -290,8 +288,6 @@ (com/link {"@click" "import_batch_id=null; $nextTick(() => $dispatch('change'))"} svg/x)]])])) - - (defn bank-account-filter* [request] [:div {:hx-trigger "clientSelected from:body" :hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter) @@ -313,7 +309,6 @@ {:value (:db/id ba) :content (:bank-account/name ba)}))}))))]) - (defn filters [request] [:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes @@ -324,94 +319,93 @@ (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})) - (com/field {:label "Financial Account"} - (com/typeahead {:name "account" - :id "account" - :url (bidi/path-for ssr-routes/only-routes :account-search) - :value (:account (:query-params request)) - :value-fn :db/id - :content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %)) - (:db/id (:client request))))})) - (bank-account-filter* request) + (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})) + (com/field {:label "Financial Account"} + (com/typeahead {:name "account" + :id "account" + :url (bidi/path-for ssr-routes/only-routes :account-search) + :value (:account (:query-params request)) + :value-fn :db/id + :content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %)) + (:db/id (:client request))))})) + (bank-account-filter* request) (date-range-field* request) - (com/field {:label "Description"} - (com/text-input {:name "description" - :id "description" - :class "hot-filter" - :value (:description (:query-params request)) - :placeholder "e.g., Groceries" - :size :small})) + (com/field {:label "Description"} + (com/text-input {:name "description" + :id "description" + :class "hot-filter" + :value (:description (:query-params request)) + :placeholder "e.g., Groceries" + :size :small})) - (com/field {:label "Location"} - (com/text-input {:name "location" - :id "location" - :class "hot-filter" - :value (:location (:query-params request)) - :placeholder "SC" - :size :small})) + (com/field {:label "Location"} + (com/text-input {:name "location" + :id "location" + :class "hot-filter" + :value (:location (:query-params request)) + :placeholder "SC" + :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})]) + (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})]) - (com/field {:label "Linking"} - (com/radio-card {:size :small - :name "linked-to" - :value (or (:linked-to (:query-params request)) "") - :options [{:value "" - :content "All"} - {:value "none" - :content "None"} - {:value "invoice" - :content "Invoice"} - {:value "expected-deposit" - :content "Expected Deposit"} - {:value "payment" - :content "Payment"}]})) + (com/field {:label "Linking"} + (com/radio-card {:size :small + :name "linked-to" + :value (or (:linked-to (:query-params request)) "") + :options [{:value "" + :content "All"} + {:value "none" + :content "None"} + {:value "invoice" + :content "Invoice"} + {:value "expected-deposit" + :content "Expected Deposit"} + {:value "payment" + :content "Payment"}]})) - (when (is-admin? (:identity request)) - [:div.mt-4 {:x-data (hx/json {:unresolvedOnly (:unresolved (:query-params request))})} - (com/hidden {:name "unresolved" - ":value" "unresolvedOnly ? 'on' : ''"}) - (com/checkbox {:value (:unresolved (:query-params request)) - :x-model "unresolvedOnly"} - "Unresolved only")]) + (when (is-admin? (:identity request)) + [:div.mt-4 {:x-data (hx/json {:unresolvedOnly (:unresolved (:query-params request))})} + (com/hidden {:name "unresolved" + ":value" "unresolvedOnly ? 'on' : ''"}) + (com/checkbox {:value (:unresolved (:query-params request)) + :x-model "unresolvedOnly"} + "Unresolved only")]) - (when (and (is-admin? (:identity request)) - (:db/id (:bank-account (:query-params request)))) - [:div.mt-4 {:x-data (hx/json {:potentialDuplicates (:potential-duplicates (:query-params request))})} - (com/hidden {:name "potential-duplicates" - ":value" "potentialDuplicates ? 'on' : ''"}) - (com/checkbox {:value (:potential-duplicates (:query-params request)) - :x-model "potentialDuplicates"} - "Same Amount + Date")]) - - (import-batch-id* request) - (exact-match-id* request)]]) + (when (and (is-admin? (:identity request)) + (:db/id (:bank-account (:query-params request)))) + [:div.mt-4 {:x-data (hx/json {:potentialDuplicates (:potential-duplicates (:query-params request))})} + (com/hidden {:name "potential-duplicates" + ":value" "potentialDuplicates ? 'on' : ''"}) + (com/checkbox {:value (:potential-duplicates (:query-params request)) + :x-model "potentialDuplicates"} + "Same Amount + Date")]) + (import-batch-id* request) + (exact-match-id* request)]]) (def grid-page (helper/build {:id "entity-table" @@ -420,19 +414,17 @@ :page-specific-nav filters :fetch-page fetch-page :query-schema query-schema - :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) - (some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))]) + :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) + (some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))]) :action-buttons (fn [request] - [ - (com/button {:color :primary + [(com/button {:color :primary :hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-code) :hx-target "#modal-holder" "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" - "hx-include" "#transaction-filters" - } + "hx-include" "#transaction-filters"} "Code") (com/button {:color :primary :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete) @@ -454,13 +446,13 @@ tx-date (:transaction/date entity) is-locked (and locked-until tx-date (time/before? tx-date locked-until))] (if is-locked - [ [:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content - svg/lock]] + [[:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content + svg/lock]] [(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes - ::route/edit-wizard - :db/id (:db/id entity))} - svg/pencil)]))) - + ::route/edit-wizard + :db/id (:db/id entity))} + svg/pencil)]))) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} "Transactions"]] :title (fn [r] @@ -477,9 +469,7 @@ (= (-> request :query-params :sort first :name) "Vendor") (or (-> entity :transaction/vendor :vendor/name) "No vendor") - - - + :else nil)) :page->csv-entities (fn [[transactions]] transactions) @@ -529,20 +519,20 @@ :render (fn [i] (let [db (dc/db conn) journal-entries (when (:db/id i) - (dc/q '[:find (pull ?je [:db/id :journal-entry/id]) - :in $ ?t-id - :where - [?je :journal-entry/original-entity ?t-id]] - db - (:db/id i))) + (dc/q '[:find (pull ?je [:db/id :journal-entry/id]) + :in $ ?t-id + :where + [?je :journal-entry/original-entity ?t-id]] + db + (:db/id i))) linked-invoices (when (and (:db/id i) (:transaction/payment i)) - (dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total]) - :in $ ?payment-id - :where - [?ip :invoice-payment/payment ?payment-id] - [?ip :invoice-payment/invoice ?inv]] - db - (:db/id (:transaction/payment i))))] + (dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total]) + :in $ ?payment-id + :where + [?ip :invoice-payment/payment ?payment-id] + [?ip :invoice-payment/invoice ?inv]] + db + (:db/id (:transaction/payment i))))] (link-dropdown (cond-> [] ;; Payment link @@ -553,41 +543,36 @@ {:exact-match-id (:db/id (:transaction/payment i))}) :color :primary :content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))}) - + ;; Journal entry links (seq journal-entries) (concat (for [[je] journal-entries] {:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page) - {:exact-match-id (:db/id je)}) - :color :yellow - :content "Ledger entry"})) - + {:exact-match-id (:db/id je)}) + :color :yellow + :content "Ledger entry"})) + ;; Invoice links (seq linked-invoices) (concat (for [[inv] linked-invoices] {:link (hu/url (bidi/path-for ssr-routes/only-routes - ::invoice-routes/all-page) + ::invoice-routes/all-page) {:exact-match-id (:db/id inv)}) :color :secondary - :content (format "Invoice '%s'" (:invoice/invoice-number inv))})) - - )))) + :content (format "Invoice '%s'" (:invoice/invoice-number inv))})))))) + :render-for #{:html}}]})) (defn wrap-status-from-source [handler] (fn [{:keys [matched-current-page-route] :as request}] - (let [ request (cond-> request + (let [request (cond-> request (= ::route/unapproved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/unapproved) (= ::route/approved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/approved) (= ::route/requires-feedback-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/requires-feedback) (= ::route/page matched-current-page-route) (assoc-in [:route-params :status] nil))] (handler request)))) - - - - (defn selected->ids [request params] (let [all-selected (:all-selected params) selected (:selected params) @@ -598,7 +583,6 @@ (assoc-in [:query-params :start] 0) (assoc-in [:query-params :per-page] 250)))) - :else selected)] ids)) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index b4400f4b..a5d22882 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -142,6 +142,12 @@ true (dissoc :vendor/account-overrides :vendor/terms-overrides))] vendor))) +(defn vendor-default-account [vendor-id client-id] + (when vendor-id + (let [vendor (get-vendor vendor-id) + clientized (clientize-vendor vendor client-id)] + (:vendor/default-account clientized)))) + (defn location-select* [{:keys [name account-location client-locations value]}] (let [options (into (cond account-location @@ -904,18 +910,23 @@ (transaction-rules-view request)] [:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} [:div {} - (fc/with-field :transaction/vendor - (com/validated-field - {:label "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))})])) + [:div {:hx-trigger "change" + :hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed) + :hx-target "#account-grid-body" + :hx-swap "outerHTML" + :hx-include "closest form"} + (fc/with-field :transaction/vendor + (com/validated-field + {:label "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))})]))] ;; Memo field @@ -1381,6 +1392,35 @@ [] entity))) +(defn edit-vendor-changed-handler [request] + (let [snapshot (:snapshot (:multi-form-state request)) + client-id (or (:transaction/client snapshot) + (-> request :entity :transaction/client :db/id)) + vendor-id (:transaction/vendor snapshot) + total (Math/abs (or (:transaction/amount snapshot) 0.0)) + amount-mode (or (:amount-mode snapshot) "$")] + (if (and (empty? (:transaction/accounts snapshot)) + vendor-id + client-id) + (if-let [default-account (vendor-default-account vendor-id client-id)] + (let [new-account {:db/id (str (java.util.UUID/randomUUID)) + :transaction-account/account (:db/id default-account) + :transaction-account/location (or (:account/location default-account) "Shared")} + new-account (if (= amount-mode "%") + (assoc new-account :transaction-account/amount 100.0) + (assoc new-account :transaction-account/amount total)) + updated-snapshot (assoc snapshot :transaction/accounts [new-account]) + updated-request (assoc-in request [:multi-form-state :snapshot] updated-snapshot)] + (html-response + [:div#account-grid-body + (account-grid-body* updated-request)])) + (html-response + [:div#account-grid-body + (account-grid-body* request)])) + (html-response + [:div#account-grid-body + (account-grid-body* request)])))) + (def key->handler (apply-middleware-to-all-handlers {::route/edit-wizard (-> mm/open-wizard-handler @@ -1399,6 +1439,9 @@ (wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read) (mm/wrap-wizard edit-wizard) (mm/wrap-decode-multi-form-state)) + ::route/edit-vendor-changed (-> edit-vendor-changed-handler + (mm/wrap-wizard edit-wizard) + (mm/wrap-decode-multi-form-state)) ::route/location-select (-> location-select (wrap-schema-enforce :query-schema [:map [:name :string] diff --git a/src/clj/auto_ap/ssr/transaction/insights.clj b/src/clj/auto_ap/ssr/transaction/insights.clj index bb91ef38..79df77c7 100644 --- a/src/clj/auto_ap/ssr/transaction/insights.clj +++ b/src/clj/auto_ap/ssr/transaction/insights.clj @@ -57,7 +57,7 @@ :args [(dc/db conn) (iol-ion.query/recent-date 300) (map :db/id clients) - + pull-expr]}) (map first) (drop-while (fn [x] @@ -71,44 +71,42 @@ (take 50) (into [])))) - (defn get-pinecone [transaction-id] - (-> - (http/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch" - url/url - (assoc :query {:ids transaction-id}) - str) - {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} - :as :json - :keywordize? false}) - :body - :vectors - ((keyword (str transaction-id))) - :values)) + (-> + (http/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch" + url/url + (assoc :query {:ids transaction-id}) + str) + {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} + :as :json + :keywordize? false}) + :body + :vectors + ((keyword (str transaction-id))) + :values)) (defn get-pinecone-similarities [transaction-id] - (filter - (fn [{:keys [score]}] - (> score 0.95) - ) - (-> - (http/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query" - url/url - str) - {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} - :form-params {"vector" (get-pinecone transaction-id) - "topK" 100, - "includeMetadata" true - "namespace" ""} - :content-type :json - :as :json}) - :body - :matches))) + (filter + (fn [{:keys [score]}] + (> score 0.95)) + (-> + (http/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query" + url/url + str) + {:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"} + :form-params {"vector" (get-pinecone transaction-id) + "topK" 100, + "includeMetadata" true + "namespace" ""} + :content-type :json + :as :json}) + :body + :matches))) (defn pinecone-similarity-list [transaction-id] (for [{{:keys [amount date description vendor]} :metadata score :score id :id} (get-pinecone-similarities transaction-id) - :let [vendor-name (:vendor/name (:transaction/vendor (dc/pull (dc/db conn) [{:transaction/vendor [:vendor/name]} ] (Long/parseLong id)))) - account-code (-> (dc/pull (dc/db conn) [{:transaction/accounts [{:transaction-account/account [:account/numeric-code]}]} ] (Long/parseLong id)) + :let [vendor-name (:vendor/name (:transaction/vendor (dc/pull (dc/db conn) [{:transaction/vendor [:vendor/name]}] (Long/parseLong id)))) + account-code (-> (dc/pull (dc/db conn) [{:transaction/accounts [{:transaction-account/account [:account/numeric-code]}]}] (Long/parseLong id)) :transaction/accounts first :transaction-account/account @@ -121,7 +119,6 @@ :description description :score score})) - (defn transaction-row [r & {:keys [hide-actions? class last? other-params]}] (com/data-grid-row (cond-> {:class class} @@ -219,8 +216,8 @@ @(dc/transact conn [updated-transaction]) (html-response (transaction-row (parse-outcome (dc/pull db-before - pull-expr - (Long/parseLong transaction-id))) + pull-expr + (Long/parseLong transaction-id))) :hide-actions? true :class "live-added" :other-params (hx/alpine-mount-then-disappear {}))))) @@ -237,48 +234,48 @@ (defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}] (let [r (dc/pull (dc/db conn) - pull-expr - (Long/parseLong transaction-id)) + pull-expr + (Long/parseLong transaction-id)) similar (pinecone-similarity-list transaction-id)] (modal-response - (com/modal {} - (com/modal-card {:style {:width "900px"}} - [:div.flex [:div.p-2 "Similar Transactions"]] - (com/data-grid {:headers [(com/data-grid-header {:name "Date" - :key "date"}) - (com/data-grid-header {:name "Description" - :key "description"}) - (com/data-grid-header {:name "Amount" - :key "amount"}) - (com/data-grid-header {:name "Vendor" - :key "vendor"}) - (com/data-grid-header {:name "Account" - :key "account"}) - (com/data-grid-header {:name "Score" - :key "score"})]} + (com/modal {} + (com/modal-card {:style {:width "900px"}} + [:div.flex [:div.p-2 "Similar Transactions"]] + (com/data-grid {:headers [(com/data-grid-header {:name "Date" + :key "date"}) + (com/data-grid-header {:name "Description" + :key "description"}) + (com/data-grid-header {:name "Amount" + :key "amount"}) + (com/data-grid-header {:name "Vendor" + :key "vendor"}) + (com/data-grid-header {:name "Account" + :key "account"}) + (com/data-grid-header {:name "Score" + :key "score"})]} - (com/data-grid-row {:class "bg-primary-200"} - (com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))) - (com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original) ) - (com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0) - [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))] - [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])) - (com/data-grid-cell {}) - (com/data-grid-cell {}) - (com/data-grid-cell {})) + (com/data-grid-row {:class "bg-primary-200"} + (com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))) + (com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original)) + (com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0) + [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))] + [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])) + (com/data-grid-cell {}) + (com/data-grid-cell {}) + (com/data-grid-cell {})) - (com/data-grid-row {} - (take 10 - (for [{:keys [amount date description vendor-name numeric-code score]} similar] - (com/data-grid-row - {} - (com/data-grid-cell {:class "text-left"} (subs date 0 10)) - (com/data-grid-cell {:class "text-left"} description ) - (com/data-grid-cell {} (some->> amount double (format "$%.2f"))) - (com/data-grid-cell {} vendor-name) - (com/data-grid-cell {} numeric-code) - (com/data-grid-cell {} (format "%.1f%%" (* 100 (double score))))))))) - [:div]))))) + (com/data-grid-row {} + (take 10 + (for [{:keys [amount date description vendor-name numeric-code score]} similar] + (com/data-grid-row + {} + (com/data-grid-cell {:class "text-left"} (subs date 0 10)) + (com/data-grid-cell {:class "text-left"} description) + (com/data-grid-cell {} (some->> amount double (format "$%.2f"))) + (com/data-grid-cell {} vendor-name) + (com/data-grid-cell {} numeric-code) + (com/data-grid-cell {} (format "%.1f%%" (* 100 (double score))))))))) + [:div]))))) (defn transaction-rows* [{:keys [clients identity after]}] (let [recommendations (transaction-recommendations identity clients :after after)] @@ -286,7 +283,7 @@ (for [r recommendations :let [last? (= r (last recommendations))]] (transaction-row r :last? last?)) - [:tr [:td.has-text-centered.has-text-gray {:colspan 7 } + [:tr [:td.has-text-centered.has-text-gray {:colspan 7} [:i "That's the last of 'em!"]]]))) (defn transaction-rows [{:keys [session identity route-params clients]}] diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 524c57d9..b653589e 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -13,7 +13,6 @@ {} hiccup))}) - (defn base-page [request contents page-name] (html-page [:html @@ -28,7 +27,7 @@ [:script {:src "//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"}] [:link {:rel "stylesheet", :href "/output.css"}] [:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}] - [:script { :src "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" :defer true}] + [:script {:src "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" :defer true}] [:link {:rel "stylesheet" :href "/css/tippy/tippy.css"}] [:link {:rel "stylesheet" :href "/css/tippy/light.css"}] [:script {:src "/js/htmx.min.js" @@ -71,7 +70,6 @@ input[type=number] { contents [:script {:src "/js/flowbite.min.js"}] - [:div#modal-holder {:class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" "x-show" "open" @@ -106,4 +104,4 @@ input[type=number] { "x-transition:leave-start" "!opacity-100 !translate-y-0" "x-transition:leave-end" "!opacity-0 !translate-y-32"} - [:div#modal-content.flex.items-center.justify-center {:class "md:p-12"}]]]]]])) + [:div#modal-content.flex.items-center.justify-center {:class "md:p-12"}]]]]]])) diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index 8c2c6107..729c7879 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -30,7 +30,7 @@ (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes - :user-table) + :user-table) "hx-target" "#user-table" "hx-indicator" "#user-table"} @@ -64,7 +64,7 @@ (com/field {:label "Role"} (com/radio-card {:size :small :name "role" -:value (:role (:query-params request)) + :value (:role (:query-params request)) :options [{:value "" :content "All"} {:value "admin" @@ -84,7 +84,7 @@ [:maybe (into [:map {} [:role {:optional true} [:maybe (ref->enum-schema "user-role")]] - [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ] + [:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]] default-grid-fields-schema)])) (def default-read '[:db/id @@ -93,19 +93,19 @@ :user/profile-image-url [:user/last-login :xform clj-time.coerce/from-date] {[:user/role :xform iol-ion.query/ident] [:db/ident] - + :user/clients [:client/code :db/id :client/locations :client/name]}]) (defn fetch-ids [db request] (let [query-params (:query-params request) query (cond-> {:query {:find [] - :in '[$ ] + :in '[$] :where '[]} - :args [db ]} + :args [db]} (:sort query-params) (add-sorter-fields {"name" ['[?e :user/name ?un] '[(clojure.string/upper-case ?un) ?sort-name]] "email" ['[(get-else $ ?e :user/email "") ?sort-email]] - + "role" ['[?e :user/role ?r] '[?r :db/ident ?ri] '[(name ?ri) ?sort-role]] @@ -136,16 +136,14 @@ (some->> query-params :role) (merge-query {:query {:find [] :in ['?r] - :where ['[?e :user/role ?r] ]} + :where ['[?e :user/role ?r]]} :args [(some->> query-params :role)]}) - - true (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :user/name ?un] '[(clojure.string/upper-case ?un) ?sort-default]]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -186,14 +184,12 @@ [:div.flex.space-x-2 (for [{:client/keys [code]} (take 3 (:user/clients user))] (com/pill {:color :primary} - code) - ) + code)) (let [remainder (- (count (:user/clients user)) 3)] (when (> remainder 0) (com/pill {:color :white} (format "%d more" remainder))))]) - (def grid-page (helper/build {:id "user-table" :nav com/admin-aside-nav @@ -223,10 +219,10 @@ :sort-key "name" :render (fn [user] [:div.flex.space-x-2.place-items-center - (when-let [profile-image (:user/profile-image-url user) ] + (when-let [profile-image (:user/profile-image-url user)] [:div.rounded-full.overflow-hidden.w-8.h-8.display-inline - [:img {:src profile-image }]]) - [:span.inline-block ] (:user/name user)])} + [:img {:src profile-image}]]) + [:span.inline-block] (:user/name user)])} {:key "email" :name "Email" @@ -242,8 +238,7 @@ :render #(some-> % (:user/last-login) (atime/unparse-local atime/standard-time))} {:key "clients" :name "Clients" - :render user->client-pills} - ]})) + :render user->client-pills}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) @@ -266,19 +261,17 @@ (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors (:db/id fc/*current*))} (com/typeahead {:name (fc/field-name (:db/id fc/*current*)) - :class "w-full" - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :value (fc/field-value) - :value-fn :db/id + :class "w-full" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :value-fn :db/id - - :content-fn #(pull-attr (dc/db conn) :client/name (:db/id %)) - :size :small}))) + :content-fn #(pull-attr (dc/db conn) :client/name (:db/id %)) + :size :small}))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) - (defn dialog* [{:keys [form-params form-errors entity]}] (println "FORM PARMS" form-params) (fc/start-form @@ -328,65 +321,64 @@ (defn user-edit-save [{:keys [form-params identity] :as request}] (let [_ @(dc/transact conn [[:upsert-entity form-params]]) user (some-> form-params :db/id (#(dc/pull (dc/db conn) default-read %)))] - - (html-response - (row* identity user {:flash? true}) - :headers {"hx-trigger" "modalclose" - "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) + (html-response + (row* identity user {:flash? true}) + :headers {"hx-trigger" "modalclose" + "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) (def form-schema (mc/schema - [:map - [:db/id entity-id] - [:user/clients {:optional true} - [:maybe - (many-entity {} [:db/id entity-id])]] - [:user/role (ref->enum-schema "user-role")]])) + [:map + [:db/id entity-id] + [:user/clients {:optional true} + [:maybe + (many-entity {} [:db/id entity-id])]] + [:user/role (ref->enum-schema "user-role")]])) (defn user-dialog [{:keys [form-params entity form-errors]}] (modal-response - (dialog* {:form-params (or (when (seq form-params) - form-params) - (when entity - (mc/decode form-schema entity main-transformer)) - {}) - :entity entity - :form-errors form-errors}))) + (dialog* {:form-params (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + :entity entity + :form-errors form-errors}))) -(defn new-client [{ {:keys [index]} :query-params}] - (html-response - (fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil - :new? true} [] - (client-row* fc/*current*)))) +(defn new-client [{{:keys [index]} :query-params}] + (html-response + (fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil + :new? true} [] + (client-row* fc/*current*)))) (def key->handler (apply-middleware-to-all-handlers - {:users (helper/page-route grid-page) - :user-table (helper/table-route grid-page) - :user-edit-save (-> user-edit-save - (wrap-entity [:form-params :db/id] default-read) - (wrap-schema-enforce :form-schema form-schema) - (wrap-nested-form-params) - (wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read))) - :user-client-new (-> new-client + {:users (helper/page-route grid-page) + :user-table (helper/table-route grid-page) + :user-edit-save (-> user-edit-save + (wrap-entity [:form-params :db/id] default-read) + (wrap-schema-enforce :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read))) + :user-client-new (-> new-client (wrap-schema-enforce :query-schema [:map - [:index {:optional true - :default 0} [nat-int? {:default 0}]]])) - :user-edit-dialog (-> user-dialog - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce - :route-schema (mc/schema [:map [:db/id entity-id]]))) - :user-impersonate (-> impersonate - (wrap-entity [:params :db/id] default-read) - (wrap-schema-enforce - :params-schema (mc/schema [:map [:db/id entity-id]])))} - (fn [h] - (-> h -(wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-merge-prior-hx) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + [:index {:optional true + :default 0} [nat-int? {:default 0}]]])) + :user-edit-dialog (-> user-dialog + (wrap-entity [:route-params :db/id] default-read) + (wrap-schema-enforce + :route-schema (mc/schema [:map [:db/id entity-id]]))) + :user-impersonate (-> impersonate + (wrap-entity [:params :db/id] default-read) + (wrap-schema-enforce + :params-schema (mc/schema [:map [:db/id entity-id]])))} + (fn [h] + (-> h + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-merge-prior-hx) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 68ad44f9..a54da4c6 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -67,8 +67,6 @@ (assoc-in [:headers "hx-retarget"] "#modal-content") (assoc-in [:headers "hx-reswap"] "innerHTML")))))) - - (defn form-data->map [form-data] (reduce-kv (fn [acc k v] @@ -91,7 +89,6 @@ (str/join "_" (map path->name k)) :else k)) - (defn forced-vector [x] [:vector {:decode/json {:enter (fn [x] (if (sequential? x) @@ -172,7 +169,6 @@ [x]))}) schema])) - (defn str->keyword [s] (if (string? s) (let [[ns k] (str/split s #"/")] @@ -186,10 +182,9 @@ (subs (str k) 1) (string? k) k - :else + :else k)) - ;; TODO make this bubble the form data automatically (defn field-validation-error [m path & {:as data}] (throw+ (ex-info m (merge data {:type :field-validation @@ -201,19 +196,18 @@ :form-validation-errors [m]})))) (def clj-date-schema - (mc/schema [:and [inst? {:date-format atime/normal-date - }] + (mc/schema [:and [inst? {:date-format atime/normal-date}] [:fn {:error/message "Invalid date"} (fn [d] - (if d + (if d (time/after? (coerce/to-date-time d) (coerce/to-date-time #inst "2000-01-01")) true))] [:fn {:error/message "Can not look more than four years into the future."} (fn [d] - (if d + (if d (time/before? (coerce/to-date-time d) (time/plus (time/now) (time/years 4))) true))]])) @@ -270,7 +264,7 @@ "year" (assoc m start-date-key (atime/as-local-time (time/date-time (time/year (atime/local-today)) - 1 + 1 1)) end-date-key nil) @@ -333,7 +327,7 @@ (when (:coerce? (m/properties schema)) (fn [data] (cond - (vector? data) + (vector? data) data (sequential? data) data @@ -360,7 +354,6 @@ (into {}))))) (handler request))))) - (def dissoc-nil-transformer (let [e {:map {:compile (fn [schema _] (fn [data] @@ -374,26 +367,25 @@ :decoders e}))) (def unspecified-transformer - (mt2/transformer - {:decoders {:map {:compile (fn [x g] - (fn [value] - (if (or (nil? value) - (map? value)) - (let [ specified-keys (set (keys value))] - (reduce - (fn [value [k params]] - (cond (and (:unspecified/fn params) - (not (get specified-keys k))) - (assoc value k ((:unspecified/fn params))) - (and (:unspecified/value params) - (not (get specified-keys k))) - (assoc value k (:unspecified/value params)) - :else - value - )) - value - (m/children x))) - value)))}}})) + (mt2/transformer + {:decoders {:map {:compile (fn [x g] + (fn [value] + (if (or (nil? value) + (map? value)) + (let [specified-keys (set (keys value))] + (reduce + (fn [value [k params]] + (cond (and (:unspecified/fn params) + (not (get specified-keys k))) + (assoc value k ((:unspecified/fn params))) + (and (:unspecified/value params) + (not (get specified-keys k))) + (assoc value k (:unspecified/value params)) + :else + value)) + value + (m/children x))) + value)))}}})) (def main-transformer (mt2/transformer @@ -407,8 +399,7 @@ coerce-vector date-range-transformer pull-transformer - mt2/default-value-transformer - )) + mt2/default-value-transformer)) (defn strip [s] (cond (and (string? s) (str/blank? s)) @@ -434,7 +425,6 @@ :decoded entity :error {:explain (mc/explain schema entity)}})))) - (defn schema-enforce-request [{:keys [form-params query-params hx-query-params multipart-params params] :as request} & {:keys [form-schema multipart-schema hx-schema query-schema route-schema params-schema]}] (let [request (try (cond-> request @@ -451,7 +441,7 @@ route-schema (:route-params request) main-transformer)) - + (and (:multipart-params request) multipart-schema) (assoc :multipart-params (mc/coerce @@ -473,23 +463,22 @@ hx-query-params main-transformer)) - (and query-schema query-params) (assoc :query-params (mc/coerce - query-schema - query-params - main-transformer))) + query-schema + query-params + main-transformer))) (catch Exception e - (alog/warn ::validation-error + (alog/warn ::validation-error :error e ::errors (-> e - (ex-data) - :data - :explain - (me/humanize {:errors (assoc me/default-errors - ::mc/missing-key {:error/message {:en "required"}})}))) + (ex-data) + :data + :explain + (me/humanize {:errors (assoc me/default-errors + ::mc/missing-key {:error/message {:en "required"}})}))) (throw (ex-info (->> (-> e (ex-data) :data @@ -520,7 +509,6 @@ :route-schema route-schema :params-schema params-schema)))) - (defn schema-decode-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}] (let [request (cond-> request (and (:params request) params-schema) @@ -588,7 +576,6 @@ :when (= n (namespace ident))] {:value (name ident) :content (str/replace (str/capitalize (name ident)) "-" " ")}))) - (defn wrap-form-4xx-2 [handler form-handler] (fn [request] (try+ @@ -613,8 +600,7 @@ (form-handler (assoc request :form-params (or (:form e) ;; TODO is :form actually used? (:form-params e) - (:form-params request) - ) + (:form-params request)) :form-errors (:form-errors e)))) (catch [:type :form-validation] e (form-handler (assoc request @@ -624,7 +610,6 @@ :form-validation-errors (:form-validation-errors e) :form-errors {:errors (:form-validation-errors e)})))))) - (defn apply-middleware-to-all-handlers [key->handler f] (->> key->handler (reduce @@ -645,7 +630,6 @@ (str "[" (k->n k) "]")) rest))))) - (defn wrap-entity [handler path read] (fn wrap-entity-request [request] (let [entity (some->> @@ -664,7 +648,7 @@ :entity-map (mc/-simple-schema {:type :entity-map :pred map? - :type-properties { :error/message "required"}}) + :type-properties {:error/message "required"}}) #_[:map {:name :entity-map} [:db/id nat-int?]]})) (comment @@ -681,8 +665,6 @@ (with-precision 2 (double (.setScale (bigdec d) 2 java.math.RoundingMode/HALF_UP)))) - - (defn wrap-implied-route-param [handler & {:as route-params}] (fn [request] (handler (update-in request [:route-params] merge route-params)))) @@ -694,7 +676,7 @@ allowance (allowance-key (dc/pull (dc/db conn) '[{[:account/invoice-allowance :xform iol-ion.query/ident] [:db/ident] [:account/vendor-allowance :xform iol-ion.query/ident] [:db/ident] [:account/default-allowance :xform iol-ion.query/ident] [:db/ident]}] - account-id))] + account-id))] (not= :allowance/denied allowance))) @@ -713,9 +695,8 @@ (throw (ex-info "Exception." {:type "'A' not allowed"}))) true)) -(def default-grid-fields-schema - [ - [:sort {:optional true} [:maybe [:any]]] +(def default-grid-fields-schema + [[:sort {:optional true} [:maybe [:any]]] [:per-page {:optional true :default 25} [:maybe :int]] [:start {:optional true :default 0} [:maybe :int]] [:exact-match-id {:optional true} [:maybe entity-id]]]) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/vendor.clj b/src/clj/auto_ap/ssr/vendor.clj index 0519faf1..05159231 100644 --- a/src/clj/auto_ap/ssr/vendor.clj +++ b/src/clj/auto_ap/ssr/vendor.clj @@ -8,7 +8,7 @@ [ring.middleware.json :refer [wrap-json-response]])) (defn best-match [q] - + (let [name-like-ids (when (not-empty q) (map (comp #(Long/parseLong %) :id) (solr/query solr/impl "vendors" @@ -21,7 +21,7 @@ (first valid-clients))) (defn search [{:keys [clients query-params identity]}] - + (let [name-like-ids (when (not-empty (get query-params "q")) (map (comp #(Long/parseLong %) :id) (solr/query solr/impl "vendors" @@ -30,7 +30,7 @@ "fields" "id" "limit" 300}))) valid-clients (for [n name-like-ids] - {"value" n "label" (pull-attr (dc/db conn) :vendor/name n)} )] + {"value" n "label" (pull-attr (dc/db conn) :vendor/name n)})] {:body (take 10 valid-clients)})) (def search (wrap-json-response search)) @@ -38,40 +38,39 @@ #_(comment (solr/delete solr/impl "vendors") - (count (let [valid-ids (->> (dc/q '[:find ?v - :in $ - :where [?v :vendor/name]] - (dc/db conn)) - (map first) - (into #{}))] - (for [v (solr/query solr/impl "vendors" - {"query" "*" - "limit" 10000}) - :when (not (valid-ids (Long/parseLong (:id v))))] - v))) + (count (let [valid-ids (->> (dc/q '[:find ?v + :in $ + :where [?v :vendor/name]] + (dc/db conn)) + (map first) + (into #{}))] + (for [v (solr/query solr/impl "vendors" + {"query" "*" + "limit" 10000}) + :when (not (valid-ids (Long/parseLong (:id v))))] + v))) - (let [name-like-ids (when (not-empty "A&J") - (map (comp (juxt identity #(Long/parseLong %)) :id) - (solr/query solr/impl "vendors" - {"query" (cond-> (format "name:(%s*)" (str/upper-case "A&J")) - (not (is-admin? identity)) (str " hidden:false")) - "fields" "id,name" - "limit" 300}))) - valid-clients (for [[z n] name-like-ids] - {"value" n "internal-label" z "label" (dc/pull (dc/db conn) '[*] n)})] - (take 5 valid-clients)) + (let [name-like-ids (when (not-empty "A&J") + (map (comp (juxt identity #(Long/parseLong %)) :id) + (solr/query solr/impl "vendors" + {"query" (cond-> (format "name:(%s*)" (str/upper-case "A&J")) + (not (is-admin? identity)) (str " hidden:false")) + "fields" "id,name" + "limit" 300}))) + valid-clients (for [[z n] name-like-ids] + {"value" n "internal-label" z "label" (dc/pull (dc/db conn) '[*] n)})] + (take 5 valid-clients)) + (solr/query solr/impl "vendors" + {"query" (cond-> (format "name:(%s*)" (str/upper-case (solr/escape "A&J Pr"))) + (not true) (str " hidden:false")) + "fields" "id, name" + "limit" 300}) - (solr/query solr/impl "vendors" - {"query" (cond-> (format "name:(%s*)" (str/upper-case (solr/escape "A&J Pr"))) - (not true) (str " hidden:false")) - "fields" "id, name" - "limit" 300}) + (solr/escape "A&J") - (solr/escape "A&J") - - (first (solr/query solr/impl "vendors" - {"query" (cond-> (format "name:(A\\&J PRO*)") - (not true) (str " hidden:false")) - "fields" "id, name" - "limit" 300}))) \ No newline at end of file + (first (solr/query solr/impl "vendors" + {"query" (cond-> (format "name:(A\\&J PRO*)") + (not true) (str " hidden:false")) + "fields" "id, name" + "limit" 300}))) \ No newline at end of file diff --git a/src/clj/auto_ap/time.clj b/src/clj/auto_ap/time.clj index 070c00c3..ea166fc9 100644 --- a/src/clj/auto_ap/time.clj +++ b/src/clj/auto_ap/time.clj @@ -46,7 +46,6 @@ (catch Exception _ nil))) - (defn day-of-week-seq [day] (let [next-day (loop [d (local-now)] (if (= (time/day-of-week d) day) @@ -57,10 +56,8 @@ (defn local-today [] (coerce/in-time-zone (time/now) (time/time-zone-for-id "America/Los_Angeles"))) - (defn last-monday [] (loop [current (local-today)] (if (= 1 (time/day-of-week current)) current (recur (time/minus current (time/days 1)))))) - \ No newline at end of file diff --git a/src/clj/auto_ap/yodlee/core2.clj b/src/clj/auto_ap/yodlee/core2.clj index 087525a8..0b9816a9 100644 --- a/src/clj/auto_ap/yodlee/core2.clj +++ b/src/clj/auto_ap/yodlee/core2.clj @@ -57,107 +57,99 @@ (defn login-cobrand [] (retry-thrice - (fn [] - (-> (str (:yodlee2-base-url env) "/auth/token") - (client/post (merge {:headers (assoc base-headers - "loginName" (:yodlee2-admin-user env) - "Content-Type" "application/x-www-form-urlencoded") - :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) - :as :json} - other-config) - ) - :body - :token - :accessToken)))) + (fn [] + (-> (str (:yodlee2-base-url env) "/auth/token") + (client/post (merge {:headers (assoc base-headers + "loginName" (:yodlee2-admin-user env) + "Content-Type" "application/x-www-form-urlencoded") + :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) + :as :json} + other-config)) + :body + :token + :accessToken)))) (defn login-user [client-code] (retry-thrice - (fn [] - (alog/info ::logging-in :client client-code) - (-> (str (:yodlee2-base-url env) "/auth/token") - (client/post (merge {:headers (assoc base-headers - "loginName" (if (:yodlee2-test-user env) - (:yodlee2-test-user env) - (if (<= (count client-code) 3) - (str client-code client-code) - client-code)) - "Content-Type" "application/x-www-form-urlencoded") - :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) - :as :json} - other-config) - ) - :body - :token - :accessToken)))) + (fn [] + (alog/info ::logging-in :client client-code) + (-> (str (:yodlee2-base-url env) "/auth/token") + (client/post (merge {:headers (assoc base-headers + "loginName" (if (:yodlee2-test-user env) + (:yodlee2-test-user env) + (if (<= (count client-code) 3) + (str client-code client-code) + client-code)) + "Content-Type" "application/x-www-form-urlencoded") + :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) + :as :json} + other-config)) + :body + :token + :accessToken)))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(defn get-accounts [client-code ] +(defn get-accounts [client-code] (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/accounts") - (client/get (merge {:headers (merge base-headers {"Authorization" (str "Bearer " cob-session)}) + (-> (str (:yodlee2-base-url env) "/accounts") + (client/get (merge {:headers (merge base-headers {"Authorization" (str "Bearer " cob-session)}) :as :json} other-config)) :body :account))) (defn get-accounts-for-provider-account [client-code provider-account-id] - (try + (try (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/accounts?providerAccountId=" provider-account-id) - (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (-> (str (:yodlee2-base-url env) "/accounts?providerAccountId=" provider-account-id) + (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :as :json} other-config)) :body :account)) (catch Exception e - (alog/error ::error + (alog/error ::error :error e) []))) -(defn get-provider-accounts [client-code ] +(defn get-provider-accounts [client-code] (retry-thrice - (fn [] - (alog/info ::logging-in :client client-code) - (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts") - (-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )}) - :as :json} - other-config)) - :body - :providerAccount)))))) - - + (fn [] + (alog/info ::logging-in :client client-code) + (let [cob-session (login-user (client-code->login client-code))] + (-> (str (:yodlee2-base-url env) "/providerAccounts") + (-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + :as :json} + other-config)) + :body + :providerAccount)))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn get-transactions [client-code] (let [cob-session (login-user (client-code->login client-code)) batch-size 100 get-transaction-batch (fn [skip] - (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip) - + (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip) - (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :as :json} other-config)) :body - :transaction - ))] + :transaction))] - (loop [transactions [] + (loop [transactions [] skip 0] (let [transaction-batch (get-transaction-batch skip)] (if (seq transaction-batch) (recur (concat transactions transaction-batch) (+ batch-size skip)) transactions))))) - - (defn get-provider-account [client-code id] (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts/" id) + (-> (str (:yodlee2-base-url env) "/providerAccounts/" id) - (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :as :json} other-config)) :body @@ -167,9 +159,9 @@ (defn get-provider-account-detail [client-code id] (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts/" id ) + (-> (str (:yodlee2-base-url env) "/providerAccounts/" id) - (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :query-params {"include" "credentials,preferences"} :as :json} other-config)) @@ -181,41 +173,34 @@ (defn update-provider-account [client-code pa] (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa) + (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa) - (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :body "{\"dataSetName\": [\"BASIC_AGG_DATA\"]}" :as :json} other-config))))) - - - - - (defn get-specific-transactions [client-code account] (let [cob-session (login-user (client-code->login client-code)) batch-size 100 get-transaction-batch (fn [skip] - (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip "&accountId=" account) + (-> (str (:yodlee2-base-url env) "/transactions?top=" batch-size "&skip=" skip "&accountId=" account) - (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :as :json} other-config)) :body - :transaction - ))] + :transaction))] - (loop [transactions [] + (loop [transactions [] skip 0] (let [transaction-batch (get-transaction-batch skip)] (if (seq transaction-batch) (recur (concat transactions transaction-batch) (+ batch-size skip)) transactions))))) - (defn get-access-token [client-code] - (try + (try (alog/info ::getting-access-token :client client-code) (let [cob-session (login-user (client-code->login client-code))] (alog/info ::got-cob-session :cob-ession cob-session) @@ -227,10 +212,9 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn create-user [client-code] (let [cob-session (login-cobrand)] - (-> (str (:yodlee2-base-url env) "/user/register") - (client/post (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) - :body (json/write-str {"user" { - "loginName" client-code + (-> (str (:yodlee2-base-url env) "/user/register") + (client/post (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + :body (json/write-str {"user" {"loginName" client-code "email" "bryce@integreatconsult.com" "name" {"first" client-code "last" client-code} @@ -247,10 +231,7 @@ other-config)) :body))) - - - -(defn get-provider-accounts-with-details [client-code ] +(defn get-provider-accounts-with-details [client-code] (let [provider-accounts (get-provider-accounts client-code) concurrent 20 output-chan (async/chan)] @@ -270,12 +251,11 @@ (get-accounts-for-provider-account client-code provider-account-id)])) provider-account-ids))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn get-provider-accounts-with-accounts [client-code] (let [provider-accounts (by :id (get-provider-accounts-with-details client-code)) accounts (get-accounts-for-providers client-code (keys provider-accounts))] - (->> accounts + (->> accounts (reduce (fn [provider-accounts [which accounts]] (assoc-in provider-accounts [which :accounts] accounts)) @@ -285,9 +265,9 @@ (defn delete-provider-account [client-code id] (let [cob-session (login-user (client-code->login client-code))] - (-> (str (:yodlee2-base-url env) "/providerAccounts/" id ) + (-> (str (:yodlee2-base-url env) "/providerAccounts/" id) - (client/delete (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) + (client/delete (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session)}) :as :json} other-config)) :body @@ -301,23 +281,23 @@ ([client-code provider-accounts] (let [accounts (get-accounts-for-providers client-code (map :id provider-accounts))] (map (fn [pa] - (cond-> - {:yodlee-provider-account/id (:id pa) - :yodlee-provider-account/status (:status pa) - :yodlee-provider-account/detailed-status (or (-> pa :dataset first :additionalStatus) "unknown") - :yodlee-provider-account/client [:client/code client-code] - - :yodlee-provider-account/accounts (mapv - (fn [a] - {:yodlee-account/id (:id a) - :yodlee-account/name (str (:providerName a) " (" (:accountName a) ")") - :yodlee-account/number (or (:accountNumber a) "Unknown") - :yodlee-account/status (or (-> a :dataset first :additionalStatus) "unknown") - :yodlee-account/available-balance (or (-> a :currentBalance :amount) - 0.0)}) - (get accounts (:id pa)))} + (cond-> + {:yodlee-provider-account/id (:id pa) + :yodlee-provider-account/status (:status pa) + :yodlee-provider-account/detailed-status (or (-> pa :dataset first :additionalStatus) "unknown") + :yodlee-provider-account/client [:client/code client-code] - (-> pa :dataset first :lastUpdated) (assoc :yodlee-provider-account/last-updated (-> pa :dataset first :lastUpdated coerce/to-date)) )) + :yodlee-provider-account/accounts (mapv + (fn [a] + {:yodlee-account/id (:id a) + :yodlee-account/name (str (:providerName a) " (" (:accountName a) ")") + :yodlee-account/number (or (:accountNumber a) "Unknown") + :yodlee-account/status (or (-> a :dataset first :additionalStatus) "unknown") + :yodlee-account/available-balance (or (-> a :currentBalance :amount) + 0.0)}) + (get accounts (:id pa)))} + + (-> pa :dataset first :lastUpdated) (assoc :yodlee-provider-account/last-updated (-> pa :dataset first :lastUpdated coerce/to-date)))) provider-accounts)))) (defn refresh-provider-account [client-code id] @@ -325,7 +305,7 @@ :client client-code :provider-acconut-id id) @(dc/transact conn (upsert-accounts-tx client-code - [(get-provider-account client-code id)]))) + [(get-provider-account client-code id)]))) (defn upsert-accounts [] (let [concurrent 20 @@ -335,29 +315,26 @@ (mapcat (fn [client] (alog/info ::upserting-accounts :client (:client/code client)) - (mu/with-context {:client-code (:client/code client)} - (try - (upsert-accounts-tx (:client/code client)) - (catch Exception e - (alog/error ::error :error e :client (:client/code client))))))) + (mu/with-context {:client-code (:client/code client)} + (try + (upsert-accounts-tx (:client/code client)) + (catch Exception e + (alog/error ::error :error e :client (:client/code client))))))) (async/to-chan! (d-clients/get-all))) (let [result (async/ (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa) + (-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa) - (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header (login-user (client-code->login client-code)))}) + (client/put (merge {:headers (merge base-headers {"Authorization" (auth-header (login-user (client-code->login client-code)))}) :body (json/write-str data) :as :json} other-config))) (catch Exception e - (alog/error ::error :error e))) ) + (alog/error ::error :error e)))) (defn reauthenticate-and-recache [client-code pa data] (reauthenticate client-code pa data) diff --git a/src/clj/user.clj b/src/clj/user.clj index 6b7083c1..efbc6e5d 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -1,32 +1,32 @@ (ns user - (:require [amazonica.aws.s3 :as s3] - [auto-ap.server] - [auto-ap.datomic :refer [conn pull-attr random-tempid]] - [auto-ap.solr :as solr] - [auto-ap.time :as atime] - [auto-ap.utils :refer [by]] - [clj-time.coerce :as c] - [clj-time.core :as t] - [clojure.core.async :as async] - [auto-ap.handler :refer [app]] - [ring.adapter.jetty :refer [run-jetty]] - [clojure.data.csv :as csv] - [clojure.java.io :as io] - [clojure.pprint] - [clojure.string :as str] - [clojure.tools.namespace.repl :refer [refresh set-refresh-dirs]] - [com.brunobonacci.mulog :as mu] - [com.brunobonacci.mulog.buffer :as rb] - [config.core :refer [env]] - [datomic.api :as dc] - [puget.printer :as puget] - [datomic.api :as d] - #_[figwheel.main.api] - [hawk.core] - [mount.core :as mount] - [nrepl.middleware.print]) - (:import (org.apache.commons.io.input BOMInputStream) - [org.eclipse.jetty.server.handler.gzip GzipHandler])) + (:require [amazonica.aws.s3 :as s3] + [auto-ap.server] + [auto-ap.datomic :refer [conn pull-attr random-tempid]] + [auto-ap.solr :as solr] + [auto-ap.time :as atime] + [auto-ap.utils :refer [by]] + [clj-time.coerce :as c] + [clj-time.core :as t] + [clojure.core.async :as async] + [auto-ap.handler :refer [app]] + [ring.adapter.jetty :refer [run-jetty]] + [clojure.data.csv :as csv] + [clojure.java.io :as io] + [clojure.pprint] + [clojure.string :as str] + [clojure.tools.namespace.repl :refer [refresh set-refresh-dirs]] + [com.brunobonacci.mulog :as mu] + [com.brunobonacci.mulog.buffer :as rb] + [config.core :refer [env]] + [datomic.api :as dc] + [puget.printer :as puget] + [datomic.api :as d] + #_[figwheel.main.api] + [hawk.core] + [mount.core :as mount] + [nrepl.middleware.print]) + (:import (org.apache.commons.io.input BOMInputStream) + [org.eclipse.jetty.server.handler.gzip GzipHandler])) (defn println-event [item] #_(printf "%s: %s - %s:%s by %s\n" @@ -44,8 +44,7 @@ item :user))) (when (= :auto-ap.logging/peek (:mulog/event-name item)) - (println "\u001B[31mTEST") - ) + (println "\u001B[31mTEST")) (when (:error item) (println (:error item))) (puget/cprint (reduce @@ -58,18 +57,15 @@ {:seq-limit 10}) (println)) - (deftype DevPublisher [config buffer transform] com.brunobonacci.mulog.publisher.PPublisher (agent-buffer [_] buffer) - (publish-delay [_] 200) - (publish [_ buffer] ;; items are pairs [offset ] (doseq [item (transform (map second (rb/items buffer)))] @@ -77,8 +73,6 @@ (flush) (rb/clear buffer))) - - (defn dev-publisher [{:keys [transform pretty?] :as config}] (DevPublisher. config (rb/agent-buffer 10000) (or transform identity))) @@ -87,8 +81,6 @@ [config] (dev-publisher config)) - - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn load-accounts [conn] (let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv) @@ -161,7 +153,6 @@ (also-merge-txes also-merge old-account-id)) tx))))) - conj [] rows)] @@ -192,7 +183,6 @@ '[(<= ?z 9999)]]} (dc/db conn))))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn find-conflicting-accounts [] (filter @@ -226,8 +216,6 @@ :where [['?e :account-client-override/client '?client-id]]} (dc/db conn) client-id) - - _ (when-let [bad-rows (seq (->> rows (group-by (fn [[_ account]] account)) @@ -285,7 +273,6 @@ txes #_@(d/transact conn txes))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn fix-transactions-without-locations [client-code location] (->> @@ -307,7 +294,6 @@ accounts))) vec)) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn entity-history [i] (vec (sort-by first (dc/q @@ -342,17 +328,15 @@ {:start (- i 100) :end (+ i 100)})))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn start-db [] (mu/start-publisher! {:type :dev}) (mount.core/start (mount.core/only #{#'auto-ap.datomic/conn}))) - (defn- auto-reset-handler [ctx event] #_(require 'figwheel.main.api) (binding [*ns* *ns*] - (clojure.tools.namespace.repl/refresh) + (clojure.tools.namespace.repl/refresh) ctx)) (defn auto-reset @@ -363,11 +347,9 @@ (hawk.core/watch! [{:paths ["src/" "test/"] :handler auto-reset-handler}])) - (defn start-http [] (mount.core/start (mount.core/only #{#'auto-ap.server/port #'auto-ap.server/jetty}))) - (defn start-dev [] (set-refresh-dirs "src") #_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.server)) @@ -392,7 +374,6 @@ (for [r data] ((apply juxt columns) r))))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn find-queries [words] (let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env) @@ -418,7 +399,6 @@ (println "failed " e))) (async/> (dc/q '[:find ?i @@ -537,7 +513,6 @@ (defn init-repl [] (set! nrepl.middleware.print/*print-fn* clojure.pprint/pprint)) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn sample-ledger-import ([client-code] @@ -564,7 +539,6 @@ a) :separator \tab)))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn sample-manual-yodlee ([client-code] @@ -582,8 +556,6 @@ ["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code]) :separator \tab)))) - - (defn index-solr [] (println "invoice") diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc index 3a7f79fa..cb68c6d2 100644 --- a/src/cljc/auto_ap/client_routes.cljc +++ b/src/cljc/auto_ap/client_routes.cljc @@ -8,7 +8,7 @@ "payments/" :payments "admin/" {"vendors" :admin-vendors} "vendor/" {"new" :new-vendor} - + "transactions/" {"" :transactions "unapproved" :unapproved-transactions "approved" :approved-transactions diff --git a/src/cljc/auto_ap/expense_accounts.cljc b/src/cljc/auto_ap/expense_accounts.cljc index 418a32cb..a2f7e49c 100644 --- a/src/cljc/auto_ap/expense_accounts.cljc +++ b/src/cljc/auto_ap/expense_accounts.cljc @@ -2,366 +2,366 @@ (def expense-accounts {0 {:name "Uncategorized" :parent nil} - 1100 { :name "1100 Cash and Bank Accounts" :parent nil :location "A" } - 1101 { :name "A1101 Cash on Hand" :parent 1100 :location "A" } - 1102 { :name "A1102 Petty Cash" :parent 1100 :location "A" } - 1120 { :name "A1120 Bank 1" :parent 1100 :location "A" } - 1121 { :name "A1121 Bank 2" :parent 1100 :location "A" } - 1122 { :name "A1122 Bank 3" :parent 1100 :location "A" } - 1123 { :name "A1123 Bank 4" :parent 1100 :location "A" } - 1124 { :name "A1124 Bank 5" :parent 1100 :location "A" } - 1125 { :name "A1125 Bank 6" :parent 1100 :location "A" } - 1126 { :name "A1126 Bank 7" :parent 1100 :location "A" } - 1127 { :name "A1127 Bank 8" :parent 1100 :location "A" } - 1128 { :name "A1128 Bank 9" :parent 1100 :location "A" } - 1129 { :name "A1129 Bank 10" :parent 1100 :location "A" } - 1130 { :name "A1130 Bank 11" :parent 1100 :location "A" } - 1131 { :name "A1131 Bank 12" :parent 1100 :location "A" } - 1132 { :name "A1132 Bank 13" :parent 1100 :location "A" } - 1133 { :name "A1133 Bank 14" :parent 1100 :location "A" } - 1134 { :name "A1134 Bank 15" :parent 1100 :location "A" } - 1135 { :name "A1135 Bank 16" :parent 1100 :location "A" } - 1136 { :name "A1136 Bank 17" :parent 1100 :location "A" } - 1137 { :name "A1137 Bank 18" :parent 1100 :location "A" } - 1138 { :name "A1138 Bank 19" :parent 1100 :location "A" } - 1139 { :name "A1139 Bank 20" :parent 1100 :location "A" } - 1200 { :name "1200 Accounts Receivable" :parent nil :location "A" } - 1210 { :name "A1210 CCP" :parent 1200 :location "A" } - 1220 { :name "A1220 Invoice Receivable" :parent 1200 :location "A" } - 1211 { :name "A1211 Catering Receivable" :parent 1200 :location "A" } - 1230 { :name "A1230 Employee Loans and Advances" :parent nil :location "A" } - 1240 { :name "A1240 Owner/ Investor Loans" :parent nil :location "A" } - 1250 { :name "A1250 Tradeouts" :parent nil :location "A" } - 1260 { :name "A1260 Chargebacks" :parent nil :location "A" } - 1270 { :name "A1270 Allowance for Doubtful Accts" :parent nil :location "A" } - 1280 { :name "A1280 Other AR 1" :parent nil :location "A" } - 1281 { :name "A1281 Other AR 2" :parent 1280 :location "A" } - 1282 { :name "A1282 Other AR 3" :parent 1280 :location "A" } - 1283 { :name "A1283 Other AR 4" :parent 1280 :location "A" } - 1284 { :name "A1284 Other AR 5" :parent 1280 :location "A" } - 1285 { :name "A1285 Other AR 6" :parent 1280 :location "A" } - 1286 { :name "A1286 Other AR 7" :parent 1280 :location "A" } - 1287 { :name "A1287 Other AR 8" :parent 1280 :location "A" } - 1288 { :name "A1288 Other AR 9" :parent 1280 :location "A" } - 1298 { :name "A1298 Transfer in Process" :parent nil :location "A" } - 1299 { :name "A1299 Receipts Split" :parent nil :location "A" } - 1300 { :name "1300 Inventory" :parent nil :location "A" } - 1310 { :name "L1 1310 Food Inventory" :parent 1300 :location "A" } - 1330 { :name "L1 1330 Soft Beverage Inventory" :parent 1300 :location "A" } - 1340 { :name "L1 1340 Liquor Inventory" :parent 1300 :location "A" } - 1350 { :name "L1 1350 Beer Inventory" :parent 1300 :location "A" } - 1360 { :name "L1 1360 Wine Inventory" :parent 1300 :location "A" } - 1370 { :name "L1 1370 Paper Products Inventory" :parent 1300 :location "A" } - 1380 { :name "L1 1380 Merchandise Inventory" :parent 1300 :location "A" } - 1390 { :name "L1 1390 Supplies Inventory" :parent 1300 :location "A" } - 1400 { :name "1400 Prepaid Expenses" :parent nil :location "A" } - 1410 { :name "A1410 Insurance - Prepaid" :parent 1400 :location "A" } - 1420 { :name "A1420 Taxes - Prepaid" :parent 1400 :location "A" } - 1430 { :name "A1430 Licenses - Prepaid" :parent 1400 :location "A" } - 1440 { :name "A1440 Other - Prepaid" :parent 1400 :location "A" } - 1500 { :name "1500 Property and Equipment" :parent nil :location "A" } - 1510 { :name "A1510 Land" :parent 1500 :location "A" } - 1511 { :name "A1511 Land 2" :parent 1510 :location "A" } - 1512 { :name "A1512 Land 3" :parent 1510 :location "A" } - 1513 { :name "A1513 Land 4" :parent 1510 :location "A" } - 1514 { :name "A1514 Land 5" :parent 1510 :location "A" } - 1515 { :name "A1515 Land 6" :parent 1510 :location "A" } - 1516 { :name "A1516 Land 7" :parent 1510 :location "A" } - 1517 { :name "A1517 Land 8" :parent 1510 :location "A" } - 1518 { :name "A1518 Land 9" :parent 1510 :location "A" } - 1520 { :name "A1520 Building" :parent 1500 :location "A" } - 1521 { :name "A1521 Building 2" :parent 1520 :location "A" } - 1522 { :name "A1522 Building 3" :parent 1520 :location "A" } - 1523 { :name "A1523 Building 4" :parent 1520 :location "A" } - 1524 { :name "A1524 Building 5" :parent 1520 :location "A" } - 1525 { :name "A1525 Building 6" :parent 1520 :location "A" } - 1526 { :name "A1526 Building 7" :parent 1520 :location "A" } - 1527 { :name "A1527 Building 8" :parent 1520 :location "A" } - 1528 { :name "A1528 Building 9" :parent 1520 :location "A" } - 1530 { :name "A1530 Leasehold Improvements" :parent 1500 :location "A" } - 1531 { :name "A1531 Leasehold Improv 2" :parent 1530 :location "A" } - 1532 { :name "A1532 Leasehold Improv 3" :parent 1530 :location "A" } - 1533 { :name "A1533 Leasehold Improv 4" :parent 1530 :location "A" } - 1534 { :name "A1534 Leasehold Improv 5" :parent 1530 :location "A" } - 1535 { :name "A1535 Leasehold Improv 6" :parent 1530 :location "A" } - 1536 { :name "A1536 Leasehold Improv 7" :parent 1530 :location "A" } - 1537 { :name "A1537 Leasehold Improv 8" :parent 1530 :location "A" } - 1538 { :name "A1538 Leasehold Improv 9" :parent 1530 :location "A" } - 1540 { :name "A1540 Furniture and Equipment" :parent 1500 :location "A" } - 1541 { :name "A1541 Kitchen Equipment" :parent 1540 :location "A" } - 1542 { :name "A1542 Furniture and Equip 3" :parent 1540 :location "A" } - 1543 { :name "A1543 Furniture and Equip 3" :parent 1540 :location "A" } - 1544 { :name "A1544 Furniture and Equip 4" :parent 1540 :location "A" } - 1545 { :name "A1545 Furniture and Equip 5" :parent 1540 :location "A" } - 1546 { :name "A1546 Furniture and Equip 6" :parent 1540 :location "A" } - 1547 { :name "A1547 Furniture and Equip 7" :parent 1540 :location "A" } - 1548 { :name "A1548 Furniture and Equip 8" :parent 1540 :location "A" } - 1550 { :name "A1550 Autos and Trucks" :parent 1500 :location "A" } - 1551 { :name "A1551 Furniture and Equip 2" :parent 1550 :location "A" } - 1552 { :name "A1552 Furniture and Equip 3" :parent 1550 :location "A" } - 1553 { :name "A1553 Furniture and Equip 4" :parent 1550 :location "A" } - 1554 { :name "A1554 Furniture and Equip 5" :parent 1550 :location "A" } - 1555 { :name "A1555 Furniture and Equip 6" :parent 1550 :location "A" } - 1556 { :name "A1556 Furniture and Equip 7" :parent 1550 :location "A" } - 1557 { :name "A1557 Furniture and Equip 8" :parent 1550 :location "A" } - 1558 { :name "A1558 Furniture and Equip 9" :parent 1550 :location "A" } - 1560 { :name "A1560 Construction in Progress" :parent 1500 :location "A" } - 1590 { :name "A1590 Accum Depr - Building" :parent 1500 :location "A" } - 1592 { :name "A1592 Accum Depr - Leasehold Improvements" :parent 1590 :location "A" } - 1594 { :name "A1594 Accum Depr - Furniture and Equipment" :parent 1590 :location "A" } - 1596 { :name "A1596 Accum Depr - Autos and Trucks" :parent 1590 :location "A" } - 1600 { :name "1600 Intangible Assets" :parent nil :location "A" } - 1610 { :name "A1610 Deposits" :parent 1600 :location "A" } - 1620 { :name "A1620 Organization Costs" :parent 1600 :location "A" } - 1630 { :name "A1630 Start Up Costs" :parent 1600 :location "A" } - 1631 { :name "A1631 Start Up Costs 2" :parent 1630 :location "A" } - 1632 { :name "A1632 Start Up Costs 3" :parent 1630 :location "A" } - 1633 { :name "A1633 Start Up Costs 4" :parent 1630 :location "A" } - 1634 { :name "A1634 Start Up Costs 5" :parent 1630 :location "A" } - 1635 { :name "A1635 Start Up Costs 6" :parent 1630 :location "A" } - 1636 { :name "A1636 Start Up Costs 7" :parent 1630 :location "A" } - 1637 { :name "A1637 Start Up Costs 8" :parent 1630 :location "A" } - 1638 { :name "A1638 Start Up Costs 9" :parent 1630 :location "A" } - 1640 { :name "A1640 Liquor License" :parent 1600 :location "A" } - 1641 { :name "A1641 Liquor License 2" :parent 1640 :location "A" } - 1642 { :name "A1642 Liquor License 3" :parent 1640 :location "A" } - 1643 { :name "A1643 Liquor License 4" :parent 1640 :location "A" } - 1644 { :name "A1644 Liquor License 5" :parent 1640 :location "A" } - 1645 { :name "A1645 Liquor License 6" :parent 1640 :location "A" } - 1646 { :name "A1646 Liquor License 7" :parent 1640 :location "A" } - 1647 { :name "A1647 Liquor License 8" :parent 1640 :location "A" } - 1648 { :name "A1648 Liquor License 9" :parent 1640 :location "A" } - 1650 { :name "A1650 Other Intangible" :parent 1600 :location "A" } - 1651 { :name "A1651 Other Intengible 2" :parent 1650 :location "A" } - 1652 { :name "A1652 Other Intengible 3" :parent 1650 :location "A" } - 1653 { :name "A1653 Other Intengible 4" :parent 1650 :location "A" } - 1654 { :name "A1654 Other Intengible 5" :parent 1650 :location "A" } - 1655 { :name "A1655 Other Intengible 6" :parent 1650 :location "A" } - 1656 { :name "A1656 Other Intengible 7" :parent 1650 :location "A" } - 1657 { :name "A1657 Other Intengible 8" :parent 1650 :location "A" } - 1658 { :name "A1658 Other Intengible 9" :parent 1650 :location "A" } - 1690 { :name "A1690 Accum Amortization" :parent 1500 :location "A" } - 2100 { :name "2100 Accounts Payable" :parent nil :location "A" } - 2110 { :name "A2110 Bills Payable" :parent 2100 :location "A" } - 2190 { :name "A2190 Payroll Payable" :parent 2100 :location "A" } - 2200 { :name "A2200 Payroll Taxes Payable" :parent nil :location "A" } - 2201 { :name "A2201 Federal Payroll Taxes Payable" :parent 2200 :location "A" } - 2202 { :name "A2202 CA Payroll Taxes Payable" :parent 2200 :location "A" } - 2300 { :name "A2300 Sales Taxes Payable" :parent nil :location "A" } - 2400 { :name "2400 Accrued Expenses" :parent nil :location "A" } - 2410 { :name "A2410 Salaries Accrued" :parent 2400 :location "A" } - 2420 { :name "A2420 Vacation Pay Accrued" :parent 2400 :location "A" } - 2430 { :name "A2430 Rent Accrued" :parent 2400 :location "A" } - 2440 { :name "A2440 Utilities Accrued" :parent 2400 :location "A" } - 2500 { :name "2500 Other Liabilities" :parent nil :location "A" } - 2510 { :name "A2510 Gift Card Outstanding" :parent 2500 :location "A" } - 2511 { :name "A2511 Payroll Outstanding" :parent 2510 :location "A" } - 2520 { :name "A2520 Customer Deposits" :parent 2500 :location "A" } - 2521 { :name "A2521 Promos Outstanding" :parent 2520 :location "A" } - 2530 { :name "A2530 Due to Employees" :parent 2500 :location "A" } - 2531 { :name "A2531 Tips Payable" :parent 2530 :location "A" } - 2540 { :name "A2540 Other Current Liabilites" :parent 2500 :location "A" } - 2600 { :name "2600 Split Accounts" :parent nil :location "A" } - 2690 { :name "A2690 Splits" :parent 2600 :location "A" } - 2691 { :name "A2691 Payroll Split" :parent 2690 :location "A" } - 2692 { :name "A2692 12->13 Splits" :parent 2690 :location "A" } - 2700 { :name "2700 Current Portion of Long-Term Debt" :parent nil :location "A" } - 2710 { :name "2710 Current Portion of Notes Payable" :parent 2700 :location "A" } - 2800 { :name "2800 Notes Payable" :parent nil :location "A" } - 2810 { :name "A2810 Notes Payable" :parent 2800 :location "A" } - 2811 { :name "A2811 Note Payable 2" :parent 2810 :location "A" } - 2812 { :name "A2812 Note Payable 3" :parent 2810 :location "A" } - 2813 { :name "A2813 Note Payable 4" :parent 2810 :location "A" } - 2814 { :name "A2814 Note Payable 5" :parent 2810 :location "A" } - 2815 { :name "A2815 Note Payable 6" :parent 2810 :location "A" } - 2816 { :name "A2816 Note Payable 7" :parent 2810 :location "A" } - 2817 { :name "A2817 Note Payable 8" :parent 2810 :location "A" } - 2818 { :name "A2818 Note Payable 9" :parent 2810 :location "A" } - 2850 { :name "A2850 Loan from Member/ Partner/ Shareholder" :parent 2800 :location "A" } - 2851 { :name "A2851 Owner Loan 2" :parent 2850 :location "A" } - 2852 { :name "A2852 Owner Loan 3" :parent 2850 :location "A" } - 2853 { :name "A2853 Owner Loan 4" :parent 2850 :location "A" } - 2854 { :name "A2854 Owner Loan 5" :parent 2850 :location "A" } - 2855 { :name "A2855 Owner Loan 6" :parent 2850 :location "A" } - 2856 { :name "A2856 Owner Loan 7" :parent 2850 :location "A" } - 2857 { :name "A2857 Owner Loan 8" :parent 2850 :location "A" } - 2858 { :name "A2858 Owner Loan 9" :parent 2850 :location "A" } - 3001 { :name "A3001 Opening Balance" :parent nil :location "A" } - 3100 { :name "A3100 Common Stock" :parent nil :location "A" } - 3200 { :name "A3200 Paid in Capital" :parent nil :location "A" } - 3201 { :name "A3201 Contributions - Owner 1" :parent 3200 :location "A" } - 3202 { :name "A3202 Contributions - Owner 2" :parent 3200 :location "A" } - 3203 { :name "A3203 Contributions - Owner 3" :parent 3200 :location "A" } - 3204 { :name "A3204 Contributions - Owner 4" :parent 3200 :location "A" } - 3205 { :name "A3205 Contributions - Owner 5" :parent 3200 :location "A" } - 3206 { :name "A3206 Contributions - Owner 6" :parent 3200 :location "A" } - 3207 { :name "A3207 Contributions - Owner 7" :parent 3200 :location "A" } - 3208 { :name "A3208 Contributions - Owner 8" :parent 3200 :location "A" } - 3209 { :name "A3209 Contributions - Owner 9" :parent 3200 :location "A" } - 3210 { :name "A3210 Contributions - Owner 10" :parent 3200 :location "A" } - 3300 { :name "A3300 Contributions" :parent nil :location "A" } - 3301 { :name "A3301 Contributions - Owner 1" :parent 3300 :location "A" } - 3302 { :name "A3302 Contributions - Owner 2" :parent 3300 :location "A" } - 3303 { :name "A3303 Contributions - Owner 3" :parent 3300 :location "A" } - 3304 { :name "A3304 Contributions - Owner 4" :parent 3300 :location "A" } - 3305 { :name "A3305 Contributions - Owner 5" :parent 3300 :location "A" } - 3306 { :name "A3306 Contributions - Owner 6" :parent 3300 :location "A" } - 3307 { :name "A3307 Contributions - Owner 7" :parent 3300 :location "A" } - 3308 { :name "A3308 Contributions - Owner 8" :parent 3300 :location "A" } - 3309 { :name "A3309 Contributions - Owner 9" :parent 3300 :location "A" } - 3310 { :name "A3310 Contributions - Owner 10" :parent 3300 :location "A" } - 3400 { :name "A3400 Retained Earnings" :parent nil :location "A" } - 3401 { :name "A3401 Undistributed Net Inc / Loss" :parent 3400 :location "A" } - 5110 {:name "Food Cost" :parent nil} - 5111 {:name "Proteins Cost" :parent 5110} - 5112 {:name "Beef/ Pork Costs" :parent 5111} + 1100 {:name "1100 Cash and Bank Accounts" :parent nil :location "A"} + 1101 {:name "A1101 Cash on Hand" :parent 1100 :location "A"} + 1102 {:name "A1102 Petty Cash" :parent 1100 :location "A"} + 1120 {:name "A1120 Bank 1" :parent 1100 :location "A"} + 1121 {:name "A1121 Bank 2" :parent 1100 :location "A"} + 1122 {:name "A1122 Bank 3" :parent 1100 :location "A"} + 1123 {:name "A1123 Bank 4" :parent 1100 :location "A"} + 1124 {:name "A1124 Bank 5" :parent 1100 :location "A"} + 1125 {:name "A1125 Bank 6" :parent 1100 :location "A"} + 1126 {:name "A1126 Bank 7" :parent 1100 :location "A"} + 1127 {:name "A1127 Bank 8" :parent 1100 :location "A"} + 1128 {:name "A1128 Bank 9" :parent 1100 :location "A"} + 1129 {:name "A1129 Bank 10" :parent 1100 :location "A"} + 1130 {:name "A1130 Bank 11" :parent 1100 :location "A"} + 1131 {:name "A1131 Bank 12" :parent 1100 :location "A"} + 1132 {:name "A1132 Bank 13" :parent 1100 :location "A"} + 1133 {:name "A1133 Bank 14" :parent 1100 :location "A"} + 1134 {:name "A1134 Bank 15" :parent 1100 :location "A"} + 1135 {:name "A1135 Bank 16" :parent 1100 :location "A"} + 1136 {:name "A1136 Bank 17" :parent 1100 :location "A"} + 1137 {:name "A1137 Bank 18" :parent 1100 :location "A"} + 1138 {:name "A1138 Bank 19" :parent 1100 :location "A"} + 1139 {:name "A1139 Bank 20" :parent 1100 :location "A"} + 1200 {:name "1200 Accounts Receivable" :parent nil :location "A"} + 1210 {:name "A1210 CCP" :parent 1200 :location "A"} + 1220 {:name "A1220 Invoice Receivable" :parent 1200 :location "A"} + 1211 {:name "A1211 Catering Receivable" :parent 1200 :location "A"} + 1230 {:name "A1230 Employee Loans and Advances" :parent nil :location "A"} + 1240 {:name "A1240 Owner/ Investor Loans" :parent nil :location "A"} + 1250 {:name "A1250 Tradeouts" :parent nil :location "A"} + 1260 {:name "A1260 Chargebacks" :parent nil :location "A"} + 1270 {:name "A1270 Allowance for Doubtful Accts" :parent nil :location "A"} + 1280 {:name "A1280 Other AR 1" :parent nil :location "A"} + 1281 {:name "A1281 Other AR 2" :parent 1280 :location "A"} + 1282 {:name "A1282 Other AR 3" :parent 1280 :location "A"} + 1283 {:name "A1283 Other AR 4" :parent 1280 :location "A"} + 1284 {:name "A1284 Other AR 5" :parent 1280 :location "A"} + 1285 {:name "A1285 Other AR 6" :parent 1280 :location "A"} + 1286 {:name "A1286 Other AR 7" :parent 1280 :location "A"} + 1287 {:name "A1287 Other AR 8" :parent 1280 :location "A"} + 1288 {:name "A1288 Other AR 9" :parent 1280 :location "A"} + 1298 {:name "A1298 Transfer in Process" :parent nil :location "A"} + 1299 {:name "A1299 Receipts Split" :parent nil :location "A"} + 1300 {:name "1300 Inventory" :parent nil :location "A"} + 1310 {:name "L1 1310 Food Inventory" :parent 1300 :location "A"} + 1330 {:name "L1 1330 Soft Beverage Inventory" :parent 1300 :location "A"} + 1340 {:name "L1 1340 Liquor Inventory" :parent 1300 :location "A"} + 1350 {:name "L1 1350 Beer Inventory" :parent 1300 :location "A"} + 1360 {:name "L1 1360 Wine Inventory" :parent 1300 :location "A"} + 1370 {:name "L1 1370 Paper Products Inventory" :parent 1300 :location "A"} + 1380 {:name "L1 1380 Merchandise Inventory" :parent 1300 :location "A"} + 1390 {:name "L1 1390 Supplies Inventory" :parent 1300 :location "A"} + 1400 {:name "1400 Prepaid Expenses" :parent nil :location "A"} + 1410 {:name "A1410 Insurance - Prepaid" :parent 1400 :location "A"} + 1420 {:name "A1420 Taxes - Prepaid" :parent 1400 :location "A"} + 1430 {:name "A1430 Licenses - Prepaid" :parent 1400 :location "A"} + 1440 {:name "A1440 Other - Prepaid" :parent 1400 :location "A"} + 1500 {:name "1500 Property and Equipment" :parent nil :location "A"} + 1510 {:name "A1510 Land" :parent 1500 :location "A"} + 1511 {:name "A1511 Land 2" :parent 1510 :location "A"} + 1512 {:name "A1512 Land 3" :parent 1510 :location "A"} + 1513 {:name "A1513 Land 4" :parent 1510 :location "A"} + 1514 {:name "A1514 Land 5" :parent 1510 :location "A"} + 1515 {:name "A1515 Land 6" :parent 1510 :location "A"} + 1516 {:name "A1516 Land 7" :parent 1510 :location "A"} + 1517 {:name "A1517 Land 8" :parent 1510 :location "A"} + 1518 {:name "A1518 Land 9" :parent 1510 :location "A"} + 1520 {:name "A1520 Building" :parent 1500 :location "A"} + 1521 {:name "A1521 Building 2" :parent 1520 :location "A"} + 1522 {:name "A1522 Building 3" :parent 1520 :location "A"} + 1523 {:name "A1523 Building 4" :parent 1520 :location "A"} + 1524 {:name "A1524 Building 5" :parent 1520 :location "A"} + 1525 {:name "A1525 Building 6" :parent 1520 :location "A"} + 1526 {:name "A1526 Building 7" :parent 1520 :location "A"} + 1527 {:name "A1527 Building 8" :parent 1520 :location "A"} + 1528 {:name "A1528 Building 9" :parent 1520 :location "A"} + 1530 {:name "A1530 Leasehold Improvements" :parent 1500 :location "A"} + 1531 {:name "A1531 Leasehold Improv 2" :parent 1530 :location "A"} + 1532 {:name "A1532 Leasehold Improv 3" :parent 1530 :location "A"} + 1533 {:name "A1533 Leasehold Improv 4" :parent 1530 :location "A"} + 1534 {:name "A1534 Leasehold Improv 5" :parent 1530 :location "A"} + 1535 {:name "A1535 Leasehold Improv 6" :parent 1530 :location "A"} + 1536 {:name "A1536 Leasehold Improv 7" :parent 1530 :location "A"} + 1537 {:name "A1537 Leasehold Improv 8" :parent 1530 :location "A"} + 1538 {:name "A1538 Leasehold Improv 9" :parent 1530 :location "A"} + 1540 {:name "A1540 Furniture and Equipment" :parent 1500 :location "A"} + 1541 {:name "A1541 Kitchen Equipment" :parent 1540 :location "A"} + 1542 {:name "A1542 Furniture and Equip 3" :parent 1540 :location "A"} + 1543 {:name "A1543 Furniture and Equip 3" :parent 1540 :location "A"} + 1544 {:name "A1544 Furniture and Equip 4" :parent 1540 :location "A"} + 1545 {:name "A1545 Furniture and Equip 5" :parent 1540 :location "A"} + 1546 {:name "A1546 Furniture and Equip 6" :parent 1540 :location "A"} + 1547 {:name "A1547 Furniture and Equip 7" :parent 1540 :location "A"} + 1548 {:name "A1548 Furniture and Equip 8" :parent 1540 :location "A"} + 1550 {:name "A1550 Autos and Trucks" :parent 1500 :location "A"} + 1551 {:name "A1551 Furniture and Equip 2" :parent 1550 :location "A"} + 1552 {:name "A1552 Furniture and Equip 3" :parent 1550 :location "A"} + 1553 {:name "A1553 Furniture and Equip 4" :parent 1550 :location "A"} + 1554 {:name "A1554 Furniture and Equip 5" :parent 1550 :location "A"} + 1555 {:name "A1555 Furniture and Equip 6" :parent 1550 :location "A"} + 1556 {:name "A1556 Furniture and Equip 7" :parent 1550 :location "A"} + 1557 {:name "A1557 Furniture and Equip 8" :parent 1550 :location "A"} + 1558 {:name "A1558 Furniture and Equip 9" :parent 1550 :location "A"} + 1560 {:name "A1560 Construction in Progress" :parent 1500 :location "A"} + 1590 {:name "A1590 Accum Depr - Building" :parent 1500 :location "A"} + 1592 {:name "A1592 Accum Depr - Leasehold Improvements" :parent 1590 :location "A"} + 1594 {:name "A1594 Accum Depr - Furniture and Equipment" :parent 1590 :location "A"} + 1596 {:name "A1596 Accum Depr - Autos and Trucks" :parent 1590 :location "A"} + 1600 {:name "1600 Intangible Assets" :parent nil :location "A"} + 1610 {:name "A1610 Deposits" :parent 1600 :location "A"} + 1620 {:name "A1620 Organization Costs" :parent 1600 :location "A"} + 1630 {:name "A1630 Start Up Costs" :parent 1600 :location "A"} + 1631 {:name "A1631 Start Up Costs 2" :parent 1630 :location "A"} + 1632 {:name "A1632 Start Up Costs 3" :parent 1630 :location "A"} + 1633 {:name "A1633 Start Up Costs 4" :parent 1630 :location "A"} + 1634 {:name "A1634 Start Up Costs 5" :parent 1630 :location "A"} + 1635 {:name "A1635 Start Up Costs 6" :parent 1630 :location "A"} + 1636 {:name "A1636 Start Up Costs 7" :parent 1630 :location "A"} + 1637 {:name "A1637 Start Up Costs 8" :parent 1630 :location "A"} + 1638 {:name "A1638 Start Up Costs 9" :parent 1630 :location "A"} + 1640 {:name "A1640 Liquor License" :parent 1600 :location "A"} + 1641 {:name "A1641 Liquor License 2" :parent 1640 :location "A"} + 1642 {:name "A1642 Liquor License 3" :parent 1640 :location "A"} + 1643 {:name "A1643 Liquor License 4" :parent 1640 :location "A"} + 1644 {:name "A1644 Liquor License 5" :parent 1640 :location "A"} + 1645 {:name "A1645 Liquor License 6" :parent 1640 :location "A"} + 1646 {:name "A1646 Liquor License 7" :parent 1640 :location "A"} + 1647 {:name "A1647 Liquor License 8" :parent 1640 :location "A"} + 1648 {:name "A1648 Liquor License 9" :parent 1640 :location "A"} + 1650 {:name "A1650 Other Intangible" :parent 1600 :location "A"} + 1651 {:name "A1651 Other Intengible 2" :parent 1650 :location "A"} + 1652 {:name "A1652 Other Intengible 3" :parent 1650 :location "A"} + 1653 {:name "A1653 Other Intengible 4" :parent 1650 :location "A"} + 1654 {:name "A1654 Other Intengible 5" :parent 1650 :location "A"} + 1655 {:name "A1655 Other Intengible 6" :parent 1650 :location "A"} + 1656 {:name "A1656 Other Intengible 7" :parent 1650 :location "A"} + 1657 {:name "A1657 Other Intengible 8" :parent 1650 :location "A"} + 1658 {:name "A1658 Other Intengible 9" :parent 1650 :location "A"} + 1690 {:name "A1690 Accum Amortization" :parent 1500 :location "A"} + 2100 {:name "2100 Accounts Payable" :parent nil :location "A"} + 2110 {:name "A2110 Bills Payable" :parent 2100 :location "A"} + 2190 {:name "A2190 Payroll Payable" :parent 2100 :location "A"} + 2200 {:name "A2200 Payroll Taxes Payable" :parent nil :location "A"} + 2201 {:name "A2201 Federal Payroll Taxes Payable" :parent 2200 :location "A"} + 2202 {:name "A2202 CA Payroll Taxes Payable" :parent 2200 :location "A"} + 2300 {:name "A2300 Sales Taxes Payable" :parent nil :location "A"} + 2400 {:name "2400 Accrued Expenses" :parent nil :location "A"} + 2410 {:name "A2410 Salaries Accrued" :parent 2400 :location "A"} + 2420 {:name "A2420 Vacation Pay Accrued" :parent 2400 :location "A"} + 2430 {:name "A2430 Rent Accrued" :parent 2400 :location "A"} + 2440 {:name "A2440 Utilities Accrued" :parent 2400 :location "A"} + 2500 {:name "2500 Other Liabilities" :parent nil :location "A"} + 2510 {:name "A2510 Gift Card Outstanding" :parent 2500 :location "A"} + 2511 {:name "A2511 Payroll Outstanding" :parent 2510 :location "A"} + 2520 {:name "A2520 Customer Deposits" :parent 2500 :location "A"} + 2521 {:name "A2521 Promos Outstanding" :parent 2520 :location "A"} + 2530 {:name "A2530 Due to Employees" :parent 2500 :location "A"} + 2531 {:name "A2531 Tips Payable" :parent 2530 :location "A"} + 2540 {:name "A2540 Other Current Liabilites" :parent 2500 :location "A"} + 2600 {:name "2600 Split Accounts" :parent nil :location "A"} + 2690 {:name "A2690 Splits" :parent 2600 :location "A"} + 2691 {:name "A2691 Payroll Split" :parent 2690 :location "A"} + 2692 {:name "A2692 12->13 Splits" :parent 2690 :location "A"} + 2700 {:name "2700 Current Portion of Long-Term Debt" :parent nil :location "A"} + 2710 {:name "2710 Current Portion of Notes Payable" :parent 2700 :location "A"} + 2800 {:name "2800 Notes Payable" :parent nil :location "A"} + 2810 {:name "A2810 Notes Payable" :parent 2800 :location "A"} + 2811 {:name "A2811 Note Payable 2" :parent 2810 :location "A"} + 2812 {:name "A2812 Note Payable 3" :parent 2810 :location "A"} + 2813 {:name "A2813 Note Payable 4" :parent 2810 :location "A"} + 2814 {:name "A2814 Note Payable 5" :parent 2810 :location "A"} + 2815 {:name "A2815 Note Payable 6" :parent 2810 :location "A"} + 2816 {:name "A2816 Note Payable 7" :parent 2810 :location "A"} + 2817 {:name "A2817 Note Payable 8" :parent 2810 :location "A"} + 2818 {:name "A2818 Note Payable 9" :parent 2810 :location "A"} + 2850 {:name "A2850 Loan from Member/ Partner/ Shareholder" :parent 2800 :location "A"} + 2851 {:name "A2851 Owner Loan 2" :parent 2850 :location "A"} + 2852 {:name "A2852 Owner Loan 3" :parent 2850 :location "A"} + 2853 {:name "A2853 Owner Loan 4" :parent 2850 :location "A"} + 2854 {:name "A2854 Owner Loan 5" :parent 2850 :location "A"} + 2855 {:name "A2855 Owner Loan 6" :parent 2850 :location "A"} + 2856 {:name "A2856 Owner Loan 7" :parent 2850 :location "A"} + 2857 {:name "A2857 Owner Loan 8" :parent 2850 :location "A"} + 2858 {:name "A2858 Owner Loan 9" :parent 2850 :location "A"} + 3001 {:name "A3001 Opening Balance" :parent nil :location "A"} + 3100 {:name "A3100 Common Stock" :parent nil :location "A"} + 3200 {:name "A3200 Paid in Capital" :parent nil :location "A"} + 3201 {:name "A3201 Contributions - Owner 1" :parent 3200 :location "A"} + 3202 {:name "A3202 Contributions - Owner 2" :parent 3200 :location "A"} + 3203 {:name "A3203 Contributions - Owner 3" :parent 3200 :location "A"} + 3204 {:name "A3204 Contributions - Owner 4" :parent 3200 :location "A"} + 3205 {:name "A3205 Contributions - Owner 5" :parent 3200 :location "A"} + 3206 {:name "A3206 Contributions - Owner 6" :parent 3200 :location "A"} + 3207 {:name "A3207 Contributions - Owner 7" :parent 3200 :location "A"} + 3208 {:name "A3208 Contributions - Owner 8" :parent 3200 :location "A"} + 3209 {:name "A3209 Contributions - Owner 9" :parent 3200 :location "A"} + 3210 {:name "A3210 Contributions - Owner 10" :parent 3200 :location "A"} + 3300 {:name "A3300 Contributions" :parent nil :location "A"} + 3301 {:name "A3301 Contributions - Owner 1" :parent 3300 :location "A"} + 3302 {:name "A3302 Contributions - Owner 2" :parent 3300 :location "A"} + 3303 {:name "A3303 Contributions - Owner 3" :parent 3300 :location "A"} + 3304 {:name "A3304 Contributions - Owner 4" :parent 3300 :location "A"} + 3305 {:name "A3305 Contributions - Owner 5" :parent 3300 :location "A"} + 3306 {:name "A3306 Contributions - Owner 6" :parent 3300 :location "A"} + 3307 {:name "A3307 Contributions - Owner 7" :parent 3300 :location "A"} + 3308 {:name "A3308 Contributions - Owner 8" :parent 3300 :location "A"} + 3309 {:name "A3309 Contributions - Owner 9" :parent 3300 :location "A"} + 3310 {:name "A3310 Contributions - Owner 10" :parent 3300 :location "A"} + 3400 {:name "A3400 Retained Earnings" :parent nil :location "A"} + 3401 {:name "A3401 Undistributed Net Inc / Loss" :parent 3400 :location "A"} + 5110 {:name "Food Cost" :parent nil} + 5111 {:name "Proteins Cost" :parent 5110} + 5112 {:name "Beef/ Pork Costs" :parent 5111} 5113 {:name "Chicken/ Poultry Costs" :parent 5111} 5114 {:name "Seafood Costs" :parent 5111} 5120 {:name "Produce Costs" :parent 5110} 5130 {:name "Dairy Costs" :parent 5110} 5140 {:name "Bread and Bun Costs" :parent 5110} - 5210 {:name "Soft Beverage Cost" :parent 5110} - 5220 {:name "Coffee Costs" :parent 5110} - 5310 {:name "Catering Cost" :parent 5110} - 5400 {:name "Alcolhol Cost" :parent nil} - 5410 {:name "Beer Cost" :parent 5400} - 5411 {:name "CO2 Costs" :parent 5400} - 5510 {:name "Wine Cost" :parent 5400} - 5610 {:name "Liquor Cost" :parent 5400} - 5700 {:name "Merchandise Cost" :parent nil} - 5710 {:name "Merchandise Cost" :parent 5700} - 5800 {:name "Other Operating Cost" :parent nil} - 5810 {:name "Delivery Van Rental Cost" :parent 5810} - 5900 {:name "Paper Cost" :parent nil} - 5910 {:name "Paperware Cost" :parent 5900} - 7100 {:name "Ops related" :parent nil} - 7110 {:name "Banquet and Catering Supplies" :parent 7100} - 7120 {:name "Bar Utensils and Supplies" :parent 7100} - 7130 {:name "Glassware" :parent 7100} - 7140 {:name "Tableware" :parent 7100} - 7150 {:name "Paper and Packaging" :parent 7100} - 7160 {:name "Security Guards" :parent 7100} + 5210 {:name "Soft Beverage Cost" :parent 5110} + 5220 {:name "Coffee Costs" :parent 5110} + 5310 {:name "Catering Cost" :parent 5110} + 5400 {:name "Alcolhol Cost" :parent nil} + 5410 {:name "Beer Cost" :parent 5400} + 5411 {:name "CO2 Costs" :parent 5400} + 5510 {:name "Wine Cost" :parent 5400} + 5610 {:name "Liquor Cost" :parent 5400} + 5700 {:name "Merchandise Cost" :parent nil} + 5710 {:name "Merchandise Cost" :parent 5700} + 5800 {:name "Other Operating Cost" :parent nil} + 5810 {:name "Delivery Van Rental Cost" :parent 5810} + 5900 {:name "Paper Cost" :parent nil} + 5910 {:name "Paperware Cost" :parent 5900} + 7100 {:name "Ops related" :parent nil} + 7110 {:name "Banquet and Catering Supplies" :parent 7100} + 7120 {:name "Bar Utensils and Supplies" :parent 7100} + 7130 {:name "Glassware" :parent 7100} + 7140 {:name "Tableware" :parent 7100} + 7150 {:name "Paper and Packaging" :parent 7100} + 7160 {:name "Security Guards" :parent 7100} 7200 {:name "Customer Related" :parent nil} - 7210 {:name "Flowers and ST Decorations" :parent 7200} - 7220 {:name "Menus" :parent 7200} - 7225 {:name "In Store Printing" :parent 7200} - 7230 {:name "Advertising" :parent 7200} - 7235 {:name "Door Dash Advertising" :parent 7200} - 7240 {:name "Cable Television" :parent 7200} - 7242 {:name "Music Licensing Fees" :parent 7200} - 7244 {:name "Bands and DJ's" :parent 7200} - 7246 {:name "Entertainment - Other" :parent 7200} - 7250 {:name "Reservation System" :parent 7200} + 7210 {:name "Flowers and ST Decorations" :parent 7200} + 7220 {:name "Menus" :parent 7200} + 7225 {:name "In Store Printing" :parent 7200} + 7230 {:name "Advertising" :parent 7200} + 7235 {:name "Door Dash Advertising" :parent 7200} + 7240 {:name "Cable Television" :parent 7200} + 7242 {:name "Music Licensing Fees" :parent 7200} + 7244 {:name "Bands and DJ's" :parent 7200} + 7246 {:name "Entertainment - Other" :parent 7200} + 7250 {:name "Reservation System" :parent 7200} 7300 {:name "Employee Related" :parent nil} - 7310 {:name "Auto and Truck Expenses" :parent 7300} - 7315 {:name "Freight and Fuel Charges" :parent 7300} - 7320 {:name "Kitchen Supplies" :parent 7300} - 7325 {:name "Kitchen Utensils and Smallwares" :parent 7300} - 7330 {:name "Parking" :parent 7300} - 7340 {:name "Uniforms" :parent 7300} - 7350 {:name "Recruiting" :parent 7300} - 7360 {:name "Employee Training" :parent 7300} + 7310 {:name "Auto and Truck Expenses" :parent 7300} + 7315 {:name "Freight and Fuel Charges" :parent 7300} + 7320 {:name "Kitchen Supplies" :parent 7300} + 7325 {:name "Kitchen Utensils and Smallwares" :parent 7300} + 7330 {:name "Parking" :parent 7300} + 7340 {:name "Uniforms" :parent 7300} + 7350 {:name "Recruiting" :parent 7300} + 7360 {:name "Employee Training" :parent 7300} 7400 {:name "Building and Equipment Related" :parent nil} - 7410 {:name "Cleaning Supplies" :parent 7400} - 7415 {:name "Contract Cleaning" :parent 7400} - 7420 {:name "Short Term Equipment Rental" :parent 7400} - 7430 {:name "Laundry and Drycleaning" :parent 7400} - 7435 {:name "Linens" :parent 7400} - 7440 {:name "Repairs to Building" :parent 7400} - 7450 {:name "Building Cleaning & Maintenance" :parent 7400} - 7455 {:name "Pest Control" :parent 7400} - 7460 {:name "Repairs to Equipment" :parent 7400} - 7461 {:name "Contract Labor" :parent 7400} + 7410 {:name "Cleaning Supplies" :parent 7400} + 7415 {:name "Contract Cleaning" :parent 7400} + 7420 {:name "Short Term Equipment Rental" :parent 7400} + 7430 {:name "Laundry and Drycleaning" :parent 7400} + 7435 {:name "Linens" :parent 7400} + 7440 {:name "Repairs to Building" :parent 7400} + 7450 {:name "Building Cleaning & Maintenance" :parent 7400} + 7455 {:name "Pest Control" :parent 7400} + 7460 {:name "Repairs to Equipment" :parent 7400} + 7461 {:name "Contract Labor" :parent 7400} 7500 {:name "Office / Management Related" :parent nil} - 7510 {:name "Office Supplies" :parent 7500} - 7520 {:name "Printing - Internal" :parent 7500} - 7530 {:name "Restaurant Software Fees" :parent 7500} - 7540 {:name "Credit Card Processing" :parent 7500} - 7550 {:name "Franchise Fee" :parent 7500} - 7560 {:name "Unassigned Expenses" :parent 7500} - 8100 {:name "Operational" :parent nil } - 8110 {:name "Professional Fees" :parent 8100 } - 8120 {:name "Accounting" :parent 8100 } - 8130 {:name "Membership Dues and Associations" :parent 8100 } - 8200 {:name "Occupancy Costs" :parent nil } - 8210 {:name "Rent" :parent 8200 } - 8220 {:name "CAM" :parent 8200 } - 8230 {:name "Real Estate Taxes" :parent 8200 } - 8300 {:name "Utilities" :parent nil } - 8310 {:name "Electric" :parent 8300 } - 8320 {:name "Gas" :parent 8300 } - 8330 {:name "Trash Removal" :parent 8300 } - 8340 {:name "Water and Sewage" :parent 8300 } - 8350 {:name "Internet" :parent 8300 } - 8400 {:name "Equipment Rental" :parent nil } - 8410 {:name "Kitchen Equipment Rental" :parent 8400 } - 8420 {:name "POS System" :parent 8400 } - 8430 {:name "Other Rental" :parent 8400 } - 8500 {:name "Taxes and Insurance" :parent nil } - 8510 {:name "Liability Insurance" :parent 8500 } - 8511 {:name "Workers Comp Insurance" :parent 8500 } - 8610 {:name "Business License" :parent 8500 } - 8620 {:name "Health Permit" :parent 8500 } - 8710 {:name "Personal Property Taxes" :parent nil } - 8800 {:name "Depriciation" :parent nil } - 8810 {:name "Amortization of Lease" :parent 8800 } - 8820 {:name "Amortization of Leasehold Improvements" :parent 8800 } - 8830 {:name "Amortization of Start Up Costs" :parent 8800 } - 8850 {:name "Depreciation on Building" :parent 8800 } - 8860 {:name "Depreciation on Furnitire and Fixtures" :parent 8800 } - 9100 {:name "HQ Promotion and Outreach" :parent nil :location "HQ" } - 9110 {:name "Marketing and Advertising - HQ" :parent 9100 :location "HQ" } - 9120 {:name "Marketing Consultant - HQ" :parent 9100 :location "HQ" } - 9130 {:name "Advertisements - HQ" :parent 9100 :location "HQ" } - 9140 {:name "Design - HQ" :parent 9100 :location "HQ" } - 9150 {:name "Charitable Contricutions - HQ" :parent 9100 :location "HQ" } - 9160 {:name "Meals and Entertainment - HQ" :parent 9100 :location "HQ" } - 9170 {:name "Travel - HQ" :parent 9100 :location "HQ" } - 9180 {:name "Food Research - HQ" :parent 9100 :location "HQ" } - 9190 {:name "Membership Dues and Assocations - HQ" :parent 9100 :location "HQ" } - 9200 {:name "HQ Employee Morale and Training" :parent nil :location "HQ" } - 9210 {:name "Company Picnics - HQ" :parent 9200 :location "HQ" } - 9220 {:name "Employee Gifts - HQ" :parent 9200 :location "HQ" } - 9230 {:name "Employee Medical Expenses - HQ" :parent 9200 :location "HQ" } - 9240 {:name "Employee Mileage Reimbursements - HQ" :parent 9200 :location "HQ" } - 9250 {:name "Recruiting Costs - HQ" :parent 9200 :location "HQ" } - 9260 {:name "Employee Training - HQ" :parent 9200 :location "HQ" } - 9300 {:name "HQ Operational" :parent nil :location "HQ" } - 9310 {:name "Legal Fees - HQ" :parent 9300 :location "HQ" } - 9315 {:name "Accounting - HQ" :parent 9300 :location "HQ" } - 9320 {:name "Consultants - HQ" :parent 9300 :location "HQ" } - 9330 {:name "Liability Insurance - HQ" :parent 9300 :location "HQ" } - 9340 {:name "Office Rent - HQ" :parent 9300 :location "HQ" } - 9345 {:name "Office CAM - HQ" :parent 9300 :location "HQ" } - 9350 {:name "Office Supplies - HQ" :parent 9300 :location "HQ" } - 9355 {:name "Office Snacks - HQ" :parent 9300 :location "HQ" } - 9360 {:name "Office Repairs - HQ" :parent 9300 :location "HQ" } - 9365 {:name "Office Maintenance - HQ" :parent 9300 :location "HQ" } - 9370 {:name "Utilities - HQ" :parent 9300 :location "HQ" } - 9380 {:name "Telephone - HQ" :parent 9300 :location "HQ" } - 9383 {:name "Storage - HQ" :parent 9380 :location "HQ" } - 9500 {:name "HQ Interest and Bank Expenses" :parent nil :location "HQ" } - 9510 {:name "Bank Fees - HQ" :parent 9500 :location "HQ" } - 9520 {:name "NSF Fees - HQ" :parent 9500 :location "HQ" } - 9530 {:name "Late Payment Fees - HQ" :parent 9500 :location "HQ" } - 9540 {:name "Interest Expense - HQ" :parent 9500 :location "HQ" } - 9541 {:name "Late Payment Penalties" :parent 9500 :location "HQ" } - 9600 {:name "HQ Depreciation" :parent nil :location "HQ" } - 9610 {:name "Amortization of Lease - HQ" :parent 9600 :location "HQ" } - 9620 {:name "Amortization of Leasehold Improvements - HQ" :parent 9600 :location "HQ" } - 9630 {:name "Amortization of Start Up Costs - HQ" :parent 9600 :location "HQ" } - 9650 {:name "Depreciation on Building - HQ" :parent 9600 :location "HQ" } - 9660 {:name "Depreciation on Furniture and Fixtures - HQ" :parent 9600 :location "HQ" } - 9700 {:name "HQ Taxes" :parent nil :location "HQ" } - 9710 {:name "Federal Taxes - HQ" :parent 9700 :location "HQ" } - 9720 {:name "State Taxes - HQ" :parent 9700 :location "HQ" } - 9725 {:name "LLC Fee - HQ" :parent 9700 :location "HQ" } - 9730 {:name "Local Taxes - HQ" :parent 9700 :location "HQ" } - 9800 {:name "HQ Other Expenses" :parent nil :location "HQ" } - 9810 {:name "Sales Tax Received Adjustments - HQ" :parent 9800 :location "HQ" } - 9820 {:name "Judgments - HQ" :parent 9800 :location "HQ" } - 9880 {:name "Misc Payments - HQ" :parent 9800 :location "HQ" } - 9890 {:name "Unassigned Exp - HQ" :parent 9800 :location "HQ" }}) + 7510 {:name "Office Supplies" :parent 7500} + 7520 {:name "Printing - Internal" :parent 7500} + 7530 {:name "Restaurant Software Fees" :parent 7500} + 7540 {:name "Credit Card Processing" :parent 7500} + 7550 {:name "Franchise Fee" :parent 7500} + 7560 {:name "Unassigned Expenses" :parent 7500} + 8100 {:name "Operational" :parent nil} + 8110 {:name "Professional Fees" :parent 8100} + 8120 {:name "Accounting" :parent 8100} + 8130 {:name "Membership Dues and Associations" :parent 8100} + 8200 {:name "Occupancy Costs" :parent nil} + 8210 {:name "Rent" :parent 8200} + 8220 {:name "CAM" :parent 8200} + 8230 {:name "Real Estate Taxes" :parent 8200} + 8300 {:name "Utilities" :parent nil} + 8310 {:name "Electric" :parent 8300} + 8320 {:name "Gas" :parent 8300} + 8330 {:name "Trash Removal" :parent 8300} + 8340 {:name "Water and Sewage" :parent 8300} + 8350 {:name "Internet" :parent 8300} + 8400 {:name "Equipment Rental" :parent nil} + 8410 {:name "Kitchen Equipment Rental" :parent 8400} + 8420 {:name "POS System" :parent 8400} + 8430 {:name "Other Rental" :parent 8400} + 8500 {:name "Taxes and Insurance" :parent nil} + 8510 {:name "Liability Insurance" :parent 8500} + 8511 {:name "Workers Comp Insurance" :parent 8500} + 8610 {:name "Business License" :parent 8500} + 8620 {:name "Health Permit" :parent 8500} + 8710 {:name "Personal Property Taxes" :parent nil} + 8800 {:name "Depriciation" :parent nil} + 8810 {:name "Amortization of Lease" :parent 8800} + 8820 {:name "Amortization of Leasehold Improvements" :parent 8800} + 8830 {:name "Amortization of Start Up Costs" :parent 8800} + 8850 {:name "Depreciation on Building" :parent 8800} + 8860 {:name "Depreciation on Furnitire and Fixtures" :parent 8800} + 9100 {:name "HQ Promotion and Outreach" :parent nil :location "HQ"} + 9110 {:name "Marketing and Advertising - HQ" :parent 9100 :location "HQ"} + 9120 {:name "Marketing Consultant - HQ" :parent 9100 :location "HQ"} + 9130 {:name "Advertisements - HQ" :parent 9100 :location "HQ"} + 9140 {:name "Design - HQ" :parent 9100 :location "HQ"} + 9150 {:name "Charitable Contricutions - HQ" :parent 9100 :location "HQ"} + 9160 {:name "Meals and Entertainment - HQ" :parent 9100 :location "HQ"} + 9170 {:name "Travel - HQ" :parent 9100 :location "HQ"} + 9180 {:name "Food Research - HQ" :parent 9100 :location "HQ"} + 9190 {:name "Membership Dues and Assocations - HQ" :parent 9100 :location "HQ"} + 9200 {:name "HQ Employee Morale and Training" :parent nil :location "HQ"} + 9210 {:name "Company Picnics - HQ" :parent 9200 :location "HQ"} + 9220 {:name "Employee Gifts - HQ" :parent 9200 :location "HQ"} + 9230 {:name "Employee Medical Expenses - HQ" :parent 9200 :location "HQ"} + 9240 {:name "Employee Mileage Reimbursements - HQ" :parent 9200 :location "HQ"} + 9250 {:name "Recruiting Costs - HQ" :parent 9200 :location "HQ"} + 9260 {:name "Employee Training - HQ" :parent 9200 :location "HQ"} + 9300 {:name "HQ Operational" :parent nil :location "HQ"} + 9310 {:name "Legal Fees - HQ" :parent 9300 :location "HQ"} + 9315 {:name "Accounting - HQ" :parent 9300 :location "HQ"} + 9320 {:name "Consultants - HQ" :parent 9300 :location "HQ"} + 9330 {:name "Liability Insurance - HQ" :parent 9300 :location "HQ"} + 9340 {:name "Office Rent - HQ" :parent 9300 :location "HQ"} + 9345 {:name "Office CAM - HQ" :parent 9300 :location "HQ"} + 9350 {:name "Office Supplies - HQ" :parent 9300 :location "HQ"} + 9355 {:name "Office Snacks - HQ" :parent 9300 :location "HQ"} + 9360 {:name "Office Repairs - HQ" :parent 9300 :location "HQ"} + 9365 {:name "Office Maintenance - HQ" :parent 9300 :location "HQ"} + 9370 {:name "Utilities - HQ" :parent 9300 :location "HQ"} + 9380 {:name "Telephone - HQ" :parent 9300 :location "HQ"} + 9383 {:name "Storage - HQ" :parent 9380 :location "HQ"} + 9500 {:name "HQ Interest and Bank Expenses" :parent nil :location "HQ"} + 9510 {:name "Bank Fees - HQ" :parent 9500 :location "HQ"} + 9520 {:name "NSF Fees - HQ" :parent 9500 :location "HQ"} + 9530 {:name "Late Payment Fees - HQ" :parent 9500 :location "HQ"} + 9540 {:name "Interest Expense - HQ" :parent 9500 :location "HQ"} + 9541 {:name "Late Payment Penalties" :parent 9500 :location "HQ"} + 9600 {:name "HQ Depreciation" :parent nil :location "HQ"} + 9610 {:name "Amortization of Lease - HQ" :parent 9600 :location "HQ"} + 9620 {:name "Amortization of Leasehold Improvements - HQ" :parent 9600 :location "HQ"} + 9630 {:name "Amortization of Start Up Costs - HQ" :parent 9600 :location "HQ"} + 9650 {:name "Depreciation on Building - HQ" :parent 9600 :location "HQ"} + 9660 {:name "Depreciation on Furniture and Fixtures - HQ" :parent 9600 :location "HQ"} + 9700 {:name "HQ Taxes" :parent nil :location "HQ"} + 9710 {:name "Federal Taxes - HQ" :parent 9700 :location "HQ"} + 9720 {:name "State Taxes - HQ" :parent 9700 :location "HQ"} + 9725 {:name "LLC Fee - HQ" :parent 9700 :location "HQ"} + 9730 {:name "Local Taxes - HQ" :parent 9700 :location "HQ"} + 9800 {:name "HQ Other Expenses" :parent nil :location "HQ"} + 9810 {:name "Sales Tax Received Adjustments - HQ" :parent 9800 :location "HQ"} + 9820 {:name "Judgments - HQ" :parent 9800 :location "HQ"} + 9880 {:name "Misc Payments - HQ" :parent 9800 :location "HQ"} + 9890 {:name "Unassigned Exp - HQ" :parent 9800 :location "HQ"}}) (def chooseable-expense-accounts (dissoc expense-accounts 0)) diff --git a/src/cljc/auto_ap/ledger/reports.cljc b/src/cljc/auto_ap/ledger/reports.cljc index f884f6f3..7e138156 100644 --- a/src/cljc/auto_ap/ledger/reports.cljc +++ b/src/cljc/auto_ap/ledger/reports.cljc @@ -16,16 +16,14 @@ [auto-ap.time-utils :refer [user-friendly-date]])])) (defn date->str [d] - #?(:clj + #?(:clj (if (inst? d) (atime/unparse-local (coerce/to-date-time d) atime/normal-date) (atime/unparse-local d atime/normal-date)) :cljs (au/date->str d au/pretty))) - - (def groupings - {:assets [["1100 Cash and Bank Accounts" 11000 11999] + {:assets [["1100 Cash and Bank Accounts" 11000 11999] ["1200 Accounts Receivable" 12000 12999] ["1300 Inventory" 13000 13999] ["1400 Prepaid Expenses" 14000 14999] @@ -44,8 +42,7 @@ ["47000 Merchandise Sales" 47000 47999] ["48000 Other Operating Income" 48000 48999] ["49000 Non-Business Income" 49000 49999]] - :cogs [ - ["50000-54000 Food Costs" 50000 53999] + :cogs [["50000-54000 Food Costs" 50000 53999] ["54000-56000 Alcohol Costs" 54000 55999] ["56000 Merchandise Costs" 56000 56999] ["57000-60000 Other Costs of Sales" 57000 59999]] @@ -75,27 +72,22 @@ ["97000 Taxes" 97000 97999] ["98000 Other Expenses" 98000 98999]] - :operating-activities [ - ;; BEN EDIT STARTING HERE + :operating-activities [ ;; BEN EDIT STARTING HERE ["20100-20199 Credit Card Balances" 20100 20199 :add] ["21000-24000 Accounts Payable" 21000 23999 :add] ["25000-28000 Accounts Payable" 25000 27999 :add] ["24000-25000 Accrual Liabilities" 24000 24999 :add] - ["12000-13000 Accounts Receivable" 12000 13000 :subtract] + ["12000-13000 Accounts Receivable" 12000 13000 :subtract] ["96000-97000 Depreciation Expense" 96000 96999 :add] ["13000-15000 Inventory" 13000 14999 :subtract] ;; BEN ENDING HERE ] - :investment-activities [ - - ;; BEN EDIT STARTING HERE + :investment-activities [ ;; BEN EDIT STARTING HERE ["15000-18000 Investments" 15000 17999 :subtract] ;; BEN ENDING HERE - ] - :financing-activities [ - ;; BEN EDIT STARTING HERE + :financing-activities [ ;; BEN EDIT STARTING HERE ["30000-33000 Other Equity Accounts" 30000 32999 :add] ["33000-34000 Owner's Contributions" 33000 33999 :add] ["34000-35000 Owner's Distributions" 34000 34999 :add] @@ -103,19 +95,16 @@ ["28000-29000 Loans (payable)" 28000 28999 :add] ;; BEN ENDING HERE ] - :cash [ - ;; BEN EDIT STARTING HERE + :cash [ ;; BEN EDIT STARTING HERE ["11000-11400 Bank Accounts / Cash" 11000 11399 :add] ;; BEN ENDING HERE - ]}) - -(def cashflow-aggregation +(def cashflow-aggregation (->> (select-keys groupings [:operating-activities :investment-activities :financing-activities]) vals (mapcat identity) (map (fn [[_ start end rule]] - [start end rule])) + [start end rule])) (into []))) (defn cashflow-account->amount [account-code amount] @@ -143,21 +132,20 @@ (map second) (apply max))) - -(def flat-categories +(def flat-categories (for [[category groups] groupings [_ start end] groups] [category start end])) (defn in-range? [code] - (if code + (if code (reduce - (fn [acc [_ start end]] - (if (<= start code end) - (reduced true) - acc)) - false - flat-categories) + (fn [acc [_ start end]] + (if (<= start code end) + (reduced true) + acc)) + false + flat-categories) false)) (defn client-locations [pnl-data] @@ -166,7 +154,7 @@ (filter (comp in-range? :numeric-code)) (filter #(not= "A" (:location %))) (group-by (juxt :client-id :location)) - (filter (fn [[_ as]] + (filter (fn [[_ as]] (not (dollars-0? (reduce + 0 (map (comp or-0 :amount) as)))))) (mapcat second) (map (fn [a] @@ -177,10 +165,10 @@ (:location a)]))) (filter identity) (set) - + (sort-by (fn [x] [(:client-id x) - (if (= (:location x) "HQ" ) + (if (= (:location x) "HQ") "ZZZZZZ" (:location x))])))) @@ -209,7 +197,7 @@ (defn filter-client [pnl-data client] (-> pnl-data (update :data (fn [data] - ((group-by :client-id data) client))) + ((group-by :client-id data) client))) (update :filters (fn [f] (assoc f :client-id client))))) @@ -226,11 +214,11 @@ (assoc f :locations locations))))) (defn filter-numeric-code [pnl-data from to] - (-> pnl-data + (-> pnl-data (update :data (fn [data] (filter - #(<= from (or (:numeric-code %) 0) to) - data))) + #(<= from (or (:numeric-code %) 0) to) + data))) (update :filters (fn [f] (assoc f :numeric-code [{:from from @@ -259,68 +247,64 @@ (defn filter-period [pnl-data period] (-> pnl-data - (update :data (fn [data] - ((group-by :period data) period))) - (update :filters (fn [f] - (assoc f :date-range period))))) + (update :data (fn [data] + ((group-by :period data) period))) + (update :filters (fn [f] + (assoc f :date-range period))))) (defn zebra [pnl-data i] (if (odd? i) (assoc-in pnl-data [:cell-args :bg-color] [240 240 240]) pnl-data)) - - (defn negate [pnl-data types] - (update pnl-data :data + (update pnl-data :data (fn [accounts] - (map - (fn [account] - (if (types (best-category account)) - (update account :amount #(- (or % 0.0))) - account)) - accounts)))) - + (map + (fn [account] + (if (types (best-category account)) + (update account :amount #(- (or % 0.0))) + account)) + accounts)))) (defn used-accounts [pnl-datas] (->> - pnl-datas - (mapcat :data) - (map #(select-keys % [:numeric-code :name])) - (set) - (sort-by :numeric-code))) + pnl-datas + (mapcat :data) + (map #(select-keys % [:numeric-code :name])) + (set) + (sort-by :numeric-code))) (defn subtotal-by-column-row [pnl-datas title & [cell-args]] (into [{:value title :bold true}] (map - (fn [p] - (merge - {:format :dollar - :value (aggregate-accounts p) - :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes - (:filters p))} - (:cell-args p) - cell-args)) - pnl-datas))) + (fn [p] + (merge + {:format :dollar + :value (aggregate-accounts p) + :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes + (:filters p))} + (:cell-args p) + cell-args)) + pnl-datas))) (defn cashflow-subtotal-by-column-row [pnl-datas title & [cell-args]] (into [{:value title :bold true}] (mapcat (fn [p] - [ - (merge + [(merge {:format :text :value nil} (:cell-args p) cell-args) - (merge + (merge {:format :text :value nil} (:cell-args p) cell-args) - (merge + (merge {:format :dollar :value (aggregate-cashflow-accounts p) :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes @@ -337,25 +321,25 @@ (->> table (map (fn [[_ & values]] (map - (fn [v s] - {:border (:border v) - :bg-color (:bg-color v) - :format (if (string? (:value v)) - :text - :percent) - :color [128 128 128] - :value (cond - (string? (:value v)) - "" + (fn [v s] + {:border (:border v) + :bg-color (:bg-color v) + :format (if (string? (:value v)) + :text + :percent) + :color [128 128 128] + :value (cond + (string? (:value v)) + "" - (dollars-0? s) + (dollars-0? s) 0.0 :else (/ (:value v) s))}) - values sales)))))) + values sales)))))) (defn calc-deltas [table] - (->> table + (->> table (map (fn [[_ & values]] (->> values (partition 2 1) @@ -373,19 +357,19 @@ (defn combine-tables ([[pnl-data] table percent-of-sales deltas] - (map (fn [[title & row] percent-of-sales deltas ] + (map (fn [[title & row] percent-of-sales deltas] (let [deltas (cons {:value 0.0 :format :dollar :border (:border (first row))} deltas)] (into [title] (mapcat - (fn [v p d] - (if (:include-deltas (:args pnl-data)) - [v p d] - [v p])) - row - percent-of-sales - deltas)))) + (fn [v p d] + (if (:include-deltas (:args pnl-data)) + [v p d] + [v p])) + row + percent-of-sales + deltas)))) table percent-of-sales deltas))) @@ -394,7 +378,7 @@ (let [big-header (into [{:value header-title :bold true}] (map-indexed (fn [i p] - (cond-> {:value + (cond-> {:value (str (date->str (:start p)) " - " (date->str (:end p))) @@ -411,51 +395,51 @@ :align :center :bold true} (odd? i) (assoc :bg-color [240 240 240]))) - (:periods (:args pnl-data)))) + (:periods (:args pnl-data)))) sub-header (into [{:value ""}] (if (-> pnl-data :args :column-per-location) (mapcat - (fn [p] - (cond-> [(merge {:value (or (when (-> p :filters :location) - (str ((-> p :client-codes) (-> p :filters :client-id)) "-" (-> p :filters :location))) - "Total") - :align :right} - (:cell-args p)) - (merge {:value "%" - :align :right} - (:cell-args p))] - (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" - :align :right} - (:cell-args p))))) - pnl-datas) + (fn [p] + (cond-> [(merge {:value (or (when (-> p :filters :location) + (str ((-> p :client-codes) (-> p :filters :client-id)) "-" (-> p :filters :location))) + "Total") + :align :right} + (:cell-args p)) + (merge {:value "%" + :align :right} + (:cell-args p))] + (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" + :align :right} + (:cell-args p))))) + pnl-datas) (mapcat - (fn [p] - (cond-> [(merge {:value "Amt" - :align :right} - (:cell-args p)) - (merge {:value "%" - :align :right} - (:cell-args p))] - (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" - :align :right} - (:cell-args p))))) - pnl-datas)))] + (fn [p] + (cond-> [(merge {:value "Amt" + :align :right} + (:cell-args p)) + (merge {:value "%" + :align :right} + (:cell-args p))] + (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-" + :align :right} + (:cell-args p))))) + pnl-datas)))] [big-header sub-header])) (defn location-summary-table [pnl-datas title] (let [table [(subtotal-by-column-row (map #(filter-categories % [:sales]) - pnl-datas) - "Sales") - (subtotal-by-column-row (map #(filter-categories % [:cogs ]) pnl-datas) + pnl-datas) + "Sales") + (subtotal-by-column-row (map #(filter-categories % [:cogs]) pnl-datas) "Cogs") - (subtotal-by-column-row (map #(filter-categories % [:payroll ]) pnl-datas) + (subtotal-by-column-row (map #(filter-categories % [:payroll]) pnl-datas) "Payroll") (subtotal-by-column-row (map #(-> % - (filter-categories [:sales :payroll :cogs]) - (negate #{:payroll :cogs})) + (filter-categories [:sales :payroll :cogs]) + (negate #{:payroll :cogs})) pnl-datas) "Gross Profits") @@ -464,8 +448,8 @@ "Overhead") (subtotal-by-column-row (map #(-> % - (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) - (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) + (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) + (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Net Income")] percent-of-sales (calc-percent-of-sales table pnl-datas) @@ -473,7 +457,6 @@ {:header (headers pnl-datas title) :rows (combine-tables pnl-datas table percent-of-sales deltas)})) - (defn detail-rows [pnl-datas grouping title] (let [pnl-datas (map #(filter-categories % [grouping]) @@ -486,36 +469,35 @@ :when (seq account-codes) row (-> [(into [{:value (str "---" grouping-name "---")}] (map - (fn [p] - (assoc (:cell-args p) :value "" :format "")) - pnl-datas) - )] + (fn [p] + (assoc (:cell-args p) :value "" :format "")) + pnl-datas))] (into (for [{:keys [numeric-code name]} account-codes] (into [{:value (str name ":" numeric-code)}] (map - (fn [p] - (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code)) - this-name-exists? (->> (:data p) - (filter (comp #{name} :name)) - seq)] - (merge - (if this-name-exists? - {:format :dollar - :filters (:filters pnl-data) - :value (aggregate-accounts pnl-data)} - {:filters (:filters pnl-data) - :value ""}) - (:cell-args p)))) + (fn [p] + (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code)) + this-name-exists? (->> (:data p) + (filter (comp #{name} :name)) + seq)] + (merge + (if this-name-exists? + {:format :dollar + :filters (:filters pnl-data) + :value (aggregate-accounts pnl-data)} + {:filters (:filters pnl-data) + :value ""}) + (:cell-args p)))) - pnl-datas)))) + pnl-datas)))) (conj (subtotal-by-column-row pnl-datas "" {:border [:top]})))] row)] (-> [(into [{:value title :bold true}] (map - (fn [p] - (assoc (:cell-args p) :value "" :format "")) - pnl-datas))] + (fn [p] + (assoc (:cell-args p) :value "" :format "")) + pnl-datas))] (into individual-accounts) (conj (subtotal-by-column-row pnl-datas title))))) @@ -528,36 +510,36 @@ :cogs (str prefix " COGS"))) (into (detail-rows - pnl-datas - :payroll - (str prefix " Payroll"))) + pnl-datas + :payroll + (str prefix " Payroll"))) (conj (subtotal-by-column-row (map #(filter-categories % [:payroll :cogs]) pnl-datas) - (str prefix " Prime Costs"))) + (str prefix " Prime Costs"))) (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :payroll :cogs]) (negate #{:payroll :cogs})) pnl-datas) - (str prefix " Gross Profits"))) + (str prefix " Gross Profits"))) (into (detail-rows - pnl-datas - :controllable - (str prefix " Controllable Expenses"))) + pnl-datas + :controllable + (str prefix " Controllable Expenses"))) (into (detail-rows - pnl-datas - :fixed-overhead - (str prefix " Fixed Overhead"))) + pnl-datas + :fixed-overhead + (str prefix " Fixed Overhead"))) (into (detail-rows - pnl-datas - :ownership-controllable - (str prefix " Ownership Controllable"))) + pnl-datas + :ownership-controllable + (str prefix " Ownership Controllable"))) (conj (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable]) pnl-datas) - (str prefix " Overhead"))) + (str prefix " Overhead"))) (conj (subtotal-by-column-row (map #(-> % - (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) - (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) - pnl-datas) - (str prefix " Net Income")))) + (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) + (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) + pnl-datas) + (str prefix " Net Income")))) table (if (seq client-datas) (conj table (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) @@ -574,7 +556,7 @@ (let [big-header (into [{:value header-title :bold true}] (map-indexed (fn [i p] - (cond-> {:value + (cond-> {:value (str (date->str (:start p)) " - " (date->str (:end p))) @@ -582,20 +564,20 @@ :align :center :bold true} (odd? i) (assoc :bg-color [240 240 240]))) - (:periods (:args pnl-data)))) + (:periods (:args pnl-data)))) sub-header (into [{:value "Account"}] (mapcat - (fn [p] - [(merge {:value "Increases" - :align :right} - (:cell-args p)) - (merge {:value "Decreases" - :align :right} - (:cell-args p)) - (merge {:value "+/- in Cash" - :align :right} - (:cell-args p))]) - pnl-datas))] + (fn [p] + [(merge {:value "Increases" + :align :right} + (:cell-args p)) + (merge {:value "Decreases" + :align :right} + (:cell-args p)) + (merge {:value "+/- in Cash" + :align :right} + (:cell-args p))]) + pnl-datas))] [big-header sub-header])) (defn cash-flow-detail-rows @@ -614,8 +596,7 @@ [(assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "") (assoc (:cell-args p) :value "" :format "")]) - pnl-datas) - )] + pnl-datas))] (into (for [{:keys [numeric-code name]} account-codes] (into [{:value name}] (mapcat @@ -625,36 +606,36 @@ credits (aggregate-credits pnl-data) debits (aggregate-debits pnl-data)] (if (dollars= (- debits credits) aggregated) - [(merge - {:format :dollar - :filters (:filters pnl-data) - :value debits} - (:cell-args p)) - (merge - {:format :dollar - :filters (:filters pnl-data) - :value credits} - (:cell-args p)) - (merge - {:format :dollar - :filters (:filters pnl-data) - :value (cashflow-account->amount numeric-code aggregated)} - (:cell-args p))] - [(merge - {:format :dollar - :filters (:filters pnl-data) - :value credits} - (:cell-args p)) - (merge - {:format :dollar - :filters (:filters pnl-data) - :value debits} - (:cell-args p)) - (merge - {:format :dollar - :filters (:filters pnl-data) - :value (cashflow-account->amount numeric-code aggregated)} - (:cell-args p))]))) + [(merge + {:format :dollar + :filters (:filters pnl-data) + :value debits} + (:cell-args p)) + (merge + {:format :dollar + :filters (:filters pnl-data) + :value credits} + (:cell-args p)) + (merge + {:format :dollar + :filters (:filters pnl-data) + :value (cashflow-account->amount numeric-code aggregated)} + (:cell-args p))] + [(merge + {:format :dollar + :filters (:filters pnl-data) + :value credits} + (:cell-args p)) + (merge + {:format :dollar + :filters (:filters pnl-data) + :value debits} + (:cell-args p)) + (merge + {:format :dollar + :filters (:filters pnl-data) + :value (cashflow-account->amount numeric-code aggregated)} + (:cell-args p))]))) pnl-datas)))) (conj (cashflow-subtotal-by-column-row pnl-datas "" {:border [:top]})))] @@ -673,19 +654,19 @@ (defn cash-flows-table [pnl-datas #_client-datas title prefix] (let [table (-> [] (conj (cashflow-subtotal-by-column-row (map #(-> % - (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) - (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) - pnl-datas) - "Net Income")) + (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) + (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) + pnl-datas) + "Net Income")) (into (cash-flow-detail-rows pnl-datas - :operating-activities - (str prefix " Operating Activities"))) + :operating-activities + (str prefix " Operating Activities"))) (into (cash-flow-detail-rows pnl-datas - :investment-activities - (str prefix " Investment Activities"))) + :investment-activities + (str prefix " Investment Activities"))) (into (cash-flow-detail-rows pnl-datas - :financing-activities - (str prefix " Financing Activities"))) + :financing-activities + (str prefix " Financing Activities"))) (conj (cashflow-subtotal-by-column-row (map #(-> % (filter-categories [:operating-activities :investment-activities :financing-activities :sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) @@ -695,9 +676,8 @@ (into (cash-flow-detail-rows pnl-datas :cash - (str prefix " Bank Accounts / Cash"))) + (str prefix " Bank Accounts / Cash"))))] - )] {:header (cash-flow-headers pnl-datas "Cash Flow") :rows table})) @@ -720,7 +700,7 @@ :data (filter (fn [{:keys [numeric-code]}] (nil? numeric-code))) - (map :sample)) ] + (map :sample))] errors)) (defn summarize-pnl [pnl-data] @@ -728,19 +708,19 @@ :invalid-ids (invalid-ids pnl-data) :summaries (if (-> pnl-data :args :column-per-location) - [(location-summary-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] - (concat - (for [[client-id location] (client-locations pnl-data)] - (-> pnl-data - (filter-client client-id) - (filter-location location) - (filter-period period) - (zebra i))) - [(zebra (filter-locations (filter-period pnl-data period) - (map second (client-locations pnl-data))) i)]))) + [(location-summary-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods) (range))] + (concat + (for [[client-id location] (client-locations pnl-data)] + (-> pnl-data + (filter-client client-id) + (filter-location location) + (filter-period period) + (zebra i))) + [(zebra (filter-locations (filter-period pnl-data period) + (map second (client-locations pnl-data))) i)]))) "All location Summary")] (for [[client-id location] (client-locations pnl-data)] - (location-summary-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] + (location-summary-table (for [[period i] (map vector (-> pnl-data :args :periods) (range))] (-> pnl-data (filter-client client-id) (filter-location location) @@ -749,29 +729,29 @@ (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Summary")))) :details (doall (if (-> pnl-data :args :column-per-location) - [(location-detail-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] + [(location-detail-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods) (range))] (concat - (for [[client-id location] (client-locations pnl-data)] - (-> pnl-data - (filter-client client-id) - (filter-location location) - (filter-period period) - (zebra i))) - [(-> pnl-data + (for [[client-id location] (client-locations pnl-data)] + (-> pnl-data + (filter-client client-id) + (filter-location location) (filter-period period) - (filter-locations (map second (client-locations pnl-data))) - (zebra i))]))) + (zebra i))) + [(-> pnl-data + (filter-period period) + (filter-locations (map second (client-locations pnl-data))) + (zebra i))]))) nil "All location Detail" "")] (for [[client-id location] (client-locations pnl-data)] - (location-detail-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] + (location-detail-table (for [[period i] (map vector (-> pnl-data :args :periods) (range))] (-> pnl-data (filter-client client-id) (filter-location location) (filter-period period) (zebra i))) - (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] + (for [[period i] (map vector (-> pnl-data :args :periods) (range))] (-> pnl-data (filter-client client-id) (filter-period period) @@ -784,11 +764,11 @@ (let [client-ids (->> (client-locations pnl-data) (map first) set)] - + {:warning (warning-message pnl-data) :details (doall (for [client-id client-ids] - (cash-flows-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))] + (cash-flows-table (for [[period i] (map vector (-> pnl-data :args :periods) (range))] (-> pnl-data (filter-client client-id) (filter-period period) @@ -796,7 +776,6 @@ (str (-> pnl-data :clients-by-id (get client-id)) " Detail") "")))})) - (defn balance-sheet-headers [pnl-data] (let [period-count (count (:periods (:args pnl-data))) client-ids (set (map :client-id (:data pnl-data))) @@ -805,7 +784,7 @@ (cond-> [] (> client-count 1) (conj (cond-> (into [{:value "Client"}] - (mapcat identity + (mapcat identity (for [client client-ids] (cond-> [{:value (str (-> pnl-data :client-codes (get client)))}] (> period-count 1) @@ -814,15 +793,15 @@ true (conj (cond-> (into [{:value "Period Ending"}] - (for [client client-ids + (for [client client-ids [index p] (map vector (range) (:periods (:args pnl-data))) :let [is-first? (= 0 index) period-date (date->str p) period-headers (if (or is-first? (not (:include-deltas (:args pnl-data)))) - [{:value period-date}] - [{:value period-date} - {:value "+/-"}])] + [{:value period-date}] + [{:value period-date} + {:value "+/-"}])] header period-headers] header)) show-total? (conj {:value (date->str (first (:periods (:args pnl-data)))) :border [:left]})))))) @@ -851,48 +830,47 @@ result)))))) #_(defn summarize-balance-sheet [pnl-data] - (reduce - (fn [result table] - (-> result - (update :header into (:header table)) - (update :rows - (fn [current-rows] - (if (seq current-rows) - (map + (reduce + (fn [result table] + (-> result + (update :header into (:header table)) + (update :rows + (fn [current-rows] + (if (seq current-rows) + (map concat current-rows (:rows table)) - (:rows table)))))) - {:header [] - :rows []} - (for [client-id (set (map :client-id (:data pnl-data)))] - (let [pnl-datas (map (fn [p] - (-> pnl-data - (filter-client client-id) - (filter-period p))) - (:periods (:args pnl-data))) - table (-> [] - (into (detail-rows pnl-datas - :assets - "Assets")) - (into (detail-rows pnl-datas - :liabilities - "Liabilities")) - (into (detail-rows pnl-datas - :equities - "Owner's Equity")) - (conj (subtotal-by-column-row + (:rows table)))))) + {:header [] + :rows []} + (for [client-id (set (map :client-id (:data pnl-data)))] + (let [pnl-datas (map (fn [p] + (-> pnl-data + (filter-client client-id) + (filter-period p))) + (:periods (:args pnl-data))) + table (-> [] + (into (detail-rows pnl-datas + :assets + "Assets")) + (into (detail-rows pnl-datas + :liabilities + "Liabilities")) + (into (detail-rows pnl-datas + :equities + "Owner's Equity")) + (conj (subtotal-by-column-row (map #(-> % (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable]) (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable})) pnl-datas) "Retained Earnings"))) - table (if (:include-comparison (:args pnl-data)) - (append-deltas table) - table)] - {:header (balance-sheet-headers pnl-data) - :rows table}))) - ) + table (if (:include-comparison (:args pnl-data)) + (append-deltas table) + table)] + {:header (balance-sheet-headers pnl-data) + :rows table})))) (defn add-total-border [rows] (map (fn [row] @@ -913,7 +891,7 @@ show-total? (and (> client-count 1) (= 1 period-count)) pnl-datas (for [client-id client-ids p (:periods (:args pnl-data))] - (-> pnl-data + (-> pnl-data (filter-client client-id) (filter-period p))) total-data (when show-total? @@ -938,15 +916,13 @@ pnl-datas) "Retained Earnings"))) table (if (and (> period-count 1) - (:include-deltas (:args pnl-data))) - (append-deltas table) - table) + (:include-deltas (:args pnl-data))) + (append-deltas table) + table) table (if show-total? (add-total-border table) table)] {:warning (warning-message pnl-data) :header (balance-sheet-headers pnl-data) - :rows table})) - ) - + :rows table}))) (defn journal-detail-report [args data client-codes] {:header [[{:value "Category"} @@ -956,51 +932,46 @@ {:value "Credit"} {:value "Running Balance"}]] :rows (reduce - (fn [rows category] - (into rows + (fn [rows category] + (into rows ;; TODO colspan ? - (concat (when (seq (:journal-entries category)) - [[ - {:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))} - {:value ""} - {:value ""} - {:value ""} - {:value ""} - {:value ""}]]) - (map - (fn [je] - [{:value ""} - {:value (user-friendly-date (:date je))} - {:value (:description je "")} - {:value (get je :debit) - :format :dollar} - {:value (get je :credit) - :format :dollar} - {:value (get je :running-balance) - :format :dollar}]) - (:journal-entries category)) - [[ - {:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category))) - :bold true - :border [:top]} - {:value "" - :border [:top]} - {:value (str "Total" ) - :bold true - :border [:top]} - {:value "" - :border [:top]} - {:value "" - :border [:top]} - {:value (:total category) - :format :dollar - :bold true - :border [:top]}]])) - - ) - [] - (:categories data))} - ) + (concat (when (seq (:journal-entries category)) + [[{:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))} + {:value ""} + {:value ""} + {:value ""} + {:value ""} + {:value ""}]]) + (map + (fn [je] + [{:value ""} + {:value (user-friendly-date (:date je))} + {:value (:description je "")} + {:value (get je :debit) + :format :dollar} + {:value (get je :credit) + :format :dollar} + {:value (get je :running-balance) + :format :dollar}]) + (:journal-entries category)) + [[{:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category))) + :bold true + :border [:top]} + {:value "" + :border [:top]} + {:value (str "Total") + :bold true + :border [:top]} + {:value "" + :border [:top]} + {:value "" + :border [:top]} + {:value (:total category) + :format :dollar + :bold true + :border [:top]}]]))) + [] + (:categories data))}) (defrecord PNLData [args data client-codes]) diff --git a/src/cljc/auto_ap/numeric.cljc b/src/cljc/auto_ap/numeric.cljc index c13f7b28..ea915920 100644 --- a/src/cljc/auto_ap/numeric.cljc +++ b/src/cljc/auto_ap/numeric.cljc @@ -86,9 +86,8 @@ parts)))] (recur parts n (inc mag))))))) - (defn num->words [num] - (let [total-cents (int (Math/round (* 100 num))) + (let [total-cents (int (Math/round (* 100 num))) dollar-num (int (quot total-cents 100)) cent-num (int (rem total-cents 100)) dollars (str (words dollar-num) " dollars") diff --git a/src/cljc/auto_ap/permissions.cljc b/src/cljc/auto_ap/permissions.cljc index 19ad34e1..c75a3b39 100644 --- a/src/cljc/auto_ap/permissions.cljc +++ b/src/cljc/auto_ap/permissions.cljc @@ -1,5 +1,5 @@ (ns auto-ap.permissions - #?(:clj + #?(:clj (:require [cemerick.url :as url]))) ;; TODO after getting rid of cljs, use malli schemas to decode this @@ -107,29 +107,28 @@ (cond (#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject) true - + (= [:vendor :create] [subject activity]) true - + (= [:vendor :edit] [subject activity]) true - + (= [:signature :edit] [subject activity]) true - - + (= [:invoice :create] [subject activity]) true - + (= [:invoice :pay] [subject activity]) true - + (= [:invoice :edit] [subject activity]) true - + (= [:invoice :delete] [subject activity]) true - + (= [:ledger :read] [subject activity]) true @@ -142,19 +141,19 @@ false))) #? (:clj - (defn wrap-must - ( [handler policy] - (fn [request] - (if (can? (:identity request) policy) - (handler request) - {:status 302 - :headers {"Location" (str "/login?" - (url/map->query {"redirect-to" (:uri request)}))}}))) - ( [handler policy get-client] - (fn [request] - (if (can? (:identity request) (assoc policy :client (get-client request))) - (handler request) - {:status 302 - :headers {"Location" (str "/login?" - (url/map->query {"redirect-to" (:uri request)}))}}))))) + (defn wrap-must + ([handler policy] + (fn [request] + (if (can? (:identity request) policy) + (handler request) + {:status 302 + :headers {"Location" (str "/login?" + (url/map->query {"redirect-to" (:uri request)}))}}))) + ([handler policy get-client] + (fn [request] + (if (can? (:identity request) (assoc policy :client (get-client request))) + (handler request) + {:status 302 + :headers {"Location" (str "/login?" + (url/map->query {"redirect-to" (:uri request)}))}}))))) diff --git a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc index cdb39a20..b625ac32 100644 --- a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc +++ b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc @@ -15,5 +15,4 @@ ["/" [#"\d+" :db/id] "/delete"] ::delete ["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog :post ::execute} - "/check-badges" ::check-badges - }) + "/check-badges" ::check-badges}) diff --git a/src/cljc/auto_ap/routes/admin/vendors.cljc b/src/cljc/auto_ap/routes/admin/vendors.cljc index 110e4c1d..f2f62eee 100644 --- a/src/cljc/auto_ap/routes/admin/vendors.cljc +++ b/src/cljc/auto_ap/routes/admin/vendors.cljc @@ -13,5 +13,4 @@ "/new" {:get ::new} "/merge" {:get ::merge :put ::merge-submit} - ["/" [#"\d+" :db/id] "/edit"] {:get ::edit - }}) + ["/" [#"\d+" :db/id] "/edit"] {:get ::edit}}) diff --git a/src/cljc/auto_ap/routes/dashboard.cljc b/src/cljc/auto_ap/routes/dashboard.cljc index 5f2d46e5..95fe033d 100644 --- a/src/cljc/auto_ap/routes/dashboard.cljc +++ b/src/cljc/auto_ap/routes/dashboard.cljc @@ -1,6 +1,6 @@ (ns auto-ap.routes.dashboard) (def routes {"" - {:get ::page } + {:get ::page} "/expense-card" ::expense-card "/pnl-card" ::pnl-card "/sales-card" ::sales-card diff --git a/src/cljc/auto_ap/routes/invoice.cljc b/src/cljc/auto_ap/routes/invoice.cljc index d1fd2632..4f46578c 100644 --- a/src/cljc/auto_ap/routes/invoice.cljc +++ b/src/cljc/auto_ap/routes/invoice.cljc @@ -21,12 +21,12 @@ "/account/prediction" ::account-prediction "/total" ::expense-account-total "/balance" ::expense-account-balance} - + "/pay-button" ::pay-button "/pay" {:get ::pay-wizard "/using-credit" ::pay-using-credit - + "/navigate" ::pay-wizard-navigate :post ::pay-submit} "/bulk-delete" {:get ::bulk-delete @@ -35,12 +35,12 @@ :put ::bulk-edit-submit "/account" ::bulk-edit-new-account "/total" ::bulk-edit-total - "/balance" ::bulk-edit-balance} + "/balance" ::bulk-edit-balance} ["/" [#"\d+" :db/id]] {:delete ::delete "/undo-autopay" ::undo-autopay "/unvoid" ::unvoid "/edit" ::edit-wizard} - "/table" ::table }) + "/table" ::table}) (def legacy-routes {"" ::legacy-invoices "import" ::legacy-import-invoices diff --git a/src/cljc/auto_ap/routes/ledger.cljc b/src/cljc/auto_ap/routes/ledger.cljc index 060a52e4..d602f1bf 100644 --- a/src/cljc/auto_ap/routes/ledger.cljc +++ b/src/cljc/auto_ap/routes/ledger.cljc @@ -13,15 +13,15 @@ "/import" ::external-import-import} "/investigate" {"" ::investigate "/results" ::investigate-results} - "/table" ::table - "/csv" ::csv + "/table" ::table + "/csv" ::csv "/bank-account-filter" ::bank-account-filter "/reports/balance-sheet" {"" ::balance-sheet "/run" ::run-balance-sheet "/export" ::export-balance-sheet} "/reports/cash-flows" {"" ::cash-flows - "/run" ::run-cash-flows - "/export" ::export-cash-flows} + "/run" ::run-cash-flows + "/export" ::export-cash-flows} "/reports/profit-and-loss" {"" ::profit-and-loss - "/run" ::run-profit-and-loss - "/export" ::export-profit-and-loss}}) \ No newline at end of file + "/run" ::run-profit-and-loss + "/export" ::export-profit-and-loss}}) \ No newline at end of file diff --git a/src/cljc/auto_ap/routes/outgoing_invoice.cljc b/src/cljc/auto_ap/routes/outgoing_invoice.cljc index cef47a0f..fc35192a 100644 --- a/src/cljc/auto_ap/routes/outgoing_invoice.cljc +++ b/src/cljc/auto_ap/routes/outgoing_invoice.cljc @@ -1,4 +1,4 @@ (ns auto-ap.routes.outgoing-invoice) -(def routes {"/" {"new" {:get ::new +(def routes {"/" {"new" {:get ::new :post ::new-submit} "line-item/new" {:get ::new-line-item}}}) \ No newline at end of file diff --git a/src/cljc/auto_ap/routes/pos/sales_summaries.cljc b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc index 5f9312d9..a5b7c32a 100644 --- a/src/cljc/auto_ap/routes/pos/sales_summaries.cljc +++ b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc @@ -2,7 +2,7 @@ (def routes {"" {:get ::page :put ::edit-wizard-submit} "/table" ::table - ["/" [#"\d+" :db/id]] {:get ::edit-wizard } + ["/" [#"\d+" :db/id]] {:get ::edit-wizard} "/edit/navigate" ::edit-wizard-navigate "/edit/sales-summary-item" ::new-summary-item "/edit/item-account" ::edit-item-account diff --git a/src/cljc/auto_ap/routes/transactions.cljc b/src/cljc/auto_ap/routes/transactions.cljc index f90fe6fa..1226a8b7 100644 --- a/src/cljc/auto_ap/routes/transactions.cljc +++ b/src/cljc/auto_ap/routes/transactions.cljc @@ -9,7 +9,8 @@ "/bulk-suppress" ::bulk-suppress "/bulk-code" {:get ::bulk-code :put ::bulk-code-submit - "/new-account" ::bulk-code-new-account}} + "/new-account" ::bulk-code-new-account + "/vendor-changed" ::bulk-code-vendor-changed}} "/new" {:get ::new :post ::new-submit "/location-select" ::location-select @@ -20,19 +21,19 @@ "/external-import-new" {"" ::external-import-page "/parse" ::external-import-parse "/import" ::external-import-import} - - "/table" ::table - "/csv" ::csv + + "/table" ::table + "/csv" ::csv "/bank-account-filter" ::bank-account-filter - - ["/" [#"\d+" :db/id]] {"/edit" {:get ::edit-wizard - } } + + ["/" [#"\d+" :db/id]] {"/edit" {:get ::edit-wizard}} "/edit-submit" ::edit-submit + "/edit-vendor-changed" ::edit-vendor-changed "/location-select" ::location-select - "/account-total" ::account-total - "/account-balance" ::account-balance - "/toggle-amount-mode" ::toggle-amount-mode - "/edit-wizard-new-account" ::edit-wizard-new-account + "/account-total" ::account-total + "/account-balance" ::account-balance + "/toggle-amount-mode" ::toggle-amount-mode + "/edit-wizard-new-account" ::edit-wizard-new-account "/match-payment" ::link-payment "/match-autopay-invoices" ::link-autopay-invoices "/match-unpaid-invoices" ::link-unpaid-invoices diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 3ca667b0..26eda78e 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -21,7 +21,7 @@ "login" :login "search" :search "indicators" indicator-routes/routes - + "dashboard" d-routes/routes "account" {"/search" {:get :account-search}} "admin" {"" :auto-ap.routes.admin/page @@ -73,13 +73,13 @@ "/table" {:get :pos-refund-table}} "/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts} "/table" {:get :pos-cash-drawer-shift-table}}} - + "outgoing-invoice" oi-routes/routes "payment" p-routes/routes "invoice" i-routes/routes "invoices/" i-routes/legacy-routes "invoices" i-routes/legacy-routes - + "vendor" {"/search" :vendor-search} ;; TODO Include IDS in routes for company-specific things, as opposed to headers "company" {"" :company @@ -109,7 +109,7 @@ "/fastlink" {:get :company-yodlee-fastlink-dialog} "/refresh" {:put :company-yodlee-provider-account-refresh} "/reauthenticate" {:put :company-yodlee-provider-account-reauthenticate}} - + "/plaid" {"" {:get :company-plaid} "/table" {:get :company-plaid-table} "/link" {:post :company-plaid-link} @@ -117,6 +117,5 @@ #_#_"/fastlink" {:get :company-yodlee-fastlink-dialog} #_#_"/refresh" {:put :company-yodlee-provider-account-refresh}}}}) - (def only-routes ["/" routes]) diff --git a/src/cljc/auto_ap/time_utils.cljc b/src/cljc/auto_ap/time_utils.cljc index c39d3df3..983529b8 100644 --- a/src/cljc/auto_ap/time_utils.cljc +++ b/src/cljc/auto_ap/time_utils.cljc @@ -10,7 +10,7 @@ (some->> d (format/unparse pretty))) (defn next-dom [date dom] - (when date + (when date (let [candidate (time/date-time (time/year date) (time/month date) #?(:clj (Math/min (int dom) (time/day (time/last-day-of-the-month (time/year date) (time/month date)))) diff --git a/src/cljc/auto_ap/utils.cljc b/src/cljc/auto_ap/utils.cljc index 0f59063b..8e03d66a 100644 --- a/src/cljc/auto_ap/utils.cljc +++ b/src/cljc/auto_ap/utils.cljc @@ -1,9 +1,9 @@ (ns auto-ap.utils #?@ - (:clj - [(:require [com.unbounce.dogstatsd.core :as statsd] - [com.brunobonacci.mulog :as mu] - [auto-ap.logging :as alog])])) + (:clj + [(:require [com.unbounce.dogstatsd.core :as statsd] + [com.brunobonacci.mulog :as mu] + [auto-ap.logging :as alog])])) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn by @@ -35,7 +35,7 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn dollars= [amt1 amt2] - (dollars-0? (- amt1 amt2) )) + (dollars-0? (- amt1 amt2))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn deep-merge [v & vs] @@ -55,18 +55,17 @@ (let [in-progress? (atom false)] (fn [] (when (= false @in-progress?) - (try + (try (reset! in-progress? true) (f) (finally (reset! in-progress? false))))))) - #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn heartbeat [f id] (fn [] #?(:clj (mu/with-context {:source id} - (try + (try (alog/info ::starting-process :id id) (f) (alog/info ::ending-process :id id) diff --git a/test/clj/auto_ap/ezcater_test.clj b/test/clj/auto_ap/ezcater_test.clj index 1966198f..2348d166 100644 --- a/test/clj/auto_ap/ezcater_test.clj +++ b/test/clj/auto_ap/ezcater_test.clj @@ -53,28 +53,26 @@ (t/testing "It should find the order from ezcater" (with-redefs [sut/get-caterer (fn [k] (t/is (= k "91541331-d7ae-4634-9e8b-ccbbcfb2ce70")) - { - :ezcater-integration/_caterers {:ezcater-integration/api-key "bmlrdHNpZ2FyaXNAZ21haWwuY29tOmQwMzQwMjYzOWI2ODQxNmVkMjdmZWYxMWFhZTk3YzU1MDlmNTcyNjYwMDAzOTA5MDE2OGMzODllNDBjNTVkZGE"} + {:ezcater-integration/_caterers {:ezcater-integration/api-key "bmlrdHNpZ2FyaXNAZ21haWwuY29tOmQwMzQwMjYzOWI2ODQxNmVkMjdmZWYxMWFhZTk3YzU1MDlmNTcyNjYwMDAzOTA5MDE2OGMzODllNDBjNTVkZGE"} :ezcater-location/_caterer [{:ezcater-location/location "DT" - :client/_ezcater-locations {:client/code "ABC"}}] - })] + :client/_ezcater-locations {:client/code "ABC"}}]})] (t/is (= known-order (sut/lookup-order sample-event)))))) (t/deftest order->sales-order (t/testing "It should use the date" (t/is (= #clj-time/date-time "2022-01-01T00:00:00-08:00" - (-> known-order - (assoc-in [:event :timestamp] - "2022-01-01T08:00:00Z") - (sut/order->sales-order) - (:sales-order/date )))) + (-> known-order + (assoc-in [:event :timestamp] + "2022-01-01T08:00:00Z") + (sut/order->sales-order) + (:sales-order/date)))) (t/is (= #clj-time/date-time "2022-06-01T00:00:00-07:00" (-> known-order (assoc-in [:event :timestamp] "2022-06-01T07:00:00Z") (sut/order->sales-order) - (:sales-order/date ))))) + (:sales-order/date))))) (t/testing "It should simulate a single line item for everything" (t/is (= 1 (-> known-order @@ -83,49 +81,48 @@ count))) (t/is (= #{"EZCater Catering"} (->> known-order - sut/order->sales-order - :sales-order/line-items - (map :order-line-item/category) - set)))) + sut/order->sales-order + :sales-order/line-items + (map :order-line-item/category) + set)))) (t/testing "It should generate an external-id" (t/is (= "ezcater/order/ABC-DT-9ab05fee-a9c5-483b-a7f2-14debde4b7a8" (:sales-order/external-id (sut/order->sales-order known-order))))) - (t/testing "Should capture amounts" (t/is (= 35.09 (-> known-order sut/order->sales-order :sales-order/tax))) (t/is (= 0.0 (-> known-order - sut/order->sales-order - :sales-order/tip)))) + sut/order->sales-order + :sales-order/tip)))) (t/testing "Should calculate 7% commision on ezcater orders" - (t/is (dollars= 7.0 - (-> known-order - (assoc :orderSourceType "EZCATER") - (assoc-in [:totals :subTotal :subunits] 10000) - sut/commision))) + (t/is (dollars= 7.0 + (-> known-order + (assoc :orderSourceType "EZCATER") + (assoc-in [:totals :subTotal :subunits] 10000) + sut/commision))) (t/testing "Should inlclude delivery fee in commision" - (t/is (dollars= 14.0 - (-> known-order - (assoc :orderSourceType "EZCATER") - (assoc-in [:totals :subTotal :subunits] 10000) - (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) - sut/commision))))) + (t/is (dollars= 14.0 + (-> known-order + (assoc :orderSourceType "EZCATER") + (assoc-in [:totals :subTotal :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) + sut/commision))))) (t/testing "Should calculate 15% commision on marketplace orders" - (t/is (dollars= 15.0 - (-> known-order - (assoc :orderSourceType "MARKETPLACE") - (assoc-in [:totals :subTotal :subunits] 10000) - sut/commision))) + (t/is (dollars= 15.0 + (-> known-order + (assoc :orderSourceType "MARKETPLACE") + (assoc-in [:totals :subTotal :subunits] 10000) + sut/commision))) (t/testing "Should inlclude delivery fee in commision" - (t/is (dollars= 30.0 - (-> known-order - (assoc :orderSourceType "MARKETPLACE") - (assoc-in [:totals :subTotal :subunits] 10000) - (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) - sut/commision))))) + (t/is (dollars= 30.0 + (-> known-order + (assoc :orderSourceType "MARKETPLACE") + (assoc-in [:totals :subTotal :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) + sut/commision))))) (t/testing "Should calculate 2.75% ccp fee" (t/is (dollars= 8.97 (-> known-order @@ -138,7 +135,7 @@ (t/is (dollars= 454.09 (-> known-order sut/order->sales-order - :sales-order/total)))) + :sales-order/total)))) (t/testing "Should derive adjustments food-total + sales-tax - caterer-total - service fee - ccp fee" (t/is (dollars= -42.99 (-> known-order diff --git a/test/clj/auto_ap/import/plaid_test.clj b/test/clj/auto_ap/import/plaid_test.clj index 8514753e..23717a01 100644 --- a/test/clj/auto_ap/import/plaid_test.clj +++ b/test/clj/auto_ap/import/plaid_test.clj @@ -8,9 +8,8 @@ :amount 123.45 :date "2023-01-01"}) -(t/deftest plaid->transaction +(t/deftest plaid->transaction - (t/testing "Should assign a plaid merchant if a merchant is found" (t/is (= "Home Depot" (-> (sut/plaid->transaction (assoc base-transaction :merchant_name "Home Depot") @@ -19,6 +18,6 @@ :plaid-merchant/name)))) (t/testing "Should assign a default vendor if a merchant is found, with a matching vendor lookup" (t/is (= 12354 (-> (sut/plaid->transaction (assoc base-transaction - :merchant_name "Home Depot") - {"Home Depot" 12354}) - :transaction/default-vendor))))) + :merchant_name "Home Depot") + {"Home Depot" 12354}) + :transaction/default-vendor))))) diff --git a/test/clj/auto_ap/import/transactions_test.clj b/test/clj/auto_ap/import/transactions_test.clj index ad982856..e374bc15 100644 --- a/test/clj/auto_ap/import/transactions_test.clj +++ b/test/clj/auto_ap/import/transactions_test.clj @@ -71,7 +71,6 @@ bank-account {})))))) - (t/deftest transaction->txs (t/testing "Should import and code transactions" (t/testing "Should import one transaction" @@ -83,8 +82,8 @@ :client/locations ["Z" "E"] :client/bank-accounts ["bank-account-id"]}])) result (sut/transaction->txs base-transaction - (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) - noop-rule)] + (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) + noop-rule)] (t/is (= (assoc base-transaction :transaction/approval-status :transaction-approval-status/unapproved :transaction/bank-account bank-account-id @@ -92,11 +91,11 @@ result)))) (t/testing "Should apply a default vendor" - (let [ {:strs [test-client-id test-bank-account-id test-vendor-id]} (setup-test-data []) + (let [{:strs [test-client-id test-bank-account-id test-vendor-id]} (setup-test-data []) result (sut/transaction->txs (assoc base-transaction :transaction/default-vendor test-vendor-id) - (dc/pull (dc/db conn) sut/bank-account-pull test-bank-account-id) - noop-rule)] + (dc/pull (dc/db conn) sut/bank-account-pull test-bank-account-id) + noop-rule)] (t/is (= (assoc base-transaction :transaction/approval-status :transaction-approval-status/unapproved :transaction/bank-account test-bank-account-id @@ -125,21 +124,20 @@ :transaction/description-original "CHECK 10001" :transaction/check-number 10001 :transaction/amount -30.0) - (dc/pull (dc/db conn ) sut/bank-account-pull bank-account-id) + (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) noop-rule)] - + (t/is (= {:db/id payment-id :payment/status :payment-status/cleared} (:transaction/payment transaction-result)))) - (t/testing "Should match a check that matches on amount if check number does not match" (let [transaction-result (sut/transaction->txs (assoc base-transaction :transaction/description-original "CHECK 12301" :transaction/amount -30.0) - (dc/pull (dc/db conn ) sut/bank-account-pull bank-account-id) + (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) noop-rule)] - + (t/is (= {:db/id payment-id :payment/status :payment-status/cleared} (:transaction/payment transaction-result))))) @@ -151,11 +149,10 @@ :transaction/amount -30.0) (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) noop-rule)] - + (t/is (= nil (:transaction/payment result))))))) - (t/testing "Should match expected-deposits" (let [{:strs [bank-account-id client-id expected-deposit-id]} (->> [#:expected-deposit {:client "client-id" :date #inst "2021-07-01T00:00:00-08:00" @@ -174,7 +171,6 @@ deref :tempids)] - (t/testing "Should match within 10 days" (let [transaction-result (sut/transaction->txs (assoc base-transaction :transaction/date #inst "2021-07-03T00:00:00-08:00" @@ -183,7 +179,7 @@ noop-rule)] (t/is (= expected-deposit-id (:db/id (sut/find-expected-deposit client-id 100.0 (clj-time.coerce/to-date-time #inst "2021-07-03T00:00:00-08:00"))))) - + (t/is (= {:db/id expected-deposit-id :expected-deposit/status :expected-deposit-status/cleared} (:transaction/expected-deposit transaction-result))))) @@ -194,7 +190,7 @@ (dc/pull (dc/db conn) sut/bank-account-pull bank-account-id) noop-rule)] (t/is (= :vendor/ccp-square - (:transaction/vendor transaction-result))))) + (:transaction/vendor transaction-result))))) (t/testing "Should credit CCP" (let [transaction-result (sut/transaction->txs (assoc base-transaction @@ -262,7 +258,6 @@ first :transaction/raw-id))))) - (t/deftest match-transaction-to-single-unfulfilled-payments (t/testing "Auto-pay Invoices" (let [{:strs [vendor1-id vendor2-id]} (->> [#:vendor {:name "Autopay vendor 1" @@ -281,12 +276,11 @@ :total 30.0 :db/id "invoice-id"} #:client {:name "Client" :db/id "client-id"}] - (dc/transact conn) - deref - :tempids) + (dc/transact conn) + deref + :tempids) invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)] - (t/is (= 1 (count invoices-matches))) - )) + (t/is (= 1 (count invoices-matches))))) (t/testing "Should not match paid invoice that isn't a scheduled payment" (let [{:strs [client-id]} (->> [#:invoice{:status :invoice-status/paid @@ -300,13 +294,13 @@ deref :tempids) invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)] - + (t/is (= [] invoices-matches)))) (t/testing "Should not match unpaid invoice" (let [{:strs [client-id]} (->> [#:invoice {:status :invoice-status/unpaid :scheduled-payment #inst "2019-01-04" - :vendor vendor1-id + :vendor vendor1-id :date #inst "2019-01-01" :client "client-id" :total 30.0 @@ -316,7 +310,7 @@ deref :tempids) invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)] - + (t/is (= [] invoices-matches)))) (t/testing "Should not match invoice that already has a payment" @@ -335,7 +329,7 @@ deref :tempids) invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 - client-id)] + client-id)] (t/is (= [] invoices-matches)))) (t/testing "Should match multiple invoices for same vendor that total to transaction amount" (let [{:strs [client-id]} (->> [#:invoice {:status :invoice-status/paid @@ -436,45 +430,41 @@ (t/is (= [] (sut/match-transaction-to-single-unfulfilled-autopayments -31.0 client-id)) (str "Expected to not match, because there is invoice-3 is between invoice-1 and invoice-2."))))))) - - - - #_(t/testing "Auto-pay Invoices" (t/testing "Should match paid invoice that doesn't have a payment yet" (let [{:strs [bank-account-id client-id invoice1-id invoice2-id vendor-id]} (->> [#:invoice {:status :invoice-status/paid - :vendor "vendor-id" - :scheduled-payment #inst "2019-01-04" - :date #inst "2019-01-01" - :client "client-id" - :total 20.0 - :db/id "invoice1-id"} - #:invoice {:status :invoice-status/paid - :vendor "vendor-id" - :scheduled-payment #inst "2019-01-04" - :date #inst "2019-01-01" - :client "client-id" - :total 10.0 - :db/id "invoice2-id"} - #:vendor {:name "Autopay vendor" - :db/id "vendor-id"} - #:bank-account {:name "Bank account" - :db/id "bank-account-id"} - #:client {:name "Client" - :db/id "client-id" - :bank-accounts ["bank-account-id"]}] - (d/transact (d/connect uri)) - deref - :tempids) + :vendor "vendor-id" + :scheduled-payment #inst "2019-01-04" + :date #inst "2019-01-01" + :client "client-id" + :total 20.0 + :db/id "invoice1-id"} + #:invoice {:status :invoice-status/paid + :vendor "vendor-id" + :scheduled-payment #inst "2019-01-04" + :date #inst "2019-01-01" + :client "client-id" + :total 10.0 + :db/id "invoice2-id"} + #:vendor {:name "Autopay vendor" + :db/id "vendor-id"} + #:bank-account {:name "Bank account" + :db/id "bank-account-id"} + #:client {:name "Client" + :db/id "client-id" + :bank-accounts ["bank-account-id"]}] + (d/transact (d/connect uri)) + deref + :tempids) [[transaction-tx payment-tx invoice-payments1-tx invoice-payments2-tx]] (sut/yodlees->transactions [(assoc base-yodlee-transaction - :amount {:amount 30.0} - :bank-account {:db/id bank-account-id - :client/_bank-accounts {:db/id client-id - :client/locations ["A"]}})] - :bank-account - noop-rule - #{})] - + :amount {:amount 30.0} + :bank-account {:db/id bank-account-id + :client/_bank-accounts {:db/id client-id + :client/locations ["A"]}})] + :bank-account + noop-rule + #{})] + (t/is (= :transaction-approval-status/approved (:transaction/approval-status transaction-tx)) (str "Should have approved transaction " transaction-tx)) @@ -519,14 +509,14 @@ deref :tempids) [[transaction-tx payment-tx]] (sut/yodlees->transactions [(assoc base-yodlee-transaction - :amount {:amount 30.0} - :bank-account {:db/id bank-account-id - :client/_bank-accounts {:db/id client-id - :client/locations ["A"]}})] - :bank-account - noop-rule - #{})] - + :amount {:amount 30.0} + :bank-account {:db/id bank-account-id + :client/_bank-accounts {:db/id client-id + :client/locations ["A"]}})] + :bank-account + noop-rule + #{})] + (t/is (= :transaction-approval-status/unapproved (:transaction/approval-status transaction-tx))) (t/is (nil? (:transaction/payment transaction-tx)))))) diff --git a/test/clj/auto_ap/import/yodlee_test.clj b/test/clj/auto_ap/import/yodlee_test.clj index be1e03d2..7cd442eb 100644 --- a/test/clj/auto_ap/import/yodlee_test.clj +++ b/test/clj/auto_ap/import/yodlee_test.clj @@ -2,7 +2,6 @@ (:require [auto-ap.import.yodlee2 :as sut] [clojure.test :as t])) - (def base-transaction {:postDate "2014-01-04" :accountId 1234 :date "2014-01-02" @@ -26,6 +25,6 @@ :baseType "DEBIT") false)))) (t/is (= 12.0 (:transaction/amount (sut/yodlee->transaction (assoc base-transaction - :amount {:amount 12.0} - :baseType "CREDIT") + :amount {:amount 12.0} + :baseType "CREDIT") false)))))) diff --git a/test/clj/auto_ap/integration/graphql.clj b/test/clj/auto_ap/integration/graphql.clj index 1ecbb196..a6d0feb3 100644 --- a/test/clj/auto_ap/integration/graphql.clj +++ b/test/clj/auto_ap/integration/graphql.clj @@ -6,7 +6,6 @@ [auto-ap.integration.util :refer [wrap-setup admin-token user-token setup-test-data test-transaction]] [auto-ap.datomic :refer [conn]])) - (defn new-client [args] (merge {:client/name "Test client" :client/code (.toString (java.util.UUID/randomUUID)) @@ -29,7 +28,7 @@ (deftest transaction-page (testing "transaction page" (let [{:strs [test-client-id]} (setup-test-data [(test-transaction :transaction/description-original "hi")])] - + (testing "It should find all transactions" (let [result (:transaction-page (:data (sut/query (admin-token) "{ transaction_page(filters: {}) { count, start, data { id } }}" {:clients [{:db/id test-client-id}]})))] (is (= 1 (:count result))) @@ -42,13 +41,12 @@ (is (= 0 (:start result))) (is (= 0 (count (:data result))))))))) - (deftest invoice-page (testing "invoice page" @(dc/transact conn - [(new-client {:db/id "client"}) - (new-invoice {:invoice/client "client" - :invoice/status :invoice-status/paid})]) + [(new-client {:db/id "client"}) + (new-invoice {:invoice/client "client" + :invoice/status :invoice-status/paid})]) (testing "It should find all invoices" (let [result (first (:invoice-page (:data (sut/query (admin-token) "{ invoice_page(filters: { status:paid}) { count, start, invoices { id } }}"))))] (is (= 1 (:count result))) @@ -69,7 +67,6 @@ (is (int? (:start result))) (is (seqable? (:journal-entries result))))))) - (deftest vendors (testing "vendors" (testing "it should find vendors" @@ -88,17 +85,17 @@ (is (seqable? (:transaction-rules result)))))) (deftest upsert-transaction-rule - (let [{:strs [vendor-id account-id yodlee-merchant-id]} (-> - @(dc/transact - conn - [{:vendor/name "Bryce's Meat Co" - :db/id "vendor-id"} - {:account/name "hello" - :db/id "account-id"} - {:yodlee-merchant/name "yodlee" - :db/id "yodlee-merchant-id"}]) - - :tempids)] + (let [{:strs [vendor-id account-id yodlee-merchant-id]} (-> + @(dc/transact + conn + [{:vendor/name "Bryce's Meat Co" + :db/id "vendor-id"} + {:account/name "hello" + :db/id "account-id"} + {:yodlee-merchant/name "yodlee" + :db/id "yodlee-merchant-id"}]) + + :tempids)] (testing "it should reject rules that don't add up to 100%" (let [q (v/graphql-query {:venia/operation {:operation/type :mutation :operation/name "UpsertTransactionRule"} @@ -106,10 +103,9 @@ {:transaction-rule {:accounts [{:account-id account-id :percentage "0.25" :location "Shared"}]}} - [:id ]])}]})] + [:id]])}]})] (is (thrown? clojure.lang.ExceptionInfo (sut/query (admin-token) q))))) - (testing "It should reject rules that are missing both description and merchant" (let [q (v/graphql-query {:venia/operation {:operation/type :mutation :operation/name "UpsertTransactionRule"} @@ -117,7 +113,7 @@ {:transaction-rule {:accounts [{:account-id account-id :percentage "1.0" :location "Shared"}]}} - [:id ]])}]})] + [:id]])}]})] (is (thrown? clojure.lang.ExceptionInfo (sut/query (admin-token) q))))) (testing "it should add rules" (let [q (v/graphql-query {:venia/operation {:operation/type :mutation @@ -141,12 +137,12 @@ result (-> (sut/query (admin-token) q) :data :upsert-transaction-rule)] - + (is (= "123" (:description result))) (is (= "Bryce's Meat Co" (-> result :vendor :name))) (is (= "yodlee" (-> result :yodlee-merchant :name))) (is (= :approved (:transaction-approval-status result))) - (is (= "hello" (-> result :accounts (get 0) :account :name ))) + (is (= "hello" (-> result :accounts (get 0) :account :name))) (is (:id result)) (testing "it should unset removed fields" @@ -185,40 +181,39 @@ (is (= 1 (count (:accounts result)))))))))) - (deftest test-transaction-rule (testing "it should match rules" (let [matching-transaction @(dc/transact conn - [{:transaction/description-original "matching-desc" - :transaction/date #inst "2019-01-05T00:00:00.000-08:00" - :transaction/client {:client/name "1" - :db/id "client-1"} - :transaction/bank-account {:db/id "bank-account-1" - :bank-account/name "1"} + [{:transaction/description-original "matching-desc" + :transaction/date #inst "2019-01-05T00:00:00.000-08:00" + :transaction/client {:client/name "1" + :db/id "client-1"} + :transaction/bank-account {:db/id "bank-account-1" + :bank-account/name "1"} - :transaction/amount 1.00 - :transaction/id "2019-01-05 matching-desc 1" - :db/id "a"} + :transaction/amount 1.00 + :transaction/id "2019-01-05 matching-desc 1" + :db/id "a"} - {:transaction/description-original "nonmatching-desc" - :transaction/client {:client/name "2" - :db/id "client-2"} - :transaction/bank-account {:db/id "bank-account-2" - :bank-account/name "2"} - :transaction/date #inst "2019-01-15T23:23:00.000-08:00" - :transaction/amount 2.00 - :transaction/id "2019-01-15 nonmatching-desc 2" - :db/id "b"}]) + {:transaction/description-original "nonmatching-desc" + :transaction/client {:client/name "2" + :db/id "client-2"} + :transaction/bank-account {:db/id "bank-account-2" + :bank-account/name "2"} + :transaction/date #inst "2019-01-15T23:23:00.000-08:00" + :transaction/amount 2.00 + :transaction/id "2019-01-15 nonmatching-desc 2" + :db/id "b"}]) {:strs [a b client-1 client-2 bank-account-1 bank-account-2]} (get-in matching-transaction [:tempids]) a (str a) b (str b) rule-test (fn [rule] (-> (sut/query (admin-token) (v/graphql-query {:venia/operation {:operation/type :query - :operation/name "TestTransactionRule"} - :venia/queries [{:query/data (sut/->graphql [:test-transaction-rule - {:transaction-rule rule} - [:id]])}]})) + :operation/name "TestTransactionRule"} + :venia/queries [{:query/data (sut/->graphql [:test-transaction-rule + {:transaction-rule rule} + [:id]])}]})) :data :test-transaction-rule))] (testing "based on date " @@ -233,8 +228,8 @@ (testing "based on amount" (is (= [{:id a}] (rule-test {:amount-gte 1.0 :amount-lte 1.0}))) - (is (= (set [{:id a} {:id b}]) (set (rule-test {:amount-gte 1.0 }))) ) - (is (= (set [{:id a} {:id b}]) (set (rule-test {:amount-lte 2.0 }))) )) + (is (= (set [{:id a} {:id b}]) (set (rule-test {:amount-gte 1.0})))) + (is (= (set [{:id a} {:id b}]) (set (rule-test {:amount-lte 2.0}))))) (testing "based on client" (is (= [{:id a}] (rule-test {:client-id (str client-1)}))) @@ -247,40 +242,40 @@ (deftest test-match-transaction-rule (testing "it should apply a rules" (let [{:strs [transaction-id transaction-rule-id uneven-transaction-rule-id]} (-> @(dc/transact conn - [{:transaction/description-original "matching-desc" - :transaction/date #inst "2019-01-05T00:00:00.000-08:00" - :transaction/client {:client/name "1" - :db/id "client-1"} - :transaction/bank-account {:db/id "bank-account-1" - :bank-account/name "1"} - :transaction/amount 1.00 - :db/id "transaction-id"} + [{:transaction/description-original "matching-desc" + :transaction/date #inst "2019-01-05T00:00:00.000-08:00" + :transaction/client {:client/name "1" + :db/id "client-1"} + :transaction/bank-account {:db/id "bank-account-1" + :bank-account/name "1"} + :transaction/amount 1.00 + :db/id "transaction-id"} - {:db/id "transaction-rule-id" - :transaction-rule/note "transaction rule note" - :transaction-rule/description "matching-desc" - :transaction-rule/accounts [{:transaction-rule-account/location "A" - :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} - :transaction-rule-account/percentage 1.0}]} - {:db/id "uneven-transaction-rule-id" - :transaction-rule/note "transaction rule note" - :transaction-rule/description "matching-desc" - :transaction-rule/accounts [{:transaction-rule-account/location "A" - :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} - :transaction-rule-account/percentage 0.3333333} - {:transaction-rule-account/location "B" - :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} - :transaction-rule-account/percentage 0.33333333} - {:transaction-rule-account/location "c" - :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} - :transaction-rule-account/percentage 0.333333}]}]) - :tempids) + {:db/id "transaction-rule-id" + :transaction-rule/note "transaction rule note" + :transaction-rule/description "matching-desc" + :transaction-rule/accounts [{:transaction-rule-account/location "A" + :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} + :transaction-rule-account/percentage 1.0}]} + {:db/id "uneven-transaction-rule-id" + :transaction-rule/note "transaction rule note" + :transaction-rule/description "matching-desc" + :transaction-rule/accounts [{:transaction-rule-account/location "A" + :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} + :transaction-rule-account/percentage 0.3333333} + {:transaction-rule-account/location "B" + :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} + :transaction-rule-account/percentage 0.33333333} + {:transaction-rule-account/location "c" + :transaction-rule-account/account {:account/numeric-code 123 :db/id "123"} + :transaction-rule-account/percentage 0.333333}]}]) + :tempids) rule-test (-> (sut/query (admin-token) (v/graphql-query {:venia/operation {:operation/type :mutation :operation/name "MatchTransactionRules"} :venia/queries [{:query/data (sut/->graphql [:match-transaction-rules {:transaction-rule-id transaction-rule-id :transaction-ids [transaction-id]} - [[:matched-rule [:id :note]] [:accounts [:id]] ]])}]})) + [[:matched-rule [:id :note]] [:accounts [:id]]]])}]})) :data :match-transaction-rules)] @@ -293,7 +288,7 @@ :venia/queries [{:query/data (sut/->graphql [:match-transaction-rules {:transaction-rule-id transaction-rule-id :transaction-ids [transaction-id]} - [[:matched-rule [:id :note]] [:accounts [:id]] ]])}]})) + [[:matched-rule [:id :note]] [:accounts [:id]]]])}]})) :data :match-transaction-rules)] (is (= 1 (-> rule-test first :accounts count))))) @@ -304,7 +299,7 @@ :venia/queries [{:query/data (sut/->graphql [:match-transaction-rules {:transaction-rule-id uneven-transaction-rule-id :transaction-ids [transaction-id]} - [[:matched-rule [:id :note]] [:accounts [:id :amount]] ]])}]})) + [[:matched-rule [:id :note]] [:accounts [:id :amount]]]])}]})) :data :match-transaction-rules)] (is (= 3 (-> rule-test first :accounts count))) diff --git a/test/clj/auto_ap/integration/graphql/accounts.clj b/test/clj/auto_ap/integration/graphql/accounts.clj index 0fedb0dd..6e2b4594 100644 --- a/test/clj/auto_ap/integration/graphql/accounts.clj +++ b/test/clj/auto_ap/integration/graphql/accounts.clj @@ -10,206 +10,206 @@ #_(deftest test-account-search - (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))] - (testing "It should find matching account names" - @(dc/transact conn [{:account/name "Food Research" - :db/ident :client-specific-account - :account/numeric-code 51100 - :account/search-terms "Food Research" - :account/applicability :account-applicability/global - :account/default-allowance :allowance/allowed}]) - (sut/rebuild-search-index) - (clojure.pprint/pprint auto-ap.solr/impl) - (is (> (count (sut/search {:id (admin-token)} - {:query "Food Research"} - nil)) - 0))) - (testing "It should find exact matches by numbers" - (is (= (count (sut/search {:id (admin-token)} - {:query "51100"} - nil)) - 1))) - (testing "It should filter out accounts that are not allowed for clients" - @(dc/transact conn [{:account/name "CLIENT SPECIFIC" - :db/ident :client-specific-account - :account/numeric-code 99999 - :account/search-terms "CLIENTSPECIFIC" - :account/applicability :account-applicability/customized - :account/default-allowance :allowance/allowed}]) - (sut/rebuild-search-index) - (is (= [] (sut/search {:id (admin-token)} - {:query "CLIENTSPECIFIC"} - nil))) - - (testing "It should show up for the client specific version" - (let [client-id (-> @(dc/transact conn [{:client/name "CLIENT" - :db/id "client"} - {:db/ident :client-specific-account - :account/client-overrides [{:account-client-override/client "client" - :account-client-override/name "HI" - :account-client-override/search-terms "HELLOWORLD"}]}]) - :tempids - (get "client"))] - (sut/rebuild-search-index) - (is (= 1 (count (sut/search {:id (admin-token)} - {:query "HELLOWORLD" - :client_id client-id} - nil)))))) - - (testing "It should hide accounts that arent applicable" - @(dc/transact conn [{:account/name "DENIED" - :db/ident :denied-account - :account/numeric-code 99998 - :account/search-terms "DENIED" + (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))] + (testing "It should find matching account names" + @(dc/transact conn [{:account/name "Food Research" + :db/ident :client-specific-account + :account/numeric-code 51100 + :account/search-terms "Food Research" :account/applicability :account-applicability/global - :account/default-allowance :allowance/denied - :account/vendor-allowance :allowance/denied - :account/invoice-allowance :allowance/denied}]) - (is (= 0 (count (sut/search {:id (admin-token)} - {:query "DENIED"} - nil)))) - (is (= 0 (count (sut/search {:id (admin-token)} - {:query "DENIED" - :allowance :invoice} - nil)))) - (is (= 0 (count (sut/search {:id (admin-token)} - {:query "DENIED" - :allowance :vendor} - nil))))) - - (testing "It should warn when using a warn account" - @(dc/transact conn [{:account/name "WARNING" - :db/ident :warn-account - :account/numeric-code 99997 - :account/search-terms "WARNING" - :account/applicability :account-applicability/global - :account/default-allowance :allowance/warn - :account/vendor-allowance :allowance/warn - :account/invoice-allowance :allowance/warn}]) + :account/default-allowance :allowance/allowed}]) (sut/rebuild-search-index) - (is (some? (:warning (first (sut/search {:id (admin-token)} - {:query "WARNING" - :allowance :global} - nil))))) - (is (some? (:warning (first (sut/search {:id (admin-token)} - {:query "WARNING" - :allowance :invoice} - nil))))) - (is (some? (:warning (first (sut/search {:id (admin-token)} - {:query "WARNING" - :allowance :vendor} - nil)))))) - (testing "It should only include admin accounts for admins" - @(dc/transact conn [{:account/name "ADMINONLY" - :db/ident :warn-account - :account/numeric-code 99997 - :account/search-terms "ADMINONLY" - :account/applicability :account-applicability/global - :account/default-allowance :allowance/admin-only - :account/vendor-allowance :allowance/admin-only - :account/invoice-allowance :allowance/admin-only}]) + (clojure.pprint/pprint auto-ap.solr/impl) + (is (> (count (sut/search {:id (admin-token)} + {:query "Food Research"} + nil)) + 0))) + (testing "It should find exact matches by numbers" + (is (= (count (sut/search {:id (admin-token)} + {:query "51100"} + nil)) + 1))) + (testing "It should filter out accounts that are not allowed for clients" + @(dc/transact conn [{:account/name "CLIENT SPECIFIC" + :db/ident :client-specific-account + :account/numeric-code 99999 + :account/search-terms "CLIENTSPECIFIC" + :account/applicability :account-applicability/customized + :account/default-allowance :allowance/allowed}]) (sut/rebuild-search-index) - (is (= 1 (count (sut/search {:id (admin-token)} - {:query "ADMINONLY"} - nil)))) - (is (= 0 (count (sut/search {:id (user-token)} - {:query "ADMINONLY"} - nil))))) + (is (= [] (sut/search {:id (admin-token)} + {:query "CLIENTSPECIFIC"} + nil))) - (testing "It should allow searching for vendor accounts for invoices" - (let [vendor-id (-> @(dc/transact conn [{:account/name "VENDORONLY" - :db/id "vendor-only" - :db/ident :vendor-only - :account/numeric-code 99996 - :account/search-terms "VENDORONLY" - :account/applicability :account-applicability/global - :account/default-allowance :allowance/allowed - :account/vendor-allowance :allowance/allowed - :account/invoice-allowance :allowance/denied} - {:vendor/name "Allowed" - :vendor/default-account "vendor-only" - :db/id "vendor"}]) - :tempids - (get "vendor"))] - (sut/rebuild-search-index) + (testing "It should show up for the client specific version" + (let [client-id (-> @(dc/transact conn [{:client/name "CLIENT" + :db/id "client"} + {:db/ident :client-specific-account + :account/client-overrides [{:account-client-override/client "client" + :account-client-override/name "HI" + :account-client-override/search-terms "HELLOWORLD"}]}]) + :tempids + (get "client"))] + (sut/rebuild-search-index) + (is (= 1 (count (sut/search {:id (admin-token)} + {:query "HELLOWORLD" + :client_id client-id} + nil)))))) + + (testing "It should hide accounts that arent applicable" + @(dc/transact conn [{:account/name "DENIED" + :db/ident :denied-account + :account/numeric-code 99998 + :account/search-terms "DENIED" + :account/applicability :account-applicability/global + :account/default-allowance :allowance/denied + :account/vendor-allowance :allowance/denied + :account/invoice-allowance :allowance/denied}]) (is (= 0 (count (sut/search {:id (admin-token)} - {:query "VENDORONLY" + {:query "DENIED"} + nil)))) + (is (= 0 (count (sut/search {:id (admin-token)} + {:query "DENIED" :allowance :invoice} nil)))) + (is (= 0 (count (sut/search {:id (admin-token)} + {:query "DENIED" + :allowance :vendor} + nil))))) + (testing "It should warn when using a warn account" + @(dc/transact conn [{:account/name "WARNING" + :db/ident :warn-account + :account/numeric-code 99997 + :account/search-terms "WARNING" + :account/applicability :account-applicability/global + :account/default-allowance :allowance/warn + :account/vendor-allowance :allowance/warn + :account/invoice-allowance :allowance/warn}]) + (sut/rebuild-search-index) + (is (some? (:warning (first (sut/search {:id (admin-token)} + {:query "WARNING" + :allowance :global} + nil))))) + (is (some? (:warning (first (sut/search {:id (admin-token)} + {:query "WARNING" + :allowance :invoice} + nil))))) + (is (some? (:warning (first (sut/search {:id (admin-token)} + {:query "WARNING" + :allowance :vendor} + nil)))))) + (testing "It should only include admin accounts for admins" + @(dc/transact conn [{:account/name "ADMINONLY" + :db/ident :warn-account + :account/numeric-code 99997 + :account/search-terms "ADMINONLY" + :account/applicability :account-applicability/global + :account/default-allowance :allowance/admin-only + :account/vendor-allowance :allowance/admin-only + :account/invoice-allowance :allowance/admin-only}]) + (sut/rebuild-search-index) (is (= 1 (count (sut/search {:id (admin-token)} - {:query "VENDORONLY" - :allowance :invoice - :vendor_id vendor-id} - nil))))))) + {:query "ADMINONLY"} + nil)))) + (is (= 0 (count (sut/search {:id (user-token)} + {:query "ADMINONLY"} + nil))))) - (deftest get-graphql - (testing "should retrieve a single account" - @(dc/transact conn [{:account/numeric-code 1 - :account/default-allowance :allowance/allowed - :account/type :account-type/asset - :account/location "A" - :account/name "Test"}]) + (testing "It should allow searching for vendor accounts for invoices" + (let [vendor-id (-> @(dc/transact conn [{:account/name "VENDORONLY" + :db/id "vendor-only" + :db/ident :vendor-only + :account/numeric-code 99996 + :account/search-terms "VENDORONLY" + :account/applicability :account-applicability/global + :account/default-allowance :allowance/allowed + :account/vendor-allowance :allowance/allowed + :account/invoice-allowance :allowance/denied} + {:vendor/name "Allowed" + :vendor/default-account "vendor-only" + :db/id "vendor"}]) + :tempids + (get "vendor"))] + (sut/rebuild-search-index) + (is (= 0 (count (sut/search {:id (admin-token)} + {:query "VENDORONLY" + :allowance :invoice} + nil)))) - (is (= {:name "Test", - :invoice_allowance nil, - :numeric_code 1, - :vendor_allowance nil, - :location "A", - :applicability nil} - (dissoc (first (:accounts (sut/get-graphql {:id (admin-token)} {} nil))) - :id - :type - :default_allowance))))))) + (is (= 1 (count (sut/search {:id (admin-token)} + {:query "VENDORONLY" + :allowance :invoice + :vendor_id vendor-id} + nil))))))) -#_(deftest upsert-account - (testing "should create a new account" - (let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] - :numeric_code 123 - :location "A" - :applicability :global - :account-set "global" - :name "Test" - :invoice-allowance :allowed - :vendor-allowance :allowed - :type :asset}} nil)] - (is (= {:search_terms "Test", - :name "Test", - :invoice_allowance :allowed, - :numeric_code 123, - :code "123", - :account_set "global", - :vendor_allowance :allowed, - :location "A", - :applicability :global} - (dissoc result - :id - :type - :default_allowance))) - (testing "Should allow updating account" - (let [edit-result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] - :id (:id result) - :numeric_code 890 - :location "B" - :applicability :global - :account-set "global" - :name "Hello" - :invoice-allowance :denied - :vendor-allowance :denied - :type :expense}} nil)] - (is (= {:search_terms "Hello", - :name "Hello", - :invoice_allowance :denied, - :code "123", - :account_set "global", - :vendor_allowance :denied, - :location "B", - :applicability :global} - (dissoc edit-result + (deftest get-graphql + (testing "should retrieve a single account" + @(dc/transact conn [{:account/numeric-code 1 + :account/default-allowance :allowance/allowed + :account/type :account-type/asset + :account/location "A" + :account/name "Test"}]) + + (is (= {:name "Test", + :invoice_allowance nil, + :numeric_code 1, + :vendor_allowance nil, + :location "A", + :applicability nil} + (dissoc (first (:accounts (sut/get-graphql {:id (admin-token)} {} nil))) :id :type - :default_allowance - :numeric_code))) - (testing "Should not allow changing numeric code" + :default_allowance))))))) - (is (= 123 (:numeric_code edit-result))))))))) +#_(deftest upsert-account + (testing "should create a new account" + (let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] + :numeric_code 123 + :location "A" + :applicability :global + :account-set "global" + :name "Test" + :invoice-allowance :allowed + :vendor-allowance :allowed + :type :asset}} nil)] + (is (= {:search_terms "Test", + :name "Test", + :invoice_allowance :allowed, + :numeric_code 123, + :code "123", + :account_set "global", + :vendor_allowance :allowed, + :location "A", + :applicability :global} + (dissoc result + :id + :type + :default_allowance))) + (testing "Should allow updating account" + (let [edit-result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] + :id (:id result) + :numeric_code 890 + :location "B" + :applicability :global + :account-set "global" + :name "Hello" + :invoice-allowance :denied + :vendor-allowance :denied + :type :expense}} nil)] + (is (= {:search_terms "Hello", + :name "Hello", + :invoice_allowance :denied, + :code "123", + :account_set "global", + :vendor_allowance :denied, + :location "B", + :applicability :global} + (dissoc edit-result + :id + :type + :default_allowance + :numeric_code))) + (testing "Should not allow changing numeric code" + + (is (= 123 (:numeric_code edit-result))))))))) diff --git a/test/clj/auto_ap/integration/graphql/checks.clj b/test/clj/auto_ap/integration/graphql/checks.clj index ddc4d34c..deef551c 100644 --- a/test/clj/auto_ap/integration/graphql/checks.clj +++ b/test/clj/auto_ap/integration/graphql/checks.clj @@ -16,7 +16,7 @@ (use-fixtures :each wrap-setup) (defn sample-payment [& kwargs] - (apply assoc + (apply assoc {:db/id "check-id" :payment/check-number 1000 :payment/bank-account "bank-id" @@ -28,31 +28,30 @@ :payment/date #inst "2022-01-01"} kwargs)) - (deftest get-payment-page (testing "Should list payments" (let [{{:strs [bank-id check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client" - :db/id "client-id"} - {:db/id "check-id" - :payment/check-number 1000 - :payment/bank-account "bank-id" - :payment/client "client-id" - :payment/type :payment-type/check - :payment/amount 123.50 - :payment/paid-to "Someone" - :payment/status :payment-status/pending - :payment/date #inst "2022-01-01"}])] - (is (= [ {:amount 123.5, - :type :check, - :bank_account {:id bank-id, :code "bank"}, - :client {:id client-id, :code "client"}, - :status :pending, - :id check-id, - :paid_to "Someone", - :_payment [], - :check_number 1000}], + :db/id "bank-id"} + {:client/code "client" + :db/id "client-id"} + {:db/id "check-id" + :payment/check-number 1000 + :payment/bank-account "bank-id" + :payment/client "client-id" + :payment/type :payment-type/check + :payment/amount 123.50 + :payment/paid-to "Someone" + :payment/status :payment-status/pending + :payment/date #inst "2022-01-01"}])] + (is (= [{:amount 123.5, + :type :check, + :bank_account {:id bank-id, :code "bank"}, + :client {:id client-id, :code "client"}, + :status :pending, + :id check-id, + :paid_to "Someone", + :_payment [], + :check_number 1000}], (map #(dissoc % :date) (:payments (first (sut/get-payment-page {:clients [{:db/id client-id}]} {} nil)))))) (testing "Should omit clients that can't be seen" (is (not (seq (:payments (first (sut/get-payment-page {:clients nil} {} nil)))))) @@ -76,47 +75,43 @@ :payments seq))) (is (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {:filters {:date_range {:end #inst "2022-01-02"}}} nil) - first - :payments - seq)))) - - ) - - ) + first + :payments + seq)))))) (deftest void-payment (testing "Should void payments" (let [{{:strs [bank-id check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client" - :db/id "client-id"} - (sample-payment :db/id "check-id")])] + :db/id "bank-id"} + {:client/code "client" + :db/id "client-id"} + (sample-payment :db/id "check-id")])] (sut/void-payment {:id (admin-token)} {:payment_id check-id} nil) - (is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident ]}] check-id) + (is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] check-id) :payment/status :db/ident))))) (testing "Should not void payments if account is locked" (let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client" - :db/id "client-id" - :client/locked-until #inst "2030-01-01"} - (sample-payment :payment/client "client-id" - :db/id "check-id" - :payment/date #inst "2020-01-01")])] + :db/id "bank-id"} + {:client/code "client" + :db/id "client-id" + :client/locked-until #inst "2030-01-01"} + (sample-payment :payment/client "client-id" + :db/id "check-id" + :payment/date #inst "2020-01-01")])] (is (thrown? Exception (sut/void-payment {:id (admin-token)} {:payment_id check-id} nil)))))) (deftest void-payments (testing "bulk void" (testing "Should bulk void payments if account is not locked" (let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client-new" - :db/id "client-id"} - (sample-payment :payment/client "client-id" - :db/id "check-id" - :payment/date #inst "2020-01-01")])] + :db/id "bank-id"} + {:client/code "client-new" + :db/id "client-id"} + (sample-payment :payment/client "client-id" + :db/id "check-id" + :payment/date #inst "2020-01-01")])] (sut/void-payments {:id (admin-token)} {:filters {:date_range {:start #inst "2000-01-01"}}} nil) (is (= :payment-status/voided (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id) :payment/status @@ -124,12 +119,12 @@ (testing "Should only void a payment if it matches filter criteria" (let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client-new" - :db/id "client-id"} - (sample-payment :payment/client "client-id" - :db/id "check-id" - :payment/date #inst "2020-01-01")])] + :db/id "bank-id"} + {:client/code "client-new" + :db/id "client-id"} + (sample-payment :payment/client "client-id" + :db/id "check-id" + :payment/date #inst "2020-01-01")])] (sut/void-payments {:id (admin-token)} {:filters {:date_range {:start #inst "2022-01-01"}}} nil) (is (= :payment-status/pending (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id) :payment/status @@ -137,13 +132,13 @@ (testing "Should not bulk void payments if account is locked" (let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client" - :db/id "client-id" - :client/locked-until #inst "2030-01-01"} - (sample-payment :payment/client "client-id" - :db/id "check-id" - :payment/date #inst "2020-01-01")])] + :db/id "bank-id"} + {:client/code "client" + :db/id "client-id" + :client/locked-until #inst "2030-01-01"} + (sample-payment :payment/client "client-id" + :db/id "check-id" + :payment/date #inst "2020-01-01")])] (sut/void-payments {:id (admin-token)} {:filters {:date_range {:start #inst "2000-01-01"}}} nil) (is (= :payment-status/pending (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id) :payment/status @@ -151,38 +146,37 @@ (testing "Only admins should be able to bulk void" (let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank" - :db/id "bank-id"} - {:client/code "client" - :db/id "client-id"} - (sample-payment :payment/client "client-id" - :db/id "check-id" - :payment/date #inst "2020-01-01")])] + :db/id "bank-id"} + {:client/code "client" + :db/id "client-id"} + (sample-payment :payment/client "client-id" + :db/id "check-id" + :payment/date #inst "2020-01-01")])] (is (thrown? Exception (sut/void-payments {:id (user-token)} {:filters {:date_range {:start #inst "2000-01-01"}}} nil))))))) - (deftest print-checks (testing "Print checks" (testing "Should allow 'printing' cash checks" (let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client" - :db/id "client-id" - :client/locked-until #inst "2030-01-01" - :client/bank-accounts [{:bank-account/code "bank" - :db/id "bank-id"}]} - {:db/id "vendor-id" - :vendor/name "V" - :vendor/default-account "account-id"} - {:db/id "account-id" - :account/name "My account" - :account/numeric-code 21000} - {:db/id "invoice-id" - :invoice/client "client-id" - :invoice/date #inst "2022-01-01" - :invoice/vendor "vendor-id" - :invoice/total 30.0 - :invoice/outstanding-balance 30.0 - :invoice/expense-accounts [{:db/id "invoice-expense-account" - :invoice-expense-account/account "account-id" - :invoice-expense-account/amount 30.0}]}])] + :db/id "client-id" + :client/locked-until #inst "2030-01-01" + :client/bank-accounts [{:bank-account/code "bank" + :db/id "bank-id"}]} + {:db/id "vendor-id" + :vendor/name "V" + :vendor/default-account "account-id"} + {:db/id "account-id" + :account/name "My account" + :account/numeric-code 21000} + {:db/id "invoice-id" + :invoice/client "client-id" + :invoice/date #inst "2022-01-01" + :invoice/vendor "vendor-id" + :invoice/total 30.0 + :invoice/outstanding-balance 30.0 + :invoice/expense-accounts [{:db/id "invoice-expense-account" + :invoice-expense-account/account "account-id" + :invoice-expense-account/amount 30.0}]}])] (let [paid-invoice (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id :amount 30.0}] :client_id client-id @@ -200,36 +194,36 @@ :amount)))) (testing "Should create a transaction for cash payments" (is (seq (d/q '[:find (pull ?t [* {:transaction/payment [*]}]) - :in $ ?p - :where [?t :transaction/payment] - [?t :transaction/amount -30.0]] - (d/db conn) - (-> paid-invoice - :payments - first - :payment - :id)))))))) + :in $ ?p + :where [?t :transaction/payment] + [?t :transaction/amount -30.0]] + (d/db conn) + (-> paid-invoice + :payments + first + :payment + :id)))))))) (testing "Should allow 'printing' debit checks" (let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client" - :db/id "client-id" - :client/bank-accounts [{:bank-account/code "bank" - :db/id "bank-id"}]} - {:db/id "vendor-id" - :vendor/name "V" - :vendor/default-account "account-id"} - {:db/id "account-id" - :account/name "My account" - :account/numeric-code 21000} - {:db/id "invoice-id" - :invoice/client "client-id" - :invoice/date #inst "2022-01-01" - :invoice/vendor "vendor-id" - :invoice/total 50.0 - :invoice/outstanding-balance 50.0 - :invoice/expense-accounts [{:db/id "invoice-expense-account" - :invoice-expense-account/account "account-id" - :invoice-expense-account/amount 50.0}]}])] + :db/id "client-id" + :client/bank-accounts [{:bank-account/code "bank" + :db/id "bank-id"}]} + {:db/id "vendor-id" + :vendor/name "V" + :vendor/default-account "account-id"} + {:db/id "account-id" + :account/name "My account" + :account/numeric-code 21000} + {:db/id "invoice-id" + :invoice/client "client-id" + :invoice/date #inst "2022-01-01" + :invoice/vendor "vendor-id" + :invoice/total 50.0 + :invoice/outstanding-balance 50.0 + :invoice/expense-accounts [{:db/id "invoice-expense-account" + :invoice-expense-account/account "account-id" + :invoice-expense-account/amount 50.0}]}])] (let [paid-invoice (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id :amount 50.0}] :client_id client-id @@ -259,43 +253,43 @@ (testing "Should allow printing checks" (let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client" - :db/id "client-id" - :client/bank-accounts [{:bank-account/code "bank" - :bank-account/type :bank-account-type/check + :db/id "client-id" + :client/bank-accounts [{:bank-account/code "bank" + :bank-account/type :bank-account-type/check - :bank-account/check-number 10000 - :db/id "bank-id"}]} - {:db/id "vendor-id" - :vendor/name "V" - :vendor/default-account "account-id"} - {:db/id "account-id" - :account/name "My account" - :account/numeric-code 21000} - {:db/id "invoice-id" - :invoice/client "client-id" - :invoice/date #inst "2022-01-01" - :invoice/vendor "vendor-id" - :invoice/total 150.0 - :invoice/outstanding-balance 150.0 - :invoice/expense-accounts [{:db/id "invoice-expense-account" - :invoice-expense-account/account "account-id" - :invoice-expense-account/amount 150.0}]}])] + :bank-account/check-number 10000 + :db/id "bank-id"}]} + {:db/id "vendor-id" + :vendor/name "V" + :vendor/default-account "account-id"} + {:db/id "account-id" + :account/name "My account" + :account/numeric-code 21000} + {:db/id "invoice-id" + :invoice/client "client-id" + :invoice/date #inst "2022-01-01" + :invoice/vendor "vendor-id" + :invoice/total 150.0 + :invoice/outstanding-balance 150.0 + :invoice/expense-accounts [{:db/id "invoice-expense-account" + :invoice-expense-account/account "account-id" + :invoice-expense-account/amount 150.0}]}])] (let [result (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id :amount 150.0}] :client_id client-id :bank_account_id bank-id :type :check} nil) - :invoices - first) + :invoices + first) paid-invoice result] (testing "Paying full balance should complete invoice" (is (= :paid (:status paid-invoice))) (is (= 0.0 (:outstanding_balance paid-invoice)))) (testing "Payment should exist" (is (= 150.0 (-> paid-invoice - :payments - first - :amount)))) + :payments + first + :amount)))) (testing "Should create pdf" (is (-> paid-invoice :payments @@ -303,20 +297,19 @@ :payment :s3_url)))))))) - (deftest get-potential-payments (testing "should match payments for a transaction" (let [{:strs [transaction-id payment-id test-client-id]} (setup-test-data [(test-payment - :db/id "payment-id" - :payment/status :payment-status/pending - :payment/amount 100.0 - :payment/date #inst "2021-05-25") - (test-transaction - :db/id "transaction-id" - :transaction/amount -100.0 - :transaction/date #inst "2021-06-01")])] + :db/id "payment-id" + :payment/status :payment-status/pending + :payment/amount 100.0 + :payment/date #inst "2021-05-25") + (test-transaction + :db/id "transaction-id" + :transaction/amount -100.0 + :transaction/date #inst "2021-06-01")])] (is (= [payment-id] (->> (sut/get-potential-payments {:id (admin-token) :clients [{:db/id test-client-id}]} {:transaction_id transaction-id} nil) @@ -325,26 +318,26 @@ (let [{:strs [transaction-id older-payment-id newer-payment-id]} (setup-test-data [(test-payment - :db/id "newer-payment-id" - :payment/status :payment-status/pending - :payment/amount 100.0 - :payment/date #inst "2021-05-25") - (test-payment - :db/id "older-payment-id" - :payment/status :payment-status/pending - :payment/amount 100.0 - :payment/date #inst "2021-05-20") - (test-payment - :db/id "payment-too-old-id" - :payment/status :payment-status/pending - :payment/amount 100.0 - :payment/date #inst "2021-01-01") - (test-transaction - :db/id "transaction-id" - :transaction/amount -100.0 - :transaction/date #inst "2021-06-01")])] + :db/id "newer-payment-id" + :payment/status :payment-status/pending + :payment/amount 100.0 + :payment/date #inst "2021-05-25") + (test-payment + :db/id "older-payment-id" + :payment/status :payment-status/pending + :payment/amount 100.0 + :payment/date #inst "2021-05-20") + (test-payment + :db/id "payment-too-old-id" + :payment/status :payment-status/pending + :payment/amount 100.0 + :payment/date #inst "2021-01-01") + (test-transaction + :db/id "transaction-id" + :transaction/amount -100.0 + :transaction/date #inst "2021-06-01")])] (is (= [newer-payment-id older-payment-id] (->> (sut/get-potential-payments {:id (admin-token)} - {:transaction_id transaction-id} - nil) - (map :id))))))) + {:transaction_id transaction-id} + nil) + (map :id))))))) diff --git a/test/clj/auto_ap/integration/graphql/invoices.clj b/test/clj/auto_ap/integration/graphql/invoices.clj index 85ded831..8ec1fa4f 100644 --- a/test/clj/auto_ap/integration/graphql/invoices.clj +++ b/test/clj/auto_ap/integration/graphql/invoices.clj @@ -77,7 +77,7 @@ :expense_accounts [{:amount 100.0 :location "DT" :account_id new-account-id}]}} - nil))) + nil))) (is (= #:invoice{:invoice-number "890213" :date #inst "2023-01-01T00:00:00.000-00:00" :total 100.0 @@ -118,11 +118,11 @@ (setup-test-data [(test-invoice :db/id "invoice-id") (test-account :db/id "new-account-id")])] (is (some? (sut/edit-expense-accounts {:id (admin-token)} - {:invoice_id invoice-id - :expense_accounts [{:amount 100.0 - :account_id new-account-id - :location "DT"}]} - nil))) + {:invoice_id invoice-id + :expense_accounts [{:amount 100.0 + :account_id new-account-id + :location "DT"}]} + nil))) (is (= [#:invoice-expense-account{:amount 100.0 :location "DT" :account {:db/id new-account-id}}] @@ -145,7 +145,7 @@ :accounts [{:percentage 1.0 :account_id new-account-id :location "Shared"}]} - nil))) + nil))) (is (= [#:invoice-expense-account{:amount 100.0 :location "DT" :account {:db/id new-account-id}}] @@ -163,35 +163,7 @@ (test-account :db/id "new-account-id")])] (is (some? (sut/void-invoices {:id (admin-token) :clients [{:db/id test-client-id}]} - {:filters {:client_id test-client-id}} - nil))) - (is (= :invoice-status/voided - (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] - invoice-id) - :invoice/status - :db/ident))) - - (testing "Should unvoid invoice" - (is (some? (sut/unvoid-invoice {:id (admin-token)} - {:invoice_id invoice-id} - nil))) - (is (= :invoice-status/unpaid - (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] - invoice-id) - :invoice/status - :db/ident))))))) - - -(deftest void-invoice - (testing "It should voide invoices in bulk" - (let [{:strs [invoice-id]} - (setup-test-data [(test-invoice :db/id "invoice-id" - :invoice/status :invoice-status/unpaid) - (test-account :db/id "new-account-id")])] - - - (is (some? (sut/void-invoice {:id (admin-token)} - {:invoice_id invoice-id} + {:filters {:client_id test-client-id}} nil))) (is (= :invoice-status/voided (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] @@ -201,8 +173,34 @@ (testing "Should unvoid invoice" (is (some? (sut/unvoid-invoice {:id (admin-token)} - {:invoice_id invoice-id} - nil))) + {:invoice_id invoice-id} + nil))) + (is (= :invoice-status/unpaid + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))))))) + +(deftest void-invoice + (testing "It should voide invoices in bulk" + (let [{:strs [invoice-id]} + (setup-test-data [(test-invoice :db/id "invoice-id" + :invoice/status :invoice-status/unpaid) + (test-account :db/id "new-account-id")])] + + (is (some? (sut/void-invoice {:id (admin-token)} + {:invoice_id invoice-id} + nil))) + (is (= :invoice-status/voided + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))) + + (testing "Should unvoid invoice" + (is (some? (sut/unvoid-invoice {:id (admin-token)} + {:invoice_id invoice-id} + nil))) (is (= :invoice-status/unpaid (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] invoice-id) diff --git a/test/clj/auto_ap/integration/graphql/ledger/running_balance.clj b/test/clj/auto_ap/integration/graphql/ledger/running_balance.clj index 950b13cc..03664c8b 100644 --- a/test/clj/auto_ap/integration/graphql/ledger/running_balance.clj +++ b/test/clj/auto_ap/integration/graphql/ledger/running_balance.clj @@ -23,11 +23,11 @@ line-2-2 line-3-1 line-3-2]} (:tempids @(d/transact conn [{:db/id "test-account-1" - :account/type :account-type/asset} - {:db/id "test-account-2" - :account/type :account-type/equity} - {:db/id "test-client" - :client/code "TEST"} + :account/type :account-type/asset} + {:db/id "test-account-2" + :account/type :account-type/equity} + {:db/id "test-client" + :client/code "TEST"} [:upsert-ledger {:db/id "journal-entry-1" :journal-entry/external-id "1" :journal-entry/date #inst "2022-01-01" @@ -66,19 +66,18 @@ :journal-entry-line/credit 150.0}]}]]))] (testing "should set running-balance on ledger entries missing them" - + (sut/upsert-running-balance) (println (d/pull (d/db conn) '[*] line-1-1)) (is (= [-10.0 -60.0 -210.0] - (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-1 line-2-1 line-3-1 - ]))) + (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-1 line-2-1 line-3-1]))) (is (= [10.0 60.0 210.0] (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-2 line-2-2 line-3-2])))) (testing "should recompute if the data is out of date" - (d/transact conn + (d/transact conn [{:db/id line-1-1 :journal-entry-line/dirty true :journal-entry-line/running-balance 123810.23}]) @@ -89,7 +88,7 @@ (testing "should recompute every entry after the out of date one" - (d/transact conn + (d/transact conn [{:db/id line-1-1 :journal-entry-line/dirty true :journal-entry-line/debit 70.0}]) @@ -98,40 +97,39 @@ (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-1 line-2-1 line-3-1])))) (testing "should not recompute entries that aren't dirty" - (d/transact conn + (d/transact conn [{:db/id line-1-1 :journal-entry-line/dirty false :journal-entry-line/debit 90.0}]) (sut/upsert-running-balance) (is (= [-70.0 -120.0 -270.0] - (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-1 line-2-1 line-3-1]))) + (map #(pull-attr (d/db conn) :journal-entry-line/running-balance %) [line-1-1 line-2-1 line-3-1])))) - ) (testing "changing a ledger entry should mark the line items as dirty" (println "AFTER HERE") - @(d/transact conn - [[:upsert-ledger {:db/id journal-entry-2 - :journal-entry/date #inst "2022-01-02" - :journal-entry/client test-client - :journal-entry/external-id "2" - :journal-entry/line-items [{:db/id "line-2-1" - :journal-entry-line/account test-account-1 - :journal-entry-line/location "A" - :journal-entry-line/debit 50.0} - {:db/id "line-2-2" - :journal-entry-line/account test-account-2 - :journal-entry-line/location "A" - :journal-entry-line/credit 50.0}]}]]) + @(d/transact conn + [[:upsert-ledger {:db/id journal-entry-2 + :journal-entry/date #inst "2022-01-02" + :journal-entry/client test-client + :journal-entry/external-id "2" + :journal-entry/line-items [{:db/id "line-2-1" + :journal-entry-line/account test-account-1 + :journal-entry-line/location "A" + :journal-entry-line/debit 50.0} + {:db/id "line-2-2" + :journal-entry-line/account test-account-2 + :journal-entry-line/location "A" + :journal-entry-line/credit 50.0}]}]]) + (is (= [true true] + (->> (d/pull (d/db conn) '[{:journal-entry/line-items [:journal-entry-line/dirty]}] journal-entry-2) + (:journal-entry/line-items) + (map :journal-entry-line/dirty)))) + (testing "should also mark the next entry as dirty, so that if a ledger entry is changed, the old accounts get updated" + (is (= [false false] + (->> (d/pull (d/db conn) '[{:journal-entry/line-items [:journal-entry-line/dirty]}] journal-entry-1) + (:journal-entry/line-items) + (map :journal-entry-line/dirty)))) (is (= [true true] (->> (d/pull (d/db conn) '[{:journal-entry/line-items [:journal-entry-line/dirty]}] journal-entry-2) (:journal-entry/line-items) - (map :journal-entry-line/dirty)))) - (testing "should also mark the next entry as dirty, so that if a ledger entry is changed, the old accounts get updated" - (is (= [false false] - (->> (d/pull (d/db conn) '[{:journal-entry/line-items [:journal-entry-line/dirty]}] journal-entry-1) - (:journal-entry/line-items) - (map :journal-entry-line/dirty)))) - (is (= [true true] - (->> (d/pull (d/db conn) '[{:journal-entry/line-items [:journal-entry-line/dirty]}] journal-entry-2) - (:journal-entry/line-items) - (map :journal-entry-line/dirty)))))))) + (map :journal-entry-line/dirty)))))))) diff --git a/test/clj/auto_ap/integration/graphql/transaction_rules.clj b/test/clj/auto_ap/integration/graphql/transaction_rules.clj index 02a66eaf..5685823c 100644 --- a/test/clj/auto_ap/integration/graphql/transaction_rules.clj +++ b/test/clj/auto_ap/integration/graphql/transaction_rules.clj @@ -11,11 +11,11 @@ (testing "Should find a single rule that matches a transaction" (let [{:strs [transaction-id transaction-rule-id]} (setup-test-data [(test-transaction - :db/id "transaction-id" - :transaction/description-original "Disneyland") + :db/id "transaction-id" + :transaction/description-original "Disneyland") (test-transaction-rule - :db/id "transaction-rule-id" - :transaction-rule/description ".*")])] + :db/id "transaction-rule-id" + :transaction-rule/description ".*")])] (is (= [transaction-rule-id] (->> (sut2/get-transaction-rule-matches {:id (admin-token)} {:transaction_id transaction-id} nil) diff --git a/test/clj/auto_ap/integration/graphql/transactions.clj b/test/clj/auto_ap/integration/graphql/transactions.clj index 64325bbd..9782e3ec 100644 --- a/test/clj/auto_ap/integration/graphql/transactions.clj +++ b/test/clj/auto_ap/integration/graphql/transactions.clj @@ -22,8 +22,8 @@ (testing "Should list transactions" (let [{:strs [transaction-id test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/client "test-client-id" - :transaction/bank-account "test-bank-account-id")])] + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id")])] (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {} nil)))) (is (= transaction-id (:id (first (:data (sut/get-transaction-page {:id (admin-token)} {} nil)))))) (testing "Should only show transactions you have access to" @@ -39,18 +39,17 @@ (testing "Should only show potential duplicates if filtered enough" (is (thrown? Exception (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:potential_duplicates true}} nil)))))))) - (deftest bulk-change-status (testing "Should change status of multiple transactions" (let [{:strs [transaction-id test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/client "test-client-id" - :transaction/approval-status :transaction-approval-status/approved - :transaction/bank-account "test-bank-account-id")])] + :transaction/client "test-client-id" + :transaction/approval-status :transaction-approval-status/approved + :transaction/bank-account "test-bank-account-id")])] (is (= "Succesfully changed 1 transactions to be unapproved." (:message (sut/bulk-change-status {:id (admin-token) :clients [{:db/id test-client-id}]} {:filters {} - :status :unapproved} nil)))) + :status :unapproved} nil)))) (is (= :transaction-approval-status/unapproved (:db/ident (:transaction/approval-status (dc/pull (dc/db conn) '[{:transaction/approval-status [:db/ident]}] transaction-id))))) @@ -65,9 +64,9 @@ test-client-id test-account-id test-vendor-id]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/client "test-client-id" - :transaction/bank-account "test-bank-account-id" - :transaction/amount 40.0)])] + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount 40.0)])] (is (= "Successfully coded 1 transactions." (:message (sut/bulk-code-transactions {:id (admin-token) :clients [{:db/id test-client-id}]} @@ -94,18 +93,17 @@ (let [{:strs [transaction-id-1 transaction-id-2 test-client-id-2 - test-client-id]} (setup-test-data [ - (test-transaction :db/id "transaction-id-1" - :transaction/client "test-client-id" - :transaction/bank-account "test-bank-account-id" - :transaction/amount 40.0) - (test-transaction :db/id "transaction-id-2" - :transaction/client "test-client-id-2" - :transaction/bank-account "test-bank-account-id-2" - :transaction/amount 40.0) - (test-client :db/id "test-client-id-2" - :client/locations ["GR"]) - (test-bank-account :db/id "test-bank-account-id-2")])] + test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id-1" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount 40.0) + (test-transaction :db/id "transaction-id-2" + :transaction/client "test-client-id-2" + :transaction/bank-account "test-bank-account-id-2" + :transaction/amount 40.0) + (test-client :db/id "test-client-id-2" + :client/locations ["GR"]) + (test-bank-account :db/id "test-bank-account-id-2")])] (is (= "Successfully coded 2 transactions." (:message (sut/bulk-code-transactions {:id (admin-token) :clients [{:db/id test-client-id} @@ -116,7 +114,7 @@ :accounts [{:account_id test-account-id :location "Shared" :percentage 1.0}]} nil)))) - + (is (= #:transaction{:vendor {:db/id test-vendor-id} :approval-status {:db/ident :transaction-approval-status/unapproved} :accounts [#:transaction-account{:account {:db/id test-account-id} @@ -143,8 +141,7 @@ (testing "should reject a location that doesnt exist" (let [{:strs [test-client-id-1 - test-client-id-2]} (setup-test-data [ - (test-transaction :db/id "transaction-id-1" + test-client-id-2]} (setup-test-data [(test-transaction :db/id "transaction-id-1" :transaction/client "test-client-id-1" :transaction/bank-account "test-bank-account-id" :transaction/amount 40.0) @@ -158,14 +155,14 @@ :client/locations ["GR" "BOTH"]) (test-bank-account :db/id "test-bank-account-id-2")])] (is (thrown? Exception (sut/bulk-code-transactions {:id (admin-token) - :clients [{:db/id test-client-id} - {:db/id test-client-id-2}]} - {:filters {} - :vendor test-vendor-id - :approval_status :unapproved - :accounts [{:account_id test-account-id - :location "OG" - :percentage 1.0}]} nil))) + :clients [{:db/id test-client-id} + {:db/id test-client-id-2}]} + {:filters {} + :vendor test-vendor-id + :approval_status :unapproved + :accounts [{:account_id test-account-id + :location "OG" + :percentage 1.0}]} nil))) (is (thrown? Exception (sut/bulk-code-transactions {:id (admin-token) :clients [{:db/id test-client-id} {:db/id test-client-id-2}]} @@ -223,7 +220,6 @@ :location "DT" :amount 20.0}]}} nil))))))) - (deftest match-transaction (testing "Should link a transaction to a payment, mark it as accounts payable" (let [{:strs [transaction-id @@ -275,36 +271,33 @@ :payment/bank-account "mismatched-bank-account-id" :payment/amount 50.0)])] (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-amount-payment-id} nil))) - (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-bank-account-payment-id} nil))) - ))) + (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-bank-account-payment-id} nil)))))) (deftest match-transaction-autopay-invoices (testing "Should link transaction to a set of autopaid invoices" (let [{:strs [transaction-id test-vendor-id invoice-1 - invoice-2 - ]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/amount -50.0) - (test-invoice :db/id "invoice-1" - :invoice/total 30.0) - (test-invoice :db/id "invoice-2" - :invoice/total 20.0)])] + invoice-2]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0) + (test-invoice :db/id "invoice-2" + :invoice/total 20.0)])] (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil) (let [result (dc/pull (dc/db conn) '[:transaction/vendor {:transaction/payment [:db/id {:payment/status [:db/ident]}]} {:transaction/approval-status [:db/ident] :transaction/accounts [:transaction-account/account :transaction-account/location - :transaction-account/amount]} - ] + :transaction-account/amount]}] transaction-id)] (testing "should have created a payment" (is (some? (:transaction/payment result))) (is (= :payment-status/cleared (-> result - :transaction/payment - :payment/status - :db/ident))) + :transaction/payment + :payment/status + :db/ident))) (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident)))) (testing "Should have completed the invoice" (is (= :invoice-status/paid (->> invoice-1 @@ -320,11 +313,10 @@ (let [{:strs [transaction-id test-vendor-id invoice-1 - invoice-2 - ]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/amount -50.0) - (test-invoice :db/id "invoice-1" - :invoice/total 30.0)])] + invoice-2]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0)])] (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil)))))) (deftest match-transaction-unpaid-invoices @@ -332,30 +324,28 @@ (let [{:strs [transaction-id test-vendor-id invoice-1 - invoice-2 - ]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/amount -50.0) - (test-invoice :db/id "invoice-1" - :invoice/outstanding-balance 30.0 ;; TODO this part is a little different - :invoice/total 30.0) - (test-invoice :db/id "invoice-2" - :invoice/outstanding-balance 20.0 ;; TODO this part is a little different - :invoice/total 20.0)])] + invoice-2]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/outstanding-balance 30.0 ;; TODO this part is a little different + :invoice/total 30.0) + (test-invoice :db/id "invoice-2" + :invoice/outstanding-balance 20.0 ;; TODO this part is a little different + :invoice/total 20.0)])] (sut/match-transaction-unpaid-invoices {:id (admin-token)} {:transaction_id transaction-id :unpaid_invoice_ids [invoice-1 invoice-2]} nil) (let [result (dc/pull (dc/db conn) '[:transaction/vendor {:transaction/payment [:db/id {:payment/status [:db/ident]}]} {:transaction/approval-status [:db/ident] :transaction/accounts [:transaction-account/account :transaction-account/location - :transaction-account/amount]} - ] + :transaction-account/amount]}] transaction-id)] (testing "should have created a payment" (is (some? (:transaction/payment result))) (is (= :payment-status/cleared (-> result - :transaction/payment - :payment/status - :db/ident))) + :transaction/payment + :payment/status + :db/ident))) (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident)))) (testing "Should have completed the invoice" (is (= :invoice-status/paid (->> invoice-1 @@ -371,29 +361,25 @@ (let [{:strs [transaction-id test-vendor-id invoice-1 - invoice-2 - ]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/amount -50.0) - (test-invoice :db/id "invoice-1" - :invoice/total 30.0)])] + invoice-2]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0)])] (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil)))))) - (deftest match-transaction-rules (testing "Should match transactions without linked payments" (let [{:strs [transaction-id - transaction-rule-id - ]} (setup-test-data [(test-transaction :db/id "transaction-id" - :transaction/amount -50.0) - (test-transaction-rule :db/id "transaction-rule-id" - :transaction-rule/client "test-client-id" - :transaction-rule/transaction-approval-status :transaction-approval-status/excluded - :transaction-rule/description ".*" - )])] + transaction-rule-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-transaction-rule :db/id "transaction-rule-id" + :transaction-rule/client "test-client-id" + :transaction-rule/transaction-approval-status :transaction-approval-status/excluded + :transaction-rule/description ".*")])] (is (= transaction-rule-id (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) - first - :matched_rule - :id))) + first + :matched_rule + :id))) (testing "Should apply statuses" (is (= :excluded @@ -401,8 +387,7 @@ {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) first - :approval_status - )))))) + :approval_status)))))) (testing "Should not apply to transactions if they don't match" (let [{:strs [transaction-id @@ -410,12 +395,11 @@ (setup-test-data [(test-transaction :db/id "transaction-id" :transaction/amount -50.0) (test-transaction-rule :db/id "transaction-rule-id" - :transaction-rule/description "NOMATCH" - )])] + :transaction-rule/description "NOMATCH")])] (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) - first - :matched_rule - :id))))) + first + :matched_rule + :id))))) (testing "Should not apply to transactions if they are already matched" (let [{:strs [transaction-id transaction-rule-id]} @@ -424,8 +408,7 @@ :transaction/payment {:db/id "extant-payment-id"} :transaction/amount -50.0) (test-transaction-rule :db/id "transaction-rule-id" - :transaction-rule/description ".*" - )])] + :transaction-rule/description ".*")])] (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) first :matched_rule @@ -438,8 +421,7 @@ :transaction/description-original "MATCH" :transaction/amount -50.0) (test-transaction-rule :db/id "transaction-rule-id" - :transaction-rule/description ".*" - )])] + :transaction-rule/description ".*")])] (sut/match-transaction-rules {:id (admin-token)} {:all true :transaction_rule_id transaction-rule-id} nil) (= {:transaction/matched-rule {:db/id transaction-rule-id}} diff --git a/test/clj/auto_ap/integration/graphql/users.clj b/test/clj/auto_ap/integration/graphql/users.clj index b50df7bf..dc9aeea8 100644 --- a/test/clj/auto_ap/integration/graphql/users.clj +++ b/test/clj/auto_ap/integration/graphql/users.clj @@ -9,27 +9,25 @@ (use-fixtures :each wrap-setup) - #_(deftest edit-user - (testing "should allow editing a user" - + (testing "should allow editing a user" - (let [{{:strs [user-id] } :tempids} @(d/transact conn [{:db/id "user-id" :user/name "Bryce"}]) - result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user :id user-id}} nil)] - (is (some? (:id result)) - (= :power_user (:role result))) - (testing "Should allow adding clients" - (let [{{:strs [client-id] } :tempids} @(d/transact conn [{:db/id "client-id" :client/name "Bryce"}]) - result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user - :id user-id - :clients [(str client-id)]}} nil)] - (is (= client-id (get-in result [:clients 0 :id]))))) - (testing "Should allow adding clients" - (let [result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user - :id user-id - :clients []}} nil)] - (is (not (seq (:clients result)))))) - (testing "Should disallow normies" - (is (thrown? Exception (sut/edit-user {:id (user-token)} {:edit_user {:role :power_user - :id user-id - :clients []}} nil))))))) + (let [{{:strs [user-id]} :tempids} @(d/transact conn [{:db/id "user-id" :user/name "Bryce"}]) + result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user :id user-id}} nil)] + (is (some? (:id result)) + (= :power_user (:role result))) + (testing "Should allow adding clients" + (let [{{:strs [client-id]} :tempids} @(d/transact conn [{:db/id "client-id" :client/name "Bryce"}]) + result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user + :id user-id + :clients [(str client-id)]}} nil)] + (is (= client-id (get-in result [:clients 0 :id]))))) + (testing "Should allow adding clients" + (let [result (sut/edit-user {:id (admin-token)} {:edit_user {:role :power_user + :id user-id + :clients []}} nil)] + (is (not (seq (:clients result)))))) + (testing "Should disallow normies" + (is (thrown? Exception (sut/edit-user {:id (user-token)} {:edit_user {:role :power_user + :id user-id + :clients []}} nil))))))) diff --git a/test/clj/auto_ap/integration/graphql/vendors.clj b/test/clj/auto_ap/integration/graphql/vendors.clj index 1bf1d36e..f89e375b 100644 --- a/test/clj/auto_ap/integration/graphql/vendors.clj +++ b/test/clj/auto_ap/integration/graphql/vendors.clj @@ -3,16 +3,14 @@ [auto-ap.integration.util :refer [wrap-setup admin-token setup-test-data test-vendor test-account dissoc-id]] [clojure.test :as t :refer [deftest is testing use-fixtures]])) - (use-fixtures :each wrap-setup) - (deftest vendors (testing "vendors" (let [{:strs [test-vendor-id]} (setup-test-data [])] (testing "it should find vendors" (let [result (sut2/get-graphql {:id (admin-token)} {} {})] - (is ((into #{} (map :id (:vendors result))) test-vendor-id ))))))) + (is ((into #{} (map :id (:vendors result))) test-vendor-id))))))) (deftest upsert-vendor (testing "Should allow upsert of an extant vendor" @@ -38,9 +36,9 @@ :schedule_payment_dom [{:client_id test-client-id :dom 12}] :terms_overrides [{:client_id test-client-id - :terms 100}] + :terms 100}] :account_overrides [{:client_id test-client-id - :account_id test-account-id-2}] + :account_id test-account-id-2}] :automatically_paid_when_due [test-client-id]}} nil)] (is (= {:address {:street1 "1900 Penn ave", @@ -52,7 +50,7 @@ :search_terms ["New Vendor Name!"], :terms 30, :name "New Vendor Name!", - :secondary_contact { :name "Ben"}, + :secondary_contact {:name "Ben"}, :usage nil, :hidden true, :id test-vendor-id, @@ -72,7 +70,6 @@ (update :schedule_payment_dom #(map dissoc-id %)) (update :terms_overrides #(map dissoc-id %)) (update :account_overrides #(map dissoc-id %))))) - (is (= 1 (count (:automatically_paid_when_due result)))) - )))) + (is (= 1 (count (:automatically_paid_when_due result)))))))) diff --git a/test/clj/auto_ap/integration/jobs/ntg.clj b/test/clj/auto_ap/integration/jobs/ntg.clj index a8a2929f..e950a5ba 100644 --- a/test/clj/auto_ap/integration/jobs/ntg.clj +++ b/test/clj/auto_ap/integration/jobs/ntg.clj @@ -4,7 +4,6 @@ [clojure.test :as t :refer [deftest is testing use-fixtures]] [clojure.java.io :as io])) - (use-fixtures :each wrap-setup) (deftest extract-invoice-details-cintas @@ -14,23 +13,22 @@ :client/locations ["OP"] :client/matches ["2034 BROADWAY ST"]}] (is (= - [{:invoice/invoice-number "1500000592" - :invoice/date #inst "2023-03-09T08:00:00-00:00" - :invoice/due #inst "2023-04-08T07:00:00-00:00" - :invoice/import-status :import-status/imported - :invoice/client-identifier "2034 BROADWAY ST" - :invoice/location "OP" - :invoice/status :invoice-status/unpaid - :invoice/vendor :vendor/cintas - :invoice/scheduled-payment #inst "2023-04-08T07:00:00-00:00" - :invoice/client 1 - :invoice/total 39.88 - :invoice/outstanding-balance 39.88 - }] - (map #(dissoc % :invoice/expense-accounts :db/id) - (sut/extract-invoice-details "ntg-invoices/Cintas/123.zcic" - (io/input-stream (io/resource "test-cintas/o.zcic.230310093903")) - [client])))))) + [{:invoice/invoice-number "1500000592" + :invoice/date #inst "2023-03-09T08:00:00-00:00" + :invoice/due #inst "2023-04-08T07:00:00-00:00" + :invoice/import-status :import-status/imported + :invoice/client-identifier "2034 BROADWAY ST" + :invoice/location "OP" + :invoice/status :invoice-status/unpaid + :invoice/vendor :vendor/cintas + :invoice/scheduled-payment #inst "2023-04-08T07:00:00-00:00" + :invoice/client 1 + :invoice/total 39.88 + :invoice/outstanding-balance 39.88}] + (map #(dissoc % :invoice/expense-accounts :db/id) + (sut/extract-invoice-details "ntg-invoices/Cintas/123.zcic" + (io/input-stream (io/resource "test-cintas/o.zcic.230310093903")) + [client])))))) (testing "Should disable automatic payment based on feature flag" (let [client {:db/id 1 @@ -50,8 +48,8 @@ :client/locations ["OP"] :client/matches ["123 time square"]}] (is (= - [] - (sut/extract-invoice-details "ntg-invoices/Cintas/123" - (io/input-stream (io/resource "test-cintas/o.zcic.230310093903")) - [client])))))) + [] + (sut/extract-invoice-details "ntg-invoices/Cintas/123" + (io/input-stream (io/resource "test-cintas/o.zcic.230310093903")) + [client])))))) diff --git a/test/clj/auto_ap/integration/routes/invoice_test.clj b/test/clj/auto_ap/integration/routes/invoice_test.clj index 7b9c29b7..338e2d93 100644 --- a/test/clj/auto_ap/integration/routes/invoice_test.clj +++ b/test/clj/auto_ap/integration/routes/invoice_test.clj @@ -24,12 +24,12 @@ :account/name "Food"}) (defn invoice-count-for-client [c] - (or - (first (first (dc/q '[:find (count ?i) + (or + (first (first (dc/q '[:find (count ?i) :in $ ?c :where [?i :invoice/client ?c]] (dc/db conn) c))) - 0)) + 0)) (def invoice {:customer-identifier "ABC" :date (coerce/to-date-time #inst "2021-01-01") @@ -49,11 +49,10 @@ (t/testing "Should only import the same invoice once" (t/is (thrown? Exception (sut/import-uploaded-invoice user [(assoc invoice :customer-identifier "ABC")]))) - - + (t/is (thrown? Exception (sut/import-uploaded-invoice user [(assoc invoice - :customer-identifier "ABC" - :total "456.32")])))) + :customer-identifier "ABC" + :total "456.32")])))) (t/testing "Should override location" (sut/import-uploaded-invoice user [(assoc invoice @@ -61,26 +60,26 @@ :customer-identifier "ABC" :invoice-number "789")]) (t/is (= #{["DE"]} (dc/q '[:find ?l - :where [?i :invoice/invoice-number "789"] - [?i :invoice/expense-accounts ?ea] - [?ea :invoice-expense-account/location ?l]] - (dc/db conn))))) + :where [?i :invoice/invoice-number "789"] + [?i :invoice/expense-accounts ?ea] + [?ea :invoice-expense-account/location ?l]] + (dc/db conn))))) (t/testing "Should code invoice" (let [{{:strs [my-default-account coded-vendor]} :tempids} @(dc/transact conn - [{:vendor/name "Coded" - :db/id "coded-vendor" - :vendor/terms 12 - :vendor/default-account "my-default-account"} - {:db/id "my-default-account" - :account/name "My default-account"}])] + [{:vendor/name "Coded" + :db/id "coded-vendor" + :vendor/terms 12 + :vendor/default-account "my-default-account"} + {:db/id "my-default-account" + :account/name "My default-account"}])] (sut/import-uploaded-invoice user [(assoc invoice - :invoice-number "456" - :customer-identifier "ABC" - :vendor-code "Coded")]) + :invoice-number "456" + :customer-identifier "ABC" + :vendor-code "Coded")]) (let [[[result]] (dc/q '[:find (pull ?i [*]) - :where [?i :invoice/invoice-number "456"]] - (dc/db conn))] + :where [?i :invoice/invoice-number "456"]] + (dc/db conn))] (t/is (= coded-vendor (:db/id (:invoice/vendor result)))) (t/is (= [my-default-account] (map (comp :db/id :invoice-expense-account/account) (:invoice/expense-accounts result)))) diff --git a/test/clj/auto_ap/integration/rule_matching.clj b/test/clj/auto_ap/integration/rule_matching.clj index 32f7bfd7..3c0593ad 100644 --- a/test/clj/auto_ap/integration/rule_matching.clj +++ b/test/clj/auto_ap/integration/rule_matching.clj @@ -19,15 +19,13 @@ :approval-status :transaction-approval-status/unapproved :description-simple "simple-description"}) - - (t/deftest rule-applying-fn (t/testing "Should apply if description matches" (t/is (sut/rule-applies? base-transaction {:transaction-rule/description #"original-description" :transaction-rule/transaction-approval-status :transaction-approval-status/approved})) - + (t/is (not (sut/rule-applies? base-transaction {:transaction-rule/description #"xxx" @@ -42,7 +40,7 @@ (let [process (sut/rule-applying-fn [{:transaction-rule/description "simple-description" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}]) transaction (assoc base-transaction :transaction/description-original "simple-description")] - (t/is (= :transaction-approval-status/approved + (t/is (= :transaction-approval-status/approved (:transaction/approval-status (process transaction ["NG"])))))) (t/testing "spread cents" @@ -79,5 +77,4 @@ (t/is (= [0.01 0.01] (map :transaction-account/amount (:transaction/accounts (process (assoc transaction :transaction/amount 0.02) ["NG" "BT" "DE"]))))) (t/is (= [0.02 0.01 0.01] - (map :transaction-account/amount (:transaction/accounts (process (assoc transaction :transaction/amount 0.04) ["NG" "BT" "DE"]))))) - ))) + (map :transaction-account/amount (:transaction/accounts (process (assoc transaction :transaction/amount 0.04) ["NG" "BT" "DE"])))))))) diff --git a/test/clj/auto_ap/integration/util.clj b/test/clj/auto_ap/integration/util.clj index 405e6bd0..df869296 100644 --- a/test/clj/auto_ap/integration/util.clj +++ b/test/clj/auto_ap/integration/util.clj @@ -22,15 +22,11 @@ (defn user-token ([] (user-token 1)) ([client-id] - {:user "TEST USER" - :exp (time/plus (time/now) (time/days 1)) - :user/role "user" - :user/name "TEST USER" - :user/clients [{:db/id client-id}]})) - - - - + {:user "TEST USER" + :exp (time/plus (time/now) (time/days 1)) + :user/role "user" + :user/name "TEST USER" + :user/clients [{:db/id client-id}]})) (defn test-client [& kwargs] (apply assoc {:db/id "client-id" @@ -102,15 +98,15 @@ (defn setup-test-data [data] (:tempids @(dc/transact conn (into data - [(test-account :db/id "test-account-id") - (test-client :db/id "test-client-id" - :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")]) - (test-vendor :db/id "test-vendor-id") - {:db/id "accounts-payable-id" - :account/name "Accounts Payable" - :db/ident :account/accounts-payable - :account/numeric-code 21000 - :account/account-set "default"}])))) + [(test-account :db/id "test-account-id") + (test-client :db/id "test-client-id" + :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")]) + (test-vendor :db/id "test-vendor-id") + {:db/id "accounts-payable-id" + :account/name "Accounts Payable" + :db/ident :account/accounts-payable + :account/numeric-code 21000 + :account/account-set "default"}])))) (defn apply-tx [data] (:db-after @(dc/transact conn data))) diff --git a/test/clj/auto_ap/ledger_test.clj b/test/clj/auto_ap/ledger_test.clj index d1332b5c..12b0099a 100644 --- a/test/clj/auto_ap/ledger_test.clj +++ b/test/clj/auto_ap/ledger_test.clj @@ -5,40 +5,37 @@ (t/use-fixtures :each wrap-setup) - (t/deftest entity-change->ledger #_(t/testing "Should code an expected deposit" - (let [{:strs [ed ccp receipts-split client]} - (:tempids @(d/transact conn [#:expected-deposit {:status :expected-deposit-status/pending - :client {:db/id "client" - :client/code "BRYCE" - :client/locations ["M"]} - :total 4.0 - :fee 1.0 - :date #inst "2021-01-01T00:00:00-08:00" - :location "M" - :db/id "ed"}])) - result (sut/entity-change->ledger (d/db conn) [:expected-deposit ed])] - (t/is (= #:journal-entry - {:source "expected-deposit" - :client {:db/id client} - :date #inst "2021-01-01T00:00:00-08:00" - :original-entity ed - :vendor :vendor/ccp-square - :amount 4.0 - } - (dissoc result :journal-entry/line-items))) + (let [{:strs [ed ccp receipts-split client]} + (:tempids @(d/transact conn [#:expected-deposit {:status :expected-deposit-status/pending + :client {:db/id "client" + :client/code "BRYCE" + :client/locations ["M"]} + :total 4.0 + :fee 1.0 + :date #inst "2021-01-01T00:00:00-08:00" + :location "M" + :db/id "ed"}])) + result (sut/entity-change->ledger (d/db conn) [:expected-deposit ed])] + (t/is (= #:journal-entry + {:source "expected-deposit" + :client {:db/id client} + :date #inst "2021-01-01T00:00:00-08:00" + :original-entity ed + :vendor :vendor/ccp-square + :amount 4.0} + (dissoc result :journal-entry/line-items))) - (t/testing "should debit ccp" - (t/is (= [#:journal-entry-line - {:debit 4.0 - :location "A" - :account :account/ccp}] - (filter :journal-entry-line/debit (:journal-entry/line-items result)))) - ) - (t/testing "should credit receipts split ccp" - (t/is (= [#:journal-entry-line - {:credit 4.0 - :location "A" - :account :account/receipts-split}] - (filter :journal-entry-line/credit (:journal-entry/line-items result)))))))) + (t/testing "should debit ccp" + (t/is (= [#:journal-entry-line + {:debit 4.0 + :location "A" + :account :account/ccp}] + (filter :journal-entry-line/debit (:journal-entry/line-items result))))) + (t/testing "should credit receipts split ccp" + (t/is (= [#:journal-entry-line + {:credit 4.0 + :location "A" + :account :account/receipts-split}] + (filter :journal-entry-line/credit (:journal-entry/line-items result)))))))) diff --git a/test/clj/auto_ap/ssr/admin/accounts_test.clj b/test/clj/auto_ap/ssr/admin/accounts_test.clj index 9b261458..690e330c 100644 --- a/test/clj/auto_ap/ssr/admin/accounts_test.clj +++ b/test/clj/auto_ap/ssr/admin/accounts_test.clj @@ -126,8 +126,8 @@ ;; Test passes if sorting parameter is accepted and function returns successfully (is (number? matching-count))))) -(deftest account-sorting-by-numeric-code - (testing "Account sorting by numeric code should work (default)" + (deftest account-sorting-by-numeric-code + (testing "Account sorting by numeric code should work (default)" (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))] ;; Create test accounts before sorting (sut/account-save {:form-params {:account/numeric-code 12372 :account/name "Numeric Account" :account/type :account-type/asset :account/location "A"} :request-method :post :identity (admin-token)}) @@ -138,8 +138,8 @@ ;; Test passes if sorting parameter is accepted and function returns successfully (is (number? matching-count)))))) -(deftest account-sorting-by-type - (testing "Account sorting by type should work" + (deftest account-sorting-by-type + (testing "Account sorting by type should work" (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))] ;; Create test accounts before sorting (sut/account-save {:form-params {:account/numeric-code 12374 :account/name "Type Test" :account/type :account-type/asset :account/location "A"} :request-method :post :identity (admin-token)}) diff --git a/test/clj/auto_ap/ssr/ledger_test.clj b/test/clj/auto_ap/ssr/ledger_test.clj index 81fc001c..96f181ae 100644 --- a/test/clj/auto_ap/ssr/ledger_test.clj +++ b/test/clj/auto_ap/ssr/ledger_test.clj @@ -411,7 +411,7 @@ :location "HQ"}]} admin-identity (admin-token)] (is (thrown? Exception (sut/import-ledger {:form-params form-params - :identity admin-identity}))))) + :identity admin-identity}))))) (testing "Should produce form-errors for invalid account entries" (let [_ (setup-test-data [(test-client :db/id "err-client-1" @@ -430,7 +430,7 @@ :location "HQ"}]} admin-identity (admin-token)] (is (thrown? Exception (sut/import-ledger {:form-params form-params - :identity admin-identity})))))) + :identity admin-identity})))))) (deftest import-ledger-with-warnings-test (testing "Should ignore entries with only warnings" diff --git a/test/clj/auto_ap/ssr/transaction/edit_test.clj b/test/clj/auto_ap/ssr/transaction/edit_test.clj index 388b1f9e..e2f1cb3d 100644 --- a/test/clj/auto_ap/ssr/transaction/edit_test.clj +++ b/test/clj/auto_ap/ssr/transaction/edit_test.clj @@ -15,14 +15,14 @@ [datomic.api :as dc])) #_(t/use-fixtures :each (fn [f] - (dc/transact conn [{:db/id "vendor-id" - :vendor/name "Test Vendor" - :vendor/default-account {:db/id "account-id"}} - {:db/id "client-id" - :client/code "TEST" - :client/locations ["Z" "E"] - :client/bank-accounts ["bank-account-id"]}]) - (f))) + (dc/transact conn [{:db/id "vendor-id" + :vendor/name "Test Vendor" + :vendor/default-account {:db/id "account-id"}} + {:db/id "client-id" + :client/code "TEST" + :client/locations ["Z" "E"] + :client/bank-accounts ["bank-account-id"]}]) + (f))) (t/use-fixtures :each wrap-setup) (t/deftest get-vendor-test @@ -39,11 +39,11 @@ (t/deftest clientize-vendor-test (t/testing "Should transform vendor data for a specific client" (let [vendor {:db/id "vendor-id" - :vendor/name "Test Vendor" - :vendor/default-account {:db/id "account-id"} - :vendor/terms-overrides [{:vendor-terms-override/client {:db/id "client-id"} + :vendor/name "Test Vendor" + :vendor/default-account {:db/id "account-id"} + :vendor/terms-overrides [{:vendor-terms-override/client {:db/id "client-id"} :vendor-terms-override/terms "New Terms"}] - :vendor/account-overrides [{:vendor-account-override/client {:db/id "client-id"} + :vendor/account-overrides [{:vendor-account-override/client {:db/id "client-id"} :vendor-account-override/account {:db/id "client-specific-account-id"}}]} clientized-vendor (clientize-vendor vendor "client-id")] (t/is (= "New Terms" (:vendor/terms clientized-vendor))) diff --git a/test/clj/auto_ap/test_server.clj b/test/clj/auto_ap/test_server.clj index 41c09559..02615533 100644 --- a/test/clj/auto_ap/test_server.clj +++ b/test/clj/auto_ap/test_server.clj @@ -53,59 +53,59 @@ (defn seed-test-data [conn] (let [tx-result @(dc/transact conn - [(assoc (test-client :db/id "client-id" - :client/code "TEST" - :client/locations ["DT"]) - :client/bank-accounts [(test-bank-account :db/id "bank-account-id")]) - {:db/id "account-id" - :account/name "Test Account" - :account/type :account-type/expense - :account/numeric-code 50000 - :account/applicability :account-applicability/global - :account/default-allowance {:db/ident :allowance/allowed}} - {:db/id "account-id-2" - :account/name "Second Account" - :account/type :account-type/expense - :account/numeric-code 50001 - :account/applicability :account-applicability/global - :account/default-allowance {:db/ident :allowance/allowed}} - {:db/id "account-id-fixed-loc" - :account/name "Fixed Location Account" - :account/type :account-type/expense - :account/numeric-code 50002 - :account/applicability :account-applicability/global - :account/location "DT" - :account/default-allowance {:db/ident :allowance/allowed}} - {:db/id "ap-account-id" - :account/name "Accounts Payable" - :db/ident :account/accounts-payable - :account/numeric-code 21000 - :account/account-set "default" - :account/applicability :account-applicability/global - :account/default-allowance {:db/ident :allowance/allowed}} - {:db/id "vendor-id" - :vendor/name "Test Vendor" - :vendor/default-account "account-id"} - (test-transaction :db/id "transaction-id" - :transaction/client "client-id" - :transaction/bank-account "bank-account-id" - :transaction/amount 100.0 - :transaction/description-original "Test transaction" - :transaction/approval-status :transaction-approval-status/unapproved) - (test-transaction :db/id "transaction-id-2" - :transaction/client "client-id" - :transaction/bank-account "bank-account-id" - :transaction/amount 200.0 - :transaction/description-original "Second transaction" - :transaction/approval-status :transaction-approval-status/unapproved) - (test-transaction :db/id "transaction-id-3" - :transaction/client "client-id" - :transaction/bank-account "bank-account-id" - :transaction/amount 300.0 - :transaction/description-original "Third transaction" - :transaction/approval-status :transaction-approval-status/unapproved)]) - tempids (:tempids tx-result) - tx-entity-id (get tempids "transaction-id")] + [(assoc (test-client :db/id "client-id" + :client/code "TEST" + :client/locations ["DT"]) + :client/bank-accounts [(test-bank-account :db/id "bank-account-id")]) + {:db/id "account-id" + :account/name "Test Account" + :account/type :account-type/expense + :account/numeric-code 50000 + :account/applicability :account-applicability/global + :account/default-allowance {:db/ident :allowance/allowed}} + {:db/id "account-id-2" + :account/name "Second Account" + :account/type :account-type/expense + :account/numeric-code 50001 + :account/applicability :account-applicability/global + :account/default-allowance {:db/ident :allowance/allowed}} + {:db/id "account-id-fixed-loc" + :account/name "Fixed Location Account" + :account/type :account-type/expense + :account/numeric-code 50002 + :account/applicability :account-applicability/global + :account/location "DT" + :account/default-allowance {:db/ident :allowance/allowed}} + {:db/id "ap-account-id" + :account/name "Accounts Payable" + :db/ident :account/accounts-payable + :account/numeric-code 21000 + :account/account-set "default" + :account/applicability :account-applicability/global + :account/default-allowance {:db/ident :allowance/allowed}} + {:db/id "vendor-id" + :vendor/name "Test Vendor" + :vendor/default-account "account-id"} + (test-transaction :db/id "transaction-id" + :transaction/client "client-id" + :transaction/bank-account "bank-account-id" + :transaction/amount 100.0 + :transaction/description-original "Test transaction" + :transaction/approval-status :transaction-approval-status/unapproved) + (test-transaction :db/id "transaction-id-2" + :transaction/client "client-id" + :transaction/bank-account "bank-account-id" + :transaction/amount 200.0 + :transaction/description-original "Second transaction" + :transaction/approval-status :transaction-approval-status/unapproved) + (test-transaction :db/id "transaction-id-3" + :transaction/client "client-id" + :transaction/bank-account "bank-account-id" + :transaction/amount 300.0 + :transaction/description-original "Third transaction" + :transaction/approval-status :transaction-approval-status/unapproved)]) + tempids (:tempids tx-result) + tx-entity-id (get tempids "transaction-id")] (println "Test transaction entity ID:" tx-entity-id) (reset! test-account-ids {:test-account (get tempids "account-id") diff --git a/test/clj/iol_ion/integration/tx.clj b/test/clj/iol_ion/integration/tx.clj index d127e424..529bb966 100644 --- a/test/clj/iol_ion/integration/tx.clj +++ b/test/clj/iol_ion/integration/tx.clj @@ -23,64 +23,61 @@ :journal-entry-line/dirty :journal-entry-line/debit]}]) - (deftest upsert-invoice (testing "Importing should create a journal entry" (let [{:strs [invoice-id test-client-id - test-vendor-id - ]} (setup-test-data - [(test-invoice :db/id "invoice-id" - :invoice/import-status :import-status/pending - :invoice/total 200.0 - )])] - + test-vendor-id]} (setup-test-data + [(test-invoice :db/id "invoice-id" + :invoice/import-status :import-status/pending + :invoice/total 200.0)])] + (is (nil? (:db/id (dc/pull (dc/db conn) journal-pull [:journal-entry/original-entity invoice-id])))) (let [db-after (apply-tx (sut-i/upsert-invoice - (dc/db conn) - {:db/id invoice-id - :invoice/import-status :import-status/imported}))] - + (dc/db conn) + {:db/id invoice-id + :invoice/import-status :import-status/imported}))] + (is (= #:journal-entry{:date #inst "2022-01-01T00:00:00.000-00:00", - :original-entity #:db{:id invoice-id}, - :client #:db{:id test-client-id}, - :line-items - [#:journal-entry-line{:account - #:account{:name - "Accounts Payable"}, - :credit 200.0, - :location "A", - :dirty true} - #:journal-entry-line{:account - #:account{:name "Account"}, - :location "DT", - :dirty true, - :debit 100.0}], - :source "invoice", - :cleared false, - :amount 200.0, - :vendor #:db{:id test-vendor-id}} + :original-entity #:db{:id invoice-id}, + :client #:db{:id test-client-id}, + :line-items + [#:journal-entry-line{:account + #:account{:name + "Accounts Payable"}, + :credit 200.0, + :location "A", + :dirty true} + #:journal-entry-line{:account + #:account{:name "Account"}, + :location "DT", + :dirty true, + :debit 100.0}], + :source "invoice", + :cleared false, + :amount 200.0, + :vendor #:db{:id test-vendor-id}} (dc/pull db-after journal-pull [:journal-entry/original-entity invoice-id]))) (testing "voiding an invoice should remove the journal entry" (let [db-after (apply-tx (sut-i/upsert-invoice - (dc/db conn) - {:db/id invoice-id - :invoice/status :invoice-status/voided}))] - - (is (= nil + (dc/db conn) + {:db/id invoice-id + :invoice/status :invoice-status/voided}))] + + (is (= nil (dc/pull db-after journal-pull [:journal-entry/original-entity invoice-id]))))) (testing "invoice should remove the journal entry" (let [db-after (apply-tx (sut-i/upsert-invoice - (dc/db conn) - {:db/id invoice-id - :invoice/status :invoice-status/unpaid - :invoice/import-status :import-status/pending}))] - - (is (= nil + (dc/db conn) + {:db/id invoice-id + :invoice/status :invoice-status/unpaid + :invoice/import-status :import-status/pending}))] + + (is (= nil (dc/pull db-after journal-pull [:journal-entry/original-entity invoice-id]))))))))) @@ -91,31 +88,28 @@ test-account-id test-vendor-id test-transaction-id - test-import-batch-id - ]} (setup-test-data - [(test-transaction :db/id "test-transaction-id" - ) - {:db/id "test-import-batch-id" - :import-batch/date #inst "2022-01-01"}]) + test-import-batch-id]} (setup-test-data + [(test-transaction :db/id "test-transaction-id") + {:db/id "test-import-batch-id" + :import-batch/date #inst "2022-01-01"}]) update (sut-t/upsert-transaction (dc/db conn) {:db/id test-transaction-id - :transaction/id "hello" - :transaction/bank-account test-bank-account-id - :transaction/amount 500.00 - :transaction/client test-client-id - :transaction/date #inst "2022-01-01" - :transaction/vendor test-vendor-id - :transaction/approval-status :transaction-approval-status/approved - :transaction/accounts [ - {:db/id "account" - :transaction-account/account test-account-id - :transaction-account/location "A" - :transaction-account/amount 500.00}]})] - + :transaction/id "hello" + :transaction/bank-account test-bank-account-id + :transaction/amount 500.00 + :transaction/client test-client-id + :transaction/date #inst "2022-01-01" + :transaction/vendor test-vendor-id + :transaction/approval-status :transaction-approval-status/approved + :transaction/accounts [{:db/id "account" + :transaction-account/account test-account-id + :transaction-account/location "A" + :transaction-account/amount 500.00}]})] + (is (nil? (:db/id (dc/pull (dc/db conn) journal-pull [:journal-entry/original-entity test-transaction-id])))) (let [db-after (apply-tx update)] (testing "should create journal entry" - (is (= #:journal-entry{:date #inst "2022-01-01T00:00:00.000-00:00", + (is (= #:journal-entry{:date #inst "2022-01-01T00:00:00.000-00:00", :original-entity #:db{:id test-transaction-id}, :client #:db{:id test-client-id}, :source "transaction", @@ -130,8 +124,6 @@ #:account{:name "Account"}, :location "A", :credit 500.0, - :dirty true}]} + :dirty true}]} (dc/pull db-after journal-pull - [:journal-entry/original-entity test-transaction-id]))))) - - ))) + [:journal-entry/original-entity test-transaction-id]))))))))