From d1a660c5c1a2c550ca044c4b6fb473a96656b3c3 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 31 Mar 2024 00:22:53 -0700 Subject: [PATCH] Adds rudimentary version of summarizing sales --- resources/schema.edn | 77 +- src/clj/auto_ap/jobs/sales_summaries.clj | 96 +++ src/clj/auto_ap/square/core3.clj | 682 +++++++++--------- src/clj/auto_ap/ssr/admin/sales_summaries.clj | 176 +++++ src/clj/auto_ap/ssr/core.clj | 2 + .../auto_ap/routes/admin/sales_summaries.cljc | 3 + src/cljc/auto_ap/ssr_routes.cljc | 4 +- 7 files changed, 691 insertions(+), 349 deletions(-) create mode 100644 src/clj/auto_ap/jobs/sales_summaries.clj create mode 100644 src/clj/auto_ap/ssr/admin/sales_summaries.clj create mode 100644 src/cljc/auto_ap/routes/admin/sales_summaries.cljc diff --git a/resources/schema.edn b/resources/schema.edn index 9176c9f6..6b6f79ac 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1850,6 +1850,81 @@ :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/total-card-payments + :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..61410139 --- /dev/null +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -0,0 +1,96 @@ +(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 [:client/code "NGOP"] (last-n-days days)))) + +(comment + (mark-all-dirty 365)) + +(str (c/to-local-date (atime/localize (time/now)))) + +(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] + :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]} (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 {:sales-summary/client c + :sales-summary/date (c/to-date (atime/parse date atime/normal-date)) + :sales-summary/dirty false + :sales-summary/client+date [c (c/to-date (atime/parse date atime/normal-date))] + :sales-summary/sales-items + + (for [[item-name category total tax discount] sales] + {: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})}] + (when (seq (:sales-summary/sales-items result)) + (alog/info ::upserting-summaries + :category-count (count (:sales-summary/sales-items result))) + @(dc/transact conn [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]))))) + +(defn -main [& _] + (execute "sales-summaries" sales-summaries)) + \ No newline at end of file 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..4fbd80b0 --- /dev/null +++ b/src/clj/auto_ap/ssr/admin/sales_summaries.clj @@ -0,0 +1,176 @@ +(ns auto-ap.ssr.admin.sales-summaries + (:require [auto-ap.client-routes :as client-routes] + [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.components.date-range :refer [date-range-field]] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.svg :as svg] + [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] + [hiccup.util :as hu])) + +(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])) + +(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 "items" + :name "items" + :sort-key "source" + :render (fn [ss] + [: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)))])] + + #_(count ))} + {:key "payments" + :name "payments" + :sort-key "payments" + :render (fn [ss] + [:ul + [:li "Card Payments: " + (format "$%,.2f" (:sales-summary/total-card-payments ss 0.0)) + + ", fees: " + (format "$%,.2f" (:sales-summary/total-card-fees ss 0.0)) + + ", refunds: " + (format "$%,.2f" (:sales-summary/total-card-refunds ss 0.0))] + + [:li "Cash Payments: " + (format "$%,.2f" (:sales-summary/total-cash-payments ss 0.0)) + + ", refunds: " + (format "$%,.2f" (:sales-summary/total-cash-refunds ss 0.0))] + + [:li "Food App Payments: " + (format "$%,.2f" (:sales-summary/total-food-app-payments ss 0.0)) + + ", refunds: " + (format "$%,.2f" (:sales-summary/total-food-app-refunds ss 0.0)) ]] + + #_(count ))}]})) + +(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 c808e95a..724bffbc 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -13,6 +13,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] @@ -91,6 +92,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 25668388..5888e8fe 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -5,6 +5,7 @@ [auto-ap.routes.indicators :as indicator-routes] [auto-ap.routes.admin.vendors :as v-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 @@ -53,7 +54,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}}