Merge branch 'feat/scheduled-sales-summary-refresh'

This commit is contained in:
2026-08-12 16:10:44 -07:00
6 changed files with 418 additions and 285 deletions

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

View File

@@ -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
@@ -100,52 +175,52 @@
(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
(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}))))
: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)
@@ -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)))))

View File

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

View File

@@ -29,13 +29,12 @@
(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
@@ -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"
@@ -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 :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
[: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")])]]))))
(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)))))
(->>
{: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)))))

View File

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

View File

@@ -386,6 +386,20 @@ module "close_auto_invoices_job" {
cpu = 512
}
module "sales_summaries_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"
ecs_cluster = var.ecs_cluster
task_role_arn = var.task_role_arn
stage = var.stage
schedule = "rate(1 day)"
job_name = "sales-summaries"
execution_role_arn = var.execution_role_arn
use_schedule = true
memory = 4096
cpu = 2048
}
module "yodlee2_accounts_job" {
count = var.enable_schedules ? 1 : 0
source = "./background-job/"