diff --git a/src/clj/auto_ap/datomic/migrate/sales.clj b/src/clj/auto_ap/datomic/migrate/sales.clj index 4b4f6094..13386624 100644 --- a/src/clj/auto_ap/datomic/migrate/sales.clj +++ b/src/clj/auto_ap/datomic/migrate/sales.clj @@ -290,5 +290,7 @@ :db/index true} {:db/ident :sales-order/client :db/index true}]] - :requires [:add-orders]}}) + :requires [:add-orders]} + :add-ezcater-vendor {:txes [[{:db/ident :vendor/ccp-ezcater + :vendor/name "EZCater CCP"}]]}}) diff --git a/src/clj/auto_ap/ezcater/core.clj b/src/clj/auto_ap/ezcater/core.clj index dc3353b0..1be6f0c9 100644 --- a/src/clj/auto_ap/ezcater/core.clj +++ b/src/clj/auto_ap/ezcater/core.clj @@ -8,7 +8,8 @@ [clj-time.coerce :as coerce] [clojure.tools.logging :as log] [clj-time.core :as time] - [clojure.set :as set])) + [clojure.set :as set] + [auto-ap.time :as atime])) (defn query [{:ezcater-integration/keys [api-key]} q] (-> (client/post "https://api.ezcater.com/graphql/" @@ -89,8 +90,7 @@ :parentId parentId :eventEntity 'Order :eventKey 'cancelled}} - [[:subscription [:parentId :parentEntity :eventEntity :eventKey]]]]]})) - ))) + [[:subscription [:parentId :parentEntity :eventEntity :eventKey]]]]]}))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn upsert-ezcater @@ -109,5 +109,127 @@ (mark-integration-status integration {:integration-status/state :integration-state/failed :integration-status/message (.getMessage e)})))))) +(defn get-caterer [caterer-uuid] + (d/pull (d/db conn) + '[:ezcater-caterer/name + {:ezcater-integration/_caterers [:ezcater-integration/api-key]} + {:ezcater-location/_caterer [:ezcater-location/location + {:client/_ezcater-locations [:client/code]}]}] + [:ezcater-caterer/uuid caterer-uuid])) + +(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)))))))) + +(defn ccp-fee [order] + (* 0.000275 + (+ + (-> order :totals :subTotal :subunits ) + (-> order :totals :salesTax :subunits ) + (reduce + + 0 + (map :subunits (: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)))}))) + + :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}) + +(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)))) + +(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"} + @(d/transact conn [(-> json + (lookup-order) + (order->sales-order) + (update :sales-order/date coerce/to-date))]) + ) diff --git a/src/clj/auto_ap/routes/ezcater.clj b/src/clj/auto_ap/routes/ezcater.clj index d81355aa..45f1f741 100644 --- a/src/clj/auto_ap/routes/ezcater.clj +++ b/src/clj/auto_ap/routes/ezcater.clj @@ -3,6 +3,7 @@ [clojure.tools.logging :as log] [compojure.core :refer [context defroutes GET POST wrap-routes]] [ring.middleware.json :refer [wrap-json-params]] + [auto-ap.ezcater.core :as e] [ring.util.request :refer [body-string]])) (defroutes routes @@ -14,8 +15,9 @@ :headers {"Content-Type" "application/json"} :body "{}"}) (POST "/event" request - ;; {"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"} (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/test/clj/auto_ap/ezcater_test.clj b/test/clj/auto_ap/ezcater_test.clj new file mode 100644 index 00000000..b6adc25d --- /dev/null +++ b/test/clj/auto_ap/ezcater_test.clj @@ -0,0 +1,184 @@ +(ns auto-ap.ezcater-test + (:require [auto-ap.ezcater.core :as sut] + [clojure.test :as t] + [datomic.api :as d ] + [auto-ap.utils :refer [dollars=]] + [auto-ap.datomic :refer [uri]] + [auto-ap.datomic.migrate :as m] + [auto-ap.time-reader])) + + +(defn wrap-setup + [f] + (with-redefs [auto-ap.datomic/uri "datomic:mem://datomic-transactor:4334/invoice"] + (d/create-database uri) + (with-redefs [auto-ap.datomic/conn (d/connect uri)] + (m/migrate auto-ap.datomic/conn) + (f) + (d/release auto-ap.datomic/conn) + (d/delete-database uri)))) + +(t/use-fixtures :each wrap-setup) + +(def sample-event {"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"}) + +(def known-order + {:uuid "9ab05fee-a9c5-483b-a7f2-14debde4b7a8", + :client-code "ABC", :client-location "DT" + :orderSourceType "MARKETPLACE", + :caterer + {:name "Nick The Greek", + :uuid "91541331-d7ae-4634-9e8b-ccbbcfb2ce70", + :address {:street "165 Plaza Dr"}}, + :event + {:timestamp "2022-07-21T19:15:00Z", + :catererHandoffFoodTime "2022-07-21T19:15:00Z", + :orderType "TAKEOUT"}, + :catererCart + {:orderItems + [{:name "Spartan Package", + :quantity 10, + :posItemId nil, + :totalInSubunits {:currency "USD", :subunits 20950}} + {:name "Spartan Package", + :quantity 10, + :posItemId nil, + :totalInSubunits {:currency "USD", :subunits 20950}}], + :totals {:catererTotalDue 420.65}, + :feesAndDiscounts []}, + :totals + {:customerTotalDue {:currency "USD", :subunits 45409}, + :pointOfSaleIntegrationFee {:currency "USD", :subunits 0}, + :tip {:currency "USD", :subunits 0}, + :salesTax {:currency "USD", :subunits 3509}, + :salesTaxRemittance {:currency "USD", :subunits 0}, + :subTotal {:currency "USD", :subunits 41900}}}) + +(t/deftest lookup-order + (t/testing "It should find the order from ezcater" + (with-redefs [sut/get-caterer (fn [k] + (t/is (= k "91541331-d7ae-4634-9e8b-ccbbcfb2ce70")) + { + :ezcater-integration/_caterers {:ezcater-integration/api-key "bmlrdHNpZ2FyaXNAZ21haWwuY29tOmQwMzQwMjYzOWI2ODQxNmVkMjdmZWYxMWFhZTk3YzU1MDlmNTcyNjYwMDAzOTA5MDE2OGMzODllNDBjNTVkZGE"} + :ezcater-location/_caterer [{:ezcater-location/location "DT" + :client/_ezcater-locations {:client/code "ABC"}}] + })] + (t/is (= known-order + (sut/lookup-order sample-event)))))) + +(t/deftest order->sales-order + (t/testing "It should use the date" + (t/is (= #clj-time/date-time "2022-01-01T00:00:00-08:00" + (-> known-order + (assoc-in [:event :timestamp] + "2022-01-01T08:00:00Z") + (sut/order->sales-order) + (:sales-order/date )))) + (t/is (= #clj-time/date-time "2022-06-01T00:00:00-07:00" + (-> known-order + (assoc-in [:event :timestamp] + "2022-06-01T07:00:00Z") + (sut/order->sales-order) + (:sales-order/date ))))) + (t/testing "It should categorize every item as 'External Catering'" + (t/is (= 2 + (-> known-order + sut/order->sales-order + :sales-order/line-items + count))) + (t/is (= #{"External 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 + sut/order->sales-order + :sales-order/tax))) + (t/is (= 0.0 (-> known-order + sut/order->sales-order + :sales-order/tip)))) + (t/testing "Should calculate 7% commision on ezcater orders" + (t/is (dollars= 7.0 + (-> known-order + (assoc :orderSourceType "EZCATER") + (assoc-in [:totals :subTotal :subunits] 10000) + sut/commision))) + (t/testing "Should inlclude delivery fee in commision" + (t/is (dollars= 14.0 + (-> known-order + (assoc :orderSourceType "EZCATER") + (assoc-in [:totals :subTotal :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + sut/commision))))) + (t/testing "Should calculate 15% commision on marketplace orders" + (t/is (dollars= 15.0 + (-> known-order + (assoc :orderSourceType "MARKETPLACE") + (assoc-in [:totals :subTotal :subunits] 10000) + sut/commision))) + (t/testing "Should inlclude delivery fee in commision" + (t/is (dollars= 30.0 + (-> known-order + (assoc :orderSourceType "MARKETPLACE") + (assoc-in [:totals :subTotal :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + sut/commision))))) + (t/testing "Should calculate 2.75% ccp fee" + (t/is (dollars= 8.25 + (-> known-order + (assoc :orderSourceType "MARKETPLACE") + (assoc-in [:totals :subTotal :subunits] 10000) + (assoc-in [:totals :salesTax :subunits] 10000) + (assoc-in [:catererCart :feesAndDiscounts 0 :subunits] 10000) + sut/ccp-fee)))) + (t/testing "Should use ezcater total paid to the customer" + (t/is (dollars= 420.65 + (-> known-order + sut/order->sales-order + :sales-order/total)))) + (t/testing "Should derive adjustments food-total + sales-tax - caterer-total - service fee - ccp fee" + (t/is (dollars= -41.8975 + (-> known-order + sut/order->sales-order + :sales-order/discount))))) + +