diff --git a/src/clj/auto_ap/jobs/sales_summaries.clj b/src/clj/auto_ap/jobs/sales_summaries.clj index ef68a69d..14251c0e 100644 --- a/src/clj/auto_ap/jobs/sales_summaries.clj +++ b/src/clj/auto_ap/jobs/sales_summaries.clj @@ -62,15 +62,24 @@ {:ledger-mapped/ledger-side [:db/ident]} {:ledger-mapped/account [:db/id]}]) -(defn dirty-sales-summaries [c] +(defn dirty-sales-summaries + "The client's dirty summaries, with enough of each item to evaluate and re-transact it. + + `index-pull` returns a lazy seq running from `:start` to the END of the index, so this must + stop at the client boundary rather than filter: `:sales-summary/client+dirty` sorts by client + first, so every later client's summaries sit beyond this client's and filtering would walk all + of them — for every client — pulling their items on the way. That is quadratic in the number of + summaries, and it showed up as a full refresh degrading from ~180 client-days a minute to ~3 as + the summary count grew." + [c] (let [client-id (dc/entid (dc/db conn) c)] (->> (dc/index-pull (dc/db conn) {:index :avet :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)))))))) + (take-while (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." diff --git a/test/clj/auto_ap/jobs/sales_summaries_test.clj b/test/clj/auto_ap/jobs/sales_summaries_test.clj index 74b8b6dc..e81b4b50 100644 --- a/test/clj/auto_ap/jobs/sales_summaries_test.clj +++ b/test/clj/auto_ap/jobs/sales_summaries_test.clj @@ -120,3 +120,27 @@ {:sales-order/external-id "ezcater/order/TEST-ezcater-vendorless" :sales-order/service-charge -75.0})]) (is (nil? (service-charges-for test-client-id)))))) + +(deftest dirty-summaries-stop-at-the-client-boundary + (testing "every dirty day for the client is returned, and none belonging to another client. + + :sales-summary/client+dirty sorts by client, so an unbounded index scan would walk + every later client's summaries too — correct, but quadratic in the summary count." + (let [{:strs [test-client-id]} (setup-test-data []) + other (get-in @(dc/transact conn [{:db/id "other" :client/code (str "OTHER" (rand-int 100000))}]) + [:tempids "other"]) + day (fn [client d dirty?] + {:sales-summary/client client + :sales-summary/date d + :sales-summary/dirty dirty?})] + @(dc/transact conn [(day test-client-id #inst "2026-08-01T07:00:00.000-00:00" true) + (day test-client-id #inst "2026-08-02T07:00:00.000-00:00" true) + (day test-client-id #inst "2026-08-03T07:00:00.000-00:00" false) + (day other #inst "2026-08-01T07:00:00.000-00:00" true) + (day other #inst "2026-08-02T07:00:00.000-00:00" true)]) + (let [mine (sut/dirty-sales-summaries test-client-id)] + (is (= 2 (count mine)) "both dirty days, and not the clean one") + (is (every? #(= test-client-id (:db/id (:sales-summary/client %))) mine) + "and nothing belonging to the other client")) + (is (= 2 (count (sut/dirty-sales-summaries other))) + "the other client's own dirty days are still found"))))