Merge branch 'feat/scheduled-sales-summary-refresh'
This commit is contained in:
55
src/clj/auto_ap/datomic/sales_summaries.clj
Normal file
55
src/clj/auto_ap/datomic/sales_summaries.clj
Normal file
@@ -0,0 +1,55 @@
|
||||
(ns auto-ap.datomic.sales-summaries
|
||||
(:require
|
||||
[iol-ion.query :refer [dollars=]]))
|
||||
|
||||
(defn- ledger-side
|
||||
"The ledger side of an item as a keyword, whether it arrived as a plain keyword (a
|
||||
transaction map, or a pull using `:xform iol-ion.query/ident`) or as the `{:db/ident ...}`
|
||||
map a plain `pull` returns. Resolving both shapes here matters: items whose side does not
|
||||
compare equal are counted on neither side, which would leave a summary looking balanced at
|
||||
zero and therefore silently accepted."
|
||||
[item]
|
||||
(let [side (:ledger-mapped/ledger-side item)]
|
||||
(if (map? side) (:db/ident side) side)))
|
||||
|
||||
(defn- side-total [side items]
|
||||
(->> items
|
||||
(filter #(= side (ledger-side %)))
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(defn total-debits [items]
|
||||
(side-total :ledger-side/debit items))
|
||||
|
||||
(defn total-credits [items]
|
||||
(side-total :ledger-side/credit items))
|
||||
|
||||
(defn fully-mapped? [items]
|
||||
(every? :ledger-mapped/account items))
|
||||
|
||||
(defn fully-sided?
|
||||
"Every item says which side of the ledger it belongs on. Guards `accepted?` against
|
||||
reading a collection of sideless items as balanced at zero."
|
||||
[items]
|
||||
(every? #(#{:ledger-side/debit :ledger-side/credit} (ledger-side %)) items))
|
||||
|
||||
(defn balanced? [items]
|
||||
(dollars= (total-debits items) (total-credits items)))
|
||||
|
||||
(defn accepted?
|
||||
"True once a summary is finished: every line is mapped to an account and debits equal
|
||||
credits. This is the same condition the sales summaries grid renders as \"Balanced\", and
|
||||
the condition the scheduled refresh treats as \"leave this alone\"."
|
||||
[items]
|
||||
(boolean (and (seq items)
|
||||
(fully-mapped? items)
|
||||
(fully-sided? items)
|
||||
(balanced? items))))
|
||||
|
||||
(defn <-pulled-item
|
||||
"Flattens the ref values on a pulled sales summary item back to the scalars a transaction
|
||||
expects. `accepted?` reads either shape, so this is only needed on the write path."
|
||||
[item]
|
||||
(cond-> item
|
||||
(map? (:ledger-mapped/ledger-side item)) (update :ledger-mapped/ledger-side :db/ident)
|
||||
(map? (:ledger-mapped/account item)) (update :ledger-mapped/account :db/id)))
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns auto-ap.jobs.sales-summaries
|
||||
(:require [auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.datomic.sales-summaries :as d-ss]
|
||||
[auto-ap.jobs.core :refer [execute]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.time :as atime]
|
||||
@@ -8,6 +9,7 @@
|
||||
[clj-time.periodic :as per]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]))
|
||||
|
||||
(defn mark-dirty [client start end]
|
||||
@@ -39,27 +41,100 @@
|
||||
(dc/db conn)
|
||||
number)))
|
||||
|
||||
|
||||
(defn delete-all []
|
||||
@(dc/transact-async conn
|
||||
(->>
|
||||
(dc/q '[:find ?ss
|
||||
:where [?ss :sales-summary/date]]
|
||||
(dc/db conn))
|
||||
(map (fn [[ ss]]
|
||||
[:db/retractEntity ss])))))
|
||||
|
||||
(->>
|
||||
(dc/q '[:find ?ss
|
||||
:where [?ss :sales-summary/date]]
|
||||
(dc/db conn))
|
||||
(map (fn [[ss]]
|
||||
[:db/retractEntity ss])))))
|
||||
|
||||
(def item-read
|
||||
"Enough of a summary item to both evaluate `d-ss/accepted?` and transact the item back
|
||||
unchanged. `:db/id` matters: `:sales-summary/items` is a component attribute upserted via
|
||||
`[:reset-rels ...]`, so an item re-transacted without its id is deleted and recreated."
|
||||
'[:db/id
|
||||
:sales-summary-item/category
|
||||
:sales-summary-item/sort-order
|
||||
:sales-summary-item/manual?
|
||||
:ledger-mapped/amount
|
||||
{:ledger-mapped/ledger-side [:db/ident]}
|
||||
{:ledger-mapped/account [:db/id]}])
|
||||
|
||||
(defn dirty-sales-summaries [c]
|
||||
(let [client-id (dc/entid (dc/db conn) c)]
|
||||
(->> (dc/index-pull (dc/db conn)
|
||||
{:index :avet
|
||||
:selector '[:sales-summary/date :sales-summary/client :db/id]
|
||||
:selector (conj '[:sales-summary/date :sales-summary/client :db/id]
|
||||
{:sales-summary/items item-read})
|
||||
:start [:sales-summary/client+dirty [client-id true]]})
|
||||
(filter (fn [sales-summary]
|
||||
(= client-id (:db/id (:sales-summary/client sales-summary))))))))
|
||||
|
||||
(def default-days
|
||||
"How far back the scheduled refresh looks for summaries that still need recomputing."
|
||||
7)
|
||||
|
||||
(defn trailing-window
|
||||
"`[start end)` covering the last `days` business days, ending with today. `end` is
|
||||
exclusive, matching both `periodic-seq`'s 3-arity and the grid's date filters, so
|
||||
`(trailing-window 7)` is day -6 through today inclusive."
|
||||
[days]
|
||||
[(.toDateMidnight (atime/localize (time/minus (time/now) (time/days (dec days)))))
|
||||
(.toDateMidnight (atime/localize (time/plus (time/now) (time/days 1))))])
|
||||
|
||||
(defn accepted-client+dates
|
||||
"Set of `[client-id date]` pairs in `[start end)` whose summary is already accepted, and
|
||||
so should be left alone rather than re-marked. Accepted means balanced with every line
|
||||
mapped to an account — the condition the grid renders as \"Balanced\"."
|
||||
[db start end]
|
||||
(->> (dc/q '[:find (pull ?ss selector)
|
||||
:in $ ?start ?end selector
|
||||
:where
|
||||
[?ss :sales-summary/date ?d]
|
||||
[(>= ?d ?start)]
|
||||
[(< ?d ?end)]]
|
||||
db
|
||||
(c/to-date start)
|
||||
(c/to-date end)
|
||||
(conj '[:sales-summary/date {:sales-summary/client [:db/id]}]
|
||||
{:sales-summary/items item-read}))
|
||||
(map first)
|
||||
(filter #(d-ss/accepted? (map d-ss/<-pulled-item (:sales-summary/items %))))
|
||||
(map (juxt (comp :db/id :sales-summary/client) :sales-summary/date))
|
||||
set))
|
||||
|
||||
(defn mark-stale-dirty
|
||||
"Marks every client/day in the trailing `days` window dirty so `sales-summaries-v2` will
|
||||
recompute it, skipping days whose summary is already accepted. Because accepted is derived
|
||||
rather than stored, a summary that later falls out of balance is picked up again on the
|
||||
next run. Returns the number of client/days marked."
|
||||
[days]
|
||||
(let [db (dc/db conn)
|
||||
[start end] (trailing-window days)
|
||||
accepted (accepted-client+dates db start end)
|
||||
clients (map first (dc/q '[:find ?c
|
||||
:in $
|
||||
:where [_ :sales-order/client ?c]]
|
||||
db))
|
||||
dates (map c/to-date (per/periodic-seq start end (time/days 1)))
|
||||
tx-data (for [client clients
|
||||
date dates
|
||||
:when (not (accepted [client date]))]
|
||||
{:sales-summary/client client
|
||||
:sales-summary/date date
|
||||
:sales-summary/dirty true
|
||||
:sales-summary/client+date [client date]})]
|
||||
(alog/info ::marking-dirty
|
||||
:days days
|
||||
:client-count (count clients)
|
||||
:accepted-count (count accepted)
|
||||
:marked (count tx-data))
|
||||
(doseq [batch (partition-all 500 tx-data)]
|
||||
@(dc/transact conn batch))
|
||||
(count tx-data)))
|
||||
|
||||
(defn- get-fee [c date]
|
||||
(- (or (ffirst (dc/q '[:find ?f
|
||||
:in $ ?client ?d
|
||||
@@ -99,53 +174,53 @@
|
||||
"food app refunds" 41400})
|
||||
|
||||
(defn get-payment-items [c date]
|
||||
(->>
|
||||
(dc/q '[:find ?processor ?type-name (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/type-name ?type-name]
|
||||
(or-join [?c ?processor]
|
||||
(and [?c :charge/processor ?p]
|
||||
[?p :db/ident ?processor])
|
||||
(and
|
||||
(not [?c :charge/processor])
|
||||
[(ground :ccp-processor/na) ?processor]))
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[c] date date])
|
||||
(reduce
|
||||
(fn [acc [processor type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Payments"
|
||||
(= type-name "CASH")
|
||||
"Cash Payments"
|
||||
(#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name)
|
||||
"Gift Card Payments"
|
||||
(#{:ccp-processor/toast
|
||||
#_:ccp-processor/ezcater
|
||||
#_:ccp-processor/koala
|
||||
:ccp-processor/doordash
|
||||
:ccp-processor/grubhub
|
||||
:ccp-processor/uber-eats} processor)
|
||||
"Food App Payments"
|
||||
:else
|
||||
"Unknown")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(map (fn [[k v]]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 0
|
||||
:sales-summary-item/category k
|
||||
|
||||
:ledger-mapped/amount (if (= "Card Payments" k)
|
||||
(- v (get-fee c date))
|
||||
v)
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))))
|
||||
(->>
|
||||
(dc/q '[:find ?processor ?type-name (sum ?total)
|
||||
:with ?c
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
|
||||
[?e :sales-order/charges ?c]
|
||||
[?c :charge/type-name ?type-name]
|
||||
(or-join [?c ?processor]
|
||||
(and [?c :charge/processor ?p]
|
||||
[?p :db/ident ?processor])
|
||||
(and
|
||||
(not [?c :charge/processor])
|
||||
[(ground :ccp-processor/na) ?processor]))
|
||||
[?c :charge/total ?total]]
|
||||
(dc/db conn)
|
||||
[[c] date date])
|
||||
(reduce
|
||||
(fn [acc [processor type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Payments"
|
||||
(= type-name "CASH")
|
||||
"Cash Payments"
|
||||
(#{"SQUARE_GIFT_CARD" "WALLET" "GIFT_CARD"} type-name)
|
||||
"Gift Card Payments"
|
||||
(#{:ccp-processor/toast
|
||||
#_:ccp-processor/ezcater
|
||||
#_:ccp-processor/koala
|
||||
:ccp-processor/doordash
|
||||
:ccp-processor/grubhub
|
||||
:ccp-processor/uber-eats} processor)
|
||||
"Food App Payments"
|
||||
:else
|
||||
"Unknown")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(map (fn [[k v]]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 0
|
||||
:sales-summary-item/category k
|
||||
|
||||
:ledger-mapped/amount (if (= "Card Payments" k)
|
||||
(- v (get-fee c date))
|
||||
v)
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))))
|
||||
|
||||
(defn get-discounts [c date]
|
||||
(when-let [discount (ffirst (dc/q '[:find (sum ?discount)
|
||||
@@ -162,7 +237,7 @@
|
||||
:ledger-mapped/ledger-side :ledger-side/debit}))
|
||||
|
||||
(defn get-refund-items [c date]
|
||||
(->>
|
||||
(->>
|
||||
(dc/q '[:find ?type-name (sum ?t)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
@@ -173,26 +248,24 @@
|
||||
(dc/db conn)
|
||||
[[c] date date])
|
||||
(reduce
|
||||
(fn [acc [type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Refunds"
|
||||
(= type-name "CASH")
|
||||
"Cash Refunds"
|
||||
:else
|
||||
"Food App Refunds")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(fn [acc [type-name total]]
|
||||
(update
|
||||
acc
|
||||
(cond (= type-name "CARD")
|
||||
"Card Refunds"
|
||||
(= type-name "CASH")
|
||||
"Cash Refunds"
|
||||
:else
|
||||
"Food App Refunds")
|
||||
(fnil + 0.0)
|
||||
total))
|
||||
{})
|
||||
(map (fn [[k v]]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 3
|
||||
:sales-summary-item/category k
|
||||
:ledger-mapped/amount v
|
||||
:ledger-mapped/ledger-side :ledger-side/credit}))))
|
||||
|
||||
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:sales-summary-item/sort-order 3
|
||||
:sales-summary-item/category k
|
||||
:ledger-mapped/amount v
|
||||
:ledger-mapped/ledger-side :ledger-side/credit}))))
|
||||
|
||||
(defn get-fees [c date]
|
||||
(when-let [fee (get-fee c date)]
|
||||
@@ -278,17 +351,18 @@
|
||||
|
||||
(defn sales-summaries-v2 []
|
||||
(doseq [[c client-code] (dc/q '[:find ?c ?client-code
|
||||
:in $
|
||||
:where [?c :client/code ?client-code]]
|
||||
(dc/db conn))
|
||||
{:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
|
||||
:in $
|
||||
:where [?c :client/code ?client-code]]
|
||||
(dc/db conn))
|
||||
{:sales-summary/keys [date] :db/keys [id] :as existing-summary} (dirty-sales-summaries c)]
|
||||
(mu/with-context {:client-code client-code
|
||||
:date date}
|
||||
(alog/info ::updating)
|
||||
(let [manual-items (->> existing-summary
|
||||
:sales-summary/items
|
||||
(filter :sales-summary-item/manual?))
|
||||
calculated-items (->>
|
||||
:date date}
|
||||
(alog/info ::updating)
|
||||
(let [manual-items (->> existing-summary
|
||||
:sales-summary/items
|
||||
(filter :sales-summary-item/manual?)
|
||||
(map d-ss/<-pulled-item))
|
||||
calculated-items (->>
|
||||
(get-sales c date)
|
||||
(concat (get-payment-items c date))
|
||||
(concat (get-refund-items c date))
|
||||
@@ -301,20 +375,19 @@
|
||||
(map (fn [z]
|
||||
(assoc z :ledger-mapped/account (some-> z :sales-summary-item/category str/lower-case name->number lookup-account)
|
||||
:sales-summary-item/manual? false))))
|
||||
all-items (concat calculated-items manual-items)
|
||||
result {:db/id id
|
||||
:sales-summary/client c
|
||||
:sales-summary/date date
|
||||
:sales-summary/dirty false
|
||||
:sales-summary/client+date [c date]
|
||||
:sales-summary/items all-items}]
|
||||
(if (seq (:sales-summary/items result))
|
||||
(do
|
||||
(alog/info ::upserting-summaries
|
||||
:category-count (count (:sales-summary/items result)))
|
||||
@(dc/transact conn [[:upsert-sales-summary result]]))
|
||||
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
|
||||
|
||||
all-items (concat calculated-items manual-items)
|
||||
result {:db/id id
|
||||
:sales-summary/client c
|
||||
:sales-summary/date date
|
||||
:sales-summary/dirty false
|
||||
:sales-summary/client+date [c date]
|
||||
:sales-summary/items all-items}]
|
||||
(if (seq (:sales-summary/items result))
|
||||
(do
|
||||
(alog/info ::upserting-summaries
|
||||
:category-count (count (:sales-summary/items result)))
|
||||
@(dc/transact conn [[:upsert-sales-summary result]]))
|
||||
@(dc/transact conn [{:db/id id :sales-summary/dirty false}]))))))
|
||||
|
||||
(defn reset-summaries []
|
||||
@(dc/transact conn (->> (dc/q '[:find ?sos
|
||||
@@ -324,9 +397,6 @@
|
||||
(map (fn [[sos]]
|
||||
[:db/retractEntity sos])))))
|
||||
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
(auto-ap.datomic/transact-schema conn)
|
||||
|
||||
@@ -336,26 +406,19 @@
|
||||
|
||||
(dirty-sales-summaries [:client/code "NGWH"])
|
||||
|
||||
|
||||
(apply mark-dirty [:client/code "NGWH"] (last-n-days 5))
|
||||
|
||||
(iol-ion.tx.upsert-sales-summary-ledger/summary->journal-entry (dc/db conn) 17592314245819)
|
||||
|
||||
|
||||
(iol-ion.tx.upsert-sales-summary-ledger/upsert-sales-summary (dc/db conn) {:db/id 17592314241429})
|
||||
|
||||
|
||||
|
||||
(mark-all-dirty 5)
|
||||
(delete-all)
|
||||
|
||||
|
||||
(sales-summaries-v2)
|
||||
|
||||
|
||||
1
|
||||
|
||||
|
||||
(dc/q '[:find (pull ?sos [* {:sales-summary/sales-items [*]}])
|
||||
:in $
|
||||
:where [?sos :sales-summary/client [:client/code "NGHW"]]
|
||||
@@ -386,15 +449,24 @@
|
||||
@(dc/transact conn [{:db/id :sales-summary/total-tax :db/ident :sales-summary/total-tax-legacy}
|
||||
{:db/id :sales-summary/total-tip :db/ident :sales-summary/total-tip-legacy}])
|
||||
|
||||
(auto-ap.datomic/transact-schema conn)
|
||||
|
||||
|
||||
|
||||
|
||||
)
|
||||
(auto-ap.datomic/transact-schema conn))
|
||||
|
||||
(defn days-arg
|
||||
"Trailing-window size from the job's `args`, e.g. `{:days 30}` set as a container override
|
||||
from the admin Background Jobs page for an ad-hoc wider backfill. Values arrive as EDN but
|
||||
may still be strings, so coerce defensively the way load-historical-sales does."
|
||||
[args]
|
||||
(let [days (:days args)]
|
||||
(cond-> (or days default-days)
|
||||
(string? days) (#(Long/parseLong %)))))
|
||||
|
||||
(defn refresh-sales-summaries
|
||||
"Marks the trailing `days` window dirty, skipping accepted summaries, then recomputes
|
||||
everything left dirty."
|
||||
([] (refresh-sales-summaries default-days))
|
||||
([days]
|
||||
(mark-stale-dirty days)
|
||||
(sales-summaries-v2)))
|
||||
|
||||
(defn -main [& _]
|
||||
(execute "sales-summaries" sales-summaries-v2))
|
||||
|
||||
(execute "sales-summaries" #(refresh-sales-summaries (days-arg (:args env)))))
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[auto-ap.jobs.load-historical-sales :as job-load-historical-sales]
|
||||
[auto-ap.jobs.plaid :as job-plaid]
|
||||
[auto-ap.jobs.register-invoice-import :as job-register-invoice-import]
|
||||
[auto-ap.jobs.sales-summaries :as job-sales-summaries]
|
||||
[auto-ap.jobs.square :as job-square]
|
||||
[auto-ap.jobs.sysco :as job-sysco]
|
||||
[auto-ap.jobs.vendor-usages :as job-vendor-usages]
|
||||
@@ -33,23 +34,22 @@
|
||||
(.addShutdownHook (Runtime/getRuntime)
|
||||
(Thread. f)))
|
||||
|
||||
|
||||
(defn gzip-handler []
|
||||
(let [gz (GzipHandler.)]
|
||||
(doto gz
|
||||
(.setIncludedMethods (into-array ["GET" "POST" "PUT" "DELETE" "PATCH"]))
|
||||
(.setIncludedMimeTypes (into-array ["text/css"
|
||||
"text/*"
|
||||
"text/plain"
|
||||
"text/javascript"
|
||||
"text/csv"
|
||||
"text/html"
|
||||
"text/html;charset=utf-8"
|
||||
"application/javascript"
|
||||
"application/csv"
|
||||
"application/edn"
|
||||
"application/json"
|
||||
"image/svg+xml"]))
|
||||
(.setIncludedMimeTypes (into-array ["text/css"
|
||||
"text/*"
|
||||
"text/plain"
|
||||
"text/javascript"
|
||||
"text/csv"
|
||||
"text/html"
|
||||
"text/html;charset=utf-8"
|
||||
"application/javascript"
|
||||
"application/csv"
|
||||
"application/edn"
|
||||
"application/json"
|
||||
"image/svg+xml"]))
|
||||
(.setMinGzipSize 1024))
|
||||
gz))
|
||||
|
||||
@@ -126,6 +126,9 @@
|
||||
(= job "close-auto-invoices")
|
||||
(job-close-auto-invoices/-main)
|
||||
|
||||
(= job "sales-summaries")
|
||||
(job-sales-summaries/-main)
|
||||
|
||||
(= job "ezcater-upsert")
|
||||
(job-ezcater-upsert/-main)
|
||||
|
||||
|
||||
@@ -28,14 +28,13 @@
|
||||
(com.amazonaws.services.ecs.model AssignPublicIp)))
|
||||
|
||||
(defn get-ecs-tasks []
|
||||
(->>
|
||||
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
|
||||
(ecs/describe-tasks :include [] :tasks)
|
||||
:tasks
|
||||
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
|
||||
(sort-by :created-at)
|
||||
reverse))
|
||||
|
||||
(->>
|
||||
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
|
||||
(ecs/describe-tasks :include [] :tasks)
|
||||
:tasks
|
||||
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
|
||||
(sort-by :created-at)
|
||||
reverse))
|
||||
|
||||
(defn is-background-job?
|
||||
"This function checks whether a given task is a background job.
|
||||
@@ -60,7 +59,7 @@
|
||||
(defn job-exited-successfully? [task]
|
||||
(if (= 0 (->> task
|
||||
:containers
|
||||
(filter (comp #{"integreat-app" } :name))
|
||||
(filter (comp #{"integreat-app"} :name))
|
||||
(first)
|
||||
:exit-code))
|
||||
true
|
||||
@@ -77,7 +76,7 @@
|
||||
:succeeded
|
||||
:failed))
|
||||
:name (task-definition->job-name (:task-definition task))
|
||||
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
|
||||
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
|
||||
:start-date (some-> (:created-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))})
|
||||
|
||||
(defn fetch-page [request]
|
||||
@@ -85,7 +84,7 @@
|
||||
(filter is-background-job?)
|
||||
(map ecs-task->job))]
|
||||
[jobs (count jobs)]))
|
||||
(def query-schema (mc/schema [:map ]))
|
||||
(def query-schema (mc/schema [:map]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "job-table"
|
||||
@@ -107,8 +106,7 @@
|
||||
:entity-name "Job"
|
||||
:query-schema query-schema
|
||||
:route :admin-job-table
|
||||
:headers [
|
||||
{:key "start"
|
||||
:headers [{:key "start"
|
||||
:name "Start"
|
||||
:render #(some-> % :start-date (atime/unparse-local atime/standard-time))}
|
||||
{:key "end"
|
||||
@@ -119,7 +117,7 @@
|
||||
:render (fn [e]
|
||||
(when (and (:start-date e)
|
||||
(:end-date e))
|
||||
(str (time/in-minutes (time/interval
|
||||
(str (time/in-minutes (time/interval
|
||||
(:start-date e)
|
||||
(:end-date e))) " minutes")))}
|
||||
{:key "name"
|
||||
@@ -150,16 +148,16 @@
|
||||
:network-configuration {:aws-vpc-configuration {:subnets ["subnet-5e675761" "subnet-8519fde2" "subnet-89bab8d4"]
|
||||
:security-groups ["sg-004e5855310c453a3" "sg-02d167406b1082698"]
|
||||
:assign-public-ip AssignPublicIp/ENABLED}}}
|
||||
args (assoc-in [:overrides :container-overrides ] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
|
||||
args (assoc-in [:overrides :container-overrides] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
|
||||
|
||||
(defn job-start [{:keys [form-params]}]
|
||||
(if (not (get (currently-running-jobs) (:name form-params)))
|
||||
(let [new-job (run-task
|
||||
(-> (:name form-params)
|
||||
(str/replace #"-" "_")
|
||||
(str/replace #":" "")
|
||||
(str "_" (:dd-env env)))
|
||||
(dissoc form-params :name))]
|
||||
(-> (:name form-params)
|
||||
(str/replace #"-" "_")
|
||||
(str/replace #":" "")
|
||||
(str "_" (:dd-env env)))
|
||||
(dissoc form-params :name))]
|
||||
{:message (str "task " (str new-job) " started.")})
|
||||
(form-validation-error "This job is already running"
|
||||
:form-params form-params)))
|
||||
@@ -170,107 +168,109 @@
|
||||
[(fc/with-field :ledger-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "ledger-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )]))]
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "ledger-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})]))]
|
||||
(= "register-invoice-import" name)
|
||||
[
|
||||
(fc/with-field :invoice-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "invoice-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )]))]
|
||||
[(fc/with-field :invoice-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "invoice-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})]))]
|
||||
(= "load-historical-sales" name)
|
||||
[
|
||||
(fc/with-field :client
|
||||
(com/validated-field {:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)})))
|
||||
(fc/with-field :days
|
||||
[(fc/with-field :client
|
||||
(com/validated-field {:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)})))
|
||||
(fc/with-field :days
|
||||
(com/validated-field {:label "Days to load"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:placeholder "60"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )))]
|
||||
:else nil))
|
||||
:value (fc/field-value)})))]
|
||||
(= "sales-summaries" name)
|
||||
[(fc/with-field :days
|
||||
(com/validated-field {:label "Days to refresh"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:placeholder "7"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})))]
|
||||
:else nil)))
|
||||
|
||||
|
||||
)
|
||||
|
||||
(defn subform [{{:keys [name]} :query-params }]
|
||||
(defn subform [{{:keys [name]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form {} nil
|
||||
(subform* {:name name}))))
|
||||
(fc/start-form {} nil
|
||||
(subform* {:name name}))))
|
||||
|
||||
(defn job-start-dialog [{:keys [form-errors form-params] :as request}]
|
||||
(fc/start-form (or form-params {}) form-errors
|
||||
(modal-response
|
||||
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
|
||||
{}
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
|
||||
:class "h-full w-full"}
|
||||
[:fieldset {:class "hx-disable h-full w-full"}
|
||||
(com/modal-card {}
|
||||
[:div.m-2 "New job"]
|
||||
[:div.space-y-6
|
||||
(modal-response
|
||||
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
|
||||
{}
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
|
||||
:class "h-full w-full"}
|
||||
[:fieldset {:class "hx-disable h-full w-full"}
|
||||
(com/modal-card {}
|
||||
[:div.m-2 "New job"]
|
||||
[:div.space-y-6
|
||||
|
||||
(fc/with-field :name
|
||||
(com/validated-field {:label "Job"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:class "w-64"
|
||||
:options [["" ""]
|
||||
["yodlee2" "Yodlee Import"]
|
||||
["yodlee2-accounts" "Yodlee Account Import"]
|
||||
["intuit" "Intuit import"]
|
||||
["plaid" "Plaid import"]
|
||||
["bulk-journal-import" "Bulk Journal Import"]
|
||||
["square-import-job" "Square Import"]
|
||||
["register-invoice-import" "Register Invoice Import "]
|
||||
["ezcater-upsert" "Upsert recent ezcater orders"]
|
||||
["load-historical-sales" "Load Historical Square Sales"]
|
||||
["export-backup" "Export Backup"]]
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-job-subform)
|
||||
:hx-target "#sub-form"
|
||||
:hx-swap "innerHTML"})))
|
||||
(fc/with-field :name
|
||||
(com/validated-field {:label "Job"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:class "w-64"
|
||||
:options [["" ""]
|
||||
["yodlee2" "Yodlee Import"]
|
||||
["yodlee2-accounts" "Yodlee Account Import"]
|
||||
["intuit" "Intuit import"]
|
||||
["plaid" "Plaid import"]
|
||||
["bulk-journal-import" "Bulk Journal Import"]
|
||||
["square-import-job" "Square Import"]
|
||||
["register-invoice-import" "Register Invoice Import "]
|
||||
["ezcater-upsert" "Upsert recent ezcater orders"]
|
||||
["load-historical-sales" "Load Historical Square Sales"]
|
||||
["sales-summaries" "Refresh Sales Summaries"]
|
||||
["export-backup" "Export Backup"]]
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-job-subform)
|
||||
:hx-target "#sub-form"
|
||||
:hx-swap "innerHTML"})))
|
||||
|
||||
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]]
|
||||
[:div
|
||||
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
|
||||
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))})]]
|
||||
[:div
|
||||
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
|
||||
|
||||
(def form-schema (mc/schema [:map
|
||||
[:name [:string {:min 1}]]
|
||||
[:ledger-url {:optional true} [:string {:min 1}]]
|
||||
[:invoice-url {:optional true} [:string {:min 1}]]
|
||||
[:client {:optional true} entity-id]
|
||||
[:days {:optional true} [:int {:min 1 :max 120}]]
|
||||
]))
|
||||
[:days {:optional true} [:int {:min 1 :max 120}]]]))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{:admin-jobs (helper/page-route grid-page)
|
||||
:admin-job-table (helper/table-route grid-page)
|
||||
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
|
||||
:admin-job-start (-> job-start
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 job-start-dialog))
|
||||
:admin-job-start-dialog job-start-dialog})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{:admin-jobs (helper/page-route grid-page)
|
||||
:admin-job-table (helper/table-route grid-page)
|
||||
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
|
||||
:admin-job-start (-> job-start
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 job-start-dialog))
|
||||
:admin-job-start-dialog job-start-dialog})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
|
||||
query2]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.sales-summaries :refer [total-credits total-debits]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
@@ -116,18 +117,6 @@
|
||||
(defn sort-items [ss]
|
||||
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
|
||||
|
||||
(defn total-debits [items]
|
||||
(->> items
|
||||
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(defn total-credits [items]
|
||||
(->> items
|
||||
(filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)))
|
||||
(map #(:ledger-mapped/amount % 0.0))
|
||||
(reduce + 0.0)))
|
||||
|
||||
(defn truncate [s max-len]
|
||||
(if (> (count s) max-len)
|
||||
(str (subs s 0 (- max-len 3)) "...")
|
||||
@@ -158,13 +147,13 @@
|
||||
[:span.text-sm account-name]
|
||||
(com/pill {:color :red} "Missing acct"))
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
||||
:client-id client-id
|
||||
:current-account-id (or account-id "")})}
|
||||
svg/pencil)]))
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
||||
:client-id client-id
|
||||
:current-account-id (or account-id "")})}
|
||||
svg/pencil)]))
|
||||
|
||||
(defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}]
|
||||
(let [account-input-name (str field-name-prefix "[ledger-mapped/account]")]
|
||||
@@ -172,23 +161,23 @@
|
||||
(account-typeahead* {:name account-input-name
|
||||
:value current-account-id
|
||||
:client-id client-id})
|
||||
[:div.flex.gap-1
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest .account-cell"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id})}
|
||||
svg/check)
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id
|
||||
:current-account-id (or current-account-id "")})}
|
||||
svg/x)]]))
|
||||
[:div.flex.gap-1
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest .account-cell"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id})}
|
||||
svg/check)
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id
|
||||
:current-account-id (or current-account-id "")})}
|
||||
svg/x)]]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -576,8 +565,8 @@
|
||||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
[:div.h-6]))]
|
||||
[:div.mt-2.border-t.pt-1
|
||||
(summary-total-display request)
|
||||
@@ -619,13 +608,13 @@
|
||||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
[:div.h-6]))]
|
||||
[:div.mt-2.border-t.pt-1
|
||||
(summary-total-display request)
|
||||
(unbalanced-display request)]]]
|
||||
[:div.mt-4.border-t.pt-2
|
||||
[:div.mt-4.border-t.pt-2
|
||||
(fc/with-field :sales-summary/items
|
||||
(com/data-grid-new-row {:colspan 2
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
|
||||
@@ -761,16 +750,16 @@
|
||||
::route/edit-wizard-navigate (-> mm/next-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
|
||||
(fn render [cursor request]
|
||||
(sales-summary-item-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :sales-summary-item/manual? true)))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
|
||||
(fn render [cursor request]
|
||||
(sales-summary-item-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :sales-summary-item/manual? true)))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/edit-item-account (-> edit-item-account
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:item-index nat-int?]
|
||||
|
||||
Reference in New Issue
Block a user