diff --git a/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md b/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md new file mode 100644 index 00000000..433c77bc --- /dev/null +++ b/docs/plans/2026-04-24-001-feat-complete-sales-summary-calculations-plan.md @@ -0,0 +1,219 @@ +--- +title: Complete Automatic Sales Summary Calculations and Ledger Posting +type: feat +status: completed +date: 2026-04-24 +--- + +# Complete Automatic Sales Summary Calculations and Ledger Posting + +## What's Incomplete +- **Automatic Totals**: Aggregate attributes (e.g., `:sales-summary/total-card-payments`) are not calculated/stored by the job. +- **Data Persistence**: Automatic recalculations risk overwriting manual user adjustments. +- **Automation Gap**: Ledger entries are currently imported from external Excel files rather than generated automatically from the summaries. +- **UI Polish**: "Clientization" and HTMX context (`client-id`) TODOs remain in the admin interface. + +--- + +## Overview +... + + +This plan completes the implementation of automatic sales summary calculations and ensures they are correctly posted to the ledger. Currently, the `sales-summaries-v2` job calculates detailed daily summaries, but it doesn't store aggregate totals, preserve manual adjustments, or trigger the creation of actual ledger entries. Additionally, the admin UI has several unresolved TODOs. + +--- + +## Problem Frame + +The system currently aggregates raw sales data into a `sales-summary` entity, but the final step—creating balanced journal entries for the general ledger—is a manual process involving external Excel calculations and subsequent imports. This creates a dependency on external tools and increases the risk of data entry errors. The goal is to automate this pipeline entirely within the product. + +--- + +## Requirements Trace + +- R1. Calculate and store aggregate totals (e.g., `:sales-summary/total-card-payments`) on the `sales-summary` entity. +- R2. Preserve user-made manual adjustments (`:sales-summary-item/manual? true`) during automatic recalculations. +- R3. Aggregate detailed `sales-summary-item`s into balanced `journal-entry` lines by account and location. +- R4. Automate the posting of these aggregated totals to the ledger. +- R5. Resolve UI TODOs in the Sales Summaries admin page, specifically regarding client-scoping ("clientize") and HTMX context (`client-id`). + +--- + +## Scope Boundaries + +- **In-Scope**: + - Enhancements to the `sales-summaries-v2` job. + - Implementation of the summary-to-ledger aggregation and posting logic. + - Cleanup of the Sales Summaries admin UI. +- **Out-of-Scope**: + - Changing the fundamental calculation logic for sales orders/refunds. + - Creating new ledger accounts (assume existing account mapping is sufficient). + - Changing the naming of refunds/returns (user requested to keep as is). + +--- + +## Context & Research + +### Relevant Code and Patterns + +- **Jobs**: `src/clj/auto_ap/jobs/sales_summaries.clj` contains the main calculation logic. +- **UI**: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` implements the admin interface. +- **Ledger Posting**: `src/clj/auto_ap/ledger.clj` and `iol_ion/src/iol_ion/tx/upsert_ledger.clj` handle journal entry creation. +- **Reconciliation Pattern**: `reconcile-ledger` in `src/clj/auto_ap/ledger.clj` shows how to find missing ledger entries and trigger their creation. + +### Institutional Learnings + +- No existing documented patterns for sales summary posting were found in `docs/solutions/`. This implementation will establish the pattern. + +--- + +## Key Technical Decisions + +- **Detailed Summary $\to$ Aggregated Ledger**: The `sales-summary` will maintain granular detail (line items, specific fee types), but the ledger posting will aggregate these items by account and location to create balanced `journal-entry` lines. +- **Automatic Posting**: Posting to the ledger will be integrated into the reconciliation process, similar to how invoices and transactions are handled in `reconcile-ledger`. +- **Location Handling**: Since `sales-summary-item`s don't have a location, a default location for the client will be used for ledger posting. + +--- + +## Open Questions + +### Resolved During Planning + +- **Architectural Decision**: Use a detailed summary that aggregates into the ledger. +- **Renaming**: Keep "Refunds/Returns" as is. + +### Deferred to Implementation + +- **Default Location Logic**: Exactly how the "default location" for a client is retrieved or defined. + +--- + +## Implementation Units + +- U1. **Enhance `sales-summaries-v2` Job** + +**Goal:** Ensure the job stores aggregate totals and preserves manual adjustments. + +**Requirements:** R1, R2 + +**Dependencies:** None + +**Files:** +- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj` + +**Approach:** +- Update `sales-summaries-v2` to calculate totals for attributes like `:sales-summary/total-card-payments`, `:sales-summary/total-cash-payments`, etc., based on the generated items. +- Implement a merge strategy: when updating a summary, keep any items where `:sales-summary-item/manual?` is true, and only replace the automatically calculated items. + +**Test scenarios:** +- Happy path: Running the job for a client with sales and refunds results in a `sales-summary` with correct `:sales-summary/total-*` attributes. +- Edge case: Running the job on a summary that already has a manual item ensures the manual item is not overwritten. + +**Verification:** +- Datomic query shows `sales-summary` entities have populated total attributes and preserved manual items. + +--- + +- U2. **Implement Summary-to-Ledger Aggregation** + +**Goal:** Create a function to transform detailed summary items into balanced ledger lines. + +**Requirements:** R3 + +**Dependencies:** U1 + +**Files:** +- Create: `src/clj/auto_ap/ledger/sales_summaries.clj` (or add to `src/clj/auto_ap/ledger.clj`) +- Test: `test/clj/auto_ap/ledger_test.clj` + +**Approach:** +- Create a function `aggregate-summary-items` that: + 1. Groups `sales-summary-item`s by `:ledger-mapped/account`. + 2. Sums the `:ledger-mapped/amount` based on `:ledger-mapped/ledger-side` (debit vs credit). + 3. Assigns a location (default client location). + 4. Returns a list of `journal-entry-line` maps. + +**Test scenarios:** +- Happy path: A set of items with mixed accounts and sides aggregates into the correct number of ledger lines with summed amounts. +- Edge case: Items with `nil` accounts are handled gracefully (e.g., mapped to an "Unknown" account or logged as error). + +**Verification:** +- Unit tests verify that a list of `sales-summary-item`s is correctly transformed into `journal-entry-line`s. + +--- + +- U3. **Implement Automatic Ledger Posting for Summaries** + +**Goal:** Ensure sales summaries trigger the creation of ledger entries. + +**Requirements:** R4 + +**Dependencies:** U2 + +**Files:** +- Modify: `src/clj/auto_ap/ledger.clj` +- Modify: `src/clj/auto_ap/jobs/sales_summaries.clj` + +**Approach:** +- Implement a `:upsert-sales-summary-ledger` transaction or function that takes a `sales-summary` and uses the aggregation logic from U2 to post to the ledger. +- Integrate this into the `reconcile-ledger` function in `src/clj/auto_ap/ledger.clj` to find summaries missing ledger entries and post them. + +**Test scenarios:** +- Integration: Running `reconcile-ledger` identifies a `sales-summary` missing a `journal-entry` and creates a balanced `journal-entry` for it. +- Happy path: The created `journal-entry` has the correct total amount and matches the summary totals. + +**Verification:** +- A `sales-summary` entity is linked to a `journal-entry` via `:journal-entry/original-entity`. + +--- + +- U4. **Resolve UI TODOs in Sales Summaries Admin** + +**Goal:** Fix client-scoping and HTMX context in the admin UI. + +**Requirements:** R5 + +**Dependencies:** None + +**Files:** +- Modify: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` + +**Approach:** +- Resolve "clientize" TODOs: Ensure the data pulled for the table and edit wizard is correctly scoped and transformed using client-specific context. +- Fix HTMX `client-id` passing: Update the `new-summary-item` trigger to correctly pass the `client-id` via `hx-vals` from the form state. +- Clean up any remaining schema TODOs in the SSR file. + +**Test scenarios:** +- Integration: Adding a new summary item in the UI correctly sends the `client-id` and the item is created for the correct client. +- Happy path: The summary table displays correctly and "missing account" warnings appear only for items without a mapped account. + +**Verification:** +- Manual verification in the browser: New items are added correctly, and the UI is free of "missing account" red pills for mapped items. + +--- + +## System-Wide Impact + +- **Interaction graph**: The `sales-summaries-v2` job now feeds into the ledger system via `reconcile-ledger`. +- **Error propagation**: Failures in the aggregation logic will prevent the `journal-entry` from being created, which will be surfaced by `reconcile-ledger` as a missing entry. +- **State lifecycle risks**: Ensuring that `manual?` items are not overwritten during automatic recalculation is critical to avoid losing user adjustments. +- **Integration coverage**: Integration tests must cover the full flow: `sales-orders` $\to$ `sales-summary` $\to$ `journal-entry`. + +--- + +## Risks & Dependencies + +| Risk | Mitigation | +|------|------------| +| Overwriting manual adjustments | Implement explicit merge logic based on the `:sales-summary-item/manual?` flag. | +| Unbalanced ledger entries | Use a strict aggregation function that ensures debits = credits for every posted summary. | +| Missing location data | Implement a robust fallback to a default client location. | + +--- + +## Sources & References + +- Related code: `src/clj/auto_ap/jobs/sales_summaries.clj` +- Related code: `src/clj/auto_ap/ssr/admin/sales_summaries.clj` +- Related code: `src/clj/auto_ap/ledger.clj` +- Related code: `iol_ion/src/iol_ion/tx/upsert_ledger.clj` diff --git a/iol_ion/src/iol_ion/tx.clj b/iol_ion/src/iol_ion/tx.clj index cf81c480..1d90c284 100644 --- a/iol_ion/src/iol_ion/tx.clj +++ b/iol_ion/src/iol_ion/tx.clj @@ -49,9 +49,9 @@ (datomic-fn :upsert-entity #'iol-ion.tx.upsert-entity/upsert-entity) (datomic-fn :upsert-invoice #'iol-ion.tx.upsert-invoice/upsert-invoice) (datomic-fn :upsert-ledger #'iol-ion.tx.upsert-ledger/upsert-ledger) - (datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction)]))) + (datomic-fn :upsert-transaction #'iol-ion.tx.upsert-transaction/upsert-transaction) + (datomic-fn :upsert-sales-summary #'iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary)]))) (comment (regenerate-literals) - - (auto-ap.datomic/install-functions)) \ No newline at end of file + (auto-ap.datomic/install-functions)) 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 new file mode 100644 index 00000000..3db00f80 --- /dev/null +++ b/iol_ion/src/iol_ion/tx/upsert_sales_summary_ledger.clj @@ -0,0 +1,70 @@ +(ns iol-ion.tx.upsert-sales-summary-ledger + (:require [datomic.api :as dc])) + +(defn summary->journal-entry [db summary-id] + (let [summary (dc/pull db '[:sales-summary/client + :sales-summary/date + {:sales-summary/items [:sales-summary-item/category + :ledger-mapped/account + :ledger-mapped/amount + {:ledger-mapped/ledger-side [:db/ident]}]}] + summary-id) + items (:sales-summary/items summary) + aggregated (->> items + (filter :ledger-mapped/account) + (group-by :ledger-mapped/account) + (map (fn [[account acc-items]] + (reduce + (fn [m item] + (update m (:db/ident (:ledger-mapped/ledger-side item)) (fnil + 0.0) (:ledger-mapped/amount item 0.0))) + {:account account} + acc-items)))) + _ (clojure.pprint/pprint aggregated) + line-items (mapv (fn [{:keys [account] :as m}] + (cond-> {:db/id (str (java.util.UUID/randomUUID)) + :journal-entry-line/account account + :journal-entry-line/location "A"} + (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]) + ] + (when (and (seq line-items) + (= (Math/round (* 1000 total-debits)) + (Math/round (* 1000 total-credits)))) + {:journal-entry/source "sales-summary" + :journal-entry/client (:db/id (:sales-summary/client summary)) + :journal-entry/date (:sales-summary/date summary) + :journal-entry/original-entity summary-id + :journal-entry/amount total-debits + :journal-entry/line-items line-items}))) + +(defn current-date [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 + last-tx))] + date)) + +(defn upsert-sales-summary [db summary] + (let [upserted-summary [[:upsert-entity summary]] + db-after (-> (dc/with db upserted-summary) :db-after) + summary-id (:db/id summary) + client-id (-> (dc/pull db-after [{:sales-summary/client [:db/id]}] summary-id) + :sales-summary/client + :db/id) + 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)]]] + + + (when client-id [{:db/id client-id + :client/ledger-last-change (current-date db)}])))))) diff --git a/resources/functions.edn b/resources/functions.edn index efe3cac1..776cf345 100644 --- a/resources/functions.edn +++ b/resources/functions.edn @@ -1 +1 @@ -[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (< -1.0E-4 new-outstanding-balance 1.0E-4) :invoice-status/paid :invoice-status/unpaid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice)))) [locked-until] (first (dc/q (quote [: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 (< (compare (:invoice/date invoice) locked-until) 0) true :else false)] (if (or existing? is-locked?) [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (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 (quote [: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) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [: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)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) 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/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__143570# p2__143571#] (assoc p1__143570# (f p2__143571#) (fv p2__143571#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [[java.util Date]], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) 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{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :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-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :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))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__143573#] (= :payment-status/cleared (:payment/status p1__143573#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))}))))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) 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) :invoice/client :db/id)] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]] {:client/ledger-last-change (current-date db), :db/id client-id}]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID Date]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [: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]}])] (letfn [(-random-tempid [] (do (dc/tempid :db.part/user))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (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])))] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__143577#] (-> p1__143577# (assoc :journal-entry-line/date (:journal-entry/date ledger-entry)) (assoc :journal-entry-line/client (:journal-entry/client ledger-entry)))) lis)))))] {:client/ledger-last-change (current-date db), :db/id (:journal-entry/client ledger-entry)}])))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [my-transaction {:transaction/bank-account 17592232681223, :transaction/date (read-string \"#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, :ident :transaction-approval-status/approved}, :transaction/amount -84.43, :transaction/accounts [{:transaction-account/account 17592231963549, :db/id \"c402c7b3-c11b-484b-b670-bd48f79a3e5f\", :transaction-account/location \"CB\", :transaction-account/amount 84.43}], :transaction/raw-id \"gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP\", :transaction/vendor 17592232627053} my-journal #:journal-entry{:vendor 17592232627053, :amount 84.43, :date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :alternate-description \"Rotten Robbie #03\", :original-entity \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :client 17592232577980, :source \"transaction\", :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\"}], :cleared true}] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file +[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (< -1.0E-4 new-outstanding-balance 1.0E-4) :invoice-status/paid :invoice-status/unpaid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice)))) [locked-until] (first (dc/q (quote [: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 (< (compare (:invoice/date invoice) locked-until) 0) true :else false)] (if (or existing? is-locked?) [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (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 (quote [: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) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [: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)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) 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/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__112282# p2__112283#] (assoc p1__112282# (f p2__112283#) (fv p2__112283#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [[java.util Date]], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) 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{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :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-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :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))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__112285#] (= :payment-status/cleared (:payment/status p1__112285#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))}))))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) 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) :invoice/client :db/id)] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]] {:client/ledger-last-change (current-date db), :db/id client-id}]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID Date]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [: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]}])] (letfn [(-random-tempid [] (do (dc/tempid :db.part/user))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (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])))] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__112289#] (-> p1__112289# (assoc :journal-entry-line/date (:journal-entry/date ledger-entry)) (assoc :journal-entry-line/client (:journal-entry/client ledger-entry)))) lis)))))] {:client/ledger-last-change (current-date db), :db/id (:journal-entry/client ledger-entry)}])))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [my-transaction {:transaction/bank-account 17592232681223, :transaction/date (read-string \"#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, :ident :transaction-approval-status/approved}, :transaction/amount -84.43, :transaction/accounts [{:transaction-account/account 17592231963549, :db/id \"c402c7b3-c11b-484b-b670-bd48f79a3e5f\", :transaction-account/location \"CB\", :transaction-account/amount 84.43}], :transaction/raw-id \"gQypbv5946F08op74wZmidDg8qD8Q1fM6gEBP\", :transaction/vendor 17592232627053} my-journal #:journal-entry{:vendor 17592232627053, :amount 84.43, :date (read-string \"#inst \\\"2024-02-24T08:00:00.000-00:00\\\"\"), :alternate-description \"Rotten Robbie #03\", :original-entity \"ac2efd80-bb03-48b2-b0d0-6b47a5c119dc\", :client 17592232577980, :source \"transaction\", :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\"}], :cleared true}] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}} #:db{:ident :upsert-sales-summary, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db summary], :code "(let [] (letfn [(summary->journal-entry [db summary-id] (do (let [summary (dc/pull db (quote [:sales-summary/client :sales-summary/date #:sales-summary{:items [:sales-summary-item/category :ledger-mapped/account :ledger-mapped/amount #:ledger-mapped{:ledger-side [:db/ident]}]}]) summary-id) items (:sales-summary/items summary) aggregated (->> items (filter :ledger-mapped/account) (group-by :ledger-mapped/account) (map (fn [[account acc-items]] (reduce (fn [m item] (update m (:db/ident (:ledger-mapped/ledger-side item)) (fnil + 0.0) (:ledger-mapped/amount item 0.0))) {:account account} acc-items)))) _ (clojure.pprint/pprint aggregated) line-items (mapv (fn [{:as m, :keys [account]}] (cond-> {:journal-entry-line/account account, :db/id (str (java.util.UUID/randomUUID)), :journal-entry-line/location \"A\"} (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 (fn* [p1__112291#] (get p1__112291# :ledger-side/debit 0.0)) aggregated)) total-credits (reduce + 0.0 (map (fn* [p1__112293#] (get p1__112293# :ledger-side/credit 0.0)) aggregated)) _ (clojure.pprint/pprint [total-debits total-credits])] (when (and (seq line-items) (= (Math/round (* 1000 total-debits)) (Math/round (* 1000 total-credits)))) #:journal-entry{:date (:sales-summary/date summary), :original-entity summary-id, :client (:db/id (:sales-summary/client summary)), :line-items line-items, :source \"sales-summary\", :amount total-debits})))) (current-date [db] (do (let [last-tx (dc/t->tx (dc/basis-t db)) [[date]] (seq (dc/q (quote [:find ?ti :in $ ?tx :where [?tx :db/txInstant ?ti]]) db last-tx))] date)))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [summary (genfn-coerce-arg summary)] (do (let [upserted-summary [[:upsert-entity summary]] db-after (-> (dc/with db upserted-summary) :db-after) summary-id (:db/id summary) client-id (-> (dc/pull db-after [#:sales-summary{:client [:db/id]}] summary-id) :sales-summary/client :db/id) journal-entry (summary->journal-entry db-after summary-id)] upserted-summary))))))"}}] \ No newline at end of file diff --git a/resources/schema.edn b/resources/schema.edn index bae45322..4570036d 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1949,12 +1949,12 @@ :db/unique :db.unique/identity :db/index true} {:db/ident :sales-summary/client+dirty - :db/valueType :db.type/tuple - :db/tupleAttrs [:sales-summary/client :sales-summary/dirty] - :db/cardinality :db.cardinality/one - :db/index true} + :db/valueType :db.type/tuple + :db/tupleAttrs [:sales-summary/client :sales-summary/dirty] + :db/cardinality :db.cardinality/one + :db/index true} - {:db/ident :sales-summary-item/category + {:db/ident :sales-summary-item/category :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :sales-summary-item/sort-order diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index e70bc375..80b9271e 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -5,10 +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-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] diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index 9ee556be..e759078b 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -278,46 +278,42 @@ (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]} (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 [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 - (->> - (get-sales c date) - (concat (get-payment-items c date)) - (concat (get-refund-items c date)) - (cons (get-discounts c date)) - (cons (get-fees c date)) - (cons (get-tax c date)) - (cons (get-tip c date)) - (cons (get-returns c date)) - (filter identity) - (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)) - )) }] - (if (seq (:sales-summary/items result)) - (do - (alog/info ::upserting-summaries - :category-count (count (:sales-summary/items result))) - @(dc/transact conn [[:upsert-entity result]])) - @(dc/transact conn [{:db/id id :sales-summary/dirty false}])))))) - -(let [c (auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL" ]) - date #inst "2024-04-14T00:00:00-07:00"] - (get-payment-items c date) - - ) + :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)) + (cons (get-discounts c date)) + (cons (get-fees c date)) + (cons (get-tax c date)) + (cons (get-tip c date)) + (cons (get-returns c date)) + (filter identity) + (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}])))))) (defn reset-summaries [] @@ -334,29 +330,39 @@ (comment (auto-ap.datomic/transact-schema conn) - @(dc/transact conn [{:db/ident :sales-summary/total-unknown-processor-payments - :db/noHistory true, - :db/valueType :db.type/double - :db/cardinality :db.cardinality/one}]) - (apply mark-dirty [:client/code "NGCL"] (last-n-days 30)) (apply mark-dirty [:client/code "NGDG"] (last-n-days 30)) - (apply mark-dirty [:client/code "NGPG"] (last-n-days 30)) + (dirty-sales-summaries [:client/code "NGWH"]) - (mark-all-dirty 50) + + (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"]] [?sos :sales-summary/date ?d] [(= ?d #inst "2024-04-10T00:00:00-07:00")]] (dc/db conn)) - + (dc/q '[:find ?n ?p2 (sum ?total) :with ?c :in $ [?clients ?start-date ?end-date] @@ -369,18 +375,21 @@ (dc/db conn) [[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGHW"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-11T00:00:00-07:00"]) - (dc/q '[:find ?n + (dc/q '[:find ?n :in $ [?clients ?start-date ?end-date] :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] [?e :sales-order/line-items ?li] - [?li :order-line-item/item-name ?n] ] + [?li :order-line-item/item-name ?n]] (dc/db conn) [[(auto-ap.datomic/pull-attr (dc/db conn) :db/id [:client/code "NGCL"])] #inst "2024-04-11T00:00:00-07:00" #inst "2024-04-24T00:00:00-07:00"]) - -@(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) + @(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) + + + ) diff --git a/src/clj/auto_ap/ledger.clj b/src/clj/auto_ap/ledger.clj index 7c714426..2f6a1f4a 100644 --- a/src/clj/auto_ap/ledger.clj +++ b/src/clj/auto_ap/ledger.clj @@ -35,27 +35,42 @@ 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) + :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}]))) - repairs (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries))] + + 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))] (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)) - @(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] diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 6e30d822..8bdfe276 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -6,7 +6,8 @@ [auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.transaction-rules :as transaction-rules] - [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.routes.pos.sales-summaries :as ss-routes] [auto-ap.routes.invoice :as invoice-route] [auto-ap.routes.ledger :as ledger-routes] [auto-ap.routes.outgoing-invoice :as oi-routes] @@ -90,8 +91,8 @@ (#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request)) "invoices" - (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request)) - "sales" + (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts ::ss-routes/page} (:matched-route request)) + "sales" (#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request)) "payments" (#{::ledger-routes/all-page ::ledger-routes/external-page ::ledger-routes/external-import-page ::ledger-routes/balance-sheet ::ledger-routes/cash-flows ::ledger-routes/profit-and-loss} (:matched-route request)) @@ -207,12 +208,18 @@ :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) + ::ss-routes/page) "?date-range=week") - :active? (= :pos-cash-drawer-shifts (:matched-route request)) + :active? (= ::ss-routes/page (:matched-route request)) :hx-boost "true"} - "Cash drawer shifts")))) + "Summaries")))) (menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} " :icon svg/payments} diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 12cf76c1..611ff0a4 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -12,7 +12,7 @@ [auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices] [auto-ap.ssr.admin.history :as history] [auto-ap.ssr.admin.import-batch :as import-batch] - [auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries] + [auto-ap.ssr.pos.sales-summaries :as pos-sales-summaries] [auto-ap.ssr.admin.transaction-rules :as admin-rules] [auto-ap.ssr.admin.vendors :as admin-vendors] [auto-ap.ssr.auth :as auth] @@ -85,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 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-sales-summaries/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/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/pos/sales_summaries.clj similarity index 84% rename from src/clj/auto_ap/ssr/admin/sales_summaries.clj rename to src/clj/auto_ap/ssr/pos/sales_summaries.clj index 55c67617..fed3d987 100644 --- a/src/clj/auto_ap/ssr/admin/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/pos/sales_summaries.clj @@ -1,4 +1,4 @@ -(ns auto-ap.ssr.admin.sales-summaries +(ns auto-ap.ssr.pos.sales-summaries (:require [auto-ap.datomic :refer [apply-pagination apply-sort-3 conn merge-query pull-many @@ -6,9 +6,7 @@ [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :refer [wrap-copy-qp-pqp]] - [auto-ap.routes.admin.sales-summaries :as route] - [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [auto-ap.routes.pos.sales-summaries :as route] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.components :as com] @@ -16,6 +14,8 @@ [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] + [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 @@ -48,38 +48,22 @@ "hx-target" "#entity-table" "hx-indicator" "#entity-table"} - #_[:fieldset.space-y-6 - (date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range"}) - (com/field {:label "Source"} - (com/select {:name "source" - :class "hot-filter w-full" - :value (:source (:query-params request)) - :placeholder "" - :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}))]]) + [:fieldset.space-y-6 + (date-range-field* request)]]) (def default-read '[:db/id * [:sales-summary/date :xform clj-time.coerce/from-date] {:sales-summary/client [:client/code :client/name :db/id]} {:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident] - } ;; TODO clientize - :ledger-mapped/account - :ledger-mapped/amount - :sales-summary-item/category - :sales-summary-item/sort-order - :db/id - :sales-summary-item/manual?] - } ]) ;; TODO + } + :ledger-mapped/account + :ledger-mapped/amount + :sales-summary-item/category + :sales-summary-item/sort-order + :db/id + :sales-summary-item/manual?] + } ]) (defn fetch-ids [db request] (let [query-params (:query-params request) @@ -129,31 +113,12 @@ [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) -#_(defn get-debits [ss] - {:card-payments (+ (:sales-summary/total-card-payments ss 0.0) - (:sales-summary/total-card-fees ss 0.0) - (- (:sales-summary/total-card-refunds ss 0.0))) - :food-app-payments (+ (:sales-summary/total-food-app-payments ss 0.0) - (:sales-summary/total-food-app-fees ss 0.0) - (- (:sales-summary/total-food-app-refunds ss 0.0))) - :gift-card-payments (+ (:sales-summary/total-gift-card-payments ss 0.0) - (:sales-summary/total-gift-card-fees ss 0.0) - (- (:sales-summary/total-gift-card-refunds ss 0.0))) - #_#_:refunds (+ (:sales-summary/total-food-app-refunds ss 0.0) - (:sales-summary/total-card-refunds ss 0.0) - (:sales-summary/total-cash-refunds ss 0.0)) - - :fees (- (:sales-summary/total-card-fees ss 0.0)) - :cash-payments (+ (:sales-summary/total-cash-payments ss 0.0) - (- (:sales-summary/total-cash-refunds ss 0.0))) - :total-unknown-processor-payments (:sales-summary/total-unknown-processor-payments ss 0.0) - :discounts (+ (:sales-summary/discount ss 0.0)) - :returns (+ (:sales-summary/total-returns ss 0.0))}) (defn sort-items [ss] (sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss)) + (defn total-debits [items] (->> items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %))) @@ -169,7 +134,7 @@ (def grid-page (helper/build {:id "entity-table" :id-fn :db/id - :nav com/admin-aside-nav + :nav com/main-aside-nav :fetch-page fetch-page :page-specific-nav filters :query-schema query-schema @@ -179,13 +144,11 @@ :db/id (:db/id entity))} svg/pencil)]) :oob-render - (fn [request] - [#_(assoc-in (date-range-field {:value {:start (:start-date (:query-params request)) - :end (:end-date (:query-params request))} - :id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :admin)} - "Admin"] + :company)} + "POS"] [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} @@ -241,14 +204,6 @@ :primary :red)} "Total: " (format "$%,.2f" total-credits))]]))}]})) -;; TODO schema cleanup -;; Decide on what should be calculated as generating ledger entries, and what should be calculated -;; as part of the summary -;; default thought here is that the summary has more detail (e.g., line items), fees broken out by type -;; and aggregated into the final ledger entry -;; that allows customization at any level. -;; TODO rename refunds/returns - (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) @@ -436,15 +391,14 @@ (com/data-grid-header {} "")]} (fc/with-field :sales-summary/items (list - (fc/cursor-map #(sales-summary-item-row* {:value % - :client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) })) - ;; TODO - (com/data-grid-new-row {:colspan 5 - :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) - :row-offset 0 - :index (count (fc/field-value)) - :tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} ;; TODO - "New Summary Item"))) + (fc/cursor-map #(sales-summary-item-row* {:value % + :client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) })) + (com/data-grid-new-row {:colspan 5 + :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) + :row-offset 0 + :index (count (fc/field-value)) + :tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} + "New Summary Item"))) (summary-total-row* request) (unbalanced-row* request)) ]) @@ -490,7 +444,7 @@ edit-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] (let [result (:snapshot multi-form-state ) - transaction [:upsert-entity {:db/id (:db/id result) + transaction [:upsert-sales-summary {:db/id (:db/id result) :sales-summary/items (map (fn [i] (if (:sales-summary-item/manual? i) @@ -553,8 +507,4 @@ (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))))) - - + (wrap-schema-enforce :hx-schema query-schema))))) diff --git a/src/cljc/auto_ap/routes/admin/sales_summaries.cljc b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc similarity index 68% rename from src/cljc/auto_ap/routes/admin/sales_summaries.cljc rename to src/cljc/auto_ap/routes/pos/sales_summaries.cljc index 9e1e8889..be26c7ff 100644 --- a/src/cljc/auto_ap/routes/admin/sales_summaries.cljc +++ b/src/cljc/auto_ap/routes/pos/sales_summaries.cljc @@ -1,9 +1,7 @@ -(ns auto-ap.routes.admin.sales-summaries) +(ns auto-ap.routes.pos.sales-summaries) (def routes {"" {:get ::page :put ::edit-wizard-submit} "/table" ::table - ["/" [#"\d+" :db/id]] {:get ::edit-wizard } - "/edit/navigate" ::edit-wizard-navigate - "/edit/sales-summary-item" ::new-summary-item}) \ No newline at end of file + "/edit/sales-summary-item" ::new-summary-item}) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 247734f6..0f2ec820 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -12,7 +12,7 @@ [auto-ap.routes.transactions :as t-routes] [auto-ap.routes.admin.clients :as ac-routes] - [auto-ap.routes.admin.sales-summaries :as ss-routes] + [auto-ap.routes.pos.sales-summaries :as ss-routes] [auto-ap.routes.admin.transaction-rules :as tr-routes])) (def routes {"impersonate" :impersonate