diff --git a/.projectile b/.projectile new file mode 100644 index 00000000..51430329 --- /dev/null +++ b/.projectile @@ -0,0 +1,4 @@ +-/target +-/node_modules +-/data +-/datomic-init diff --git a/resources/public/img/ezcater.png b/resources/public/img/ezcater.png new file mode 100644 index 00000000..09006441 Binary files /dev/null and b/resources/public/img/ezcater.png differ diff --git a/resources/public/img/koala.png b/resources/public/img/koala.png new file mode 100644 index 00000000..d39976a8 Binary files /dev/null and b/resources/public/img/koala.png differ diff --git a/resources/public/img/square.png b/resources/public/img/square.png new file mode 100644 index 00000000..4d33275f Binary files /dev/null and b/resources/public/img/square.png differ diff --git a/scratch-sessions/dynamo-replica.clj b/scratch-sessions/dynamo-replica.clj new file mode 100644 index 00000000..68e8bc2f --- /dev/null +++ b/scratch-sessions/dynamo-replica.clj @@ -0,0 +1,63 @@ +;; This buffer is for Clojure experiments and evaluation. + +;; Press C-j to evaluate the last expression. + +;; You can also press C-u C-j to evaluate the expression and pretty-print its result. + +(ns dynamo-replica + (:require [amazonica.aws.dynamodbv2 :as ddb] + [datomic.api :as d] + [auto-ap.datomic :refer [conn]])) + +(ddb/delete-table :table-name "sales-replica") + +(ddb/create-table + :table-name "sales-replica" + :key-schema [{:attribute-name "id" :key-type "HASH"} + {:attribute-name "date" :key-type "RANGE"}] + :attribute-definitions + [{:attribute-name "id" :attribute-type "S"} + {:attribute-name "date" :attribute-type "S"} + {:attribute-name "client" :attribute-type "N"}] + :global-secondary-indexes + [{:index-name "client_date" + :key-schema [{:attribute-name "client" :key-type "HASH"} + {:attribute-name "date" :key-type "RANGE"}] + :projection {:projection-type "ALL"} + :provisioned-throughput + {:read-capacity-units 500 + :write-capacity-units 500}}] + :provisioned-throughput + {:read-capacity-units 500 + :write-capacity-units 500}) + +(user/init-repl) + +(doseq [client ["NGAK"] + batch (->> (d/q '[:find [?s ...] + :in $ ?c + :where + [?s :sales-order/client ?c]] + (d/db conn) + [:client/code client]) + + (partition-all 25) + (map (fn [batch] + (for [o (d/pull-many (d/db conn) '[* {:sales-order/charges [*] :sales-order/line-items [*]}] batch)] + {:id (str (:db/id o)) + :client (:db/id (:sales-order/client o)) + :date (str (:sales-order/date o)) + :total (:sales-order/total o) + #_#_:line-items (into-array (for [i (:sales-order/line-items o)] + (java.util.HashMap. {:total (:order-line-item/total i)})))}))))] + (println "Adding 25 ... " (first batch)) + (ddb/batch-write-item :request-items + {"sales-replica" + (for [item batch] + {:put-request {:item item}})}) + (Thread/sleep 50) + #_(ddb/put-item :table-name "sales-replica" + :item item)) + +(doc d/pull-many) + diff --git a/scratch-sessions/working-on-ezcater-square-v2.clj b/scratch-sessions/working-on-ezcater-square-v2.clj new file mode 100644 index 00000000..8a67fda9 --- /dev/null +++ b/scratch-sessions/working-on-ezcater-square-v2.clj @@ -0,0 +1,235 @@ +;; This buffer is for Clojure experiments and evaluation. + +;; Press C-j to evaluate the last expression. + +;; You can also press C-u C-j to evaluate the expression and pretty-print its result. +(ns working-on-ezcater-square-v2 + (:require [auto-ap.square.core :as sc] + [clj-time.core :as time] + [auto-ap.ezcater.core :as ez]) + ) + +(auto-ap.routes.queries/put-query + "ec3bae96-95cd-40aa-b0b1-342ab58efed2" + " [:find ?d4 ?type ?p2 (sum ?total) (sum ?tip) + :with ?charge + :in $ + :where + [?charge :charge/date ?date] + [(ground (clj-time.coerce/to-date (clj-time.core/minus (auto-ap.time/local-now) (clj-time.core/days 50)))) ?min-d] + [(>= ?date ?min-d)] + [?charge :charge/client ?c] + [?c :client/code \"NGOP2\"] + [?charge :charge/type-name ?type] + [?charge :charge/total ?total] + [?charge :charge/tip ?tip] + (or + + (and [_ :expected-deposit/charges ?charge ] + [(ground :settlement) ?ccp] + [(ground :settlement) ?p]) + (and + (not [_ :expected-deposit/charges ?charge]) + [(get-else $ ?charge :charge/processor :na) ?ccp] + [(get-else $ ?ccp :db/ident :na) ?p] + )) + [(name ?p) ?p2] + [(clj-time.coerce/to-date-time ?date) ?d2] + [(auto-ap.time/localize ?d2) ?d3] + [(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]] +", "") + +(user/init-repl) + +(in-ns 'auto-ap.square.core) +(require '[auto-ap.square.core :as sc]) + +(require '[auto-ap.ezcater.core :as ez]) + +(def square-client (first (sc/get-square-clients "NGOP2"))) + + +(def square-location (first (for [client (sc/get-square-clients "NGOP2") + square-location (:client/square-locations client) + :when (:square-location/client-location square-location)] + square-location))) + + +(sc/upsert square-client square-location (time/plus (time/now) (time/days -45)) (time/now)) + +(sc/upsert-settlements square-client square-location) + +(sc/get-order square-client square-location "EYIVOtQj8FgdpgNlpUQP9S51uNbZY" ) +(sc/get-payment square-client "vhnZ67vi7BIO6mZ6R609Vsih7nDZY") +k +(sc/get-order square-client square-location "ggjaHEikb81AKjrJC7H4RRqjDXZZY") + +(sc/get-payment square-client "zhEgwmQL4cI0P00NGBAzqX5gnX6YY") + +(sc/get-order square-client square-location "CdzNsHRryhGNQ9fh4tdYcEBwA4PZY") + +(sc/get-order square-client square-location "hipr01buRJcFg2NFeFTjuJ4eV") + + +(user/start-db) + +(def my-search (doall (search (first (get-square-clients "NGOP2")) + {:db/id 17592186106155, + :square-location/name "Oyster Point", + :square-location/square-id "L3GMNBFARX9GG", + :square-location/client-location "OP"} + #clj-time/date-time "2022-06-30T00:00:00-08:00" + #clj-time/date-time "2022-07-01T00:00:00-08:00" + + ))) + +(def my-search (doall (search (first (get-square-clients "NGOP2")) + {:db/id 17592186106155, + :square-location/name "Oyster Point", + :square-location/square-id "L3GMNBFARX9GG", + :square-location/client-location "OP"} + #clj-time/date-time "2022-07-13T00:00:00-08:00" + #clj-time/date-time "2022-07-14T00:00:00-08:00" + + ))) + +(set/difference + + + (filter + + (comp (set/difference + (set (map :id + (let [search-results my-search + koala-production-ids (->> search-results + (filter #(= "koala-production" (:name (:source %)))) + (map :id) + (into #{}))] + (let [search-results my-search] + (->> search-results + (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"})))))) + (filter (fn [order] + (not= "Koala" (:name (:source order))))) + (filter (fn has-linked-koala-production? [order] + ;; if a POS order is linked (via note) to a koala-production order, we want + ;; to keep the koala-production order, because it has taxes correct + (not (and (:line_items order) ;; returns do not have line items, so they should be allowed + (->> (:line_items order) + (map :note) + (every? koala-production-ids)))))))))) + ) + (set (map :id (->> my-search + (filter (fn [order] + (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"}))) + (not= ["CUSTOM_AMOUNT"] + (mapv :item_type (:line_items order ))) + )))) + ))) + :id + ) + my-search)) + + +(doseq [p (->> + (d/query {:query {:find ['?e] + :in ['$] + :where ['(or [?e :sales-order/client [:client/code "NGOP2"]] + [?e :sales-refund/client [:client/code "NGOP2"]] + [?e :expected-deposit/client [:client/code "NGOP2"]])]} + :args [(d/db conn)]}) + (map first) + (map (fn [x] [:db/retractEntity x])) + (partition-all 100))] + @(d/transact conn p)) + + +(def weird-order + {:total_service_charge_money {:amount 0, :currency "USD"}, + :closed_at "2022-07-14T02:50:43Z", + :total_discount_money {:amount 0, :currency "USD"}, + :total_money {:amount 120533, :currency "USD"}, + :total_tip_money {:amount 0, :currency "USD"}, + :taxes + [{:uid "64a788f3-13c6-45a8-9333-c7e4ed4b45f2", + :catalog_object_id "4XTWECYYOZWTGRJ475C3QOQL", + :catalog_version 1655153159309, + :name "9.875%", + :percentage "9.875", + :type "ADDITIVE", + :applied_money {:amount 10833, :currency "USD"}, + :scope "LINE_ITEM"}], + :net_amounts + {:total_money {:amount 120533, :currency "USD"}, + :tax_money {:amount 10833, :currency "USD"}, + :discount_money {:amount 0, :currency "USD"}, + :tip_money {:amount 0, :currency "USD"}, + :service_charge_money {:amount 0, :currency "USD"}}, + :total_tax_money {:amount 10833, :currency "USD"}, + :location_id "L3GMNBFARX9GG", + :tenders + [{:id "pQgHRVNc3WzBDZVGpORONh5YvaB", + :location_id "L3GMNBFARX9GG", + :transaction_id "XBo0x9zb7rkpWLAJ79cgihweV", + :created_at "2022-07-14T02:50:42Z", + :note "ezcater 7\\13", + :amount_money {:amount 120533, :currency "USD"}, + :processing_fee_money {:amount 0, :currency "USD"}, + :type "OTHER"}], + :state "COMPLETED", + :ticket_name "Ezcater 7-13", + :updated_at "2022-07-14T02:50:43Z", + :net_amount_due_money {:amount 0, :currency "USD"}, + :return_amounts + {:total_money {:amount 0, :currency "USD"}, + :tax_money {:amount 0, :currency "USD"}, + :discount_money {:amount 0, :currency "USD"}, + :tip_money {:amount 0, :currency "USD"}, + :service_charge_money {:amount 0, :currency "USD"}}, + :line_items + [{:total_discount_money {:amount 0, :currency "USD"}, + :total_money {:amount 120533, :currency "USD"}, + :total_tax_money {:amount 10833, :currency "USD"}, + :uid "6a293751-027e-45a0-9f5d-e9f4e7cb62fd", + :variation_total_price_money {:amount 109700, :currency "USD"}, + :item_type "CUSTOM_AMOUNT", + :gross_sales_money {:amount 109700, :currency "USD"}, + :applied_taxes + [{:uid "64a788f3-13c6-45a8-9333-c7e4ed4b45f2", + :tax_uid "64a788f3-13c6-45a8-9333-c7e4ed4b45f2", + :applied_money {:amount 10833, :currency "USD"}}], + :base_price_money {:amount 109700, :currency "USD"}, + :quantity "1"}], + :id "XBo0x9zb7rkpWLAJ79cgihweV", + :created_at "2022-07-14T02:50:43Z"}) + +(order->sales-order "a" "b" weird-order) + + +(doseq [n ["185431988" "186982950" "186344682" "185281242" "184313395" "185477889" "185043055" "184552573" "185042091" "182674641" "184083782" "184200746" "181197904"]] + (clojure.pprint/pprint + @(d/transact auto-ap.datomic/conn [(-> {"id" "bf3dcf5c-a68f-42d9-9084-049133e03d3d", "parent_type" "Caterer", "parent_id" "ef505599-947f-42da-a36c-4b569e33a747", "entity_type" "Order", "entity_id" "178289110", "key" "accepted", "occurred_at" "2022-07-21T19:21:07.549Z"} + (assoc "entity_id" n) + (ez/lookup-order) + (ez/order->sales-order) + (update :sales-order/date coerce/to-date) + (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]))) + +;; +(clojure.pprint/pprint [(-> {"id" "bf3dcf5c-a68f-42d9-9084-049133e03d3d", "parent_type" "Caterer", "parent_id" "91541331-d7ae-4634-9e8b-ccbbcfb2ce70", "entity_type" "Order", "entity_id" "178289110", "key" "accepted", "occurred_at" "2022-07-21T19:21:07.549Z"} + (lookup-order) + #_(order->sales-order) + #_(update :sales-order/date coerce/to-date) + #_(update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]) diff --git a/src/clj/auto_ap/datomic/migrate/sales.clj b/src/clj/auto_ap/datomic/migrate/sales.clj index 13386624..81cb9a98 100644 --- a/src/clj/auto_ap/datomic/migrate/sales.clj +++ b/src/clj/auto_ap/datomic/migrate/sales.clj @@ -292,5 +292,43 @@ :db/index true}]] :requires [:add-orders]} :add-ezcater-vendor {:txes [[{:db/ident :vendor/ccp-ezcater - :vendor/name "EZCater CCP"}]]}}) + :vendor/name "EZCater CCP"}]]} + :add-charge-client {:txes [[{:db/ident :charge/client + :db/doc "The client for the sale" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one} + + {:db/ident :charge/location + :db/doc "The location of the sale" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one}]]} + :add-charge-date {:txes [[{:db/ident :charge/date + :db/doc "The date for the payment" + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one}]]} + :add-ezcater-processor {:txes [[{:db/ident :ccp-processor/ezcater + :db/doc "ezcater processor"}]]} + :add-koala-processor {:txes [[{:db/ident :ccp-processor/koala + :db/doc "koala processor"}]]} + :add-square-processor {:txes [[{:db/ident :ccp-processor/square + :db/doc "square processor"}]]} + :add-reference-link3 {:txes [[{:db/ident :sales-order/reference-link + :db/cardinality :db.cardinality/one + :db/valueType :db.type/string + :db/doc "A link someone can go to to look at the order"} + {:db/ident :charge/reference-link + :db/cardinality :db.cardinality/one + :db/valueType :db.type/string + :db/doc "A link someone can go to to look at the charge"}]]} + :add-source-field2 {:txes [[{:db/ident :sales-order/source + :db/cardinality :db.cardinality/one + :db/valueType :db.type/string + :db/doc "Where the POS says the order is from"} + {:db/ident :charge/note + :db/cardinality :db.cardinality/one + :db/valueType :db.type/string + :db/doc "A custom note"}]]} + :add-feature-flags {:txes [[{:db/ident :client/feature-flags + :db/valueType :db.type/string + :db/cardinality :db.cardinality/many}]]}}) diff --git a/src/clj/auto_ap/datomic/sales_orders.clj b/src/clj/auto_ap/datomic/sales_orders.clj index ecfde7bc..3d624dbe 100644 --- a/src/clj/auto_ap/datomic/sales_orders.clj +++ b/src/clj/auto_ap/datomic/sales_orders.clj @@ -29,6 +29,7 @@ (:sort args) (add-sorter-fields {"client" ['[?e :sales-order/client ?c] '[?c :client/name ?sort-client]] "location" ['[?e :sales-order/location ?sort-location]] + "source" ['[?e :sales-order/source ?sort-source]] "date" ['[?e :sales-order/date ?sort-date]] "total" ['[?e :sales-order/total ?sort-total]] "tax" ['[?e :sales-order/tax ?sort-tax]] @@ -68,6 +69,11 @@ '[?chg :charge/processor ?processor]]} :args [(keyword "ccp-processor" (name (:processor args)))]}) + (:type-name args) + (merge-query {:query {:in ['?type-name] + :where ['[?e :sales-order/charges ?chg] + '[?chg :charge/type-name ?type-name]]} + :args [(:type-name args)]}) (:total-gte args) (merge-query {:query {:in ['?total-gte] diff --git a/src/clj/auto_ap/ezcater/core.clj b/src/clj/auto_ap/ezcater/core.clj index 1be6f0c9..865a4505 100644 --- a/src/clj/auto_ap/ezcater/core.clj +++ b/src/clj/auto_ap/ezcater/core.clj @@ -9,7 +9,8 @@ [clojure.tools.logging :as log] [clj-time.core :as time] [clojure.set :as set] - [auto-ap.time :as atime])) + [auto-ap.time :as atime] + [cemerick.url :as url])) (defn query [{:ezcater-integration/keys [api-key]} q] (-> (client/post "https://api.ezcater.com/graphql/" @@ -118,118 +119,156 @@ [:ezcater-caterer/uuid caterer-uuid])) +(defn round-carry-cents [f] + (with-precision 2 (double (.setScale (bigdec f) 2 java.math.RoundingMode/HALF_UP)))) (defn commision [order] - (let [commision% (if (= "MARKETPLACE" (:orderSourceType order)) - 0.15 - 0.07)] - (* commision% - 0.01 - (+ - (-> order :totals :subTotal :subunits ) - (reduce + - 0 - (map :subunits (:feesAndDiscounts (:catererCart order)))))))) + + (let [commision% (cond + (= "CLUB_SODA" (:orderSourceType order)) + 0.25M + + (= "MARKETPLACE" (:orderSourceType order)) + 0.15M + :else + 0.07M)] + (round-carry-cents + (* commision% + 0.01M + (+ + (-> order :totals :subTotal :subunits ) + (reduce + + 0 + (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order))))))))) (defn ccp-fee [order] - (* 0.000275 - (+ - (-> order :totals :subTotal :subunits ) - (-> order :totals :salesTax :subunits ) - (reduce + - 0 - (map :subunits (:feesAndDiscounts (:catererCart order))))))) + (round-carry-cents + (* 0.000275M + (+ + (-> order :totals :subTotal :subunits ) + (-> order :totals :salesTax :subunits ) + (reduce + + 0 + (map (comp :subunits :cost) (:feesAndDiscounts (:catererCart order)))))))) (defn order->sales-order [{{:keys [timestamp]} :event {:keys [orderItems]} :catererCart :keys [client-code client-location uuid] :as order}] - #:sales-order - {:date (atime/localize (coerce/to-date-time timestamp)) - :external-id (str "ezcater/order/" client-code "-" client-location "-" uuid) - :client [:client/code client-code] - :location client-location - :line-items (->> orderItems - (map-indexed (fn [i li] - #:order-line-item - {:external-id (str "ezcater/order/" client-code "-" client-location "-" uuid "-" i) - :item-name (:name li) - :category "External Catering" - :total (* 0.01 (:subunits (:totalInSubunits li)))}))) + (let [adjustment (round-carry-cents (- (+ (-> order :totals :subTotal :subunits (* 0.01)) + (-> order :totals :salesTax :subunits (* 0.01))) + (-> order :catererCart :totals :catererTotalDue ) + (commision order) + (ccp-fee order))) + service-charge (+ (commision order) (ccp-fee order)) + tax (-> order :totals :salesTax :subunits (* 0.01)) + tip (-> order :totals :tip :subunits (* 0.01))] + #:sales-order + {:date (atime/localize (coerce/to-date-time timestamp)) + :external-id (str "ezcater/order/" client-code "-" client-location "-" uuid) + :client [:client/code client-code] + :location client-location + :reference-link (str (url/url "https://ezmanage.ezcater.com/orders/" uuid )) + :line-items [#:order-line-item + {:external-id (str "ezcater/order/" client-code "-" client-location "-" uuid "-" 0) + :item-name "EZCater Catering" + :category "EZCater Catering" + :discount adjustment + :tax tax + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip)}] + :charges [#:charge + {:type-name "CARD" + :date (atime/localize (coerce/to-date-time timestamp)) + :client [:client/code client-code] + :location client-location + :external-id (str "ezcater/charge/" uuid) + :processor :ccp-processor/ezcater + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip) + :tip tip}] - :total (-> order :catererCart :totals :catererTotalDue ) - :discount (- (+ (-> order :totals :subTotal :subunits (* 0.01)) - (-> order :totals :salesTax :subunits (* 0.01))) - (-> order :catererCart :totals :catererTotalDue ) - (commision order) - (ccp-fee order)) - :service-charge (+ (commision order) (ccp-fee order)) - :tax (-> order :totals :salesTax :subunits (* 0.01)) - :tip (-> order :totals :tip :subunits (* 0.01)) - :vendor :vendor/ccp-ezcater}) + :total (+ (-> order :totals :subTotal :subunits (* 0.01)) + tax + tip) + :discount adjustment + :service-charge service-charge + :tax tax + :tip tip + :returns 0.0 + :vendor :vendor/ccp-ezcater})) (defn lookup-order [json] (let [caterer (get-caterer (get json "parent_id")) integration (:ezcater-integration/_caterers caterer) client (-> caterer :ezcater-location/_caterer first :client/_ezcater-locations :client/code) location (-> caterer :ezcater-location/_caterer first :ezcater-location/location)] - (-> (query - integration - {:venia/queries [[:order {:id (get json "entity_id")} - [:uuid - :orderSourceType - [:caterer - [:name - :uuid - [:address [:street]]]] - [:event - [:timestamp - :catererHandoffFoodTime - :orderType]] - [:catererCart [[:orderItems - [:name - :quantity - :posItemId - [:totalInSubunits - [:currency - :subunits]]]] - [:totals - [:catererTotalDue]] - [:feesAndDiscounts - {:type 'DELIVERY_FEE} - [[:cost - [:currency - :subunits]]]]]] - [:totals [[:customerTotalDue - [ - :currency - :subunits - ]] - [:pointOfSaleIntegrationFee - [ - :currency - :subunits - ]] - [:tip - [:currency - :subunits]] - [:salesTax - [ - :currency - :subunits - ]] - [:salesTaxRemittance - [:currency - :subunits - ]] - [:subTotal - [:currency - :subunits]]]]]]]}) - (:order) - (assoc :client-code client - :client-location location)))) + (doto + (-> (query + integration + {:venia/queries [[:order {:id (get json "entity_id")} + [:uuid + :orderSourceType + [:caterer + [:name + :uuid + [:address [:street]]]] + [:event + [:timestamp + :catererHandoffFoodTime + :orderType]] + [:catererCart [[:orderItems + [:name + :quantity + :posItemId + [:totalInSubunits + [:currency + :subunits]]]] + [:totals + [:catererTotalDue]] + [:feesAndDiscounts + {:type 'DELIVERY_FEE} + [[:cost + [:currency + :subunits]]]]]] + [:totals [[:customerTotalDue + [ + :currency + :subunits + ]] + [:pointOfSaleIntegrationFee + [ + :currency + :subunits + ]] + [:tip + [:currency + :subunits]] + [:salesTax + [ + :currency + :subunits + ]] + [:salesTaxRemittance + [:currency + :subunits + ]] + [:subTotal + [:currency + :subunits]]]]]]]}) + (:order) + (assoc :client-code client + :client-location location)) + log/info))) (defn import-order [json] ;; {"id" "bf3dcf5c-a68f-42d9-9084-049133e03d3d", "parent_type" "Caterer", "parent_id" "91541331-d7ae-4634-9e8b-ccbbcfb2ce70", "entity_type" "Order", "entity_id" "9ab05fee-a9c5-483b-a7f2-14debde4b7a8", "key" "accepted", "occurred_at" "2022-07-21T19:21:07.549Z"} + (clojure.pprint/pprint [(-> json + (lookup-order) + (order->sales-order) + (update :sales-order/date coerce/to-date) + (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]) @(d/transact conn [(-> json (lookup-order) (order->sales-order) - (update :sales-order/date coerce/to-date))]) - ) + (update :sales-order/date coerce/to-date) + (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))])) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 2d64ce86..1d3f2fa9 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -506,6 +506,9 @@ :enums { :processor {:values [{:enum-value :na} {:enum-value :doordash} + {:enum-value :koala} + {:enum-value :ezcater} + {:enum-value :square} {:enum-value :uber_eats} {:enum-value :grubhub}]} :integration_state {:values [{:enum-value :failed} diff --git a/src/clj/auto_ap/graphql/sales_orders.clj b/src/clj/auto_ap/graphql/sales_orders.clj index da260b08..4beb07d0 100644 --- a/src/clj/auto_ap/graphql/sales_orders.clj +++ b/src/clj/auto_ap/graphql/sales_orders.clj @@ -31,6 +31,8 @@ {:fields {:id {:type :id} :location {:type 'String} :external_id {:type 'String} + :reference_link {:type 'String} + :source {:type 'String} :total {:type :money} :tip {:type :money} :tax {:type :money} @@ -50,11 +52,13 @@ :category {:type 'String} :discount {:type :money}}} :charge - {:fields {:id {:type :id} - :processor {:type :processor} - :type_name {:type 'String} - :total {:type :money} - :tip {:type :money} + {:fields {:id {:type :id} + :processor {:type :processor} + :reference_link {:type 'String} + :note {:type 'String} + :type_name {:type 'String} + :total {:type :money} + :tip {:type :money} :expected_deposit {:type :expected_deposit}}}}) (def queries @@ -69,6 +73,7 @@ :date_range {:type :date_range} :total_lte {:type :money} :total_gte {:type :money} + :type_name {:type 'String} :processor {:type :processor} :start {:type 'Int} :per_page {:type 'Int} diff --git a/src/clj/auto_ap/jobs/square2.clj b/src/clj/auto_ap/jobs/square2.clj new file mode 100644 index 00000000..a6510360 --- /dev/null +++ b/src/clj/auto_ap/jobs/square2.clj @@ -0,0 +1,9 @@ +(ns auto-ap.jobs.square2 + (:gen-class) + (:require + [auto-ap.jobs.core :refer [execute]] + [auto-ap.square.core2 :as square2])) + +(defn -main [& _] + (execute "square2-loading" square2/upsert-all)) + diff --git a/src/clj/auto_ap/routes/ezcater.clj b/src/clj/auto_ap/routes/ezcater.clj index 45f1f741..caa1cd62 100644 --- a/src/clj/auto_ap/routes/ezcater.clj +++ b/src/clj/auto_ap/routes/ezcater.clj @@ -17,7 +17,6 @@ (POST "/event" request (log/info (str "POST EVENT " (body-string request) request)) (e/import-order (:json-params request)) - {:status 200 :headers {"Content-Type" "application/json"} :body "{}"})) diff --git a/src/clj/auto_ap/square/core.clj b/src/clj/auto_ap/square/core.clj index 5939f5e1..9590b9e5 100644 --- a/src/clj/auto_ap/square/core.clj +++ b/src/clj/auto_ap/square/core.clj @@ -449,7 +449,8 @@ :client/square-auth-token {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}]) ...] :in $ - :where [?c :client/square-auth-token]] + :where [?c :client/square-auth-token] + (not [?c :client/feature-flags "new-square"])] (d/db conn))) ([ & codes] (d/q '[:find [(pull ?c [:db/id @@ -458,6 +459,7 @@ {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}]) ...] :in $ [?code ...] :where [?c :client/square-auth-token] + (not [?c :client/feature-flags "new-square"]) [?c :client/code ?code]] (d/db conn) codes))) diff --git a/src/clj/auto_ap/square/core2.clj b/src/clj/auto_ap/square/core2.clj new file mode 100644 index 00000000..d4114664 --- /dev/null +++ b/src/clj/auto_ap/square/core2.clj @@ -0,0 +1,541 @@ +(ns auto-ap.square.core2 + (:require + [auto-ap.datomic :refer [conn remove-nils]] + [auto-ap.time :as atime] + [clj-http.client :as client] + [clj-time.coerce :as coerce] + [clj-time.core :as time] + [clj-time.format :as f] + [clj-time.periodic :as periodic] + [clojure.core.async :as async] + [clojure.data.json :as json] + [clojure.set :as set] + [clojure.string :as str] + [clojure.tools.logging :as log] + [cemerick.url :as url] + [datomic.api :as d] + [slingshot.slingshot :refer [try+]] + [unilog.context :as lc])) + +(defn client-base-headers [client] + {"Square-Version" "2021-08-18" + "Authorization" (str "Bearer " (:client/square-auth-token client)) + "Content-Type" "application/json"}) + +(defn retry-4 [ex try-count _] + (log/warn "Retrying after failure " ex) + (if (> try-count 4) false true)) + +(defn lookup-dates [] + (->> (periodic/periodic-seq (time/plus (time/now) (time/days -50)) + (time/now) + (time/days 5)) + (map (fn [d] + [(atime/unparse (time/plus d (time/days 1)) atime/iso-date) + + (atime/unparse (time/plus d (time/days 5)) atime/iso-date)])))) + +(defn client-locations [client] + (try + (->> (client/get "https://connect.squareup.com/v2/locations" + {:headers (client-base-headers client) + :as :json}) + :body + :locations) + (catch Exception e + (log/warn e) + []))) + +(defn fetch-catalog [client i] + (if i + (try + (log/trace "looking up catalog for" (str "https://connect.squareup.com/v2/catalog/object/" i)) + (->> (client/get (str "https://connect.squareup.com/v2/catalog/object/" i) + {:headers (client-base-headers client) + :query-params {"include_related_items" "true"} + :as :json}) + :body + :object) + (catch Exception e + (log/error e) + nil)) + (log/warn "Trying to look up non existant "))) + +(def fetch-catalog-fast (memoize fetch-catalog)) + +(defn item-id->category-name [client i] + (let [item (fetch-catalog-fast client i)] + (cond (:item_variation_data item) + (item-id->category-name client (:item_id (:item_variation_data item))) + + (:category_id (:item_data item)) + (:name (:category_data (fetch-catalog-fast client (:category_id (:item_data item))))) + + (:item_data item) + "Uncategorized" + + :else + (do + (log/warn "couldn't look up" i) + "Uncategorized")))) + +(defn pc [start end] + {"query" {"filter" {"date_time_filter" + { + "created_at" { + "start_at" (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") start) + "end_at" (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") end) + } + + + + }} + + "sort" { + "sort_field" "CREATED_AT" + "sort_order" "DESC" + }}}) + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn get-order + ([client location order-id] + (log/info "Searching for" (:square-location/client-location location)) + (let [result (->> (client/get (str "https://connect.squareup.com/v2/orders/" order-id) + {:headers (client-base-headers client) + :as :json}) + :body + )] + result))) + +(defn continue-search [client location start end cursor] + (log/info "Continuing search for" cursor) + (let [result (->> (client/post "https://connect.squareup.com/v2/orders/search" + {:headers (client-base-headers client) + :body (json/write-str (cond-> {"location_ids" [(:square-location/square-id location)] + "limit" 10000 + "cursor" cursor} + start (merge (pc start end)))) + :as :json}) + :body)] + (log/info "found " (count (:orders result))) + (if (not-empty (:cursor result)) + (concat (:orders result) (continue-search client location start end (:cursor result))) + (:orders result)))) + +(defn search + ([client location start end] + (log/info "Searching for" (:square-location/client-location location)) + (let [result (->> (client/post "https://connect.squareup.com/v2/orders/search" + {:headers (client-base-headers client) + :body (json/write-str (cond-> {"location_ids" [(:square-location/square-id location)] "limit" 10000} + start (merge (pc start end)))) + :as :json}) + :body)] + (log/info "found " (count (:orders result))) + (if (not-empty (:cursor result)) + (concat (:orders result) (continue-search client location start end (:cursor result))) + (:orders result))))) + + + +(defn amount->money [amt] + (* 0.01 (or (:amount amt) 0.0))) + + +;; to get totals: +(comment + (reduce + (fn [total i] + (+ total (+ (- (:sales-order/total i) + (:sales-order/tax i) + (:sales-order/tip i) + (:sales-order/service-charge i)) + (:sales-order/returns i) + + (:sales-order/discount i) + ))) + 0.0 + [])) + +(defn tender->charge [order client location t] + (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) )) + :external-id (when (:id t) + (str "square/charge/" (:id t))) + :processor (condp = (:type t) + "OTHER" + (condp = (some-> (:note t) str/lower-case) + "doordash" :ccp-processor/doordash + "dd" :ccp-processor/doordash + "ubereats" :ccp-processor/uber-eats + "ue" :ccp-processor/uber-eats + "grubhub" :ccp-processor/grubhub + "grub" :ccp-processor/grubhub + "gh" :ccp-processor/grubhub + (condp = (:name (:source order)) + "GRUBHUB" :ccp-processor/grubhub + "UBEREATS" :ccp-processor/uber-eats + "DOORDASH" :ccp-processor/doordash + "Koala" :ccp-processor/koala + "koala-production" :ccp-processor/koala + :ccp-processor/na)) + "CARD" + :ccp-processor/square + + "SQUARE_GIFT_CARD" + :ccp-processor/square + + "CASH" + :ccp-processor/na + + :ccp-processor/na) + :total (amount->money (:amount_money t)) + :tip (amount->money (:tip_money t))})) + +(defn order->sales-order [client location order] + (let [is-order-only-for-charge? + (= ["CUSTOM_AMOUNT"] + (mapv :item_type (:line_items order )))] + (if is-order-only-for-charge? + (->> (:tenders order) + (map #(tender->charge order client location %))) + [(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 order) + (map-indexed (fn [i 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" + (item-id->category-name client (:catalog_object_id li))) + :total (amount->money (:total_money li)) + :tax (amount->money (:total_tax_money li)) + :discount (amount->money (:total_discount_money li))}))))})]))) + +(defn daily-results + ([client location] + (daily-results client location (time/plus (time/now) (time/days -45)) (time/now))) + ([client location start end] + (let [search-results (search client location start end)] + (->> search-results + (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"})))))) + (mapcat #(order->sales-order client location %)))))) + + +(defn retry + ([f] (retry f 0)) + ([f i] + (if (< i 5) + (try + (f) + (catch Exception e + (log/warn "error pulling http " e) + (retry f (inc i)))) + (log/warn "Too many failures")))) + +(defn get-payment [client p] + (:payment (:body (retry #(client/get (str "https://connect.squareup.com/v2/payments/" p) + {:headers (client-base-headers client) + :as :json + :retry-handler retry-4}))))) +(defn halt-if-error [x] + (if (instance? Throwable x) + (throw x) + x)) + +(defn get-settlement-sales-date [client settlement] + (let [concurrent 4 + output-chan (async/chan)] + (async/pipeline-blocking concurrent + output-chan + (map (fn [p] + (lc/with-context {:source "Square settlements loading "} + (log/trace "looking up payment " p " for settlement " (:id settlement)) + (or + (-> (get-payment client p) + :created_at + coerce/to-date) + (coerce/to-date (time/now)))))) + (async/to-chan! (->> settlement + :entries + (filter #(= "CHARGE" (:type %))) + (map :payment_id) + (filter identity) + (set) + (take 20) + )) + true + (fn [e] + (lc/with-context {:source "Square settlements loading "} + (log/warn "Error loading sales date details" e) + e))) + (->> (async/> (async/> lookup-dates + (mapcat (fn [[start-date end-date]] + (log/info "looking up settlements for " (:square-location/client-location location) " on dates " start-date " to " end-date) + (let [settlements (->> (retry #(client/get (str "https://connect.squareup.com/v1/" (:square-location/square-id location) "/settlements") + {:headers (client-base-headers client) + :query-params {"begin_time" start-date + "end_time" end-date} + :as :json + :retry-handler retry-4})) + :body + (map :id))] + settlements))) + set + seq + (get-settlement-details client location)))) + +(defn daily-settlements + ([client location] + (->> (for [settlement (settlements client location)] + #:expected-deposit {:external-id (str "square/settlement/" (:id settlement)) + :vendor :vendor/ccp-square + :status :expected-deposit-status/pending + :total (amount->money (:total_money settlement)) + :client (:db/id client) + :location (:square-location/client-location location) + :fee (- (reduce + 0.0 (map (fn [entry] + (if (= (:type entry) "REFUND") + (- (amount->money (:fee_money entry))) + (amount->money (:fee_money entry)))) + (:entries settlement)))) + :date (-> (:initiated_at settlement) + (coerce/to-date)) + :sales-date (or (:sales-date settlement) + (-> (:initiated_at settlement) + (coerce/to-date))) + :charges (->> (:entries settlement) + (filter :payment_id) + (map (fn [p] {:charge/external-id (str "square/charge/" (:payment_id p))})))}) + + (filter :expected-deposit/date)))) + +(defn refunds + ([client l] + (let [refunds (:refunds (:body (client/get (str "https://connect.squareup.com/v2/refunds?location_id=" (:square-location/square-id l)) + {:headers (client-base-headers client) + :as :json + :retry-handler retry-4})))] + (->> refunds + (filter (fn [r] (= "COMPLETED" (:status r)))) + (map (fn [r] + #:sales-refund {:external-id (str "square/refund/" (:id r)) + :vendor :vendor/ccp-square + :total (amount->money (:amount_money r)) + :fee (transduce + (comp (filter #(= "ADJUSTMENT" (:type %))) + (map :amount_money) + (map amount->money)) + + + 0.0 + (:processing_fee r)) + :client (:db/id client) + :location (:square-location/client-location l) + :date (coerce/to-date (:created_at r)) + :type (:source_type (get-payment client (:payment_id r)))})))))) + +(defn upsert + ([client ] + (doseq [square-location (:client/square-locations client) + :when (:square-location/client-location square-location)] + (upsert client square-location (time/plus (time/now) (time/days -45)) (time/now)))) + ([client location start end] + (lc/with-context {:source "Square loading"} + (doseq [x (partition-all 20 (daily-results client location start end))] + (log/info "Loading " (count x)) + @(d/transact conn x))))) + +(defn upsert-settlements + ([client] + (doseq [square-location (:client/square-locations client) + :when (:square-location/client-location square-location)] + (upsert-settlements client square-location))) + ([client location] + (lc/with-context {:source "Square settlements loading" + :client (:client/code client)} + (doseq [x (partition-all 20 (daily-settlements client location))] + (log/info "Loading expected deposit" (count x)) + @(d/transact conn x)) + (log/info "Done loading settlements")))) + +(defn upsert-refunds + ([client] + (doseq [square-location (:client/square-locations client) + :when (:square-location/client-location square-location)] + (upsert-refunds client square-location))) + ([client location] + (lc/with-context {:source "Loading Square Settlements" + :client (:client/code client) + :location (:square-location/client-location client)} + (doseq [x (partition-all 20 (refunds client location))] + (log/info "Loading refund" (count x)) + @(d/transact conn x)) + (log/info "Done loading refunds")))) + +(def square-read [:db/id + :client/code + :client/square-auth-token + {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}]) + +(defn get-square-clients + ([] + (d/q '[:find [(pull ?c [:db/id + :client/square-integration-status + :client/code + :client/square-auth-token + {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}]) ...] + :in $ + :where [?c :client/square-auth-token] + [?c :client/feature-flags "new-square"]] + (d/db conn))) + ([ & codes] + (d/q '[:find [(pull ?c [:db/id + :client/code + :client/square-auth-token + {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}]) ...] + :in $ [?code ...] + :where [?c :client/square-auth-token] + [?c :client/feature-flags "new-square"] + [?c :client/code ?code]] + (d/db conn) + codes))) + +(defn upsert-locations + ([] (doseq [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)))] + (->> (for [square-location (client-locations client)] + {:db/id (or (square-id->id (:id square-location)) (d/tempid :db.part/user)) + :client/_square-locations (:db/id client) + :square-location/name (:name square-location) + :square-location/square-id (:id square-location)}) + (d/transact conn) + deref)))) + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(defn reset [] + (->> + (d/query {:query {:find ['?e] + :in ['$] + :where ['(or [?e :sales-order/date] + [?e :expected-deposit/date])]} + :args [(d/db conn)]}) + (map first) + (map (fn [x] [:db/retractEntity x])))) + +(defn mark-integration-status [client integration-status] + @(d/transact conn + [{:db/id (:db/id client) + :client/square-integration-status (assoc integration-status + :db/id (or (-> client :client/square-integration-status :db/id) + #db/id [:db.part/user]))}])) + +(defn upsert-all [ & clients] + (doseq [client (apply get-square-clients clients) + :when (seq (filter :square-location/client-location (:client/square-locations client)))] + (lc/with-context {:client (:client/code client)} + (mark-integration-status client {:integration-status/last-attempt (coerce/to-date (time/now))}) + + (try+ + (upsert-locations client) + (upsert client) + (upsert-settlements client) + (upsert-refunds client) + (mark-integration-status client {:integration-status/state :integration-state/success + :integration-status/last-updated (coerce/to-date (time/now))}) + + (catch [:status 401] data + (mark-integration-status client {:integration-status/state :integration-state/unauthorized + :integration-status/message (-> data :body str)})) + + (catch [:status 503] data + (mark-integration-status client {:integration-status/state :integration-state/failed + :integration-status/message (-> data :body str)})) + + (catch Object _ + (log/warn &throw-context) + (mark-integration-status client {:integration-status/state :integration-state/failed + :integration-status/message (or (some-> (:wrapper &throw-context) (.getMessage )) + (some-> (:object &throw-context) str) + "Unknown error")})))))) + diff --git a/src/clj/user.clj b/src/clj/user.clj index f66623f3..e47305b0 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -498,8 +498,8 @@ (println "orders") (lc/with-context {:source "Historical loading data"} (doseq [d (per/periodic-seq (t/plus (t/today) (t/days (- days))) - (t/today) - (t/days 1))] + (t/today) + (t/days 1))] (println d) (square/upsert client square-location (c/to-date-time d) (c/to-date-time (t/plus d (t/days 1)))))) diff --git a/src/cljs/auto_ap/views/pages/pos/form.cljs b/src/cljs/auto_ap/views/pages/pos/form.cljs index b1b5f71c..15eaabdd 100644 --- a/src/cljs/auto_ap/views/pages/pos/form.cljs +++ b/src/cljs/auto_ap/views/pages/pos/form.cljs @@ -24,6 +24,14 @@ [form-builder/builder {:submit-event [::saving ] :id ::form} [form-builder/section {:title "Sales Order"} + [:div + "Order " (:id data) + (when (:reference-link data) + [:a {:href (:reference-link data) + :target "_new"} + [:span.icon + [:i.fa.fa-external-link + ]]])] (when-not @(re-frame/subscribe [::subs/client]) [form-builder/field-v2 {:field :client} "Client" @@ -62,7 +70,12 @@ [:ul (for [charge (:charges data)] ^{:key (:id charge)} - [:li (:type-name charge) ": " (:total charge)])]] + [:li [:span (:type-name charge) ": " (:total charge) + (when (:reference-link charge) + [:a {:href (:reference-link charge) :target "_new"} + [:span.icon + [:i.fa.fa-external-link + ]]])]])]] [form-builder/section {:title "Line Items"} [:ul diff --git a/src/cljs/auto_ap/views/pages/pos/sales_orders.cljs b/src/cljs/auto_ap/views/pages/pos/sales_orders.cljs index 7efe3182..1b4c6a0d 100644 --- a/src/cljs/auto_ap/views/pages/pos/sales_orders.cljs +++ b/src/cljs/auto_ap/views/pages/pos/sales_orders.cljs @@ -25,13 +25,14 @@ {:start (:start params 0) :sort (:sort params) :per-page (:per-page params) + :type-name (:type-name params) :total-gte (:amount-gte (:total-range params)) :total-lte (:amount-lte (:total-range params)) :date-range (:date-range params) :processor (some-> (:processor params) keyword) :client-id (:id @(re-frame/subscribe [::subs/client]))} - [[:sales-orders [:id :total :tax :tip :discount :service-charge :returns :date - [:charges [:type-name :total :processor :id [:expected-deposit [:id]] ]] + [[:sales-orders [:id :source :total :tax :tip :reference-link :discount :service-charge :returns :date + [:charges [:type-name :note :reference-link :total :processor :id [:expected-deposit [:id]] ]] [:line-items [:item-name :total :category]] [:client [:name :id]]]] :total diff --git a/src/cljs/auto_ap/views/pages/pos/side_bar.cljs b/src/cljs/auto_ap/views/pages/pos/side_bar.cljs index fd850128..981da9ac 100644 --- a/src/cljs/auto_ap/views/pages/pos/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/pos/side_bar.cljs @@ -39,9 +39,40 @@ (when (= :sales-orders ap) [:<> + [:p.menu-label "Payment Method"] + [:div + [:nav.panel + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :type-name nil])} + [:span.panel-icon] + "All"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :type-name "CASH"])} + [:span.panel-icon + [:span {:class "icon-accounting-bill" :style {:font-weight "400"}}]] + "Cash"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :type-name "CARD"])} + [:span.panel-icon + [:span {:class "icon-credit-card-1" :style {:font-weight "400"}}]] + "Card"] + + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :type-name "SQUARE_GIFT_CARD"])} + [:span.panel-icon + [:span {:class "icon-gift-box" :style {:font-weight "400"}}]] + "Gift Card"] + + + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :type-name "OTHER"])} + [:span.panel-icon ] + "Other"]]] + [:p.menu-label "Processor"] [:div [:nav.panel + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor nil])} + [:span.panel-icon] + "All"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "square"])} + [:span.panel-icon [:img.level-item {:src "/img/square.png"}]] + "Square"] [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "doordash"])} [:span.panel-icon [:img.level-item {:src "/img/doordash.png"}]] "Doordash"] @@ -51,7 +82,17 @@ "Uber Eats"] [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "grubhub"])} [:span.panel-icon [:img.level-item {:src "/img/grubhub.png"}]] - "Grubhub"]]]]) + "Grubhub"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "koala"])} + [:span.panel-icon [:img.level-item {:src "/img/koala.png"}]] + "Koala"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "ezcater"])} + [:span.panel-icon [:img.level-item {:src "/img/ezcater.png"}]] + "EZCater"] + [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "na"])} + [:span.panel-icon #_[:img.level-item {:src "/img/grubhub.png"}]] + "No Processor"] + ]]]) (when-let [exact-match-id @(re-frame/subscribe [::data-page/filter data-page :exact-match-id])] [:div diff --git a/src/cljs/auto_ap/views/pages/pos/table.cljs b/src/cljs/auto_ap/views/pages/pos/table.cljs index 8536da47..597848ce 100644 --- a/src/cljs/auto_ap/views/pages/pos/table.cljs +++ b/src/cljs/auto_ap/views/pages/pos/table.cljs @@ -17,15 +17,36 @@ (defn row [{sales-order :sales-order selected-client :selected-client}] - (let [{:keys [client date total tax tip charges line-items id]} sales-order + (let [{:keys [client date total tax tip charges source line-items id]} sales-order expected-deposits (->> charges (filter :expected-deposit) (map :expected-deposit))] [grid/row {:class (:class sales-order) :id id} (when-not selected-client [grid/cell {} (:name client)]) [grid/cell {} (date->str date)] + [grid/cell {} source] [grid/cell {:class "has-text-right"} (nf total)] [grid/cell {:class "has-text-right"} (nf tax)] [grid/cell {:class "has-text-right"} (nf tip)] + [grid/cell {} + [:div.level-left + (for [charge charges] + + (with-meta + (condp = (:type-name charge) + "CASH" + [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-accounting-bill" :style {:font-weight "400"}}]] + + "CARD" + [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-credit-card-1" :style {:font-weight "400"}}]] + + "SQUARE_GIFT_CARD" + [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-gift-box" :style {:font-weight "400"}}]] + + [:span.level-item [:span (:type-name charge) (when-let [note (:note charge)] + [:span + [:i.has-text-grey " (" note ")"]])] ]) + + {:key (:id charge)}))]] [grid/cell {} [:div.level [:div.level-left @@ -42,17 +63,15 @@ :uber-eats [:img.level-item {:src "/img/ubereats.png" :style {:width "24px" :height "24px"}}] - (condp = (:type-name charge) - "CASH" - [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-accounting-bill" :style {:font-weight "400"}}]] + :square + [:img.level-item {:src "/img/square.png" :style {:width "24px" :height "24px"}}] - "CARD" - [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-credit-card-1" :style {:font-weight "400"}}]] + :koala + [:img.level-item {:src "/img/koala.png" :style {:width "24px" :height "24px"}}] - "SQUARE_GIFT_CARD" - [:span.icon.level-item {:style {:font-size "24px"}} [:span {:class "icon-gift-box" :style {:font-weight "400"}}]] - - [:span.level-item "Other (" (:type-name charge) ")"])) + :ezcater + [:img.level-item {:src "/img/ezcater.png" :style {:width "24px" :height "24px"}}] + nil) {:key (:id charge)}))]]] @@ -105,10 +124,12 @@ (when-not selected-client [grid/sortable-header-cell {:sort-key "client" :sort-name "Client"} "Client"]) [grid/sortable-header-cell {:sort-key "date" :sort-name "Date" :style {:width "8em"}} "Date"] + [grid/sortable-header-cell {:sort-key "source" :sort-name "Source"} "Source"] [grid/sortable-header-cell {:sort-key "total" :sort-name "Total" :class "has-text-right" :style {:width "8em"}} "Total"] [grid/sortable-header-cell {:sort-key "tax" :sort-name "Tax" :class "has-text-right" :style {:width "7em"}} "Tax"] [grid/sortable-header-cell {:sort-key "tip" :sort-name "Tip" :class "has-text-right" :style {:width "7em"}} "Tip"] [grid/header-cell {} "Payment Methods"] + [grid/header-cell {} "Processor"] [grid/header-cell {} "Line Items"] [grid/header-cell {:style {:width "8em"}}]]] [grid/body diff --git a/test/clj/auto_ap/ezcater_test.clj b/test/clj/auto_ap/ezcater_test.clj index b6adc25d..f36859de 100644 --- a/test/clj/auto_ap/ezcater_test.clj +++ b/test/clj/auto_ap/ezcater_test.clj @@ -86,48 +86,23 @@ "2022-06-01T07:00:00Z") (sut/order->sales-order) (:sales-order/date ))))) - (t/testing "It should categorize every item as 'External Catering'" - (t/is (= 2 + (t/testing "It should simulate a single line item for everything" + (t/is (= 1 (-> known-order sut/order->sales-order :sales-order/line-items count))) - (t/is (= #{"External Catering"} + (t/is (= #{"EZCater Catering"} (->> known-order sut/order->sales-order :sales-order/line-items (map :order-line-item/category) set)))) - - (t/testing "It should generate an id for every line item" - (t/is (= ["ezcater/order/ABC-DT-9ab05fee-a9c5-483b-a7f2-14debde4b7a8-0" - "ezcater/order/ABC-DT-9ab05fee-a9c5-483b-a7f2-14debde4b7a8-1"] - (->> known-order - sut/order->sales-order - :sales-order/line-items - (map :order-line-item/external-id))))) - (t/testing "It should generate an external-id" (t/is (= "ezcater/order/ABC-DT-9ab05fee-a9c5-483b-a7f2-14debde4b7a8" (:sales-order/external-id (sut/order->sales-order known-order))))) - - (t/testing "Should include package name" - (t/is (= #{"Spartan Package"} - (->> known-order - sut/order->sales-order - :sales-order/line-items - (map :order-line-item/item-name) - set)))) - - (t/testing "Should use the total amount" - (t/is (= [34.29 206.75] - (->> (-> known-order - (assoc-in [:catererCart :orderItems 0 :totalInSubunits :subunits] 3429) - (assoc-in [:catererCart :orderItems 1 :totalInSubunits :subunits] 20675)) - sut/order->sales-order - :sales-order/line-items - (map :order-line-item/total))))) + (t/testing "Should capture amounts" (t/is (= 35.09 (-> known-order @@ -147,7 +122,7 @@ (-> known-order (assoc :orderSourceType "EZCATER") (assoc-in [:totals :subTotal :subunits] 10000) - (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) sut/commision))))) (t/testing "Should calculate 15% commision on marketplace orders" (t/is (dollars= 15.0 @@ -160,7 +135,7 @@ (-> known-order (assoc :orderSourceType "MARKETPLACE") (assoc-in [:totals :subTotal :subunits] 10000) - (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) sut/commision))))) (t/testing "Should calculate 2.75% ccp fee" (t/is (dollars= 8.25 @@ -168,10 +143,10 @@ (assoc :orderSourceType "MARKETPLACE") (assoc-in [:totals :subTotal :subunits] 10000) (assoc-in [:totals :salesTax :subunits] 10000) - (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :cost :subunits] 10000) sut/ccp-fee)))) (t/testing "Should use ezcater total paid to the customer" - (t/is (dollars= 420.65 + (t/is (dollars= 454.09 (-> known-order sut/order->sales-order :sales-order/total)))) @@ -179,6 +154,14 @@ (t/is (dollars= -41.8975 (-> known-order sut/order->sales-order - :sales-order/discount))))) + :sales-order/discount)))) + + (t/testing "Should create a charge for the order" + (t/is (dollars= 454.09 + (-> known-order + sut/order->sales-order + :sales-order/charges + first + :charge/total)))))