diff --git a/resources/schema.edn b/resources/schema.edn index 3c81f3a0..69e39de4 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1847,6 +1847,97 @@ :db/valueType :db.type/tuple :db/tupleAttrs [:expected-deposit/client :expected-deposit/date] :db/cardinality :db.cardinality/one - :db/index true}] + :db/index true} + + {:db/ident :sales-summary/client + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/index true} + {:db/ident :sales-summary/date + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one + :db/index true} + {:db/ident :sales-summary/sales-items + :db/valueType :db.type/ref + :db/isComponent true, + :db/cardinality :db.cardinality/many} + {:db/ident :sales-summary-item/category + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary-item/item-name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary-item/total + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary-item/tax + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary-item/discount + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/dirty + :db/noHistory true, + :db/valueType :db.type/boolean + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/client+date + :db/valueType :db.type/tuple + :db/tupleAttrs [:sales-summary/client :sales-summary/date] + :db/cardinality :db.cardinality/one + :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/ident :sales-summary/discount + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-card-payments + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-tax + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-returns + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-tip + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-card-fees + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-card-refunds + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-cash-payments + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-cash-refunds + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-food-app-payments + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + {:db/ident :sales-summary/total-food-app-refunds + :db/noHistory true, + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one}] diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj new file mode 100644 index 00000000..0277e5ac --- /dev/null +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -0,0 +1,256 @@ +(ns auto-ap.jobs.sales-summaries + (:require [auto-ap.datomic :refer [conn]] + [auto-ap.jobs.core :refer [execute]] + [auto-ap.logging :as alog] + [auto-ap.time :as atime] + [clj-time.coerce :as c] + [clj-time.core :as time] + [clj-time.periodic :as per] + [com.brunobonacci.mulog :as mu] + [datomic.api :as dc])) + +(defn mark-dirty [client start end] + (let [client (dc/entid (dc/db conn) client)] + @(dc/transact conn + (for [s (per/periodic-seq start + end + (time/days 1))] + {:sales-summary/client client + :sales-summary/date (c/to-date s) + :sales-summary/dirty true + :sales-summary/client+date [client (c/to-date s)]})))) + +(defn last-n-days [n] + [(.toDateMidnight (atime/localize (time/plus (time/now) (time/days (- n))))) + (.toDateMidnight (atime/localize (time/now)))]) + +(defn mark-all-dirty [days] + (doseq [[c] (dc/q '[:find ?c + :in $ + :where [_ :sales-order/client ?c]] + (dc/db conn))] + (apply mark-dirty c (last-n-days days)))) + + + + +(defn dirty-sales-summaries [c] + (let [client-id (dc/entid (dc/db conn) c)] + (->> (dc/index-pull (dc/db conn) + {:index :avet + :selector '[:sales-summary/date :sales-summary/client :db/id] + :start [:sales-summary/client+dirty [client-id true]]}) + (filter (fn [sales-summary] + (= client-id (:db/id (:sales-summary/client sales-summary)))))))) + +(defn sales-summaries [] + (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)] + (mu/with-context {:client-code client-code + :date date} + (alog/info ::updating) + (let [sales (->> (dc/q '[:find ?item-name ?category (sum ?total) (sum ?tax) (sum ?discount) + :with ?e ?li + :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 ?item-name] + [?li :order-line-item/category ?category] + [?li :order-line-item/total ?total] + [?li :order-line-item/tax ?tax] + [?li :order-line-item/discount ?discount]] + (dc/db conn) + [[c] date date])) + result {:db/id id + :sales-summary/client c + :sales-summary/date date + :sales-summary/dirty false + :sales-summary/client+date [c date] + :sales-summary/discount (or (ffirst (dc/q '[:find (sum ?discount) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/discount ?discount]] + (dc/db conn) + [[c] date date])) + 0.0) + :sales-summary/total-returns (or (ffirst (dc/q '[:find (sum ?r) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/returns ?r] + + #_[?e :sales-order/charges ?c] + #_[?c :charge/tax ?tax]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/sales-items + + + + (for [[item-name category total tax discount] sales] + {:db/id (str (java.util.UUID/randomUUID)) + :sales-summary-item/item-name item-name + :sales-summary-item/category category + :sales-summary-item/total total + :sales-summary-item/tax tax + :sales-summary-item/discount discount}) + + :sales-summary/total-tax + (or (ffirst (dc/q '[:find (sum ?tax) + :with ?e + :in $ [?clients ?start-date ?end-date] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/tax ?tax] + #_[?e :sales-order/charges ?c] + #_[?c :charge/tax ?tax]] + (dc/db conn) + [[c] date date])) + 0.0) + :sales-summary/total-tip + (or (ffirst (dc/q '[:find (sum ?tip) + :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/tip ?tip]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-card-payments + (or (ffirst (dc/q '[:find (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 "CARD"] + [?c :charge/total ?total]] + (dc/db conn) + [[c] date date])) + 0.0) + :sales-summary/total-card-fees + (or (ffirst (dc/q '[:find ?f + :in $ ?client ?d + :where + [?e :expected-deposit/client ?client] + [?e :expected-deposit/sales-date ?d] + [?e :expected-deposit/fee ?f]] + (dc/db conn) + c + date)) + 0.0) + :sales-summary/total-card-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-refund/type "CARD"] + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-cash-payments + (or (ffirst (dc/q '[:find (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/total ?total] + [?c :charge/type-name "CASH"]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-cash-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-refund/type "CASH"] + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0) + + :sales-summary/total-food-app-payments + + (or (ffirst (dc/q '[:find (sum ?total) + :with ?c + :in $ [?clients ?start-date ?end-date] [?processor ...] + :where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + [?e :sales-order/charges ?c] + [?c :charge/processor ?processor] + [?c :charge/total ?total]] + (dc/db conn) + [[c] date date] + #{:ccp-processor/toast + #_:ccp-processor/ezcater + #_:ccp-processor/koala + :ccp-processor/doordash + :ccp-processor/grubhub + :ccp-processor/uber-eats})) + 0.0) + :sales-summary/total-food-app-refunds + (or (ffirst (dc/q '[:find (sum ?t) + :in $ [?clients ?start-date ?end-date] + :where + :where [(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]] + (not [?e :sales-refund/type "CASH"]) + (not [?e :sales-refund/type "CARD"]) + [?e :sales-refund/total ?t]] + (dc/db conn) + [[c] date date])) + 0.0)}] + (when (seq (:sales-summary/sales-items result)) + (alog/info ::upserting-summaries + :category-count (count (:sales-summary/sales-items result))) + @(dc/transact conn [ [:upsert-entity result]])))))) + +(defn reset-summaries [] + @(dc/transact conn (->> (dc/q '[:find ?sos + :in $ + :where [?sos :sales-summary/client]] + (dc/db conn)) + (map (fn [[sos]] + [:db/retractEntity sos]))))) + + + + +(comment + (auto-ap.datomic/transact-schema conn) + + (apply mark-dirty [:client/code "NGCL"] (last-n-days 12)) + + (mark-all-dirty 30) + + (sales-summaries) + + (dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}]) + :in $ + :where [?sos :sales-summary/client [:client/code "NGCL"]] + [?sos :sales-summary/date ?d] + [(= ?d #inst "2024-03-25T00:00:00-07:00")]] + (dc/db conn)) + + + + + + + + + ) + + + +(defn -main [& _] + (execute "sales-summaries" sales-summaries)) + \ No newline at end of file diff --git a/src/clj/auto_ap/parse/templates.clj b/src/clj/auto_ap/parse/templates.clj index e5f3cb7b..400b4a4d 100644 --- a/src/clj/auto_ap/parse/templates.clj +++ b/src/clj/auto_ap/parse/templates.clj @@ -99,7 +99,7 @@ {:vendor "Southern Glazers" :keywords [#"Southern Glazer's"] :extract {:date #"INVOICE DATE(?s:.*)(?= (?:[0-9]+/[0-9]+/[0-9]+)\s+([0-9]+/[0-9]+/[0-9]+)) " - :invoice-number #"INVOICE\n(?:.*?)(?=\d{4,})(\d+)" + :invoice-number #"(?s)INVOICE\n(?:.*?)(?=\d{4,})(\d+)" :total #"PAY THIS AMOUNT(?s:.*)(?= ([0-9,]+\.[0-9]{2}))" :account-number #"ACCOUNT #.*\n.*?[\n]?\s+(\d+)"} :parser {:date [:clj-time "MM/dd/yy"] @@ -317,6 +317,17 @@ :parser {:date [:clj-time "MM/dd/yyyy"] :total [:trim-commas nil]}} + +;; Breakthru Bev + {:vendor "Wine Warehouse" + :keywords [#"BREAKTHRU BEVERAGE"] + :extract {:date #"INVOICE DATE\s+([0-9]+/[0-9]+/[0-9]+)" + :account-number #"CUSTOMER NUMBER\s+(\d+)" + :invoice-number #"INV #\s+(.*?)\n" + :total #"INVOICE AMOUNT\s+\$\s*([\d,\.\-]+)"} + :parser {:date [:clj-time "MM/dd/yyyy"] + :total [:trim-commas nil]}} + ;; THE WATER PROS {:vendor "The Water Pros" :keywords [#"The Water Pros, Inc"] diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index c23b7193..facf2961 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -31,49 +31,49 @@ (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") d)) -(def manifold-api-stream +(def manifold-api-stream (let [stream (s/stream 100)] (->> stream (s/throttle 50) (s/map (fn [[request attempt response-deferred]] (de/catch - (de/chain - (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") - (try - (client/request (assoc request - :socket-timeout 10000 - :connection-timeout 10000 - #_#_:connection-request-timeout 5000 - :as :json)) - (catch Throwable e - (log/warn ::raw-request-failed - :exception e) - (throw e))))) - (de/catch - (fn [e] - (if (>= attempt 5) - (throw e) - (de/chain - (mt/in 10000 (fn [] 1)) - (fn [_] (de/recur (inc attempt))))))) - (de/chain identity))) - (fn [result] - (de/success! response-deferred result))) - (fn [error] - (de/error! response-deferred error))))) + (de/chain + (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") + (try + (client/request (assoc request + :socket-timeout 10000 + :connection-timeout 10000 + #_#_:connection-request-timeout 5000 + :as :json)) + (catch Throwable e + (log/warn ::raw-request-failed + :exception e) + (throw e))))) + (de/catch + (fn [e] + (if (>= attempt 5) + (throw e) + (de/chain + (mt/in 10000 (fn [] 1)) + (fn [_] (de/recur (inc attempt))))))) + (de/chain identity))) + (fn [result] + (de/success! response-deferred result))) + (fn [error] + (de/error! response-deferred error))))) (s/buffer 50) (s/realize-each) (s/consume (fn [_] #_(log/info ::request-completed - :source "Square 3" - :background-job "Square 3") + :source "Square 3" + :background-job "Square 3") nil))) stream)) @@ -89,18 +89,18 @@ (defn client-locations [client] (capture-context->lc - (de/catch - (de/chain (manifold-api-call - {:url "https://connect.squareup.com/v2/locations" - :method :get - :headers (client-base-headers client)}) - :body - :locations) - (fn [error] - (mu/with-context lc - (log/error ::no-locations-found - :exception error)) - [])))) + (de/catch + (de/chain (manifold-api-call + {:url "https://connect.squareup.com/v2/locations" + :method :get + :headers (client-base-headers client)}) + :body + :locations) + (fn [error] + (mu/with-context lc + (log/error ::no-locations-found + :exception error)) + [])))) (def item-cache (atom {})) @@ -136,25 +136,25 @@ (fn [item] (mu/with-context lc (item->category-name-impl client item version)))) - (fn [e] - (log/warn ::couldnt-fetch-variation - :exception e) - "Uncategorized")) + (fn [e] + (log/warn ::couldnt-fetch-variation + :exception e) + "Uncategorized")) (:category_id (:item_data item)) (de/catch (de/chain (fetch-catalog-cache client (:category_id (:item_data item)) version) :category_data :name) - (fn [e] - (log/warn ::couldnt-fetch-category - :exception e) - "Uncategorized")) + (fn [e] + (log/warn ::couldnt-fetch-category + :exception e) + "Uncategorized")) (:item_data item) "Uncategorized" :else - (do + (do (log/warn ::no-look-up-item :item item) "Uncategorized")))) @@ -163,29 +163,25 @@ (defn item-id->category-name [client i version] (capture-context->lc (-> [client i] - (de/chain + (de/chain (fn [[client i]] (if (str/blank? i) "Uncategorized" (de/catch (de/chain (fetch-catalog-cache client i version) #(mu/with-context lc (item->category-name-impl client % version))) - (fn [error] - (log/warn ::couldnt-fetch-item - :exception error) - "Uncategorized" - (throw error))))))))) + (fn [error] + (log/warn ::couldnt-fetch-item + :exception error) + "Uncategorized" + (throw error))))))))) (defn pc [start end] {"query" {"filter" {"date_time_filter" - { - "created_at" { - "start_at" (->square-date start) - "end_at" (->square-date end) - }}} + {"created_at" {"start_at" (->square-date start) + "end_at" (->square-date end)}}} - "sort" { - "sort_field" "CREATED_AT" + "sort" {"sort_field" "CREATED_AT" "sort_order" "DESC"}}}) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} @@ -195,18 +191,17 @@ :location location :order-id order-id) (let [result (->> (client/get (str "https://connect.squareup.com/v2/orders/" order-id) - {:headers (client-base-headers client) - :as :json}) - :body - )] + {:headers (client-base-headers client) + :as :json}) + :body)] result))) (defn continue-search [client location start end cursor] (log/info ::continue-order-search :cursor cursor) - + (capture-context->lc - (de/chain (manifold-api-call + (de/chain (manifold-api-call {:url "https://connect.squareup.com/v2/orders/search" :method :post :headers (client-base-headers client) @@ -218,23 +213,23 @@ :body (fn [result] (mu/with-context - lc + lc (log/info ::orders-found :count (count (:orders result))) (if (not-empty (:cursor result)) (de/chain (continue-search client location start end (:cursor result)) (fn [continued-results] (mu/with-context - lc + lc (concat (:orders result) continued-results)))) (:orders result))))))) (defn search ([client location start end] - (capture-context->lc + (capture-context->lc (log/info ::searching - :location (:square-location/client-location location)) + :location (:square-location/client-location location)) (de/chain (manifold-api-call {:url "https://connect.squareup.com/v2/orders/search" :method :post :headers (client-base-headers client) @@ -268,20 +263,19 @@ (:sales-order/service-charge i)) (:sales-order/returns i) - (:sales-order/discount i) - ))) + (:sales-order/discount i)))) 0.0 [])) (defn tender->charge [order client location t] - (remove-nils - #:charge + (remove-nils + #:charge {:type-name (:type t) :date (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) :note (:note t) :location (:square-location/client-location location) - :reference-link (str (url/url "https://squareup.com/receipt/preview" (:id t) )) + :reference-link (str (url/url "https://squareup.com/receipt/preview" (:id t))) :external-id (when (:id t) (str "square/charge/" (:id t))) :processor (condp = (:type t) @@ -317,98 +311,97 @@ (defn order->sales-order [client location order] (capture-context->lc (let [is-order-only-for-charge? (= ["CUSTOM_AMOUNT"] - (mapv :item_type (:line_items order )))] + (mapv :item_type (:line_items order)))] (if is-order-only-for-charge? - (de/success-deferred + (de/success-deferred (->> (:tenders order) (map #(tender->charge order client location %)))) - (de/catch - (de/let-flow [line-items - (->> - (or (:line_items order) []) - (s/->source) - (s/transform - (map-indexed (fn [i li] - (mu/with-context lc - (-> - (de/let-flow [category (item-id->category-name client (:catalog_object_id li) (:catalog_version li))] - (remove-nils - #:order-line-item - {:external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order) "-" i) - :item-name (:name li) - :category (if (= "GIFT_CARD" (:item_type li)) - "Gift Card" - category) - :total (amount->money (:total_money li)) - :tax (amount->money (:total_tax_money li)) - :discount (amount->money (:total_discount_money li))})) - (de/catch (fn [e] - (log/error ::cant-transform - :exception e - :line-item li))))))) - ) - (s/buffer 5) - (s/realize-each) - (s/reduce conj []))] - [(remove-nils - #:sales-order - {:date (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) - :external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order)) - :source (or (:name (:source order)) - "Square") - :vendor :vendor/ccp-square + (de/catch + (de/let-flow [line-items + (->> + (or (:line_items order) []) + (s/->source) + (s/transform + (map-indexed (fn [i li] + (mu/with-context lc + (-> + (de/let-flow [category (item-id->category-name client (:catalog_object_id li) (:catalog_version li))] + (remove-nils + #:order-line-item + {:external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order) "-" i) + :item-name (:name li) + :category (if (= "GIFT_CARD" (:item_type li)) + "Gift Card" + category) + :total (amount->money (:total_money li)) + :tax (amount->money (:total_tax_money li)) + :discount (amount->money (:total_discount_money li))})) + (de/catch (fn [e] + (log/error ::cant-transform + :exception e + :line-item li)))))))) + (s/buffer 5) + (s/realize-each) + (s/reduce conj []))] + [(remove-nils + #:sales-order + {:date (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) + :external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order)) + :source (or (:name (:source order)) + "Square") + :vendor :vendor/ccp-square - :reference-link (str (url/url "https://squareup.com/dashboard/sales/transactions" (:id order) "by-unit" (:square-location/square-id location))) - :total (-> order :net_amounts :total_money amount->money) - :tax (-> order :net_amounts :tax_money amount->money) - :tip (-> order :net_amounts :tip_money amount->money) - :discount (-> order :net_amounts :discount_money amount->money) - :service-charge (-> order :net_amounts :service_charge_money amount->money) - :returns (+ (- (-> order :return_amounts :total_money amount->money) - (-> order :return_amounts :tax_money amount->money) - (-> order :return_amounts :tip_money amount->money) - (-> order :return_amounts :service_charge_money amount->money)) - (-> order :return_amounts :discount_money amount->money)) - :charges (->> (:tenders order) - (map #(tender->charge order client location %))) - :line-items line-items})]) - (fn [e] - (log/error ::failed-to-transform-order - :exception e))))))) + :reference-link (str (url/url "https://squareup.com/dashboard/sales/transactions" (:id order) "by-unit" (:square-location/square-id location))) + :total (-> order :net_amounts :total_money amount->money) + :tax (-> order :net_amounts :tax_money amount->money) + :tip (-> order :net_amounts :tip_money amount->money) + :discount (-> order :net_amounts :discount_money amount->money) + :service-charge (-> order :net_amounts :service_charge_money amount->money) + :returns (+ (- (-> order :return_amounts :total_money amount->money) + (-> order :return_amounts :tax_money amount->money) + (-> order :return_amounts :tip_money amount->money) + (-> order :return_amounts :service_charge_money amount->money)) + (-> order :return_amounts :discount_money amount->money)) + :charges (->> (:tenders order) + (map #(tender->charge order client location %))) + :line-items line-items})]) + (fn [e] + (log/error ::failed-to-transform-order + :exception e))))))) (defn daily-results ([client location] (daily-results client location (time/plus (time/now) (time/days -7)) (time/now))) ([client location start end] (capture-context->lc - (-> - (de/chain (search client location start end) - (fn [search-results] - (->> (or search-results []) - (s/->source) - (s/filter (fn [order] + (-> + (de/chain (search client location start end) + (fn [search-results] + (->> (or search-results []) + (s/->source) + (s/filter (fn [order] ;; sometimes orders stay open in square. At least one payment ;; is needed to import, in order to avoid importing orders in-progress. - (and - (or (> (count (:tenders order)) 0) - (seq (:returns order))) - (or (= #{} (set (map #(:status (:card_details %)) (:tenders order)))) - (not= #{} (set/difference - (set (map #(:status (:card_details %)) (:tenders order))) - #{"FAILED" "VOIDED"})))))) - (s/map #(mu/with-context lc (order->sales-order client location %))) - (s/buffer 10) - (s/realize-each) - (s/reduce into [])))) - (de/catch (fn [e] - (log/error ::cant-create-results - :exception e))))))) + (and + (or (> (count (:tenders order)) 0) + (seq (:returns order))) + (or (= #{} (set (map #(:status (:card_details %)) (:tenders order)))) + (not= #{} (set/difference + (set (map #(:status (:card_details %)) (:tenders order))) + #{"FAILED" "VOIDED"})))))) + (s/map #(mu/with-context lc (order->sales-order client location %))) + (s/buffer 10) + (s/realize-each) + (s/reduce into [])))) + (de/catch (fn [e] + (log/error ::cant-create-results + :exception e))))))) (defn get-payment [client p] - (de/chain (manifold-api-call + (de/chain (manifold-api-call {:url (str "https://connect.squareup.com/v2/payments/" p) :method :get :headers (client-base-headers client)}) @@ -418,74 +411,73 @@ (defn continue-payout-entry-list [c l poi cursor] (capture-context->lc lc - (de/chain - (manifold-api-call - {:url (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries" "?cursor=" cursor ) - :method :get - :headers (client-base-headers c "2023-04-19") - :as :json}) - :body - (fn [result] - (mu/with-context lc - (log/info ::payout-list-found - :count (count (:payout_entries result))) - (if (not-empty (:cursor result)) - (de/chain (continue-payout-entry-list c l poi (:cursor result)) - (fn [continued-results] - (mu/with-context lc - (concat (:payout_entries result) continued-results)))) - (:payout_entries result))))))) + (de/chain + (manifold-api-call + {:url (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries" "?cursor=" cursor) + :method :get + :headers (client-base-headers c "2023-04-19") + :as :json}) + :body + (fn [result] + (mu/with-context lc + (log/info ::payout-list-found + :count (count (:payout_entries result))) + (if (not-empty (:cursor result)) + (de/chain (continue-payout-entry-list c l poi (:cursor result)) + (fn [continued-results] + (mu/with-context lc + (concat (:payout_entries result) continued-results)))) + (:payout_entries result))))))) (defn get-payout-entry-list [c l poi] (capture-context->lc lc - (de/chain - (manifold-api-call - {:url (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries") - :method :get - :headers (client-base-headers c "2023-04-19") - :as :json}) - :body - (fn [result] - (mu/with-context lc - (log/info ::payout-list-found - :count (count (:payout_entries result))) - (if (not-empty (:cursor result)) - (de/chain (continue-payout-entry-list c l poi (:cursor result)) - (fn [continued-results] - (mu/with-context lc - (concat (:payout_entries result) continued-results)))) - (:payout_entries result))))))) + (de/chain + (manifold-api-call + {:url (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries") + :method :get + :headers (client-base-headers c "2023-04-19") + :as :json}) + :body + (fn [result] + (mu/with-context lc + (log/info ::payout-list-found + :count (count (:payout_entries result))) + (if (not-empty (:cursor result)) + (de/chain (continue-payout-entry-list c l poi (:cursor result)) + (fn [continued-results] + (mu/with-context lc + (concat (:payout_entries result) continued-results)))) + (:payout_entries result))))))) (defn payouts ([client location] (payouts client location (time/plus (time/now) (time/days -7)) (time/now))) ([client location start end] (with-context-as {:location (:square-location/client-location location)} lc - (de/chain (manifold-api-call - {:url (str "https://connect.squareup.com/v2/payouts/?" - (url/map->query - {:location_id (:square-location/square-id location) - :begin_time (->square-date start) - :end_time (->square-date end)}) ) - :method :get - :headers (client-base-headers client "2023-04-19") - }) - :body - :payouts - (fn [payouts] - (if (seq payouts) - (->> payouts - (s/->source) - (s/map (fn [payout] - (mu/with-context lc - (log/info ::looking-up-payout - :payout-id (:id payout)) - (de/chain (get-payout-entry-list client location (:id payout)) - (fn [payout-entries] - (assoc payout :payout_entries payout-entries )))))) - (s/buffer 10) - (s/realize-each) - (s/reduce conj [])) - [])))))) + (de/chain (manifold-api-call + {:url (str "https://connect.squareup.com/v2/payouts/?" + (url/map->query + {:location_id (:square-location/square-id location) + :begin_time (->square-date start) + :end_time (->square-date end)})) + :method :get + :headers (client-base-headers client "2023-04-19")}) + :body + :payouts + (fn [payouts] + (if (seq payouts) + (->> payouts + (s/->source) + (s/map (fn [payout] + (mu/with-context lc + (log/info ::looking-up-payout + :payout-id (:id payout)) + (de/chain (get-payout-entry-list client location (:id payout)) + (fn [payout-entries] + (assoc payout :payout_entries payout-entries)))))) + (s/buffer 10) + (s/realize-each) + (s/reduce conj [])) + [])))))) (defn transformed-payouts ([client location] @@ -493,12 +485,12 @@ ([client location start end] (transformed-payouts client location (payouts client location start end))) ([client location payouts] - (with-context-as {:location (:square-location/client-location location)} lc + (with-context-as {:location (:square-location/client-location location)} lc (de/chain payouts (fn [payouts] (mu/with-context lc (log/info ::transforming-payouts) - (try + (try (->> (for [payout payouts :let [best-sales-date (some->> (dc/q '[:find ?s4 (count ?s) :in $ ?payout-id @@ -518,7 +510,7 @@ coerce/to-date-time atime/as-local-time coerce/to-date) - + ;; TODO delete this - this is only needed during the short transformation time equivalent-already-exists? (seq (dc/q '[:find ?s :in $ ?c ?a @@ -530,12 +522,10 @@ [?s :expected-deposit/external-id ?eid] [(clojure.string/includes? ?eid "settlement")] [?s :expected-deposit/total ?t] - [(iol-ion.query/dollars= ?t ?a)] - ] + [(iol-ion.query/dollars= ?t ?a)]] (dc/db conn) (:db/id client) - (amount->money (:amount_money payout)) - ))] + (amount->money (:amount_money payout))))] :when (not equivalent-already-exists?)] #:expected-deposit {:external-id (str "square/payout/" (:id payout)) :vendor :vendor/ccp-square @@ -574,7 +564,7 @@ (fn [refunds] (->> refunds (filter (fn [r] (= "COMPLETED" (:status r)))) - (s/->source ) + (s/->source) (s/map (fn [r] (de/chain (get-payment client (:payment_id r)) @@ -597,20 +587,20 @@ (s/realize-each) (s/reduce conj [])))))) (defn upsert - ([client ] + ([client] (apply de/zip (for [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] (upsert client square-location (time/plus (time/now) (time/days -14)) (time/now))))) ([client location start end] (capture-context->lc - (de/chain (daily-results client location start end) - (fn [results ] - (mu/with-context lc - (doseq [x (partition-all 100 results)] - (log/info ::loading-orders - :count (count x)) - @(dc/transact-async conn x)))))))) + (de/chain (daily-results client location start end) + (fn [results] + (mu/with-context lc + (doseq [x (partition-all 100 results)] + (log/info ::loading-orders + :count (count x)) + @(dc/transact-async conn x)))))))) (defn upsert-payouts @@ -624,7 +614,7 @@ ([client location start end] (with-context-as {:source "Square payout loading" :client (:client/code client)} lc - + (de/chain (transformed-payouts client location start end) (fn [payouts] (mu/with-context lc @@ -637,35 +627,33 @@ (defn upsert-refunds ([client] - (apply de/zip + (apply de/zip (for [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] (upsert-refunds client square-location)))) ([client location] (with-context-as {:source "Square refunds loading" :client (:client/code client)} lc - + (de/chain (refunds client location) (fn [refunds] (mu/with-context lc - (try + (try (doseq [x (partition-all 100 refunds)] (log/info ::loading-refunds :count (count x) :sample (first x)) @(dc/transact-async conn x)) - + (catch Throwable e (log/error ::upsert-refunds-failed :exception e))) - + (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 - - )) + (de/chain (manifold-api-call {:url (str (url/url "https://connect.squareup.com/v2/cash-drawers/shifts" id)) :method :get :headers (client-base-headers client "2023-04-19") @@ -680,11 +668,11 @@ (de/chain (manifold-api-call {:url (str "https://connect.squareup.com/v2/cash-drawers/shifts" "?" - (url/map->query - {:location_id (:square-location/square-id l) - :begin_time (->square-date start) - :end_time (->square-date end) - :limit 1000})) + (url/map->query + {:location_id (:square-location/square-id l) + :begin_time (->square-date start) + :end_time (->square-date end) + :limit 1000})) :method :get :headers (client-base-headers client "2023-04-19") @@ -694,49 +682,48 @@ (fn [shifts] (->> shifts (filter (fn [r] (= "ENDED" (:state r)))) - (s/->source ) + (s/->source) (s/map (fn [s] (de/chain - (get-cash-shift client (:id s)) - (fn [cash-drawer-shift] - #:cash-drawer-shift {:external-id (str "square/cash-drawer-shift/" (:id cash-drawer-shift)) - :vendor :vendor/ccp-square - :paid-in (amount->money (:cash_paid_in_money cash-drawer-shift)) - :paid-out (amount->money (:cash_paid_out_money cash-drawer-shift)) - :expected-cash (amount->money (:expected_cash_money cash-drawer-shift)) - :opened-cash (amount->money (:opened_cash_money cash-drawer-shift)) - :date (coerce/to-date (:opened_at cash-drawer-shift)) - :client (:db/id client) - :location (:square-location/client-location l) - })))) + (get-cash-shift client (:id s)) + (fn [cash-drawer-shift] + #:cash-drawer-shift {:external-id (str "square/cash-drawer-shift/" (:id cash-drawer-shift)) + :vendor :vendor/ccp-square + :paid-in (amount->money (:cash_paid_in_money cash-drawer-shift)) + :paid-out (amount->money (:cash_paid_out_money cash-drawer-shift)) + :expected-cash (amount->money (:expected_cash_money cash-drawer-shift)) + :opened-cash (amount->money (:opened_cash_money cash-drawer-shift)) + :date (coerce/to-date (:opened_at cash-drawer-shift)) + :client (:db/id client) + :location (:square-location/client-location l)})))) (s/buffer 5) (s/realize-each) (s/reduce conj [])))))) (defn upsert-cash-shifts ([client] - (apply de/zip + (apply de/zip (for [square-location (:client/square-locations client) :when (:square-location/client-location square-location)] (upsert-cash-shifts client square-location)))) ([client location] (with-context-as {:source "Square cash shift loading" :client (:client/code client)} lc - + (de/chain (cash-drawer-shifts client location) (fn [cash-shifts] (mu/with-context lc - (try + (try (doseq [x (partition-all 100 cash-shifts)] (log/info ::loading-cash-shifts :count (count x) :sample (first x)) @(dc/transact-async conn x)) - + (catch Throwable e (log/error ::upsert-cash-shifts-failed :exception e))) - + (log/info ::done-loading-cash-shifts))))))) (def square-read [:db/id @@ -754,7 +741,7 @@ :in $ :where [?c :client/square-auth-token]] (dc/db conn)))) - ([ & codes] + ([& codes] (map first (dc/q '[:find (pull ?c [:db/id :client/code :client/square-auth-token @@ -768,110 +755,111 @@ (defn get-square-client-and-location [code] (let [[client] (get-square-clients code)] (some->> client - :client/square-locations - (filter :square-location/client-location) - seq - (conj [client])))) + :client/square-locations + (filter :square-location/client-location) + seq + (conj [client])))) (defn upsert-locations ([] - (apply de/zip + (apply de/zip (for [client (get-square-clients)] (upsert-locations client)))) ([client] (let [square-id->id (into {} (map - (fn [sl] - [(:square-location/square-id sl) - (:db/id sl)]) - (:client/square-locations client)))] + (fn [sl] + [(:square-location/square-id sl) + (:db/id sl)]) + (:client/square-locations client)))] (de/chain (client-locations client) (fn [client-locations] @(dc/transact-async conn - (for [square-location client-locations] - {:db/id (or (square-id->id (:id square-location)) (str (java.util.UUID/randomUUID))) - :client/_square-locations (:db/id client) - :square-location/name (:name square-location) - :square-location/square-id (:id square-location)}))))))) + (for [square-location client-locations] + {:db/id (or (square-id->id (:id square-location)) (str (java.util.UUID/randomUUID))) + :client/_square-locations (:db/id client) + :square-location/name (:name square-location) + :square-location/square-id (:id square-location)}))))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn reset [] - (->> - (dc/q {:find ['?e] - :in ['$] - :where ['(or [?e :sales-order/date] - [?e :expected-deposit/date])]} - (dc/db conn)) + (->> + (dc/q {:find ['?e] + :in ['$] + :where ['(or [?e :sales-order/date] + [?e :expected-deposit/date])]} + (dc/db conn)) (map first) (map (fn [x] [:db/retractEntity x])))) (defn mark-integration-status [client integration-status] @(dc/transact-async conn - [{:db/id (:db/id client) - :client/square-integration-status (assoc integration-status - :db/id (or (-> client :client/square-integration-status :db/id) - (str (java.util.UUID/randomUUID))))}])) + [{:db/id (:db/id client) + :client/square-integration-status (assoc integration-status + :db/id (or (-> client :client/square-integration-status :db/id) + (str (java.util.UUID/randomUUID))))}])) -(defn upsert-all [ & clients] +(defn upsert-all [& clients] (capture-context->lc - (log/info ::starting-upsert) - (->> (apply get-square-clients clients) - (s/->source) - (s/filter (fn [client] - (seq (filter :square-location/client-location (:client/square-locations client))))) - (s/map (fn [client] - (with-context-as (merge lc {:client (:client/code client)}) lc - (log/info ::import-started) - (mark-integration-status client {:integration-status/last-attempt (coerce/to-date (time/now))}) + (log/info ::starting-upsert) + (->> (apply get-square-clients clients) + (s/->source) + (s/filter (fn [client] + (seq (filter :square-location/client-location (:client/square-locations client))))) + (s/map (fn [client] + (with-context-as (merge lc {:client (:client/code client)}) lc + (log/info ::import-started) + (mark-integration-status client {:integration-status/last-attempt (coerce/to-date (time/now))}) - (-> - (de/chain (upsert-locations client) - (fn [_] - (mu/with-context lc - (log/info ::upsert-orders-started) - (upsert client))) - (fn [_] - (mu/with-context lc - (log/info ::upsert-payouts-started) - (upsert-payouts client))) - (fn [_] - (mu/with-context lc - (log/info ::upsert-refunds-started) - (upsert-refunds client))) + (-> + (de/chain (upsert-locations client) + (fn [_] + (mu/with-context lc + (log/info ::upsert-orders-started) + (upsert client))) + (fn [_] + (mu/with-context lc + (log/info ::upsert-payouts-started) + (upsert-payouts client))) + (fn [_] + (mu/with-context lc + (log/info ::upsert-refunds-started) + (upsert-refunds client))) - (fn [_] - (mu/with-context lc - (log/info ::upsert-cash-shifts) - (upsert-cash-shifts client))) - (fn [_] - (mu/with-context lc - (log/info ::upsert-done)) - (mark-integration-status client {:integration-status/state :integration-state/success - :integration-status/last-updated (coerce/to-date (time/now))}))) - (de/catch (fn [e] - (mu/with-context lc - (let [data (ex-data e)] - (log/info ::upsert-all-failed - :severity :error - :exception e) - (cond (= (:status data) 401) - (mark-integration-status client {:integration-status/state :integration-state/unauthorized - :integration-status/message (-> data :body str)}) + (fn [_] + (mu/with-context lc + (log/info ::upsert-cash-shifts) + (upsert-cash-shifts client))) + (fn [_] + (mu/with-context lc + (log/info ::upsert-done)) + (mark-integration-status client {:integration-status/state :integration-state/success + :integration-status/last-updated (coerce/to-date (time/now))}))) + (de/catch (fn [e] + (mu/with-context lc + (let [data (ex-data e)] + (log/info ::upsert-all-failed + :severity :error + :exception e) + (cond (= (:status data) 401) + (mark-integration-status client {:integration-status/state :integration-state/unauthorized + :integration-status/message (-> data :body str)}) - (= (:status data) 503) - (mark-integration-status client {:integration-status/state :integration-state/failed - :integration-status/message (-> data :body str)}) - :else - (mark-integration-status client {:integration-status/state :integration-state/failed - :integration-status/message (or (ex-message e) - (str e))})))))))))) - (s/buffer 5) - (s/realize-each) - (s/reduce conj [])))) + (= (:status data) 503) + (mark-integration-status client {:integration-status/state :integration-state/failed + :integration-status/message (-> data :body str)}) + :else + (mark-integration-status client {:integration-status/state :integration-state/failed + :integration-status/message (or (ex-message e) + (str e))})))))))))) + (s/buffer 5) + (s/realize-each) + (s/reduce conj [])))) (defn do-upsert-all [& clients] (mu/trace - ::upsert-all - [:clients clients] - @(apply upsert-all clients))) + ::upsert-all + [:clients clients] + @(apply upsert-all clients))) + diff --git a/src/clj/auto_ap/ssr/admin/sales_summaries.clj b/src/clj/auto_ap/ssr/admin/sales_summaries.clj new file mode 100644 index 00000000..ad99ae4d --- /dev/null +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -0,0 +1,227 @@ +(ns auto-ap.ssr.admin.sales-summaries + (:require [auto-ap.datomic + :refer [apply-pagination apply-sort-3 conn merge-query pull-many + query2]] + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.routes.admin.sales-summaries :as route] + [auto-ap.routes.utils + :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.utils + :refer [apply-middleware-to-all-handlers]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [iol-ion.query :refer [dollars=]])) + +(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) + "hx-target" "#entity-table" + "hx-indicator" "#entity-table"} + + #_[:fieldset.space-y-6 + (date-range-field {:value {:start (:start-date (:parsed-query-params request)) + :end (:end-date (:parsed-query-params request))} + :id "date-range"}) + (com/field {:label "Source"} + (com/select {:name "source" + :class "hot-filter w-full" + :value (:source (:parsed-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 (:parsed-query-params request)) + :placeholder "11101" + :size :small}))]]) + +(def default-read '[:db/id + [:sales-summary/date :xform clj-time.coerce/from-date] + *]) ;; TODO + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + valid-clients (extract-client-ids (:clients request) + (:client request) + (:client-id query-params) + (when (:client-code query-params) + [:client/code (:client-code query-params)])) + query (cond-> {:query {:find [] + :in '[$ [?client ...]] + :where '[[?e :sales-summary/client ?client]]} + :args [db valid-clients]} + (or (:start-date query-params) + (:end-date query-params)) + (merge-query {:query '{:where [[?e :sales-summary/date ?d]]}}) + + (:start-date query-params) + (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)]]} + :args [(-> query-params :end-date c/to-date)]}) + + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :sales-summary/date ?sort-default]]}}))] + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + refunds (->> ids + (map results) + (map first))] + refunds)) + +(defn 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 get-credits [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))) + :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))) + :discounts (+ (:sales-summary/discount ss 0.0)) + :returns (+ (:sales-summary/total-returns ss 0.0))}) + +(def grid-page + (helper/build {:id "entity-table" + :id-fn :db/id + :nav (com/admin-aside-nav) + :fetch-page fetch-page + :page-specific-nav filters + :row-buttons (fn [_ entity] + []) + :oob-render + (fn [request] + [#_(assoc-in (date-range-field {:value {:start (:start-date (:parsed-query-params request)) + :end (:end-date (:parsed-query-params request))} + :id "date-range"}) [1 :hx-swap-oob] true)]) ;; TODO + :parse-query-params (comp + (helper/default-parse-query-params grid-page)) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :admin)} + "Admin"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + ::route/page)} + "Sales Summaries"]] + :title "Sales Summaries" + :entity-name "Daily Summary" + :route ::route/table + :headers [{:key "date" + :name "Date" + :sort-key "date" + :render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))} + {:key "credits" + :name "Credits" + :sort-key "credits" + :render (fn [ss] + (let [total-credits (reduce + 0.0 (vals (get-credits ss))) + total-debits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) + + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))) + + (:sales-summary/total-tax ss 0.0) + (:sales-summary/total-tip ss 0.0))] + [:ul + (for [[n x] (group-by :sales-summary-item/category (:sales-summary/sales-items ss))] + [:li n ": " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total x)) + (reduce + 0.0 (map :sales-summary-item/discount x))) + (reduce + 0.0 (map :sales-summary-item/tax x))))]) + [:li "Sales subtotal: " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))))] + [:li "Tax: " (format "$%,.2f" (:sales-summary/total-tax ss))] + [:li "Tips: " (format "$%,.2f" (:sales-summary/total-tip ss))] + [:li (com/pill {:color (if (dollars= total-credits total-debits) + :primary + :red)} "Total: " (format "$%,.2f" total-debits))]]) + + #_(count))} + + {:key "debits" + :name "debits" + :sort-key "debits" + :render (fn [ss] + (let [{:keys [card-payments food-app-payments + cash-payments discounts fees + returns] :as credits} (get-credits ss) + total-credits (reduce + 0.0 (vals credits)) + total-debits (+ (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss))) + + (reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss)))) + (reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))) + + (:sales-summary/total-tax ss 0.0) + (:sales-summary/total-tip ss 0.0))] + [:ul + [:li "Card Payments: " + (format "$%,.2f" card-payments)] + + [:li "Food App Payments: " + (format "$%,.2f" food-app-payments)] + [:li "Cash Payments: " + + (format "$%,.2f" cash-payments)] + [:li "Discounts: " + + (format "$%,.2f" discounts)] + + [:li "Fees: " + (format "$%,.2f" fees)] + [:li "Returns: " + (format "$%,.2f" returns)] + + + [:li (com/pill {:color (if (dollars= total-credits total-debits) + :primary + :red)} "Total: " (format "$%,.2f" total-credits))]]) + + #_(count))}]})) + +;; 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)) +(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-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 72976415..c749245f 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -14,6 +14,7 @@ [auto-ap.ssr.admin.transaction-rules :as admin-rules] [auto-ap.ssr.admin.vendors :as admin-vendors] [auto-ap.ssr.admin.clients :as admin-clients] + [auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries] [auto-ap.ssr.auth :as auth] [auto-ap.ssr.indicators :as indicators] [auto-ap.ssr.company :as company] @@ -94,6 +95,7 @@ (into admin-excel-invoices/key->handler) (into admin/key->handler) (into admin-jobs/key->handler) + (into admin-sales-summaries/key->handler) (into admin-vendors/key->handler) (into admin-clients/key->handler) (into admin-rules/key->handler) diff --git a/src/cljc/auto_ap/routes/admin/sales_summaries.cljc b/src/cljc/auto_ap/routes/admin/sales_summaries.cljc new file mode 100644 index 00000000..e5144e71 --- /dev/null +++ b/src/cljc/auto_ap/routes/admin/sales_summaries.cljc @@ -0,0 +1,3 @@ +(ns auto-ap.routes.admin.sales-summaries) +(def routes {"" {:get ::page} + "/table" ::table}) \ No newline at end of file diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index bc7489fe..8f64509a 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -7,6 +7,7 @@ [auto-ap.routes.payments :as p-routes] [auto-ap.routes.invoice :as i-routes] [auto-ap.routes.admin.clients :as ac-routes] + [auto-ap.routes.admin.sales-summaries :as ss-routes] [auto-ap.routes.admin.transaction-rules :as tr-routes])) (def routes {"impersonate" :impersonate @@ -51,7 +52,8 @@ ["/disapprove/" [#"\d+" :transaction-id]] {:delete :transaction-insight-disapprove} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} - "pos" {"/sales" {"" {:get :pos-sales} + "pos" {"/summaries" ss-routes/routes + "/sales" {"" {:get :pos-sales} "/table" {:get :pos-sales-table}} "/expected-deposit" {"" {:get :pos-expected-deposits} "/table" {:get :pos-expected-deposit-table}}