diff --git a/project.clj b/project.clj index 7dce1266..2fea659e 100644 --- a/project.clj +++ b/project.clj @@ -13,8 +13,6 @@ org.slf4j/log4j-over-slf4j org.slf4j/slf4j-nop org.slf4j/slf4j-log4j12]] - [compojure "1.6.2" :exclusions [ring - ring/ring-core]] [com.unbounce/clojure-dogstatsd-client "0.7.0"] [bidi "2.1.6"] [ring/ring-defaults "0.3.2" :exclusions [ring ring/ring-core]] diff --git a/scratch-sessions/1099-gather.clj b/scratch-sessions/1099-gather.clj new file mode 100644 index 00000000..a52a5adb --- /dev/null +++ b/scratch-sessions/1099-gather.clj @@ -0,0 +1,56 @@ +;; 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. + + +(user/init-repl) + +(clojure.data.csv/write-csv *out* + + (->> (d/q '[:find + (pull ?c [:client/code]) + (pull ?v [:vendor/name + {:vendor/legal-entity-1099-type [:db/ident]} + {:vendor/legal-entity-tin-type [:db/ident]} + {:vendor/address [:address/street1 + :address/city + :address/state + :address/zip]} + :vendor/legal-entity-first-ein + :vendor/legal-entity-first-name + :vendor/legal-entity-middle-name + :vendor/legal-entity-last-name]) + (sum ?a) + :in $ + :where [?p :payment/date ?d ] + [(>= ?d #inst "2022-01-01T08:00")] + [(< ?d #inst "2023-01-01T08:00")] + [?p :payment/client ?c] + [?p :payment/amount ?a] + [?p :payment/type :payment-type/check] + [?p :payment/vendor ?v]] + (d/db auto-ap.datomic/conn)) + (filter (fn [[_ _ a]] + (>= a 600.0))) + (map (fn [[client vendor amount]] + [(:client/code client) + (:vendor/name vendor) + (some-> vendor :vendor/legal-entity-1099-type :db/ident name) + (-> vendor :vendor/legal-entity-first-name) + (-> vendor :vendor/legal-entity-middle-name) + (-> vendor :vendor/legal-entity-last-name) + (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + (-> vendor :vendor/legal-entity-tin) + (-> vendor :vendor/address :address/street1) + (-> vendor :vendor/address :address/street2) + (-> vendor :vendor/address :address/city) + (-> vendor :vendor/address :address/state) + (-> vendor :vendor/address :address/zip) + amount + ])) + (sort ) + (into [["Client" "Vendor Name" "1099 Type" "First Name" "Middle Name" "Last Name" "TIN type" "TIN" "Street" "Street 2" "City" "State" "Zip"]])) + :quote? (constantly true)) + diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj index 167bb911..fee27168 100644 --- a/src/clj/auto_ap/handler.clj +++ b/src/clj/auto_ap/handler.clj @@ -2,25 +2,25 @@ (:require [amazonica.core :refer [defcredential]] [auto-ap.client-routes :as client-routes] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.logging :as alog] [auto-ap.routes.auth :as auth] [auto-ap.routes.exports :as exports] [auto-ap.routes.ezcater :as ezcater] [auto-ap.routes.graphql :as graphql] + [auto-ap.routes.health :as health] [auto-ap.routes.invoices :as invoices] [auto-ap.routes.queries :as queries] [auto-ap.routes.yodlee2 :as yodlee2] - [auto-ap.ssr.admin :as ssr-admin] + [auto-ap.ssr.core :as ssr] [bidi.bidi :as bidi] + [bidi.ring :refer [->ResourcesMaybe make-handler]] [buddy.auth.backends.session :refer [session-backend]] [buddy.auth.backends.token :refer [jws-backend]] [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] - [clojure.string :as str] - [clojure.tools.logging :as log] - [compojure.core :refer [ANY context defroutes GET routes]] - [compojure.route :as route] + [com.brunobonacci.mulog :as mu] [config.core :refer [env]] - [mount.core :as mount] [ring.middleware.edn :refer [wrap-edn-params]] [ring.middleware.multipart-params :as mp] [ring.middleware.params :refer [wrap-params]] @@ -28,85 +28,153 @@ [ring.middleware.session :refer [wrap-session]] [ring.middleware.session.cookie :refer [cookie-store]] [ring.util.response :as response] - [unilog.context :as lc])) + [unilog.context :as lc] + [clj-time.coerce :as coerce] + [clj-time.core :as time])) (when (:aws-access-key-id env) (defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region env))) -(def running? (atom false)) +(defn deep-merge [v & vs] + (letfn [(rec-merge [v1 v2] + (if (and (map? v1) (map? v2)) + (merge-with deep-merge v1 v2) + v2))] + (when (some identity vs) + (reduce #(rec-merge %1 %2) v vs)))) -#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(mount/defstate manage-running? - :start (reset! running? true) - :stop (reset! running? false)) +(def all-routes ["/" (-> (into [] + (deep-merge ssr-routes/routes + (second client-routes/routes) + graphql/routes + ezcater/routes + health/routes + queries/routes2 + yodlee2/routes + auth/routes + invoices/routes + exports/routes2)) + (conj ["" (->ResourcesMaybe {:prefix "public/"})]) + (conj [true :not-found])) ;; always go for not found as last resort, have to switch to vec in order for that to work + ]) -(defroutes static-routes - (GET "/" [] (response/resource-response "index.html" {:root "public"})) - - (route/resources "/") - (routes (ANY "*" {:keys [uri]} - (if (bidi/match-route client-routes/routes uri) - (response/resource-response "index.html" {:root "public"}) - {:status 404 - :body "Not found"})))) +(defn not-found [_] + {:status 404 + :headers {} + :body ""}) -(defroutes health-check - (GET "/health-check" [] - (if @running? - {:status 200 - :body "Ok"} - {:status 503 - :body "Application shut down"}))) +(defn render-index [_] + (response/resource-response "index.html" {:root "public"})) + +(def match->handler-lookup + (-> {:not-found not-found} + (merge ssr/key->handler) + (merge graphql/match->handler) + (merge ezcater/match->handler) + (merge health/match->handler) + (merge queries/match->handler) + (merge yodlee2/match->handler) + (merge auth/match->handler) + (merge invoices/match->handler) + (merge exports/match->handler) + (merge + (into {} + (map + + (fn [k] + [k render-index]) + client-routes/all-matches))))) + +(def match->handler + (fn [route] + (or (get match->handler-lookup route) + route))) + + +(def route-handler + (make-handler all-routes + match->handler)) + +(defn wrap-guess-route [handler] + (fn [{:keys [uri request-method] :as request} ] + (let [matched-route (:handler + (bidi.bidi/match-route all-routes + uri + :request-method request-method))] + (handler (assoc request + :matched-route + matched-route))))) + +(defn test-match-route [method uri] + (bidi.bidi/match-route all-routes + uri + :request-method method)) -(defroutes api-routes - (context "/api" [] - exports/export-routes - yodlee2/routes - queries/query2-routes - invoices/routes - graphql/routes - ezcater/routes - auth/routes - health-check)) (def auth-backend (jws-backend {:secret (:jwt-secret env) :options {:alg :hs512}})) -(defn wrap-transaction [handler] - (fn [request] - (handler request))) - -(def app-routes - (routes - (wrap-transaction api-routes) - ssr-admin/admin-routes - static-routes)) - (defn wrap-logging [handler] (fn [request] - (lc/with-context {:uri (:uri request) - :source "request" + (mu/with-context {:uri (:uri request) + :query (:uri request) + :request-method (:request-method request) + :user (:identity request) :user-role (:user/role (:identity request)) :user-name (:user/name (:identity request))} + (mu/trace ::http-request-trace + [] + (lc/with-context {:uri (:uri request) + :source "request" + :user-role (:user/role (:identity request)) + :user-name (:user/name (:identity request))} + - (when-not (str/includes? (:uri request) "health-check") - (log/info "Beginning request" (:uri request))) - (handler request)))) + (when-not (str/includes? (:uri request) "health-check") + (alog/info ::http-request-starting)) + (try + (let [response (handler request)] + (alog/info ::http-request-done + :status-code (:status response)) + response) + (catch Exception e + (alog/error ::request-error + :exception e) + (throw e)))))))) + +(defn wrap-idle-session-timeout + [handler ] + (fn [request] + (let [session (:session request {}) + end-time (coerce/to-date-time (::idle-timeout session))] + (if (and end-time (time/before? end-time (time/now))) + {:session nil + :status 302 + :headers {"Location" "/login"}} + (when-let [response (handler request)] + (let [session (:session response session)] + (if (nil? session) + response + (let [end-time (time/plus (time/now) (time/days 2))] + (assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time))))))))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (def app - (-> #'app-routes - (wrap-logging) + (-> route-handler + (wrap-guess-route) (wrap-authorization auth-backend ) (wrap-authentication auth-backend (session-backend {:authfn (fn [auth] (dissoc auth :exp))})) - (wrap-session {:store (ring.middleware.session.cookie/cookie-store + (wrap-idle-session-timeout) + (wrap-session {:store (cookie-store {:key (byte-array [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )}) (wrap-reload) (wrap-params) (mp/wrap-multipart-params) - (wrap-edn-params))) + (wrap-edn-params) + (wrap-logging))) + diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index aac7b7ef..663a0654 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -1,11 +1,11 @@ (ns auto-ap.routes.auth - (:require [auto-ap.datomic.users :as users] - [buddy.sign.jwt :as jwt] - [clj-http.client :as http] - [clj-time.core :as time] - [compojure.core :refer [GET defroutes]] - [config.core :refer [env]] - [clojure.tools.logging :as log])) + (:require + [auto-ap.datomic.users :as users] + [buddy.sign.jwt :as jwt] + [clj-http.client :as http] + [clj-time.core :as time] + [clojure.tools.logging :as log] + [config.core :refer [env]])) (def google-client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com") (def google-client-secret "OC-WemHurPXYpuIw5cT-B90g") @@ -19,48 +19,50 @@ (:jwt-secret env) {:alg :hs512})) -(defroutes routes - (GET "/oauth" {{:strs [code]} :query-params {:strs [host]} :headers} - (try - (let [auth (-> "https://accounts.google.com/o/oauth2/token" - (http/post - {:form-params {"client_id" google-client-id - "client_secret" google-client-secret - "code" code - "redirect_uri" (str (:scheme env) "://" host "/api/oauth") - "grant_type" "authorization_code"} - :as :json}) - :body) - token (:access_token auth) - profile (-> (http/get "https://www.googleapis.com/oauth2/v1/userinfo" - {:headers {"Authorization" (str "Bearer " token)} :as :json}) - :body) - user (users/find-or-insert! {:user/provider "google" - :user/provider-id (:id profile) - :user/role :user-role/none - :user/name (:name profile)}) - auth {:user (:name profile) - :exp (time/plus (time/now) (time/days 30)) - :user/clients (map (fn [c] - (select-keys c [:client/code :db/id :client/name :client/locations])) - (:user/clients user)) - :user/role (name (:user/role user)) - :user/name (:name profile)} - ] - (log/info "authenticated as user" user) - ;; TODO - these namespaces are not being transmitted/deserialized properly - - (if (and token user) - (let [jwt (jwt/sign auth - (:jwt-secret env) - {:alg :hs512})] - - {:status 301 - :headers {"Location" (str "/?jwt=" jwt)} - :session {:identity (dissoc auth :exp)}}) - {:status 401 - :body "Couldn't authenticate"})) - (catch Exception e - (log/warn e ) - {:status 401 - :body (str "Couldn't authenticate " (.toString e))})))) +(defn oauth [{{:strs [code]} :query-params {:strs [host]} :headers}] + (try + (let [auth (-> "https://accounts.google.com/o/oauth2/token" + (http/post + {:form-params {"client_id" google-client-id + "client_secret" google-client-secret + "code" code + "redirect_uri" (str (:scheme env) "://" host "/api/oauth") + "grant_type" "authorization_code"} + :as :json}) + :body) + token (:access_token auth) + profile (-> (http/get "https://www.googleapis.com/oauth2/v1/userinfo" + {:headers {"Authorization" (str "Bearer " token)} :as :json}) + :body) + user (users/find-or-insert! {:user/provider "google" + :user/provider-id (:id profile) + :user/role :user-role/none + :user/name (:name profile)}) + auth {:user (:name profile) + :exp (time/plus (time/now) (time/days 30)) + :user/clients (map (fn [c] + (select-keys c [:client/code :db/id :client/name :client/locations])) + (:user/clients user)) + :user/role (name (:user/role user)) + :user/name (:name profile)} + ] + (log/info "authenticated as user" user) + ;; TODO - these namespaces are not being transmitted/deserialized properly + + (if (and token user) + (let [jwt (jwt/sign auth + (:jwt-secret env) + {:alg :hs512})] + + {:status 301 + :headers {"Location" (str "/?jwt=" jwt)} + :session {:identity (dissoc auth :exp)}}) + {:status 401 + :body "Couldn't authenticate"})) + (catch Exception e + (log/warn e ) + {:status 401 + :body (str "Couldn't authenticate " (.toString e))}))) + +(def routes {"api" {"/oauth" :oauth}}) +(def match->handler {:oauth oauth}) diff --git a/src/clj/auto_ap/routes/exports.clj b/src/clj/auto_ap/routes/exports.clj index 26f98555..bd92a79b 100644 --- a/src/clj/auto_ap/routes/exports.clj +++ b/src/clj/auto_ap/routes/exports.clj @@ -11,13 +11,13 @@ [auto-ap.routes.utils :refer [wrap-secure]] [auto-ap.time :as atime] [buddy.sign.jwt :as jwt] + [cheshire.generate :as generate] [clj-time.coerce :as coerce :refer [to-date]] [clj-time.core :as time] [clojure.data.csv :as csv] [clojure.edn :refer [read-string]] [clojure.tools.logging :as log] [com.unbounce.dogstatsd.core :as statsd] - [compojure.core :refer [context defroutes GET routes wrap-routes]] [config.core :refer [env]] [datomic.api :as d] [ring.middleware.json :refer [wrap-json-response]] @@ -30,25 +30,25 @@ (csv/write-csv w %) (.toString w)))))) -(def api-key-authed-routes - (context "/" [] - (GET "/sales/aggregated/export" {:keys [query-params]} - (let [client-id (Long/parseLong (get query-params "client-id")) - identity (jwt/unsign (get query-params "key") (:jwt-secret env) {:alg :hs512})] - (assert-can-see-client identity client-id) - (into (list) - (d/query {:query {:find '[?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge)] - :in '[$ ?c] - :where '[[?s :sales-order/client ?c] - [?s :sales-order/date ?d] - [?s :sales-order/total ?total] - [?s :sales-order/tax ?tax] - [?s :sales-order/tip ?tip] - [?s :sales-order/service-charge ?service-charge] - [(clj-time.coerce/to-date-time ?d) ?d2] - [(auto-ap.time/localize ?d2) ?d3] - [(auto-ap.time/unparse ?d3 auto-ap.time/normal-date) ?d4]]} - :args [(d/db conn) client-id]})))))) +(defn aggregated-sales-export [{:keys [query-params]}] + (let [client-id (Long/parseLong (get query-params "client-id")) + identity (jwt/unsign (get query-params "key") (:jwt-secret env) {:alg :hs512})] + (assert-can-see-client identity client-id) + (into (list) + (d/query {:query {:find '[?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge)] + :in '[$ ?c] + :where '[[?s :sales-order/client ?c] + [?s :sales-order/date ?d] + [?s :sales-order/total ?total] + [?s :sales-order/tax ?tax] + [?s :sales-order/tip ?tip] + [?s :sales-order/service-charge ?service-charge] + [(clj-time.coerce/to-date-time ?d) ?d2] + [(auto-ap.time/localize ?d2) ?d3] + [(auto-ap.time/unparse ?d3 auto-ap.time/normal-date) ?d4]]} + :args [(d/db conn) client-id]})))) + + (defn client-tag [params] (when-let [code (or (params "client-code") @@ -82,332 +82,366 @@ ])) m)) -(def admin-only-routes - (context "/" [] - (GET "/invoices/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:invoice"}}] - (list (into (list) - (map datomic-map->graphql-map) - (d/q '[:find [(pull ?i [:db/id :invoice/total :invoice/outstanding-balance :invoice/invoice-number :invoice/date :invoice/original-id - { :invoice/status [:db/ident] - :invoice/payments - [:invoice-payment/amount - {:invoice-payment/payment [:payment/check-number - :payment/memo - {:payment/bank_account [:bank-account/id :bank-account/name :bank-account/number :bank-account/bank-name :bank-account/bank-code :bank-account/code]}]}] - :invoice/vendor [:vendor/name - :db/id - {:vendor/primary-contact [:contact/name] - :vendor/address [:address/street1 :address/city :address/state :address/zip]}] - :invoice/expense-accounts [:db/id - :invoice-expense-account/amount - :invoice-expense-account/id - :invoice-expense-account/location - {:invoice-expense-account/account - [:db/id :account/numeric-code :account/name]}] - :invoice/client [:client/name :db/id :client/code :client/locations]}]) ...] - :in $ ?c - :where [?i :invoice/client ?c]] - (d/db conn) - [:client/code (query-params "client-code")]))))) - (GET "/payments/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:payment"}}] - (let [query [[:all_payments - {:client-code (query-params "client-code") - :original-id (query-params "original")} - [:id :check-number :amount :memo :date :status :type :original-id - [:invoices [[:invoice [:id :original-id]] :amount]] - [:bank-account [:number :code :bank-name :bank-code :id]] - [:vendor [:name :id [:primary-contact [:name :email :phone]] [:default-account [:name :numeric-code :id]] [:address [:street1 :city :state :zip]]]] - [:client [:id :name :code]] - ]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] - (list (:all-payments (:data payments)))))) +(defn export-invoices [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:invoice"}}] + {:body + (list (into (list) + (map datomic-map->graphql-map) + (d/q '[:find [(pull ?i [:db/id :invoice/total :invoice/outstanding-balance :invoice/invoice-number :invoice/date :invoice/original-id + { :invoice/status [:db/ident] + :invoice/payments + [:invoice-payment/amount + {:invoice-payment/payment [:payment/check-number + :payment/memo + {:payment/bank_account [:bank-account/id :bank-account/name :bank-account/number :bank-account/bank-name :bank-account/bank-code :bank-account/code]}]}] + :invoice/vendor [:vendor/name + :db/id + {:vendor/primary-contact [:contact/name] + :vendor/address [:address/street1 :address/city :address/state :address/zip]}] + :invoice/expense-accounts [:db/id + :invoice-expense-account/amount + :invoice-expense-account/id + :invoice-expense-account/location + {:invoice-expense-account/account + [:db/id :account/numeric-code :account/name]}] + :invoice/client [:client/name :db/id :client/code :client/locations]}]) ...] + :in $ ?c + :where [?i :invoice/client ?c]] + (d/db conn) + [:client/code (query-params "client-code")])))})) - (GET "/sales/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:sales"}}] - (let [query [[:all_sales_orders - (cond-> {:client-code (query-params "client-code")} - (query-params "after") (assoc :date-range {:start (query-params "after") - :end nil})) - [:id - :location - :external_id - :total - :tip - :tax - :discount - :returns - :service_charge - :date - [:charges [:type_name :total :tip]] - [:line_items [:item_name :total :tax :discount :category]] - [:client [:id :name :code]]]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)})) - parsedouble #(some-> % Double/parseDouble) ] - (seq (map - (fn [s] - (-> s - (assoc :utc_date (:date s)) - (update :date (fn [d] - (coerce/to-string (coerce/to-local-date-time (time/to-time-zone (coerce/to-date-time d) (time/time-zone-for-id "America/Los_Angeles")))))) - (update :total parsedouble) - (update :tax parsedouble) - (update :discount parsedouble) - (update :tip parsedouble) - (update :line-items (fn [lis] - (map - (fn [li] - (-> li - (update :tax parsedouble) - (update :discount parsedouble) - (update :total parsedouble))) - lis))) - (update :charges (fn [charges] - (map - (fn [charge] - (-> charge - (update :tip parsedouble) - (update :total parsedouble))) - charges))))) - (:all-sales-orders (:data payments)))))) - ) - - - - (GET "/expected-deposit/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:deposit"}}] - (let [query [[:all_expected_deposits - (cond-> {:client-code (query-params "client-code")} - (query-params "after") (assoc :date-range {:start (query-params "after") - :end nil})) - [:id - [:client [:id :name :code]] - :location - :external_id - :total - :fee - :date]]] - payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] - (seq (map - (fn [d] - (-> d - (update :fee #(some-> % Double/parseDouble)) - (update :total #(some-> % Double/parseDouble)))) - (:all-expected-deposits (:data payments))))))) - (GET "/clients/export" {:keys [identity]} - (assert-admin identity) - (map <-graphql (d-clients/get-all))) - - (GET "/vendors/export" {:keys [identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{"export:vendors"}}] - (map <-graphql (->> (d/q '[:find [?e ...] - :in $ - :where [?e :vendor/name]] - (d/db conn)) - (d/pull-many (d/db conn) vendor/default-read))))) - - (GET "/vendors/company/export" {:keys [identity query-params]} - (statsd/time! [(str "export.time") {:tags #{"export:company-vendors"}}] - (let [client (:db/id (d/pull (d/db conn) [:db/id] [:client/code (get query-params "client")])) - - _ (assert-can-see-client identity client) - data (->> (d/q '[:find (pull ?v [:vendor/name - :vendor/terms - {:vendor/default-account [:account/name :account/numeric-code - {:account/client-overrides - [:account-client-override/client - :account-client-override/name]}] - :vendor/terms-overrides [:vendor-terms-override/client - :vendor-terms-override/terms] - :vendor/account-overrides [:vendor-account-override/client - {:vendor-account-override/account [:account/numeric-code :account/name - {:account/client-overrides - [:account-client-override/client - :account-client-override/name]}]}] - :vendor/address [:address/street1 :address/city :address/state :address/zip]}]) - :in $ ?c - :where [?vu :vendor-usage/client ?c] - [?vu :vendor-usage/count ?count] - [(>= ?vu 0)] - [?vu :vendor-usage/vendor ?v] - (not [?v :vendor/hidden true])] - (d/db conn) - client) - (map (fn [[v]] - [(-> v :vendor/name) - (-> v :vendor/address :address/street1) - (-> v :vendor/address :address/city) - (-> v :vendor/address :address/state) - (-> v :vendor/address :address/zip) - (-> v (vendor/terms-for-client-id client) ) - (-> v (vendor/account-for-client-id client) (accounts/clientize client) :account/name) - (-> v (vendor/account-for-client-id client) :account/numeric-code) - ] - )) - (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] - {:body - (with-open [w (java.io.StringWriter.)] - (csv/write-csv w data - :quote? (constantly true)) - (.toString w)) - :headers {"Content-Type" "application/csv"}}))) - (GET "/ledger/export" {:keys [identity query-params]} - (let [start-date (or (some-> (query-params "start-date") - (atime/parse atime/iso-date)) - (time/plus (time/now) (time/days -120)))] - (log/info "exporting for " (query-params "client-code") "starting" start-date) - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:ledger2"}}] - (let [results (->> (d/q '[:find (pull ?e [:db/id - :journal-entry/external-id - :journal-entry/cleared - :journal-entry/alternate-description - :journal-entry/date - :journal-entry/note - :journal-entry/amount - :journal-entry/source - :journal-entry/cleared-against - :journal-entry/original-entity - {:journal-entry/client [:client/name :client/code :db/id] - :journal-entry/vendor [:vendor/name :db/id] - :journal-entry/line-items [:db/id - :journal-entry-line/location - :journal-entry-line/debit - :journal-entry-line/credit - {:journal-entry-line/account [:bank-account/include-in-reports - :bank-account/bank-name - :bank-account/numeric-code - :bank-account/code - :bank-account/visible - :bank-account/name - :bank-account/number - :account/code - :account/name - :account/numeric-code - :account/location - {:account/type [:db/ident :db/id]} - {:bank-account/type [:db/ident :db/id]}]}]}]) - :in $ ?c ?start-date - :where [?e :journal-entry/client ?c] - [?e :journal-entry/date ?date] - [(>= ?date ?start-date)]] - (d/db conn) - [:client/code (query-params "client-code")] - (coerce/to-date start-date))) - tf-result (transduce (comp - (map first) - (filter (fn [je] - (every? - (fn [jel] - (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] - (or (nil? include-in-reports) - (true? include-in-reports)))) - (:journal-entry/line-items je)))) - (map <-graphql)) - conj - (list) - results)] - tf-result)))) +(defn export-payments [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:payment"}}] + (let [query [[:all_payments + {:client-code (query-params "client-code") + :original-id (query-params "original")} + [:id :check-number :amount :memo :date :status :type :original-id + [:invoices [[:invoice [:id :original-id]] :amount]] + [:bank-account [:number :code :bank-name :bank-code :id]] + [:vendor [:name :id [:primary-contact [:name :email :phone]] [:default-account [:name :numeric-code :id]] [:address [:street1 :city :state :zip]]]] + [:client [:id :name :code]] + ]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] + {:body + (list (:all-payments (:data payments)))}))) +(defn export-sales [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:sales"}}] + (let [query [[:all_sales_orders + (cond-> {:client-code (query-params "client-code")} + (query-params "after") (assoc :date-range {:start (query-params "after") + :end nil})) + [:id + :location + :external_id + :total + :tip + :tax + :discount + :returns + :service_charge + :date + [:charges [:type_name :total :tip]] + [:line_items [:item_name :total :tax :discount :category]] + [:client [:id :name :code]]]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)})) + parsedouble #(some-> % Double/parseDouble) ] + {:body + (seq (map + (fn [s] + (-> s + (assoc :utc_date (:date s)) + (update :date (fn [d] + (coerce/to-string (coerce/to-local-date-time (time/to-time-zone (coerce/to-date-time d) (time/time-zone-for-id "America/Los_Angeles")))))) + (update :total parsedouble) + (update :tax parsedouble) + (update :discount parsedouble) + (update :tip parsedouble) + (update :line-items (fn [lis] + (map + (fn [li] + (-> li + (update :tax parsedouble) + (update :discount parsedouble) + (update :total parsedouble))) + lis))) + (update :charges (fn [charges] + (map + (fn [charge] + (-> charge + (update :tip parsedouble) + (update :total parsedouble))) + charges))))) + (:all-sales-orders (:data payments))))}))) - (GET "/accounts/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:accounts"}}] - (let [client-id (d-clients/code->id (query-params "client-code")) - query [[:all-accounts - [:id :numeric_code :type :applicability :location :name [:client_overrides [:name [:client [:id :code :name]]]]]]] - all-accounts (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] +(defn export-expected-deposits [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:deposit"}}] + (let [query [[:all_expected_deposits + (cond-> {:client-code (query-params "client-code")} + (query-params "after") (assoc :date-range {:start (query-params "after") + :end nil})) + [:id + [:client [:id :name :code]] + :location + :external_id + :total + :fee + :date]]] + payments (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] + {:body + (seq (map + (fn [d] + (-> d + (update :fee #(some-> % Double/parseDouble)) + (update :total #(some-> % Double/parseDouble)))) + (:all-expected-deposits (:data payments))))}))) - (list (transduce - (comp - (filter (fn [a] - (let [overriden-clients (set (map (comp :id :client) (:client-overrides a)))] - (or (nil? (:applicability a)) - (= :global (:applicability a)) - (overriden-clients (str client-id)))))) - (map (fn [a] - (let [client->name (reduce - (fn [override co] - (assoc override (str (:id (:client co))) (:name co))) - {} - (:client-overrides a))] - (-> a - (assoc :global-name (:name a)) - (assoc :client-name (client->name (str client-id) (:name a))) - (dissoc :client-overrides)))))) - conj - (list) - (:all-accounts (:data all-accounts))))))) - (GET "/transactions/export" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:transactions"}}] - (let [[transactions] (d-transactions/get-graphql {:client-code (query-params "client-code") - #_#_:original-id (Integer/parseInt (query-params "original")) - :count Integer/MAX_VALUE})] - - - (map (comp ->graphql (fn [i] - (cond-> i - true (update :transaction/date to-date) - true (update :transaction/post-date to-date) - (:transaction/payment i) (update-in [:transaction/payment :payment/date] to-date) - (:transaction/expected-deposit i) (update-in [:transaction/expected-deposit :expected-deposit/date] to-date)))) - transactions))) - ) +(generate/add-encoder org.joda.time.DateTime + (fn [c jsonGenerator] + (.writeString jsonGenerator (str c)))) - (GET "/transactions/export2" {:keys [query-params identity]} - (assert-admin identity) - (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) - "export:transactions2"}}] - (let [db (d/db conn)] - (->> - (d/query {:query {:find ['?e] - :in ['$ '?client-code] - :where ['[?e :transaction/client ?client-code]]} - :args [db [:client/code (query-params "client-code")]]}) - (map first) - (map (fn [e] - (let [e (d/entity db e) - client (:transaction/client e) - bank-account (:transaction/bank-account e)] - {:id (:db/id e) - :date (:transaction/date e) - :post_date (:transaction/post-date e) - :client { :code (:client/code client) - :id (:db/id client) - :name (:client/name client)} - :amount (:transaction/amount e) - :description_original (:transaction/description-original e) - :approval_status (:transaction/approval-status e) - :bank_account {:name (:bank-account/name bank-account) - :code (:bank-account/code bank-account) - :id (:db/id bank-account)}}))))))) - (GET "/raw" {:keys [query-params identity]} - (assert-admin identity) - (log/info "Executing raw query " (get query-params "query" )) - (statsd/time! [(str "export.time") {:tags #{"export:raw"}}] - (into (list) (apply d/q (read-string (get query-params "query" )) (into [(d/db conn)] (read-string (get query-params "args" "[]"))))))))) +(defn export-clients[{:keys [identity]}] + (assert-admin identity) + {:body (into [] + (map <-graphql) + (d-clients/get-all))}) -(defroutes export-routes - (routes - (wrap-routes api-key-authed-routes - wrap-csv-response) +(defn export-vendors [{:keys [identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{"export:vendors"}}] + {:body + (map <-graphql (->> (d/q '[:find [?e ...] + :in $ + :where [?e :vendor/name]] + (d/db conn)) + (d/pull-many (d/db conn) vendor/default-read)))})) - (wrap-routes (wrap-routes admin-only-routes wrap-secure) - wrap-json-response))) +(defn export-company-vendors [{:keys [identity query-params]}] + (statsd/time! [(str "export.time") {:tags #{"export:company-vendors"}}] + (let [client (:db/id (d/pull (d/db conn) [:db/id] [:client/code (get query-params "client")])) + + _ (assert-can-see-client identity client) + data (->> (d/q '[:find (pull ?v [:vendor/name + :vendor/terms + {:vendor/default-account [:account/name :account/numeric-code + {:account/client-overrides + [:account-client-override/client + :account-client-override/name]}] + :vendor/terms-overrides [:vendor-terms-override/client + :vendor-terms-override/terms] + :vendor/account-overrides [:vendor-account-override/client + {:vendor-account-override/account [:account/numeric-code :account/name + {:account/client-overrides + [:account-client-override/client + :account-client-override/name]}]}] + :vendor/address [:address/street1 :address/city :address/state :address/zip]}]) + :in $ ?c + :where [?vu :vendor-usage/client ?c] + [?vu :vendor-usage/count ?count] + [(>= ?vu 0)] + [?vu :vendor-usage/vendor ?v] + (not [?v :vendor/hidden true])] + (d/db conn) + client) + (map (fn [[v]] + [(-> v :vendor/name) + (-> v :vendor/address :address/street1) + (-> v :vendor/address :address/city) + (-> v :vendor/address :address/state) + (-> v :vendor/address :address/zip) + (-> v (vendor/terms-for-client-id client) ) + (-> v (vendor/account-for-client-id client) (accounts/clientize client) :account/name) + (-> v (vendor/account-for-client-id client) :account/numeric-code) + ] + )) + (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] + {:body + (into (list) + data)}))) + +(defn export-ledger [{:keys [identity query-params]}] + (let [start-date (or (some-> (query-params "start-date") + (atime/parse atime/iso-date)) + (time/plus (time/now) (time/days -120)))] + (log/info "exporting for " (query-params "client-code") "starting" start-date) + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:ledger2"}}] + (let [results (->> (d/q '[:find (pull ?e [:db/id + :journal-entry/external-id + :journal-entry/cleared + :journal-entry/alternate-description + :journal-entry/date + :journal-entry/note + :journal-entry/amount + :journal-entry/source + :journal-entry/cleared-against + :journal-entry/original-entity + {:journal-entry/client [:client/name :client/code :db/id] + :journal-entry/vendor [:vendor/name :db/id] + :journal-entry/line-items + [:db/id + :journal-entry-line/location + :journal-entry-line/debit + :journal-entry-line/credit + {:journal-entry-line/account + [:bank-account/include-in-reports + :bank-account/bank-name + :bank-account/numeric-code + :bank-account/code + :bank-account/visible + :bank-account/name + :bank-account/number + :account/code + :account/name + :account/numeric-code + :account/location + {:account/type [:db/ident :db/id]} + {:bank-account/type [:db/ident :db/id]}]}]}]) + :in $ ?c ?start-date + :where [?e :journal-entry/client ?c] + [?e :journal-entry/date ?date] + [(>= ?date ?start-date)]] + (d/db conn) + [:client/code (query-params "client-code")] + (coerce/to-date start-date))) + tf-result (transduce (comp + (map first) + (filter (fn [je] + (every? + (fn [jel] + (let [include-in-reports (-> jel :journal-entry-line/account :bank-account/include-in-reports)] + (or (nil? include-in-reports) + (true? include-in-reports)))) + (:journal-entry/line-items je)))) + (map <-graphql)) + conj + (list) + results)] + {:body + tf-result})))) + +(defn export-accounts [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:accounts"}}] + (let [client-id (d-clients/code->id (query-params "client-code")) + query [[:all-accounts + [:id :numeric_code :type :applicability :location :name [:client_overrides [:name [:client [:id :code :name]]]]]]] + all-accounts (graphql/query identity (venia/graphql-query {:venia/queries (->graphql query)}))] + + {:body + (list (transduce + (comp + (filter (fn [a] + (let [overriden-clients (set (map (comp :id :client) (:client-overrides a)))] + (or (nil? (:applicability a)) + (= :global (:applicability a)) + (overriden-clients (str client-id)))))) + (map (fn [a] + (let [client->name (reduce + (fn [override co] + (assoc override (str (:id (:client co))) (:name co))) + {} + (:client-overrides a))] + (-> a + (assoc :global-name (:name a)) + (assoc :client-name (client->name (str client-id) (:name a))) + (dissoc :client-overrides)))))) + conj + (list) + (:all-accounts (:data all-accounts))))}))) + +(defn export-transactions2 [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:transactions2"}}] + {:body (let [db (d/db conn)] + (->> + (d/query {:query {:find ['?e] + :in ['$ '?client-code] + :where ['[?e :transaction/client ?client-code]]} + :args [db [:client/code (query-params "client-code")]]}) + (map first) + (map (fn [e] + (let [e (d/entity db e) + client (:transaction/client e) + bank-account (:transaction/bank-account e)] + {:id (:db/id e) + :date (:transaction/date e) + :post_date (:transaction/post-date e) + :client { :code (:client/code client) + :id (:db/id client) + :name (:client/name client)} + :amount (:transaction/amount e) + :description_original (:transaction/description-original e) + :approval_status (:transaction/approval-status e) + :bank_account {:name (:bank-account/name bank-account) + :code (:bank-account/code bank-account) + :id (:db/id bank-account)}})))))})) + +(defn export-transactions [{:keys [query-params identity]}] + (assert-admin identity) + (statsd/time! [(str "export.time") {:tags #{(client-tag query-params) + "export:transactions"}}] + (let [[transactions] (d-transactions/get-graphql {:client-code (query-params "client-code") + #_#_:original-id (Integer/parseInt (query-params "original")) + :count Integer/MAX_VALUE})] + + + {:body (map + (comp ->graphql + (fn [i] + (cond-> i + true (update :transaction/date to-date) + true (update :transaction/post-date to-date) + (:transaction/payment i) (update-in [:transaction/payment :payment/date] to-date) + (:transaction/expected-deposit i) (update-in [:transaction/expected-deposit :expected-deposit/date] to-date)))) + transactions)}))) + +(defn export-raw [{:keys [query-params identity]}] + (assert-admin identity) + (log/info "Executing raw query " (get query-params "query" )) + (statsd/time! [(str "export.time") {:tags #{"export:raw"}}] + {:body + (into (list) (apply d/q (read-string (get query-params "query" )) (into [(d/db conn)] (read-string (get query-params "args" "[]")))))})) + + +(def routes2 {"api/" {"sales/" {"aggregated/" {#"export/?" {:get :aggregated-sales-export}} + #"export/?" {:get :export-sales}} + "invoices/" {#"export/?" {:get :export-invoices}} + "payments/" {#"export/?" {:get :export-payments}} + "expected-deposit/" {#"export/?" {:get :export-expected-deposits}} + "clients/" {#"export/?" {:get :export-clients}} + "vendors/" {#"export/?" {:get :export-vendors} + "/company" {#"export/?" {:get :export-company-vendors}}} + "ledger/" {#"export/?" {:get :export-ledger}} + "accounts/" {#"export/?" {:get :export-accounts}} + "transactions/" {#"export/?" {:get :export-transactions} + #"export2/?" {:get :export-transactions2}} + #"raw/?" {:get :export-raw}}}) + +(def match->handler {:aggregated-sales-export (wrap-csv-response aggregated-sales-export) + :export-invoices (-> export-invoices wrap-json-response wrap-secure ) + :export-payments (-> export-payments wrap-json-response wrap-secure) + :export-sales (-> export-sales wrap-json-response wrap-secure) + :export-expected-deposits (-> export-expected-deposits wrap-json-response wrap-secure) + :export-clients (-> export-clients wrap-json-response wrap-secure) + :export-vendors (-> export-vendors wrap-json-response wrap-secure) + :export-company-vendors (-> export-company-vendors wrap-csv-response wrap-secure) + :export-ledger (-> export-ledger wrap-json-response wrap-secure) + :export-accounts (-> export-accounts wrap-json-response wrap-secure) + :export-transactions (-> export-transactions wrap-json-response wrap-secure) + :export-transactions2 (-> export-transactions2 wrap-json-response wrap-secure) + :export-raw (-> export-raw wrap-json-response wrap-secure)}) diff --git a/src/clj/auto_ap/routes/ezcater.clj b/src/clj/auto_ap/routes/ezcater.clj index caa1cd62..4d565d84 100644 --- a/src/clj/auto_ap/routes/ezcater.clj +++ b/src/clj/auto_ap/routes/ezcater.clj @@ -1,23 +1,30 @@ (ns auto-ap.routes.ezcater (:require - [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]])) + [auto-ap.logging :as alog] + [ring.middleware.json :refer [wrap-json-params]])) -(defroutes routes - (wrap-routes - (context "/ezcater" [] - (GET "/event" request - (log/info (str "GET EVENT " (body-string request) request)) - {:status 200 - :headers {"Content-Type" "application/json"} - :body "{}"}) - (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 "{}"})) - wrap-json-params)) +(defn handle-ezcater [{:keys [request-method json-params] :as r}] + (cond + (= :get request-method) + {:status 200 + :headers {"Content-Type" "application/json"} + :body "{}"} + + (= :post request-method) + (do + (alog/info ::ezcater-request + :json-params json-params) + (e/import-order json-params) + {:status 200 + :headers {"Content-Type" "application/json"} + :body "{}"}) + + :else + {:status 404})) + + + +(def routes {"api/" {"ezcater/" {#"event/?" :ezcater-event}}}) +(def match->handler {:ezcater-event (-> handle-ezcater + wrap-json-params)}) diff --git a/src/clj/auto_ap/routes/graphql.clj b/src/clj/auto_ap/routes/graphql.clj index 13d686d7..dd03dfa1 100644 --- a/src/clj/auto_ap/routes/graphql.clj +++ b/src/clj/auto_ap/routes/graphql.clj @@ -4,8 +4,6 @@ [auto-ap.logging :refer [warn-event]] [buddy.auth :refer [throw-unauthorized]] [clojure.edn :as edn] - [compojure.core :refer [GET POST context defroutes - wrap-routes]] [clojure.tools.logging :as log])) (defn handle-graphql [{:keys [request-method query-params] :as r}] (when (= "none" (:user/role (:identity r))) @@ -39,9 +37,5 @@ :headers {"Content-Type" "application/edn"}})))))) -(defroutes routes - (wrap-routes - (context "/graphql" [] - (GET "/" x (handle-graphql x)) - (POST "/" x (handle-graphql x))) - wrap-secure)) +(def routes {"api/" {#"graphql/?" :graphql}}) +(def match->handler {:graphql (wrap-secure handle-graphql)}) diff --git a/src/clj/auto_ap/routes/health.clj b/src/clj/auto_ap/routes/health.clj new file mode 100644 index 00000000..8868130d --- /dev/null +++ b/src/clj/auto_ap/routes/health.clj @@ -0,0 +1,19 @@ +(ns auto-ap.routes.health + (:require [mount.core :as mount])) + +(def running? (atom false)) + +#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} +(mount/defstate manage-running? + :start (reset! running? true) + :stop (reset! running? false)) + +(defn health-check [request] + (if @running? + {:status 200 + :body "Ok"} + {:status 503 + :body "Application shut down"})) + +(def routes {"api/" {"health-check" :health}}) +(def match->handler {:health health-check}) diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 540b1f9a..c844b30b 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -18,7 +18,6 @@ [clojure.java.io :as io] [clojure.string :as str] [clojure.tools.logging :as log] - [compojure.core :refer [context defroutes POST wrap-routes]] [config.core :refer [env]] [datomic.api :as d] [digest] @@ -385,142 +384,147 @@ rows)] (transact-with-ledger txes nil))) -(defroutes routes - (wrap-routes - (context "/" [] - (context "/transactions" [] - (POST "/batch-upload" - {{:keys [data]} :edn-params user :identity} - (assert-admin user) - (try - (let [stats (manual/import-batch (manual/tabulate-data data) (:user/name user))] - {:status 200 - :body (pr-str stats) - :headers {"Content-Type" "application/edn"}}) - (catch Exception e - (log/error e) - {:status 500 - :body (pr-str {:message (.getMessage e) - :error (.toString e) - :data (ex-data e)}) - :headers {"Content-Type" "application/edn"}})))) +(defn batch-upload-transactions [{{:keys [data]} :edn-params user :identity}] + (assert-admin user) + (try + (let [stats (manual/import-batch (manual/tabulate-data data) (:user/name user))] + {:status 200 + :body (pr-str stats) + :headers {"Content-Type" "application/edn"}}) + (catch Exception e + (log/error e) + {:status 500 + :body (pr-str {:message (.getMessage e) + :error (.toString e) + :data (ex-data e)}) + :headers {"Content-Type" "application/edn"}}))) - (context "/invoices" [] - (POST "/upload" - {{files :file - files-2 "file" - client :client - client-2 "client" - location :location - location-2 "location" - vendor :vendor - vendor-2 "vendor"} :params - user :identity} - (let [files (or files files-2) - client (or client client-2) - location (or location location-2) - vendor (some-> (or vendor vendor-2) - (Long/parseLong)) - {:keys [filename tempfile]} files] - (lc/with-context {:parsing-file filename} - (log/info tempfile) - (try - (let [extension (last (str/split (.getName (io/file filename)) #"\.")) - s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension) - _ (s3/put-object (:data-bucket env) - s3-location - (io/input-stream tempfile) - {:content-type (if (= "csv" extension) - "text/csv" - "application/pdf") - :content-length (.length tempfile)}) - imports (->> (parse/parse-file (.getPath tempfile) filename) - (map #(assoc % - :client-override client - :location-override location - :vendor-override vendor - :source-url (str "http://" (:data-bucket env) - ".s3-website-us-east-1.amazonaws.com/" - s3-location))))] - (import-uploaded-invoice user imports)) - {:status 200 - :body (pr-str {}) - :headers {"Content-Type" "application/edn"}} - (catch Exception e - (log/warn e) - {:status 400 - :body (pr-str {:message (.getMessage e) - :error (.toString e) - :data (ex-data e)}) - :headers {"Content-Type" "application/edn"}}))))) +(defn upload-invoices [{{files :file + files-2 "file" + client :client + client-2 "client" + location :location + location-2 "location" + vendor :vendor + vendor-2 "vendor"} :params + user :identity}] + (let [files (or files files-2) + client (or client client-2) + location (or location location-2) + vendor (some-> (or vendor vendor-2) + (Long/parseLong)) + {:keys [filename tempfile]} files] + (lc/with-context {:parsing-file filename} + (log/info tempfile) + (try + (let [extension (last (str/split (.getName (io/file filename)) #"\.")) + s3-location (str "invoice-files/" (str (UUID/randomUUID)) "." extension) + _ (s3/put-object (:data-bucket env) + s3-location + (io/input-stream tempfile) + {:content-type (if (= "csv" extension) + "text/csv" + "application/pdf") + :content-length (.length tempfile)}) + imports (->> (parse/parse-file (.getPath tempfile) filename) + (map #(assoc % + :client-override client + :location-override location + :vendor-override vendor + :source-url (str "http://" (:data-bucket env) + ".s3-website-us-east-1.amazonaws.com/" + s3-location))))] + (import-uploaded-invoice user imports)) + {:status 200 + :body (pr-str {}) + :headers {"Content-Type" "application/edn"}} + (catch Exception e + (log/warn e) + {:status 400 + :body (pr-str {:message (.getMessage e) + :error (.toString e) + :data (ex-data e)}) + :headers {"Content-Type" "application/edn"}}))))) - (POST "/upload-integreat" - {{:keys [excel-rows]} :edn-params user :identity} - (assert-admin user) - (let [parsed-invoice-rows (parse-invoice-rows excel-rows) - existing-rows (set (d-invoices/get-existing-set)) - grouped-rows (group-by - (fn [i] - (cond (seq (:errors i)) - :error +(defn bulk-upload-invoices [{{:keys [excel-rows]} :edn-params user :identity}] + (assert-admin user) + (let [parsed-invoice-rows (parse-invoice-rows excel-rows) + existing-rows (set (d-invoices/get-existing-set)) + grouped-rows (group-by + (fn [i] + (cond (seq (:errors i)) + :error - (existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)]) - :exists + (existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)]) + :exists - :else - :new)) - parsed-invoice-rows) - vendors-not-found (->> parsed-invoice-rows - (filter #(and (nil? (:vendor-id %)) - (not= "Cash" (:check %)))) - (map :vendor-name) - set) - _ (transact-with-ledger (invoice-rows->transaction (:new grouped-rows) - user) - user)] - {:status 200 - :body (pr-str {:imported (count (:new grouped-rows)) - :already-imported (count (:exists grouped-rows)) - :vendors-not-found vendors-not-found - :errors (map #(dissoc % :date) (:error grouped-rows))}) - :headers {"Content-Type" "application/edn"}}))) - (POST "/transactions/cleared-against" - {{files :file - files-2 "file"} :params - user :identity} - (let [files (or files files-2) - {:keys [tempfile]} files] - (assert-admin user) - (try - (import-transactions-cleared-against (.getPath tempfile)) - {:status 200 - :body (pr-str {}) - :headers {"Content-Type" "application/edn"}} - (catch Exception e - (log/error e) - {:status 500 - :body (pr-str {:message (.getMessage e) - :error (.toString e) - :data (ex-data e)}) - :headers {"Content-Type" "application/edn"}})))) - (wrap-json-response (POST "/account-overrides" - {{files :file - files-2 "file" - client :client - client-2 "client"} :params - user :identity} - (let [files (or files files-2) - client (or client client-2) - {:keys [tempfile]} files] - (assert-admin user) - (try - {:status 200 - :body (import-account-overrides client (.getPath tempfile)) - :headers {"Content-Type" "application/json"}} - (catch Exception e - (log/error e) - {:status 500 - :body {:message (.getMessage e) - :data (ex-data e)} - :headers {"Content-Type" "application/json"}})))))) - wrap-secure)) + :else + :new)) + parsed-invoice-rows) + vendors-not-found (->> parsed-invoice-rows + (filter #(and (nil? (:vendor-id %)) + (not= "Cash" (:check %)))) + (map :vendor-name) + set) + _ (transact-with-ledger (invoice-rows->transaction (:new grouped-rows) + user) + user)] + {:status 200 + :body (pr-str {:imported (count (:new grouped-rows)) + :already-imported (count (:exists grouped-rows)) + :vendors-not-found vendors-not-found + :errors (map #(dissoc % :date) (:error grouped-rows))}) + :headers {"Content-Type" "application/edn"}})) + +(defn cleared-against [{{files :file + files-2 "file"} :params + user :identity}] + (let [files (or files files-2) + {:keys [tempfile]} files] + (assert-admin user) + (try + (import-transactions-cleared-against (.getPath tempfile)) + {:status 200 + :body (pr-str {}) + :headers {"Content-Type" "application/edn"}} + (catch Exception e + (log/error e) + {:status 500 + :body (pr-str {:message (.getMessage e) + :error (.toString e) + :data (ex-data e)}) + :headers {"Content-Type" "application/edn"}})))) + + + +(defn bulk-account-overrides [{{files :file + files-2 "file" + client :client + client-2 "client"} :params + user :identity}] + (let [files (or files files-2) + client (or client client-2) + {:keys [tempfile]} files] + (assert-admin user) + (try + {:status 200 + :body (import-account-overrides client (.getPath tempfile)) + :headers {"Content-Type" "application/json"}} + (catch Exception e + (log/error e) + {:status 500 + :body {:message (.getMessage e) + :data (ex-data e)} + :headers {"Content-Type" "application/json"}})))) + +(def routes {"api/" {"transactions/" {:post {#"batch-upload/?" :batch-upload-transactions + #"cleared-against/?" :cleared-against}} + "invoices/" {:post {#"upload/?" :upload-invoices + #"upload-integreat/?" :bulk-upload-invoices}} + :post {#"account-overrides/?" :bulk-account-overrides}}}) + +(def match->handler {:batch-upload-transactions (wrap-secure batch-upload-transactions) + :upload-invoices (wrap-secure upload-invoices) + :bulk-upload-invoices (wrap-secure bulk-upload-invoices) + :cleared-against (wrap-secure cleared-against) + :bulk-account-overrides (wrap-secure (wrap-json-response bulk-account-overrides))}) diff --git a/src/clj/auto_ap/routes/queries.clj b/src/clj/auto_ap/routes/queries.clj index 94209259..1affc898 100644 --- a/src/clj/auto_ap/routes/queries.clj +++ b/src/clj/auto_ap/routes/queries.clj @@ -1,22 +1,21 @@ (ns auto-ap.routes.queries - (:require [amazonica.aws.s3 :as s3] - [auto-ap.datomic :refer [conn]] - [auto-ap.graphql.utils :refer [assert-admin]] - [clojure.data.csv :as csv] - [clojure.java.io :as io] - [clojure.string :as str] - [clojure.tools.logging :as log] - [com.unbounce.dogstatsd.core :as statsd] - [compojure.core - :refer - [context defroutes GET POST PUT routes wrap-routes]] - [config.core :refer [env]] - [clojure.edn :as edn] - [datomic.api :as d] - [ring.middleware.json :refer [wrap-json-response]] - [ring.util.request :refer [body-string]] - [unilog.context :as lc]) - (:import java.util.UUID)) + (:require + [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn]] + [auto-ap.graphql.utils :refer [assert-admin]] + [clojure.data.csv :as csv] + [clojure.edn :as edn] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.tools.logging :as log] + [com.unbounce.dogstatsd.core :as statsd] + [config.core :refer [env]] + [datomic.api :as d] + [ring.middleware.json :refer [wrap-json-response]] + [ring.util.request :refer [body-string]] + [unilog.context :as lc]) + (:import + (java.util UUID))) (defn wrap-csv-response [handler] (fn [request] @@ -62,64 +61,77 @@ :csv-results-url (str "/api/queries/" guid "/results/csv") :json-results-url (str "/api/queries/" guid "/results/json")}})) -(def json-routes - (context "/queries" [] - (POST "/" {:keys [query-params identity] :as request} - (assert-admin identity) - (log/info "Note" (query-params "note")) - (put-query (str (UUID/randomUUID)) (body-string request) (query-params "note"))) - (PUT "/:query-id" {:keys [query-params identity params] :as request} - (assert-admin identity) - (log/info "Note" (query-params "note")) - (put-query (:query-id params) (body-string request) (query-params "note"))) - (GET "/:query-id" {:keys [identity params]} - (assert-admin identity) - (let [{:keys [query-id]} params - obj (s3/get-object :bucket-name (:data-bucket env) - :key (str "queries/" query-id)) - query-string (str (slurp (:object-content obj)))] - (log/info obj) - {:body {:query query-string - :note (:note (:user-metadata (:object-metadata obj))) - :id query-id - :csv-results-url (str "/api/queries/" query-id "/results/csv") - :json-results-url (str "/api/queries/" query-id "/results/json")}})) - - - (GET "/" {:keys [identity]} - (assert-admin identity) - (let [obj (s3/list-objects :bucket-name (:data-bucket env) - :prefix (str "queries/"))] - (log/info obj) - {:body (->> (:object-summaries obj) - (map (fn [o] - {:last-modified (.toString (:last-modified o)) - :key (str/replace (:key o) #"^queries\/" "")})))})) - - (GET "/:query-id/results/json" {:keys [query-params params]} - (statsd/time! [(str "export.query.time") {:tags #{(str "query:" (:query-id params))}}] - {:body (execute-query query-params params)})))) -(def raw-routes - (context "/queries" [] - (GET "/:query-id/raw" {:keys [identity params]} - (assert-admin identity) - (let [{:keys [query-id]} params - obj (s3/get-object :bucket-name (:data-bucket env) - :key (str "queries/" query-id)) - query-string (str (slurp (:object-content obj)))] - (log/info obj) - {:body query-string})))) +(defn get-queries [{:keys [identity]}] + (assert-admin identity) + (let [obj (s3/list-objects :bucket-name (:data-bucket env) + :prefix (str "queries/"))] + (log/info obj) + {:body (->> (:object-summaries obj) + (map (fn [o] + {:last-modified (.toString (:last-modified o)) + :key (str/replace (:key o) #"^queries\/" "")})))})) + +(defn create-query [{:keys [query-params identity] :as request}] + (assert-admin identity) + (log/info "Note" (query-params "note")) + (put-query (str (UUID/randomUUID)) (body-string request) (query-params "note"))) + +(defn get-query [{:keys [identity params]} ] + (assert-admin identity) + (let [{:keys [query-id]} params + obj (s3/get-object :bucket-name (:data-bucket env) + :key (str "queries/" query-id)) + query-string (str (slurp (:object-content obj)))] + (log/info obj) + {:body {:query query-string + :note (:note (:user-metadata (:object-metadata obj))) + :id query-id + :csv-results-url (str "/api/queries/" query-id "/results/csv") + :json-results-url (str "/api/queries/" query-id "/results/json")}})) + +(defn update-query [{:keys [query-params identity params] :as request} ] + (assert-admin identity) + (log/info "Note" (query-params "note")) + (put-query (:query-id params) (body-string request) (query-params "note"))) + +(defn results-json-query [{:keys [query-params params]}] + (statsd/time! [(str "export.query.time") {:tags #{(str "query:" (:query-id params))}}] + {:body (execute-query query-params params)})) + +(defn raw-query [{:keys [identity params]}] + (assert-admin identity) + (let [{:keys [query-id]} params + obj (s3/get-object :bucket-name (:data-bucket env) + :key (str "queries/" query-id)) + query-string (str (slurp (:object-content obj)))] + (log/info obj) + {:body query-string})) + +(defn results-csv-query [{:keys [query-params params]}] + (statsd/time! [(str "export.query.time") {:tags #{(str "query:" (:query-id params))}}] + {:body (execute-query query-params params)})) + +(def routes2 {"api/" {#"queries/?" {[:query-id "/raw"] {:get :raw-query} + [:query-id "/results/csv"] {:get :results-csv-query} + [:query-id "/results/json"] {:get :results-json-query} + [:query-id] {:get :get-query + :put :update-query} + :get :get-queries + :post :create-query}}}) + +(def match->handler {:get-queries get-queries + :create-query create-query + :raw-query raw-query + :get-query (-> get-query + wrap-json-response) + :update-query (-> update-query + wrap-json-response) + + :results-json-query (-> results-json-query + wrap-json-response) + :results-csv-query (-> results-csv-query + wrap-csv-response)}) + -(def csv-routes - (context "/queries" [] - (GET "/:query-id/results/csv" {:keys [query-params params]} - (statsd/time! [(str "export.query.time") {:tags #{(str "query:" (:query-id params))}}] - {:body (execute-query query-params params)})))) -(defroutes query2-routes - (routes - raw-routes - (wrap-routes json-routes - wrap-json-response) - (wrap-routes csv-routes wrap-csv-response))) diff --git a/src/clj/auto_ap/routes/utils.clj b/src/clj/auto_ap/routes/utils.clj index 31ed48c0..c5cd60c9 100644 --- a/src/clj/auto_ap/routes/utils.clj +++ b/src/clj/auto_ap/routes/utils.clj @@ -1,9 +1,29 @@ (ns auto-ap.routes.utils - (:require [buddy.auth :refer [authenticated?]])) + (:require + [auto-ap.graphql.utils :refer [is-admin?]] + [buddy.auth :refer [authenticated?]] + [auto-ap.logging :as alog] + [cemerick.url :as url])) (defn wrap-secure [handler] (fn [request] (if (authenticated? request) (handler request) - {:status 401 - :body "not authenticated"}))) + {:status 302 + :headers {"Location" "/login" }}))) + +(defn wrap-admin [handler] + (fn [request] + (if (is-admin? (:identity request)) + (handler request) + (do + (alog/warn ::unauthenticated) + {:status 302 + :headers {"Location" "/login"}})))) + +(defn wrap-client-redirect-unauthenticated [handler] + (fn [request] + (let [response (handler request)] + (if (= 401 (get response :status)) + (assoc-in response [:headers "hx-redirect"] "/login/") + response)))) diff --git a/src/clj/auto_ap/routes/yodlee2.clj b/src/clj/auto_ap/routes/yodlee2.clj index 7abf501c..ba8ebfea 100644 --- a/src/clj/auto_ap/routes/yodlee2.clj +++ b/src/clj/auto_ap/routes/yodlee2.clj @@ -6,99 +6,113 @@ [auto-ap.routes.utils :refer [wrap-secure]] [auto-ap.yodlee.core2 :as yodlee] [clojure.tools.logging :as log] - [compojure.core :refer [context defroutes GET POST wrap-routes]] [config.core :refer [env]] [datomic.api :as d])) -(defroutes routes - (wrap-routes - (context "/yodlee2" [] - (GET "/fastlink" {:keys [query-params identity]} - (assert-can-see-client identity (d/pull (d/db conn) [:db/id] [:client/code (get query-params "client")])) - - (let [token (if-let [client-id (get query-params "client-id")] - (-> client-id - Long/parseLong - d-clients/get-by-id - :client/code - (yodlee/get-access-token)) - (yodlee/get-access-token (get query-params "client")))] - {:status 200 - :headers {"Content-Type" "application/edn"} - :body (pr-str {:token token - :url (:yodlee2-fastlink env)}) })) - (POST "/provider-accounts/refresh/" {:keys [identity edn-params]} - (assert-admin identity) - (log/info "refreshing " edn-params) - (try - (yodlee/refresh-provider-account (-> (:client-id edn-params) - Long/parseLong - d-clients/get-by-id - :client/code) - (:provider-account-id edn-params)) - {:status 200 - :headers {"Content-Type" "application/edn"} - :body "{}" } - (catch Exception e - (log/error e) - {:status 400 - :headers {"Content-Type" "application/edn"} - :body (pr-str {:message (.getMessage e) - :error (.toString e)})}))) +(defn fastlink [{:keys [query-params identity]}] + (assert-can-see-client identity (d/pull (d/db conn) [:db/id] [:client/code (get query-params "client")])) + + (let [token (if-let [client-id (get query-params "client-id")] + (-> client-id + Long/parseLong + d-clients/get-by-id + :client/code + (yodlee/get-access-token)) + (yodlee/get-access-token (get query-params "client")))] + {:status 200 + :headers {"Content-Type" "application/edn"} + :body (pr-str {:token token + :url (:yodlee2-fastlink env)}) })) +(defn refresh-provider-accounts [{:keys [identity edn-params]}] + (assert-admin identity) + (log/info "refreshing " edn-params) + (try + (yodlee/refresh-provider-account (-> (:client-id edn-params) + Long/parseLong + d-clients/get-by-id + :client/code) + (:provider-account-id edn-params)) + {:status 200 + :headers {"Content-Type" "application/edn"} + :body "{}" } + (catch Exception e + (log/error e) + {:status 400 + :headers {"Content-Type" "application/edn"} + :body (pr-str {:message (.getMessage e) + :error (.toString e)})}))) - (GET "/provider-accounts/:client/:id" {:keys [identity] - {:keys [client id]} :route-params} - (assert-admin identity) - (log/info "looking-up " client id) - (try - - {:status 200 - :headers {"Content-Type" "application/edn"} - :body (pr-str (yodlee/get-provider-account-detail (-> client - Long/parseLong - d-clients/get-by-id - :client/code) - id))} - (catch Exception e - (log/error e) - {:status 400 - :headers {"Content-Type" "application/edn"} - :body (pr-str {:message (.getMessage e) - :error (.toString e)})}))) - (POST "/provider-accounts/delete/" {:keys [edn-params identity]} - (assert-admin identity) - (try - (yodlee/delete-provider-account (-> (:client-id edn-params) - Long/parseLong - d-clients/get-by-id - :client/code) - (:provider-account-id edn-params)) - {:status 200 - :headers {"Content-Type" "application/edn"} - :body (pr-str {}) } - (catch Exception e - (log/error e) - {:status 400 - :headers {"Content-Type" "application/edn"} - :body (pr-str {:message (.getMessage e) - :error (.toString e)})}))) - (POST "/reauthenticate/:id" {:keys [identity] {:keys [id]} :route-params - data :edn-params} - (assert-admin identity) - (try - {:status 200 - :headers {"Content-Type" "application/edn"} - :body (pr-str (yodlee/reauthenticate-and-recache - (-> (:client-id data) - Long/parseLong - d-clients/get-by-id - :client/code) - (Long/parseLong id) - (dissoc data :client-id )))} - (catch Exception e - (log/error e) - {:status 500 - :headers {"Content-Type" "application/edn"} - :body (pr-str {:message (.getMessage e) - :error (.toString e)})})))) - wrap-secure)) +(defn get-provider-account-detail [{:keys [identity] + {:keys [client id]} :route-params}] + (assert-admin identity) + (log/info "looking-up " client id) + (try + + {:status 200 + :headers {"Content-Type" "application/edn"} + :body (pr-str (yodlee/get-provider-account-detail (-> client + Long/parseLong + d-clients/get-by-id + :client/code) + id))} + (catch Exception e + (log/error e) + {:status 400 + :headers {"Content-Type" "application/edn"} + :body (pr-str {:message (.getMessage e) + :error (.toString e)})}))) +(defn reauthenticate [{:keys [identity] {:keys [id]} :route-params + data :edn-params}] + (assert-admin identity) + (try + {:status 200 + :headers {"Content-Type" "application/edn"} + :body (pr-str (yodlee/reauthenticate-and-recache + (-> (:client-id data) + Long/parseLong + d-clients/get-by-id + :client/code) + (Long/parseLong id) + (dissoc data :client-id )))} + (catch Exception e + (log/error e) + {:status 500 + :headers {"Content-Type" "application/edn"} + :body (pr-str {:message (.getMessage e) + :error (.toString e)})}))) + +(defn delete-provider-account [{:keys [edn-params identity]}] + (assert-admin identity) + (try + (yodlee/delete-provider-account (-> (:client-id edn-params) + Long/parseLong + d-clients/get-by-id + :client/code) + (:provider-account-id edn-params)) + {:status 200 + :headers {"Content-Type" "application/edn"} + :body (pr-str {}) } + (catch Exception e + (log/error e) + {:status 400 + :headers {"Content-Type" "application/edn"} + :body (pr-str {:message (.getMessage e) + :error (.toString e)})}))) + +(defn valid-for [handler & methods] + (let [methods (into #{} methods)] + (fn [request] + (if (methods (:request-method request)) + (handler request) + {:status 404})))) + +(def routes {"api" {"/yodlee2" {"/fastlink" :fastlink + "/provider-accounts/refresh/" :refresh-provider-accounts + ["/provider-accounts/" :client "/" :id ] :get-provider-account-detail + ["/reauthenticate/" :id ] :reauthenticate + "/provider-accounts/delete/" :delete-provider-account}}}) +(def match->handler {:fastlink (-> fastlink wrap-secure (valid-for :get)) + :refresh-provider-accounts (-> refresh-provider-accounts wrap-secure (valid-for :post)) + :get-provider-account-detail (-> get-provider-account-detail wrap-secure (valid-for :get)) + :reauthenticate (-> reauthenticate wrap-secure (valid-for :post)) + :delete-provider-account (-> delete-provider-account wrap-secure (valid-for :post))} ) diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index 84f79fff..265f625d 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -58,7 +58,7 @@ (.setHandler server stats-handler)) (.setStopAtShutdown server true)) -(mount/defstate port :start (Integer/parseInt (or (env :port) "3000"))) +(mount/defstate port :start (Integer/parseInt (or "3001" (env :port) "3000"))) (mount/defstate jetty :start (run-jetty app {:port port :join? false diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index ada1d9d7..125a1065 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -47,11 +47,16 @@ :attempt attempt :source "Square 3" :background-job "Square 3") - (client/request (assoc request - :socket-timeout 10000 - :connection-timeout 10000 - :as :json - :retry-handler retry-4)))) + (try + (client/request (assoc request + :socket-timeout 10000 + :connection-timeout 10000 + :as :json + :retry-handler retry-4)) + (catch Exception e + (log/error ::raw-request-failed + :exception e) + (throw e))))) (de/catch (fn [e] (if (= attempt 5) diff --git a/src/clj/auto_ap/ssr/admin.clj b/src/clj/auto_ap/ssr/admin.clj index d28d2a21..76f10946 100644 --- a/src/clj/auto_ap/ssr/admin.clj +++ b/src/clj/auto_ap/ssr/admin.clj @@ -1,94 +1,16 @@ (ns auto-ap.ssr.admin (:require [auto-ap.datomic :refer [conn]] - [auto-ap.graphql.utils :refer [assert-admin]] [auto-ap.logging :as alog] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] + [auto-ap.ssr.ui :refer [base-page html-response]] [auto-ap.time :as atime] [clj-time.coerce :as coerce] [clojure.string :as str] - [clojure.tools.logging :as log] - [compojure.core :refer [context defroutes GET POST routes]] [datomic.api :as d] [hiccup2.core :as hiccup])) -(defn html-page [hiccup] - {:status 200 - :headers {"Content-Type" "text/html"} - :body (str - "" - (hiccup/html - {} - hiccup))}) - -(defn base-page [contents] - (html-page - [:html.has-navbar-fixed-top - [:head - [:meta {:charset "utf-8"}] - [:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}] - [:meta {:name "viewport", :content "width=device-width, initial-scale=1"}] - [:title "Integreat"] - [:link {:rel "stylesheet", :href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css", :integrity "sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=", :crossorigin "anonymous"}] - [:link {:href "/css/font.min.css", :rel "stylesheet"}] - [:link {:rel "stylesheet", :href "/css/bulma.min.css"}] - [:link {:rel "stylesheet", :href "/css/bulma-calendar.min.css"}] - [:link {:rel "stylesheet", :href "/css/bulma-badge.min.css"}] - [:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}] - [:link {:rel "stylesheet", :href "/css/animate.css"}] - [:link {:rel "stylesheet", :href "/finance-font/style.css"}] - [:link {:rel "stylesheet", :href "/css/main.css"}] - [:link {:rel "stylesheet", :href "https://unpkg.com/placeholder-loading/dist/css/placeholder-loading.min.css"}] - [:script {:src "https://unpkg.com/hyperscript.org@0.9.7"}] - [:script {:src "https://unpkg.com/htmx.org@1.8.4" - :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" - :crossorigin= "anonymous"}] - [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async" }]] - - - [:body - [:div {:id "app"} - [:div - [:nav {:class "navbar has-shadow is-fixed-top is-grey"} - - [:div {:class "container"} - [:div {:class "navbar-brand"} - [:a {:class "navbar-item", :href "../"} - [:img {:src "/img/logo.png"}]]] - [:div.navbar-menu {:id "navMenu"} - [:div.navbar-start - [:a.navbar-item {:href "/"} - "Home" ] - [:a.navbar-item {:href "/invoices/"} - "Invoices" ] - [:a.navbar-item {:href "/payments/"} - "Payments" ] - [:a.navbar-item {:href "/pos/sales-orders/"} - "POS" ] - [:a.navbar-item {:href "/transactions/"} - "Transactions" ] - - [:a.navbar-item {:href "/ledger/"} - "Ledger" ]]]]] - [:div {:class "columns has-shadow", :id "mail-app", :style "margin-bottom: 0px; height: calc(100vh - 46px);"} - [:aside {:class "column aside menu is-2 "} - [:div {:class "main left-nav"} - [:div]]] - [:div {:class "column messages hero ", :id "message-feed", :style "overflow: auto;"} - [:div {:class "inbox-messages"} - contents]]] - [:div] - [:div {:id "dz-hidden"}]]]]])) - -(defn html-response [hiccup] - {:status 200 - :headers {"Content-Type" "text/html"} - :body (str - (hiccup/html - {} - hiccup))}) - - -(defn inline-add-deletes [history] +(defn tx-rows->changes [history] (->> history (group-by (fn [[a _ t]] [a t])) @@ -102,6 +24,9 @@ changes))] [t a changes]))))) +(def error-script + (hiccup/raw "on htmx:responseError from me set event.detail.target's innerHTML to event.detail.xhr.responseText end")) + (defn format-value [v] (cond (inst? v) (-> v @@ -121,29 +46,27 @@ :hx-target "#history-table"} v] " [" [:a - {:hx-get (str "/admin/history/inspect/" v) - :hx-swap "innerHTML" - :hx-target "#inspector" - :hx-trigger "click"} - "snapshot"] "]" - ] - + {:hx-get (str "/admin/history/inspect/" v) + :hx-swap "innerHTML" + :hx-target "#inspector" + :hx-trigger "click" + "_" error-script} + "snapshot"] "]"] + :else (pr-str v))) -(comment "_" ) - (defn page-template [& {:keys [table entity-id]}] [:div [:div.columns [:div.column.is-4 - [:form.hello {"hx-target" "#history-table" - "hx-post" "/admin/history/search" - "hx-swap" "innerHTML" - "_" (hiccup/raw "on htmx:beforeRequest toggle @disabled on me then toggle .is-loading on <#dig/> end + [:form {"hx-target" "#history-table" + "hx-post" "/admin/history/search" + "hx-swap" "innerHTML" + "_" (hiccup/raw "on htmx:beforeRequest toggle @disabled on me then toggle .is-loading on <#dig/> end on htmx:afterRequest toggle @disabled on me then toggle .is-loading on <#dig /> end") - } + } [:div.field.is-grouped [:p.control {} [:input.input {:type "text" :name "entity-id" :placeholder "Entity id" :value entity-id}]] @@ -153,27 +76,63 @@ [:div#history-table table]]) -(defn history-search [{:keys [form-params params] identity :identity :as request}] - (assert-admin identity) - (log/info ::request - request) +(defn table [entity-id best-guess-entity history] + [:div [:h1.title "History for " + (str/capitalize best-guess-entity) + " " + entity-id] + [:div.columns + [:div.column.is-9 + [:table.table.compact.grid {:style "width: 100%"} + [:thead + [:tr + [:td {:style "width: 14em"} "Date"] + [:td {:style "width: 14em"} "User"] + [:td {:style "width: 18em"} "Field"] + [:td "From"] + [:td "To"]]] + [:tbody + (for [[tx a c] history] + [:tr + [:td [:div [:div (some-> (:db/txInstant tx) + coerce/to-date-time + atime/localize + (atime/unparse atime/standard-time)) + ] + [:div.tag (:db/id tx)]]] + [:td (str (:audit/user tx))] + [:td (namespace a) ": " (name a)] + + [:td + [:div.tag.is-danger.is-light + [:span + (format-value (:removed c))]]] + [:td + [:div.tag.is-primary.is-light + [:span + (format-value (:added c))]]]])] + ]] + [:div.column.is-3 + [:div#inspector]]]]) + +(defn history-search [{:keys [form-params params] :as request}] (try - (let [entity-id (Long/parseLong (or (some-> (:entity-id form-params) not-empty) + (let [entity-id (Long/parseLong (or (some-> (:entity-id form-params) not-empty) (:entity-id params) (get params "entity-id") (get form-params "entity-id"))) - history (->> - (d/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad - :in $ $$ ?i - :where - [$$ ?i ?a ?v ?tx ?ad] - [$ ?a :db/ident ?a2]] - (d/db conn) - (d/history (d/db conn)) - entity-id ) - inline-add-deletes - (sort-by (comp :db/id first)) - vec) + history (->> + (d/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad + :in $ $$ ?i + :where + [$$ ?i ?a ?v ?tx ?ad] + [$ ?a :db/ident ?a2]] + (d/db conn) + (d/history (d/db conn)) + entity-id ) + tx-rows->changes + (sort-by (comp :db/id first)) + vec) best-guess-entity (or (->> history (group-by (comp @@ -185,99 +144,55 @@ (sort-by second) last first) - "?") - table [:div [:h1.title "History for " - (str/capitalize best-guess-entity) - " " - entity-id] - [:div.columns - [:div.column.is-9 - [:table.table.compact.grid {:style "width: 100%"} - [:thead - [:tr - [:td {:style "width: 14em"} "Date"] - [:td {:style "width: 14em"} "User"] - [:td {:style "width: 18em"} "Field"] - [:td "From"] - [:td "To"]]] - [:tbody - (for [[tx a c] history] - [:tr - [:td [:div [:div (some-> (:db/txInstant tx) - coerce/to-date-time - atime/localize - (atime/unparse atime/standard-time)) - ] - [:div.tag (:db/id tx)]]] - [:td (str (:audit/user tx))] - [:td (namespace a) ": " (name a)] + "?")] - [:td - [:div.tag.is-danger.is-light - [:span - (format-value (:removed c))]]] - [:td - [:div.tag.is-primary.is-light - [:span - (format-value (:added c))]]]])] - ]] - [:div.column.is-3 - [:div#inspector.box {:style {:position "sticky" - :display "inline-block" - :vertical-align "top" - :overflow-y "auto" - :max-height "100vh" - :top "0px" - :bottom "0px"}}]]]]] - - (alog/info ::trace - :bge best-guess-entity - :headers (:headers request)) (if (get (:headers request) "hx-request") (html-response - table) - (base-page (page-template :table table - :entity-id entity-id)))) - (catch NumberFormatException e + (table entity-id best-guess-entity history)) + (base-page (page-template :table (table entity-id best-guess-entity history) + :entity-id entity-id) + (admin-side-bar :admin-history)))) + (catch NumberFormatException _ (html-response - (str [:div.notification.is-danger.is-light - "Cannot parse the entity-id " (or (:entity-id form-params) - (:entity-id params)) + [:div.notification.is-danger.is-light + "Cannot parse the entity-id " (or (:entity-id form-params) + (:entity-id params)) - ". It should be a number."]))))) + ". It should be a number."])))) -(defn inspect [{{:keys [entity-id]} :params identity :identity :as request}] +(defn inspect [{{:keys [entity-id]} :params :as request}] (alog/info ::inspect :request request) - (assert-admin identity) (try (let [entity-id (Long/parseLong entity-id) data (d/pull (d/db conn) '[*] - entity-id - ) ] + entity-id)] (html-response - [:div {:style {:display "inline-block"}} - [:h1.title "Snapshot of " - entity-id] - [:ul - (for [[k v] data] - [:li [:strong k] ":" v] - )]])) - (catch NumberFormatException e + [:div.box {:style {:position "sticky" + :display "inline-block" + :vertical-align "top" + :overflow-y "auto" + :max-height "100vh" + :top "0px" + :bottom "0px"}} + [:div {:style {:display "inline-block"}} + [:h1.title "Snapshot of " + entity-id] + [:ul + (for [[k v] data] + [:li [:strong k] ":" (format-value v)] + )]]])) + (catch NumberFormatException _ (html-response [:div.notification.is-danger.is-light "Cannot parse the entity-id " entity-id ". It should be a number."])))) -(defn history [{:keys [identity] :as request}] - (base-page (page-template ))) +(defn history [{:keys [matched-route]}] + (base-page (page-template ) + (admin-side-bar matched-route))) + + + -(defroutes admin-routes - (routes - (context "/admin" [] - (GET "/history" [] history) - (GET "/history/" [] history) - (POST "/history/search" [] history-search) - (GET "/history/:entity-id" [entity-id] history-search) - (GET "/history/inspect/:entity-id" [entity-id] inspect)))) diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj new file mode 100644 index 00000000..fede4b34 --- /dev/null +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -0,0 +1,264 @@ +(ns auto-ap.ssr.company.company-1099 + (:require + [auto-ap.datomic :refer [conn remove-nils]] + [auto-ap.graphql.utils :refer [can-see-client?]] + [auto-ap.shared-views.company.sidebar :refer [company-side-bar]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.ui :refer [base-page html-response]] + [bidi.bidi :as bidi] + [clojure.string :as str] + [datomic.api :as d] + [hiccup2.core :as hiccup])) + +(defn cannot-overwrite? [vendor] + (some? (or (:vendor/legal-entity-1099-type vendor) + (:vendor/legal-entity-tin vendor) + (:vendor/legal-entity-tin-type vendor)))) + +(defn get-1099-companies [user] + (->> (d/q '[:find + (pull ?c [:client/code]) + (pull ?v [:db/id + :vendor/name + {:vendor/legal-entity-1099-type [:db/ident]} + {:vendor/legal-entity-tin-type [:db/ident]} + {:vendor/address [:address/street1 + :address/city + :address/state + :address/zip]} + :vendor/legal-entity-first-ein + :vendor/legal-entity-first-name + :vendor/legal-entity-middle-name + :vendor/legal-entity-last-name]) + (sum ?a) + :with ?p + :in $ ?user + :where [?p :payment/date ?d ] + [(>= ?d #inst "2018-01-01T08:00")] + [(< ?d #inst "2023-01-01T08:00")] + [?p :payment/client ?c] + [(auto-ap.graphql.utils/can-see-client? ?user ?c)] + [?p :payment/amount ?a] + [?p :payment/type :payment-type/check] + [?p :payment/vendor ?v]] + (d/db conn) + user) + (filter (fn [[_ _ a]] + (>= a 600.0))) + + (sort-by (fn [[client _ amount]] + [(:client/code client) amount]) ))) + +(defn dialog [header content footer] + [:div.modal.is-active + [:div.modal-background {"_" (hiccup/raw "on click remove <#modal-holder div/>")}] + [:div.modal-card + [:div.modal-card-head + header] + [:div.modal-card-body + content] + [:div.modal-card-foot + footer]] + [:button.modal-close.is-large {"_" (hiccup/raw "on click remove <#modal-holder div/>")}]]) + +(defn table [{:keys [identity]} & {:keys [flash-id]}] + [:table#vendor-table.table.grid.compact.is-fullwidth + [:thead + [:tr + [:th {:style {:width "5em"}}"Client"] + [:th "Vendor Name"] + [:th "Name"] + [:th {:style {:width "9em"}} "1099 Type"] + [:th {:style {:width "8em"}} "TIN"] + [:th "Address"] + [:th "Amount Paid"] + [:th {:style {:width "10em"}}] + ]] + [:tbody + (for [[client vendor amount] (get-1099-companies identity)] + [:tr (when (= flash-id + (:db/id vendor)) + {:class "live-added"}) + [:td (:client/code client)] + [:td (:vendor/name vendor)] + [:td (-> vendor :vendor/legal-entity-first-name) " " + (-> vendor :vendor/legal-entity-middle-name) " " + (-> vendor :vendor/legal-entity-last-name)] + [:td (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] " " + [:td + (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + (-> vendor :vendor/legal-entity-tin)] + [:td + (-> vendor :vendor/address :address/street1) " " + (-> vendor :vendor/address :address/street2) " " + (-> vendor :vendor/address :address/city) " " + (-> vendor :vendor/address :address/state) " " + (-> vendor :vendor/address :address/zip) + [:td amount] + [:td + (if (cannot-overwrite? vendor) + [:a {:href "mailto:ben@integreatconsult.com"} "Contact Integreat"] + [:button.button {:hx-get (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-dialog + :vendor-id (:db/id vendor)) + :hx-target "#modal-holder" + :hx-swap "innerHTML"} + [:span.icon [:i.fa.fa-pencil ]]])]]])]]) + +(defn form-data->map [form-data] + (reduce-kv + (fn [acc k v] + (assoc-in acc (->> (str/split k #"_") + (mapv #(apply keyword (str/split % #"/")))) v)) + {} + form-data)) + +(defn path->name [k] + (cond (keyword? k) + (str (namespace k) "/" (name k)) + + (seq k) + (str/join "_" (map path->name k)) + :else k)) + +(defn vendor-save [{:keys [form-params identity route-params] :as request}] + (when-not (cannot-overwrite? (d/pull (d/db conn) '[*] (Long/parseLong (:vendor-id route-params)))) + @(d/transact conn [(remove-nils + (-> (form-data->map form-params) + (assoc :db/id (Long/parseLong (:vendor-id route-params))) + (update :vendor/legal-entity-1099-type #(some->> % (keyword "legal-entity-1099-type"))) + (update :vendor/legal-entity-tin-type #(some->> % (keyword "legal-entity-tin-type")))))])) + (html-response + (table request :flash-id (Long/parseLong (:vendor-id route-params))))) + + +(defn vendor-dialog [request] + (let [vendor (d/pull (d/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] + :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms + (html-response + [:form {:hx-post (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-save + :request-method :post + :vendor-id (Long/parseLong (:vendor-id (:params request)))) + :hx-target "#vendor-table" + :hx-swap "outerHTML swap:0.2s" + "_" (hiccup/raw "on htmx:afterRequest transition <#modal-holder .modal-background, #modal-holder .modal-card />'s opacity from 1.0 to 0 over 100ms then remove <#modal-holder */> ")} + (dialog + [:h4.is-4.title "Vendor 1099 Info"] + [:div + [:h3.is-3.title (:vendor/name vendor)] + + [:h4.is-4.title "Address"] + [:hr] + [:div.field + [:p.help "Street1"] + [:div.control + [:input.input.is-expanded {:type "text" + :autofocus true + :name (path->name [:vendor/address :address/street1]) + :placeholder "1700 Pennsylvania Ave" + :value (-> vendor :vendor/address :address/street1)}]]] + [:div.field + [:p.help "Street 2"] + [:div.control + [:input.input.is-expanded {:type "text" + :name (path->name [:vendor/address :address/street2]) + :placeholder "SUite 400" + :value (-> vendor :vendor/address :address/street2)}]]] + [:div.level + [:div.level-left + [:div.level-item + [:div.field + [:p.help "City"] + [:div.control + [:input.input.is-expanded {:type "text" + :placeholder "Cupertino" + :name (path->name [:vendor/address :address/city]) + :value (-> vendor :vendor/address :address/city)}]]]] + [:div.level-item + [:div.field + [:p.help "State"] + [:div.control + [:input.input.is-expanded {:type "text" + :style {:width "3em"} + :placeholder "CA" + :name (path->name [:vendor/address :address/state]) + :value (-> vendor :vendor/address :address/state)}]]]] + [:div.level-item + [:div.field + [:p.help "Zip"] + [:div.control + [:input.input.is-expanded {:type "text" + :placeholder "95014" + :name (path->name [:vendor/address :address/zip]) + :value (-> vendor :vendor/address :address/zip)}]]]]]] + [:h4.is-4.title "Legal Entity"] + [:hr] + [:div.level + [:div.level-left + [:div.level-item + [:div.field + [:p.help "First Name"] + [:div.control + [:input.input.is-expanded {:type "text" + :placeholder "Josh" + :name (path->name [:vendor/legal-entity-first-name]) + :value (-> vendor :vendor/legal-entity-first-name)}]]]] + [:div.level-item + [:div.field + [:p.help "Middle Name"] + [:div.control + [:input.input.is-expanded {:type "text" + :placeholder "Caleb" + :name (path->name [:vendor/legal-entity-middle-name]) + :value (-> vendor :vendor/legal-entity-middle-name)}]]]] + [:div.level-item + [:div.field + [:p.help "Last Name"] + [:div.control + [:input.input.is-expanded {:type "text" + :placeholder "Smith" + :name (path->name [:vendor/legal-entity-last-name]) + :value (-> vendor :vendor/legal-entity-last-name)}]]]]]] + + [:div.level + [:div.level-left + [:div.level-item + [:div.field + [:p.help "TIN"] + [:div.control + [:input.input {:type "text" + :name (path->name [:vendor/legal-entity-tin]) + :placeholder "SSN or EIN" + :size "12" + :value (-> vendor :vendor/legal-entity-tin)}]]]] + [:div.level-item + [:div.field + [:p.help "TIN Type"] + [:div.control + [:div.select + [:select {:name (path->name [:vendor/legal-entity-tin-type])} + [:option {:value ""} ""] + [:option {:value "ein" :selected (= (-> vendor :vendor/legal-entity-tin-type :db/ident) :legal-entity-tin-type/ein)} "EIN"] + [:option {:value "ssn" :selected (= (-> vendor :vendor/legal-entity-tin-type :db/ident) :legal-entity-tin-type/ssn)} "SSN"]]]]]] + [:div.level-item + [:div.field + [:p.help "1099 Type"] + [:div.control + [:div.select + [:select {:name (path->name [:vendor/legal-entity-1099-type])} + [:option {:value ""} ""] + [:option {:value "none" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/none)} "None"] + [:option {:value "misc" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/misc)} "Misc"] + [:option {:value "landlord" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/landlord)} "Landlord"]]]]]]]]] + [:button.button.is-primary "Save"])]))) + + + +(defn page [{:keys [identity matched-route] :as request}] + (base-page + [:div + (table request)] + [:div + (company-side-bar matched-route)])) + diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj new file mode 100644 index 00000000..9eb6cd7d --- /dev/null +++ b/src/clj/auto_ap/ssr/core.clj @@ -0,0 +1,15 @@ +(ns auto-ap.ssr.core + (:require + [auto-ap.routes.utils + :refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr.admin :as admin] + [auto-ap.ssr.company.company-1099 :as company-1099])) + +;; from auto-ap.ssr-routes, because they're shared +(def key->handler {:admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history))) + :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history-search))) + :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/inspect))) + :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-1099/page))) + :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-1099/vendor-dialog))) + :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-1099/vendor-save)))}) + diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj new file mode 100644 index 00000000..332f6d11 --- /dev/null +++ b/src/clj/auto_ap/ssr/ui.clj @@ -0,0 +1,109 @@ +(ns auto-ap.ssr.ui + (:require + [auto-ap.logging :as alog] + [config.core :refer [env]] + [hiccup2.core :as hiccup])) + +(defn html-page [hiccup] + {:status 200 + :headers {"Content-Type" "text/html"} + :body (str + "" + (hiccup/html + {} + hiccup))}) + +(defn base-page [contents side-bar-contents] + (html-page + [:html.has-navbar-fixed-top + [:head + [:meta {:charset "utf-8"}] + [:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}] + [:meta {:name "viewport", :content "width=device-width, initial-scale=1"}] + [:title "Integreat"] + [:link {:rel "stylesheet", :href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css", :integrity "sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=", :crossorigin "anonymous"}] + [:link {:href "/css/font.min.css", :rel "stylesheet"}] + [:link {:rel "stylesheet", :href "/css/bulma.min.css"}] + [:link {:rel "stylesheet", :href "/css/bulma-calendar.min.css"}] + [:link {:rel "stylesheet", :href "/css/bulma-badge.min.css"}] + [:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}] + [:link {:rel "stylesheet", :href "/css/animate.css"}] + [:link {:rel "stylesheet", :href "/finance-font/style.css"}] + [:link {:rel "stylesheet", :href "/css/main.css"}] + [:link {:rel "stylesheet", :href "https://unpkg.com/placeholder-loading/dist/css/placeholder-loading.min.css"}] + [:script {:src "https://unpkg.com/hyperscript.org@0.9.7"}] + [:script {:src "https://unpkg.com/htmx.org@1.8.4" + :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" + :crossorigin= "anonymous"}] + [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async" }]] + + + [:body + [:div {:id "app"} + [:div + [:nav {:class "navbar has-shadow is-fixed-top is-grey"} + + [:div {:class "container"} + [:div {:class "navbar-brand"} + [:a {:class "navbar-item", :href "../"} + [:img {:src "/img/logo.png"}]]] + [:div.navbar-menu {:id "navMenu"} + [:div.navbar-start + [:a.navbar-item {:href "/"} + "Home" ] + [:a.navbar-item {:href "/invoices/"} + "Invoices" ] + [:a.navbar-item {:href "/payments/"} + "Payments" ] + [:a.navbar-item {:href "/pos/sales-orders/"} + "POS" ] + [:a.navbar-item {:href "/transactions/"} + "Transactions" ] + + [:a.navbar-item {:href "/ledger/"} + "Ledger" ]]]]] + [:div {:class "columns has-shadow", :id "mail-app", :style "margin-bottom: 0px; height: calc(100vh - 46px);"} + [:aside {:class "column aside menu is-2 "} + [:div {:class "main left-nav"} + side-bar-contents]] + [:div {:class "column messages hero ", :id "message-feed", :style "overflow: auto;"} + [:div {:class "inbox-messages"} + contents]]] + [:div] + [:div {:id "dz-hidden"}]]] + [:div#modal-holder]]])) + +(defn html-response [hiccup & {:keys [status] :or {status 200}}] + {:status status + :headers {"Content-Type" "text/html"} + :body (str + (hiccup/html + {} + hiccup))}) + +(defn wrap-error-response [handler] + (fn [request] + (try + (handler request) + (catch Exception e + (if-let [v (or (:validation-error (ex-data e)) + (:validation-error (ex-data (.getCause e))))] + + (do + (alog/warn ::request-validation-error + :exception e) + (html-response + [:div.notification.is-warning.is-light + v] + :status 400)) + (do + (alog/error ::request-error + :exception e) + (when (= "dev" (:dd-env env)) + (println e)) + (html-response + [:div.notification.is-danger.is-light + "Server error occured." + (ex-message e)] + :status 500))))))) + diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc index 26e80270..c20a8572 100644 --- a/src/cljc/auto_ap/client_routes.cljc +++ b/src/cljc/auto_ap/client_routes.cljc @@ -1,7 +1,7 @@ (ns auto-ap.client-routes) (def routes ["/" {"" :index - "login/" :login + #"login/?" :login "needs-activation/" :needs-activation "needs-activation" :needs-activation "payments/" :payments @@ -42,3 +42,12 @@ "balance-sheet" :balance-sheet "external" :external-ledger "external-import" :external-import-ledger}}]) + +(defn all-handle-keys [routes] + (let [vals (vals routes) + result (filter keyword? vals) + deeper (filter map? vals)] + (apply concat result + (map all-handle-keys deeper)))) + +(def all-matches (set (all-handle-keys (second routes)))) diff --git a/src/cljc/auto_ap/shared_views.cljc b/src/cljc/auto_ap/shared_views.cljc new file mode 100644 index 00000000..3dddec99 --- /dev/null +++ b/src/cljc/auto_ap/shared_views.cljc @@ -0,0 +1,2 @@ +(ns auto-ap.shared-views) + diff --git a/src/cljc/auto_ap/shared_views/admin/side_bar.cljc b/src/cljc/auto_ap/shared_views/admin/side_bar.cljc new file mode 100644 index 00000000..721633c2 --- /dev/null +++ b/src/cljc/auto_ap/shared_views/admin/side_bar.cljc @@ -0,0 +1,101 @@ +(ns auto-ap.shared-views.admin.side-bar + (:require [bidi.bidi :as bidi] + [auto-ap.client-routes :as client-routes] + [auto-ap.ssr-routes :as ssr-routes] + #?(:cljs [re-frame.core :as re-frame]) + #?(:cljs [reagent.core :as r]))) + +(defn deep-merge [v & vs] + (letfn [(rec-merge [v1 v2] + (if (and (map? v1) (map? v2)) + (merge-with deep-merge v1 v2) + v2))] + (when (some identity vs) + (reduce #(rec-merge %1 %2) v vs)))) +(def all-client-visible-routes + ["/" (deep-merge ssr-routes/routes (second client-routes/routes))]) + + +(defn menu-item [{:keys [label route test-route active-route icon-class icon-style]}] + [:p.menu-item + [:a.item {:href (bidi/path-for all-client-visible-routes route) + :class (when (test-route active-route) "is-active")} + (if icon-style + [:span {:class icon-class :style icon-style}] + [:span {:class "icon"} + [:i {:class icon-class}]]) + [:span {:class "name"} label]]]) + +(defn admin-side-bar-impl [active-route children] + [:div + [:p.menu-label "General"] + (menu-item {:label "Dashboard" + :icon-class "fa fa-tachometer" + :test-route #{:admin} + :active-route active-route + :route :admin}) + + + [:p.menu-label "Setup"] + (menu-item {:label "Clients" + :icon-class "fa fa-star-o" + :test-route #{:admin-clients + :admin-specific-client + :admin-specific-bank-account} + :active-route active-route + :route :admin-clients}) + (menu-item {:label "Vendors" + :icon-class "fa fa-star-o" + :test-route #{:admin-vendors} + :active-route active-route + :route :admin-vendors}) + (menu-item {:label "Users" + :icon-class "icon icon-single-neutral-book" + :test-route #{:admin-users} + :active-route active-route + :route :admin-users + :icon-style {:font-size "25px"}}) + (menu-item {:label "Accounts" + :icon-class "icon icon-list-bullets" + :test-route #{:admin-accounts} + :active-route active-route + :route :admin-accounts + :icon-style {:font-size "25px"}}) + (menu-item {:label "Rules" + :icon-class "icon icon-cog-play-1" + :test-route #{:admin-rules} + :active-route active-route + :route :admin-rules + :icon-style {:font-size "25px"}}) + (menu-item {:label "History" + :icon-class "icon icon-cog-play-1" + :test-route #{:admin-history :admin-history-search :admin-history-inspect} + :active-route active-route + :route :admin-history + :icon-style {:font-size "25px"}}) + [:p.menu-label "Import"] + (menu-item {:label "Excel Invoices" + :icon-class "fa fa-download" + :test-route #{:admin-excel-import} + :active-route active-route + :route :admin-excel-import}) + (menu-item {:label "Excel Invoices" + :icon-class "fa fa-download" + :test-route #{:admin-import-batches} + :active-route active-route + :route :admin-import-batches}) + (menu-item {:label "Background Jobs" + :icon-class "icon icon-cog-play-1" + :test-route #{:admin-jobs} + :active-route active-route + :route :admin-jobs + :icon-style {:font-size "25px"}}) + (into [:div ] children)]) + +#?(:clj + (defn admin-side-bar [active-page] + (admin-side-bar-impl active-page nil)) + :cljs + (defn admin-side-bar [] + (admin-side-bar-impl @(re-frame/subscribe [:auto-ap.subs/active-page]) + (r/children (r/current-component))))) diff --git a/src/cljc/auto_ap/shared_views/company/sidebar.cljc b/src/cljc/auto_ap/shared_views/company/sidebar.cljc new file mode 100644 index 00000000..4bd1ae63 --- /dev/null +++ b/src/cljc/auto_ap/shared_views/company/sidebar.cljc @@ -0,0 +1,83 @@ +(ns auto-ap.shared-views.company.sidebar + (:require [bidi.bidi :as bidi] + [auto-ap.client-routes :as client-routes] + [auto-ap.ssr-routes :as ssr-routes] + #?(:cljs [re-frame.core :as re-frame]) + #?(:cljs [reagent.core :as r]))) + + +(defn active-when [active-page f & rest] + (when (apply f (into [active-page] rest)) " is-active")) + +(defn deep-merge [v & vs] + (letfn [(rec-merge [v1 v2] + (if (and (map? v1) (map? v2)) + (merge-with deep-merge v1 v2) + v2))] + (when (some identity vs) + (reduce #(rec-merge %1 %2) v vs)))) + +(def all-client-visible-routes + ["/" (deep-merge ssr-routes/routes (second client-routes/routes))]) + +(defn menu-item [{:keys [label route test-route active-route icon-class icon-style]}] + [:p.menu-item + [:a.item {:href (bidi/path-for all-client-visible-routes route) + :class (when (test-route active-route) "is-active")} + (if icon-style + [:span {:class icon-class :style icon-style}] + [:span {:class "icon"} + [:i {:class icon-class}]]) + [:span {:class "name"} label]]]) + +(defn menu-item [{:keys [label route test-route active-route icon-class icon-style]}] + [:p.menu-item + [:a.item {:href (bidi/path-for all-client-visible-routes route) + :class (when (test-route active-route) "is-active")} + (if icon-style + [:span {:class icon-class :style icon-style}] + [:span {:class "icon"} + [:i {:class icon-class}]]) + [:span {:class "name"} label]]]) + +(defn company-side-bar-impl [active-route] + [:div + (menu-item {:label "Reports" + :route :reports + :test-route #{:reports} + :active-route active-route + :icon-class "icon icon-receipt" + :icon-style {:font-size "25px"}}) + + (menu-item {:label "Plaid Link" + :route :plaid + :test-route #{:plaid} + :active-route active-route + :icon-class "icon icon-saving-bank-1" + :icon-style {:font-size "25px"}}) + (menu-item {:label "Yodlee Link" + :route :yodlee2 + :test-route #{:yodlee2} + :active-route active-route + :icon-class "icon icon-saving-bank-1" + :icon-style {:font-size "25px"}}) + + (menu-item {:label "Other" + :route :company-other + :test-route #{:company-other} + :active-route active-route + :icon-class "icon icon-cog-play-1" + :icon-style {:font-size "25px"}}) + (menu-item {:label "1099 Info" + :route :company-1099 + :test-route #{:company-1099} + :active-route active-route + :icon-class "icon icon-cog-play-1" + :icon-style {:font-size "25px"}})]) + +#?(:clj + (defn company-side-bar [active-page] + (company-side-bar-impl active-page)) + :cljs + (defn company-side-bar [] + (company-side-bar-impl @(re-frame/subscribe [:auto-ap.subs/active-page])))) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc new file mode 100644 index 00000000..2d402c75 --- /dev/null +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -0,0 +1,14 @@ +(ns auto-ap.ssr-routes) + +(def routes {"admin" {"/history" {"" :admin-history + "/" :admin-history + #"/search/?" :admin-history-search + ["/" [#"\d+" :entity-id] #"/?"] :admin-history-search + ["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect}} + "company" {"/1099" :company-1099 + "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog + :post :company-1099-vendor-save}}}}) + + +(def only-routes ["/" routes]) + diff --git a/src/cljs/auto_ap/views/components/admin/side_bar.cljs b/src/cljs/auto_ap/views/components/admin/side_bar.cljs deleted file mode 100644 index b8257fe1..00000000 --- a/src/cljs/auto_ap/views/components/admin/side_bar.cljs +++ /dev/null @@ -1,72 +0,0 @@ -(ns auto-ap.views.components.admin.side-bar - (:require - [auto-ap.routes :as routes] - [auto-ap.subs :as subs] - [auto-ap.views.utils :refer [active-when]] - [bidi.bidi :as bidi] - [re-frame.core :as re-frame] - [reagent.core :as r])) - -(defn admin-side-bar [] - (let [ap @(re-frame/subscribe [::subs/active-page])] - [:div - [:p.menu-label "General"] - [:p.menu-item - [:a {:href (bidi/path-for routes/routes :admin) , :class (str "item" (active-when ap = :admin))} - [:span {:class "icon"} - [:i {:class "fa fa-tachometer"}]] - [:span {:class "name"} "Dashboard"]]] - - [:p.menu-label "Setup"] - [:ul.menu-list - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-clients) , :class (str "item" (active-when ap = :admin-clients))} - [:span {:class "icon"} - [:i {:class "fa fa-star-o"}]] - - [:span {:class "name"} "Clients"]]] - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-vendors) , :class (str "item" (active-when ap = :admin-vendors))} - [:span {:class "icon"} - [:i {:class "fa fa-star-o"}]] - - [:span {:class "name"} "Vendors"]]] - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-users), :class (str "item" (active-when ap = :admin-users))} - [:span {:class "icon icon-single-neutral-book" :style {:font-size "25px"}}] - [:span {:class "name"} "Users"]]] - - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-accounts), :class (str "item" (active-when ap = :admin-accounts))} - [:span {:class "icon icon-list-bullets" :style {:font-size "25px"}}] - [:span {:class "name"} "Accounts"]]] - - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-rules), :class (str "item" (active-when ap = :admin-rules))} - [:span {:class "icon icon-cog-play-1" :style {:font-size "25px"}}] - [:span {:class "name"} "Rules"]]] - [:li.menu-item - [:a {:href (str "/admin/history") :class (str "item" (active-when ap = :admin-history))} - [:span {:class "icon icon-cog-play-1" :style {:font-size "25px"}}] - [:span {:class "name"} "History "]]] - [:ul ]] - [:p.menu-label "Import"] - [:ul.menu-list - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-excel-import) , :class (str "item" (active-when ap = :admin-excel-import))} - [:span {:class "icon"} - [:i {:class "fa fa-download"}]] - - [:span {:class "name"} "Excel Invoices"]]] - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-import-batches) , :class (str "item" (active-when ap = :admin-import-batches))} - [:span {:class "icon"} - [:i {:class "fa fa-download"}]] - - [:span {:class "name"} "Import Batches"]]] - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-jobs) :class (str "item" (active-when ap = :admin-jobs))} - [:span {:class "icon icon-cog-play-1" :style {:font-size "25px"}}] - [:span {:class "name"} "Background Jobs"]]]] - - (into [:div ] (r/children (r/current-component)))])) diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index f7e01186..cab8e675 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -166,6 +166,10 @@ (when-not is-initial-loading [:div.navbar-end + [:a.navbar-item {:href "/company/1099"} + [:div.tag.is-info.is-rounded + "1099 data entry is now ready!"]] + (when (> (count @clients) 1) [client-dropdown] )])] diff --git a/src/cljs/auto_ap/views/pages/admin.cljs b/src/cljs/auto_ap/views/pages/admin.cljs index 16c31be3..ab154f49 100644 --- a/src/cljs/auto_ap/views/pages/admin.cljs +++ b/src/cljs/auto_ap/views/pages/admin.cljs @@ -1,6 +1,6 @@ (ns auto-ap.views.pages.admin (:require - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [side-bar-layout]])) (defn admin-page [] diff --git a/src/cljs/auto_ap/views/pages/admin/accounts.cljs b/src/cljs/auto_ap/views/pages/admin/accounts.cljs index 4ad7eef1..3a10da1d 100644 --- a/src/cljs/auto_ap/views/pages/admin/accounts.cljs +++ b/src/cljs/auto_ap/views/pages/admin/accounts.cljs @@ -2,7 +2,7 @@ (:require [auto-ap.effects.forward :as forward] [auto-ap.forms :as forms] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.buttons :as buttons] [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] diff --git a/src/cljs/auto_ap/views/pages/admin/clients.cljs b/src/cljs/auto_ap/views/pages/admin/clients.cljs index 068ef2d6..cadc5db0 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients.cljs @@ -3,7 +3,7 @@ [auto-ap.routes :as routes] [auto-ap.status :as status] [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.grid :as grid] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.clients.form :as form] diff --git a/src/cljs/auto_ap/views/pages/admin/excel_import.cljs b/src/cljs/auto_ap/views/pages/admin/excel_import.cljs index 4606dfd3..8158d955 100644 --- a/src/cljs/auto_ap/views/pages/admin/excel_import.cljs +++ b/src/cljs/auto_ap/views/pages/admin/excel_import.cljs @@ -4,7 +4,7 @@ [auto-ap.forms.builder :as form-builder] [auto-ap.schema :as schema] [auto-ap.views.components :as com] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.utils :refer [with-user]] [malli.core :as m] diff --git a/src/cljs/auto_ap/views/pages/admin/import_batches.cljs b/src/cljs/auto_ap/views/pages/admin/import_batches.cljs index 143dc846..1c404508 100644 --- a/src/cljs/auto_ap/views/pages/admin/import_batches.cljs +++ b/src/cljs/auto_ap/views/pages/admin/import_batches.cljs @@ -1,7 +1,7 @@ (ns auto-ap.views.pages.admin.import-batches (:require [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.import-batches.table :as table] [auto-ap.views.pages.data-page :as data-page] diff --git a/src/cljs/auto_ap/views/pages/admin/jobs.cljs b/src/cljs/auto_ap/views/pages/admin/jobs.cljs index 0bbe43e4..cb6fca69 100644 --- a/src/cljs/auto_ap/views/pages/admin/jobs.cljs +++ b/src/cljs/auto_ap/views/pages/admin/jobs.cljs @@ -2,7 +2,7 @@ (:require [auto-ap.status :as status] [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.jobs.table :as table] [auto-ap.views.components.modal :as modal] diff --git a/src/cljs/auto_ap/views/pages/admin/rules.cljs b/src/cljs/auto_ap/views/pages/admin/rules.cljs index fbf5e642..f9b040b7 100644 --- a/src/cljs/auto_ap/views/pages/admin/rules.cljs +++ b/src/cljs/auto_ap/views/pages/admin/rules.cljs @@ -4,7 +4,7 @@ [auto-ap.events :as events] [auto-ap.forms :as forms] [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.buttons :as buttons] [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] diff --git a/src/cljs/auto_ap/views/pages/admin/users.cljs b/src/cljs/auto_ap/views/pages/admin/users.cljs index d8a6c1a3..9a0196cd 100644 --- a/src/cljs/auto_ap/views/pages/admin/users.cljs +++ b/src/cljs/auto_ap/views/pages/admin/users.cljs @@ -3,7 +3,7 @@ [auto-ap.effects.forward :as forward] [auto-ap.status :as status] [auto-ap.utils :refer [replace-by]] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.grid :as grid] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.users.form :as form] diff --git a/src/cljs/auto_ap/views/pages/admin/vendors.cljs b/src/cljs/auto_ap/views/pages/admin/vendors.cljs index 6413d4d4..3cdb4ede 100644 --- a/src/cljs/auto_ap/views/pages/admin/vendors.cljs +++ b/src/cljs/auto_ap/views/pages/admin/vendors.cljs @@ -2,7 +2,7 @@ (:require [auto-ap.effects.forward :as forward] [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.vendors.merge-dialog :as merge-dialog] [auto-ap.views.pages.admin.vendors.side-bar :as side-bar] diff --git a/src/cljs/auto_ap/views/pages/company/other.cljs b/src/cljs/auto_ap/views/pages/company/other.cljs index bccbc805..46fcaa4e 100644 --- a/src/cljs/auto_ap/views/pages/company/other.cljs +++ b/src/cljs/auto_ap/views/pages/company/other.cljs @@ -4,7 +4,7 @@ [auto-ap.views.utils :refer [dispatch-event with-user]] [auto-ap.views.components.buttons :refer [fa-icon]] [auto-ap.views.components.layouts :refer [side-bar-layout]] - [auto-ap.views.pages.company.side-bar :refer [company-side-bar]] + [auto-ap.shared-views.company.sidebar :refer [company-side-bar]] [goog.crypt.base64 :as b64] [re-frame.core :as re-frame] [reagent.core :as reagent] diff --git a/src/cljs/auto_ap/views/pages/company/plaid.cljs b/src/cljs/auto_ap/views/pages/company/plaid.cljs index 2d71cdfd..f400502f 100644 --- a/src/cljs/auto_ap/views/pages/company/plaid.cljs +++ b/src/cljs/auto_ap/views/pages/company/plaid.cljs @@ -5,7 +5,7 @@ [auto-ap.subs :as subs] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.plaid.table :as table] - [auto-ap.views.pages.company.side-bar :refer [company-side-bar]] + [auto-ap.shared-views.company.sidebar :refer [company-side-bar]] [auto-ap.views.pages.data-page :as data-page] [auto-ap.views.utils :refer [dispatch-event with-user]] [clojure.set :as set] diff --git a/src/cljs/auto_ap/views/pages/company/yodlee2.cljs b/src/cljs/auto_ap/views/pages/company/yodlee2.cljs index a991eaf3..eeae1e3a 100644 --- a/src/cljs/auto_ap/views/pages/company/yodlee2.cljs +++ b/src/cljs/auto_ap/views/pages/company/yodlee2.cljs @@ -5,7 +5,7 @@ [auto-ap.subs :as subs] [auto-ap.views.components.grid :as grid] [auto-ap.views.components.layouts :refer [side-bar-layout]] - [auto-ap.views.pages.company.side-bar :refer [company-side-bar]] + [auto-ap.shared-views.company.sidebar :refer [company-side-bar]] [auto-ap.views.pages.company.yodlee2.table :as table] [auto-ap.views.utils :refer [dispatch-event with-user]] [re-frame.core :as re-frame] diff --git a/src/cljs/auto_ap/views/pages/invoices/form.cljs b/src/cljs/auto_ap/views/pages/invoices/form.cljs index eb5f32f8..c094ddc0 100644 --- a/src/cljs/auto_ap/views/pages/invoices/form.cljs +++ b/src/cljs/auto_ap/views/pages/invoices/form.cljs @@ -108,7 +108,6 @@ (re-frame/inject-cofx ::inject/sub (fn [[_ _ _ client]] [::subs/locations-for-client (:id client)]))] (fn [{:keys [db] ::subs/keys [locations-for-client]} [_ _ command client]] - (println locations-for-client) (when (= :create command) {:db (-> db diff --git a/src/cljs/auto_ap/views/pages/reports.cljs b/src/cljs/auto_ap/views/pages/reports.cljs index 5c3d8b83..9afceff2 100644 --- a/src/cljs/auto_ap/views/pages/reports.cljs +++ b/src/cljs/auto_ap/views/pages/reports.cljs @@ -3,7 +3,7 @@ [auto-ap.subs :as subs] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.pages.company.side-bar + [auto-ap.shared-views.company.sidebar :as side-bar :refer [company-side-bar]] [auto-ap.views.pages.reports.table :as table]