Revamps all of the IOL's routing, so that the new history page can share with the rest.

This commit is contained in:
2023-01-12 16:56:40 -08:00
parent ce01a63797
commit 46dd191391
29 changed files with 1294 additions and 1053 deletions

View File

@@ -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]]

View File

@@ -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]]
@@ -33,80 +33,129 @@
(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))))))))
#_{: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-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)))

View File

@@ -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})

View File

@@ -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)})

View File

@@ -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)})

View File

@@ -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)})

View File

@@ -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})

View File

@@ -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))})

View File

@@ -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)))

View File

@@ -1,5 +1,8 @@
(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]))
(defn wrap-secure [handler]
(fn [request]
@@ -7,3 +10,20 @@
(handler request)
{:status 401
:body "not authenticated"})))
(defn wrap-admin [handler]
(fn [request]
(if (is-admin? (:identity request))
(handler request)
(do
(alog/warn ::unauthenticated)
{:status 302
:headers "/login"}))))
(defn wrap-client-redirect-unauthenticated [handler]
(fn [request]
(let [response (handler request)]
(println )
(if (= 401 (get response :status))
(assoc-in response [:headers "hx-redirect"] "/login/")
response))))

View File

@@ -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))} )

View File

@@ -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

View File

@@ -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
"<!DOCTYPE html>"
(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))))

View File

@@ -0,0 +1,14 @@
(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-routes]))
;; 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)))})

108
src/clj/auto_ap/ssr/ui.clj Normal file
View File

@@ -0,0 +1,108 @@
(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
"<!DOCTYPE html>"
(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"}]]]]]))
(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)))))))

View File

@@ -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))))

View File

@@ -0,0 +1,2 @@
(ns auto-ap.shared-views)

View File

@@ -0,0 +1,104 @@
(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 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 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)))))

View File

@@ -0,0 +1,8 @@
(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}}})

View File

@@ -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)))]))

View File

@@ -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 []

View File

@@ -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]]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]]

View File

@@ -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]

View File

@@ -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]