From 0606da9c497d10f90d1468ff68e5b040f4e338bd Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Sun, 10 Jul 2022 06:09:14 -0700 Subject: [PATCH] Adds ability for users to configure basic ezcater integration. --- src/clj/auto_ap/datomic/clients.clj | 15 +++-- .../auto_ap/datomic/migrate/integrations.clj | 64 ++++++++++++++++++- src/clj/auto_ap/ezcater/core.clj | 62 ++++++++++++++++++ src/clj/auto_ap/graphql.clj | 2 + src/clj/auto_ap/graphql/checks.clj | 2 +- src/clj/auto_ap/graphql/clients.clj | 19 ++++++ src/clj/auto_ap/graphql/ezcater.clj | 46 +++++++++++++ src/clj/auto_ap/plaid/core.clj | 28 +++----- src/clj/auto_ap/routes/exports.clj | 1 - src/clj/auto_ap/square/core.clj | 3 +- src/cljs/auto_ap/events.cljs | 1 + .../views/pages/admin/clients/form.cljs | 44 +++++++++++-- 12 files changed, 252 insertions(+), 35 deletions(-) create mode 100644 src/clj/auto_ap/ezcater/core.clj create mode 100644 src/clj/auto_ap/graphql/ezcater.clj diff --git a/src/clj/auto_ap/datomic/clients.clj b/src/clj/auto_ap/datomic/clients.clj index 2576d7c9..157f96ce 100644 --- a/src/clj/auto_ap/datomic/clients.clj +++ b/src/clj/auto_ap/datomic/clients.clj @@ -38,6 +38,10 @@ :square-location/name :square-location/client-location :db/id]} + + {:client/ezcater-locations [{:ezcater-location/caterer [:ezcater-caterer/name :db/id]} + :ezcater-location/location + :db/id]} {:client/bank-accounts [* {:bank-account/type [*] :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number] :bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance] @@ -60,11 +64,14 @@ (->> (d/pull (d/db conn ) - '[* {:client/bank-accounts [* {:bank-account/type [*] - :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number] + '[* {:client/bank-accounts [* {:bank-account/type [*] + :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number] :bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id] - :bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]}] - :client/emails [:db/id :email-contact/email :email-contact/description]} + :bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]}] + :client/emails [:db/id :email-contact/email :email-contact/description]} + {:client/ezcater-locations [{:ezcater-location/caterer [:ezcater-caterer/name :db/id]} + :ezcater-location/location + :db/id]} {:yodlee-provider-account/_client [*]} {:plaid-item/_client [*]}] id) diff --git a/src/clj/auto_ap/datomic/migrate/integrations.clj b/src/clj/auto_ap/datomic/migrate/integrations.clj index b6d94414..d5e64fc7 100644 --- a/src/clj/auto_ap/datomic/migrate/integrations.clj +++ b/src/clj/auto_ap/datomic/migrate/integrations.clj @@ -37,4 +37,66 @@ {:db/ident :integration-state/failed} {:db/ident :integration-state/success} - {:db/ident :integration-state/unauthorized}]]}}) + {:db/ident :integration-state/unauthorized}]]} + ::add-ezcater-base7 + {:txes [[{:db/ident :ezcater-integration/subscriber-uuid + :db/doc "The subscriber uuid" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + + {:db/ident :ezcater-integration/api-key + :db/doc "The API key that can be used for requests" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + + {:db/ident :ezcater-integration/subscriber-secret + :db/doc "Used to validate events" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident :ezcater-integration/integration-status + :db/doc "The status for this integration" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/isComponent true} + + {:db/ident :ezcater-integration/caterers + :db/doc "All caterers for this integration" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true} + + {:db/ident :ezcater-caterer/uuid + :db/doc "Id of the caterer" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + + {:db/ident :ezcater-caterer/name + :db/doc "Name of the caterer" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + + {:db/ident :ezcater-caterer/search-terms + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "a name search for caterer" + :db/fulltext true} + + {:db/ident :client/ezcater-locations + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true} + + {:db/ident :ezcater-location/location + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + + {:db/ident :ezcater-location/caterer + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one}] + [{:ezcater-integration/api-key "bmlrdHNpZ2FyaXNAZ21haWwuY29tOmQwMzQwMjYzOWI2ODQxNmVkMjdmZWYxMWFhZTk3YzU1MDlmNTcyNjYwMDAzOTA5MDE2OGMzODllNDBjNTVkZGE" + :ezcater-integration/subscriber-uuid "007d4353-fbb8-4725-9c0a-81bbd565dbe5" + :ezcater-integration/subscriber-secret "3dccc547f6c4d9b3cbed15ab81d3748d2a34be7fdb9b8edfdc9abd6c75a821fe"}]] + :requires [::add-integration-status3]}}) diff --git a/src/clj/auto_ap/ezcater/core.clj b/src/clj/auto_ap/ezcater/core.clj new file mode 100644 index 00000000..acf2bdf2 --- /dev/null +++ b/src/clj/auto_ap/ezcater/core.clj @@ -0,0 +1,62 @@ +(ns auto-ap.ezcater.core + (:require + [auto-ap.datomic :refer [conn]] + [datomic.api :as d] + [clj-http.client :as client] + [venia.core :as v] + [clojure.data.json :as json] + [clj-time.coerce :as coerce] + [clj-time.core :as time])) + +(defn get-caterers [{:ezcater-integration/keys [api-key]}] + (-> (client/post "https://api.ezcater.com/graphql/" + {:headers {"Authorization" api-key + "Content-Type" "application/json"} + :body (json/write-str {"query" (v/graphql-query {:venia/queries [{:query/data [:caterers [:name :uuid [:address [:name :street]]]]}]})}) + :as :json}) + :body + :data + :caterers)) + +(defn get-integrations [] + (d/q '[:find [(pull ?i [:ezcater-integration/api-key + :db/id + :ezcater-integration/integration-status [:db/id]]) ...] + :in $ + :where [?i :ezcater-integration/api-key]] + (d/db conn))) + +(defn mark-integration-status [integration integration-status] + @(d/transact conn + [{:db/id (:db/id integration) + :ezcater-integration/integration-status (assoc integration-status + :db/id (or (-> integration :ezcater-integration/integration-status :db/id) + #db/id [:db.part/user]))}])) + +(defn upsert-caterers + ([integration] + @(d/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-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) + (mark-integration-status integration {:integration-status/state :integration-state/success + :integration-status/last-updated (coerce/to-date (time/now))}) + + (catch Exception e + (log/warn e) + (mark-integration-status integration {:integration-status/state :integration-state/failed + :integration-status/message (.getMessage e)})))))) + + + diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 9ffec996..35b86f76 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -5,6 +5,7 @@ [auto-ap.datomic.users :as d-users] [auto-ap.graphql.accounts :as gq-accounts] [auto-ap.graphql.checks :as gq-checks] + [auto-ap.graphql.ezcater :as gq-ezcater] [auto-ap.graphql.clients :as gq-clients] [auto-ap.graphql.expected-deposit :as gq-expected-deposit] [auto-ap.graphql.import-batch :as gq-import-batches] @@ -804,6 +805,7 @@ gq-import-batches/attach gq-transactions/attach gq-expected-deposit/attach + gq-ezcater/attach gq-invoices/attach gq-clients/attach gq-sales-orders/attach diff --git a/src/clj/auto_ap/graphql/checks.clj b/src/clj/auto_ap/graphql/checks.clj index a39398e0..3ddc8ba8 100644 --- a/src/clj/auto_ap/graphql/checks.clj +++ b/src/clj/auto_ap/graphql/checks.clj @@ -259,7 +259,7 @@ :check (str (+ index (:bank-account/check-number bank-account))) :memo memo :date (date->str (local-now)) - :client (dissoc client :client/bank-accounts :client/square-integration-status :client/locked-until :client/emails :client/square-auth-token :client/square-locations) + :client (dissoc client :client/bank-accounts :client/square-integration-status :client/ezcater-locations :client/locked-until :client/emails :client/square-auth-token :client/square-locations) :bank-account (dissoc bank-account :bank-account/start-date :bank-account/integration-status) #_#_:client {:name (:name client) :address (:address client) diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index 60b14676..b419e022 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -112,6 +112,14 @@ {:db/id (:id sl) :square-location/client-location (:client_location sl)})) (:square_locations edit_client)) + + :client/ezcater-locations (map + (fn [el] + (remove-nils + {:db/id (:id el) + :ezcater-location/location (:location el) + :ezcater-location/caterer (:caterer el)})) + (:ezcater_locations edit_client)) :client/week-b-credits (:week_b_credits edit_client) :client/location-matches (->> (:location_matches edit_client) (filter (fn [lm] (and (:location lm) (:match lm)))) @@ -430,6 +438,11 @@ :square_id {:type 'String} :id {:type :id}}} + :ezcater_location + {:fields {:location {:type 'String} + :caterer {:type :ezcater_caterer} + :id {:type :id}}} + :email_contact {:fields {:id {:type :id} :email {:type 'String} :description {:type 'String}}} @@ -454,6 +467,7 @@ :matches {:type '(list String)} :bank_accounts {:type '(list :bank_account)} :square_locations {:type '(list :square_location)} + :ezcater_locations {:type '(list :ezcater_location)} :forecasted_transactions {:type '(list :forecasted_transaction)} :yodlee_provider_accounts {:type '(list :yodlee_provider_account)} :plaid_items {:type '(list :plaid_item)}}} @@ -510,6 +524,10 @@ :edit_square_location {:fields {:client_location {:type 'String} :id {:type :id}}} + :edit_ezcater_location {:fields {:location {:type 'String} + :caterer {:type :id} + :id {:type :id}}} + :edit_forecasted_transaction {:fields {:identifier {:type 'String} :id {:type :id} :day_of_month {:type 'Int} @@ -534,6 +552,7 @@ :matches {:type '(list String)} :location_matches {:type '(list :edit_location_match)} :square_locations {:type '(list :edit_square_location)} + :ezcater_locations {:type '(list :edit_ezcater_location)} :bank_accounts {:type '(list :edit_bank_account)} :forecasted_transactions {:type '(list :edit_forecasted_transaction)}}} diff --git a/src/clj/auto_ap/graphql/ezcater.clj b/src/clj/auto_ap/graphql/ezcater.clj new file mode 100644 index 00000000..02954879 --- /dev/null +++ b/src/clj/auto_ap/graphql/ezcater.clj @@ -0,0 +1,46 @@ +(ns auto-ap.graphql.ezcater + (:require + [auto-ap.datomic :refer [conn]] + [auto-ap.graphql.utils :refer [assert-admin cleanse-query]] + [auto-ap.graphql.vendors :refer [partial-match-first]] + [com.walmartlabs.lacinia.util :refer [attach-resolvers]] + [datomic.api :as d])) + +(defn search [context args _] + (assert-admin (:id context)) + (let [search-query (cleanse-query (:query args)) + data (d/q '[:find ?n ?i ?s + :in $ ?q + :where [(fulltext $ :ezcater-caterer/search-terms ?q) [[?i ?n _ ?s]]]] + (d/db conn) + search-query)] + (->> data + (sort-by (comp - last)) + (partial-match-first (:query args)) + (map (fn [[n i]] + {:name n + :id i}))))) + + +(def objects + {:ezcater_caterer {:fields {:name {:type 'String} + :id {:type :id}}}}) + +(def queries + {:search_ezcater_caterer {:type '(list :search_result) + :args {:query {:type 'String}} + :resolve :search-ezcater-caterer}}) + +(def enums + {}) + +(def resolvers + {:search-ezcater-caterer search}) + +(defn attach [schema] + (-> + (merge-with merge schema + {:objects objects + :queries queries + :enums enums}) + (attach-resolvers resolvers))) diff --git a/src/clj/auto_ap/plaid/core.clj b/src/clj/auto_ap/plaid/core.clj index ea47a30c..7a598361 100644 --- a/src/clj/auto_ap/plaid/core.clj +++ b/src/clj/auto_ap/plaid/core.clj @@ -1,22 +1,10 @@ (ns auto-ap.plaid.core - (:require [auto-ap.datomic :refer [conn remove-nils]] - [amazonica.aws.s3 :as s3] - [auto-ap.utils :refer [by]] - [clj-http.client :as client] - [clj-time.coerce :as coerce] - [clj-time.core :as time] - [clj-time.format :as f] - [config.core :refer [env] :as cfg ] - [clojure.string :as str] - [clojure.data.json :as json] - [clojure.java.io :as io] - [clojure.tools.logging :as log] - [datomic.api :as d] - [mount.core :as mount] - [unilog.context :as lc] - [yang.scheduler :as scheduler] - [clojure.core.async :as async] - [clojure.core.memoize :as m])) + (:require + [clj-http.client :as client] + [clojure.data.json :as json] + [clojure.tools.logging :as log] + [config.core :as cfg :refer [env]] + [auto-ap.time :as atime])) (def base-url (-> env :plaid :base-url)) @@ -65,8 +53,8 @@ :body (json/write-str {"client_id" client-id "secret" secret-key "access_token" access-token - "start_date" (auto-ap.time/unparse start auto-ap.time/iso-date) - "end_date" (auto-ap.time/unparse end auto-ap.time/iso-date) + "start_date" (atime/unparse start atime/iso-date) + "end_date" (atime/unparse end atime/iso-date) "options" {"account_ids" [account-id]}})}) :body)) diff --git a/src/clj/auto_ap/routes/exports.clj b/src/clj/auto_ap/routes/exports.clj index ed9b4439..9b18ca49 100644 --- a/src/clj/auto_ap/routes/exports.clj +++ b/src/clj/auto_ap/routes/exports.clj @@ -23,7 +23,6 @@ (defn wrap-csv-response [handler] (fn [request] (let [response (handler request)] - (println response) (update response :body #(with-open [w (java.io.StringWriter.)] (csv/write-csv w %) (.toString w)))))) diff --git a/src/clj/auto_ap/square/core.clj b/src/clj/auto_ap/square/core.clj index 377e8237..36f8f20c 100644 --- a/src/clj/auto_ap/square/core.clj +++ b/src/clj/auto_ap/square/core.clj @@ -491,7 +491,8 @@ (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-sttus/last-attempt (coerce/to-date (time/now))}) + (mark-integration-status client {:integration-status/last-attempt (coerce/to-date (time/now))}) + (try+ (upsert-locations client) (upsert client) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index e6bc7467..a308cbff 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -18,6 +18,7 @@ (cond-> [:id :name :signature-file :code :email :matches :week-a-debits :week-a-credits :week-b-debits :week-b-credits :locations :locked-until :square-auth-token [:square-integration-status [:last-updated :last-attempt :message :state :id]] [:square-locations [:square-id :id :name :client-location]] + [:ezcater-locations [:id [:caterer [:name :id]] :location]] [:emails [:id :email :description]] [:location-matches [:id :location :match]] [:bank-accounts [:id :start-date :numeric-code :code :number :bank-name :bank-code :check-number :name :routing :type :sort-order :visible :yodlee-account-id diff --git a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs index f294ef34..7296788e 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs @@ -5,6 +5,8 @@ [auto-ap.events :as events] [auto-ap.views.components.address :refer [address-field]] [auto-ap.views.components.typeahead :refer [typeahead-v3]] + [auto-ap.views.components.typeahead.vendor + :refer [search-backed-typeahead]] [auto-ap.views.components.layouts :refer [side-bar]] [auto-ap.views.utils :refer @@ -41,7 +43,7 @@ ) :ref (fn [i] (reset! button i))} ]])}))) -(defn signature [{:keys [signature-file signature-data on-change]}] +(defn signature [_] (let [canvas (atom nil) edit-mode? (r/atom false) w (* 1.5 464) @@ -94,7 +96,7 @@ (re-frame/reg-sub ::can-submit :<- [::new-client-request] - (fn [r _] + (fn [_ _] true #_(s/valid? ::entity/client r))) @@ -115,6 +117,13 @@ :client-location (:client-location x)}) (:square-locations new-client-data)) + :ezcater-locations (map + (fn [x] + {:id (:id x) + :caterer (:id (:caterer x)) + :location (:location x)}) + (:ezcater-locations new-client-data)) + :locked-until (cond (not (:locked-until new-client-data)) nil @@ -198,13 +207,12 @@ (re-frame/reg-event-fx ::save-new-client [(forms/in-form ::form)] - (fn [{{new-client-data :data :as new-client-form} :db} _] + (fn [_ _] (let [new-client-req @(re-frame/subscribe [::new-client-request]) user @(re-frame/subscribe [::subs/token])] - {:db (-> new-client-form) - :graphql + {:graphql {:token user :owns-state {:single ::form} :query-obj {:venia/operation {:operation/type :mutation @@ -304,8 +312,8 @@ []))) -(defn bank-account-card [new-client {:keys [active? new? type visible code name number check-number id sort-order] :as bank-account} first? last?] - (let [{:keys [form field raw-field error-notification submit-button ]} client-form] +(defn bank-account-card [new-client {:keys [active? new? type visible code name sort-order]} first? last?] + (let [{:keys [field raw-field]} client-form] [:div.card {:style {:margin-bottom "1em"}} [:header.card-header [:div.card-header-title {:style {:text-overflow "ellipsis"}} @@ -692,5 +700,27 @@ :step "0.01"}]] :disable-remove? true :disable-new? true}])]] + + [:div.field + [:label.label "EZCater Locations"] + [:div.control + (raw-field + [multi-field {:type "multi-field" + :field :ezcater-locations + :template [ + [search-backed-typeahead {:search-query (fn [i] + [:search_ezcater_caterer + {:query i} + [:name :id]]) + :entity->text :name + :type "typeahead-v3" + :field [:caterer] + :style {:width "20em"}}] + [:input.input {:type "text" + :style {:width "4em"} + :field [:location] + :step "0.01"}]] + :disable-remove? true}])]] + (error-notification) (submit-button "Save")])]]))