(ns auto-ap.ezcater.core (:require [auto-ap.datomic :refer [conn random-tempid]] [datomic.api :as dc] [clj-http.client :as client] [venia.core :as v] [clojure.string :as str] [clojure.data.json :as json] [clj-time.coerce :as coerce] [clj-time.core :as time] [clojure.set :as set] [auto-ap.time :as atime] [auto-ap.logging :as alog] [cemerick.url :as url])) (defn query [{:ezcater-integration/keys [api-key]} q] (-> (client/post "https://api.ezcater.com/graphql/" {:headers {"Authorization" api-key "Content-Type" "application/json"} :body (json/write-str {"query" (v/graphql-query q)}) :as :json}) :body :data )) (defn get-caterers [integration] (:caterers (query integration {:venia/queries [{:query/data [:caterers [:name :uuid [:address [:name :street]]]]}]} ))) (defn get-subscriptions [integration] (->> (query integration {:venia/queries [{:query/data [:subscribers [:id [:subscriptions [:parentId :parentEntity :eventEntity :eventKey]] ]]}]} ) :subscribers first :subscriptions)) (defn get-integrations [] (map first (dc/q '[:find (pull ?i [:ezcater-integration/api-key :ezcater-integration/subscriber-uuid :db/id :ezcater-integration/integration-status [:db/id]]) :in $ :where [?i :ezcater-integration/api-key]] (dc/db conn)))) (defn mark-integration-status [integration integration-status] @(dc/transact conn [{:db/id (:db/id integration) :ezcater-integration/integration-status (assoc integration-status :db/id (or (-> integration :ezcater-integration/integration-status :db/id) (random-tempid)))}])) (defn upsert-caterers ([integration] @(dc/transact conn (for [caterer (get-caterers integration)] {:db/id (:db/id integration) :ezcater-integration/caterers [{:ezcater-caterer/name (str (:name caterer) " (" (:street (:address caterer)) ")") :ezcater-caterer/search-terms (str (:name caterer) " " (:street (:address caterer))) :ezcater-caterer/uuid (:uuid caterer)}]})))) (defn upsert-used-subscriptions ([integration] (let [extant (get-subscriptions integration) to-ensure (set (map first (dc/q '[:find ?cu :in $ :where [_ :client/ezcater-locations ?el] [?el :ezcater-location/caterer ?c] [?c :ezcater-caterer/uuid ?cu]] (dc/db conn)))) to-create (set/difference to-ensure (set (map :parentId extant)))] (doseq [parentId to-create] (query integration {:venia/operation {:operation/type :mutation :operation/name "createSubscription"} :venia/queries [[:createSubscription {:subscriptionParams {:subscriberId (:ezcater-integration/subscriber-uuid integration) :parentEntity 'Caterer :parentId parentId :eventEntity 'Order :eventKey 'accepted}} [[:subscription [:parentId :parentEntity :eventEntity :eventKey]]]]]}) (query integration {:venia/operation {:operation/type :mutation :operation/name "createSubscription"} :venia/queries [[:createSubscription {:subscriptionParams {:subscriberId (:ezcater-integration/subscriber-uuid integration) :parentEntity 'Caterer :parentId parentId :eventEntity 'Order :eventKey 'cancelled}} [[:subscription [:parentId :parentEntity :eventEntity :eventKey]]]]]}))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn upsert-ezcater ([] (upsert-ezcater (get-integrations))) ([integrations] (doseq [integration integrations] (mark-integration-status integration {:integration-status/last-attempt (coerce/to-date (time/now))}) (try (upsert-caterers integration) (upsert-used-subscriptions integration) (mark-integration-status integration {:integration-status/state :integration-state/success :integration-status/last-updated (coerce/to-date (time/now))}) (catch Exception e (alog/warn ::cant-upsert-ezcater :exception e) (mark-integration-status integration {:integration-status/state :integration-state/failed :integration-status/message (.getMessage e)})))))) (defn get-caterer [caterer-uuid] (dc/pull (dc/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 round-carry-cents [f] (with-precision 2 (double (.setScale (bigdec f) 2 java.math.RoundingMode/HALF_UP)))) (defn commision [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] (round-carry-cents (* 0.000299M (+ (-> 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}] (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 :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 get-by-id [integration id] (query integration {:venia/queries [[:order {:id id} [:uuid :orderNumber :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]]]]]]]})) (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)] (if (and client location) (doto (-> (get-by-id integration (get json "entity_id")) (:order) (assoc :client-code client :client-location location)) (#(alog/info ::order-details :detail %))) (alog/warn ::caterer-no-longer-has-location :json json)))) (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"} (alog/info ::try-import-order :json json) @(dc/transact conn (filter identity [(some-> json (lookup-order) (order->sales-order) (update :sales-order/date coerce/to-date) (update-in [:sales-order/charges 0 :charge/date] coerce/to-date))]))) (defn upsert-recent [] (upsert-ezcater) (let [last-sunday (coerce/to-date (time/plus (second (->> (time/today) (iterate #(time/plus % (time/days -1))) (filter #(= 7 (time/day-of-week %))))) (time/days 1))) orders-to-update (doall (for [[order uuid] (dc/q '[:find ?eid ?uuid :in $ ?start :where [?e :sales-order/vendor :vendor/ccp-ezcater] [?e :sales-order/date ?d] [(>= ?d ?start)] [?e :sales-order/external-id ?eid] [?e :sales-order/client ?c] [?c :client/ezcater-locations ?l] [?l :ezcater-location/caterer ?c2] [?c2 :ezcater-caterer/uuid ?uuid]] (dc/db conn) last-sunday) :let [_ (alog/info ::considering :order order) id (last (str/split order #"/")) id (str/join "-" (drop 2 (str/split order #"-"))) lookup-map {"id" "bf3dcf5c-a68f-42d9-9084-049133e03d3d", "parent_type" "Caterer", "parent_id" uuid, "entity_type" "Order", "entity_id" id, "key" "accepted", "occurred_at" "2022-07-21T19:21:07.549Z"} ezcater-order (lookup-order lookup-map) extant-order (dc/pull (dc/db conn) '[:sales-order/total :sales-order/tax :sales-order/tip :sales-order/discount :sales-order/external-id {:sales-order/charges [:charge/tax :charge/tip :charge/total :charge/external-id] :sales-order/line-items [:order-line-item/external-id :order-line-item/total :order-line-item/tax :order-line-item/discount]}] [:sales-order/external-id order]) updated-order (-> (order->sales-order ezcater-order) (select-keys #{:sales-order/total :sales-order/tax :sales-order/tip :sales-order/discount :sales-order/charges :sales-order/external-id :sales-order/line-items}) (update :sales-order/line-items (fn [c] (map #(select-keys % #{:order-line-item/external-id :order-line-item/total :order-line-item/tax :order-line-item/discount}) c))) (update :sales-order/charges (fn [c] (map #(select-keys % #{:charge/tax :charge/tip :charge/total :charge/external-id}) c))))] :when (not= updated-order extant-order)] updated-order))] (alog/info :found-orders-to-update :orders orders-to-update) @(dc/transact conn orders-to-update)))