From 6e7f66a78f3fe3f1baaa65acdc4ff84ce17db062 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sat, 15 Aug 2026 18:16:17 -0700 Subject: [PATCH 1/3] fix(ledger): honor include-in-reports in the register A journal entry with a line posted to a bank account flagged :bank-account/include-in-reports false shows on the SSR register but not on the GraphQL/CLJS ledger page, which has always dropped those entries in auto-ap.datomic.ledger/graphql-results (and the CSV export does the same in auto-ap.routes.exports). Same entry, same filters, visible on one page and missing from the other. The admin client form defaults the flag to false, so any bank account saved without ticking the box is affected -- 33 of 676 bank accounts carry an explicit false today. Resolve the affected entries up front off VAET and drop them before sorting rather than excluding them inside the query: the equivalent not-join measured ~36ms against ~3ms for the bare scan on a wide date range, while the two lookups cost ~2ms and are skipped entirely for clients with nothing flagged. Unlike the GraphQL page, which filters after pagination and so quietly serves short pages against an unfiltered total, this runs before pagination, so the row count matches what the register renders. Co-Authored-By: Claude Opus 5 --- src/clj/auto_ap/ssr/ledger/common.clj | 34 +++++++++++ test/clj/auto_ap/ssr/ledger_test.clj | 82 +++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/clj/auto_ap/ssr/ledger/common.clj b/src/clj/auto_ap/ssr/ledger/common.clj index 62863789..1bebd5cd 100644 --- a/src/clj/auto_ap/ssr/ledger/common.clj +++ b/src/clj/auto_ap/ssr/ledger/common.clj @@ -203,6 +203,39 @@ result)) results)) +(defn- entries-off-reports + "Journal entries with a line posted to one of `clients`' bank accounts flagged + `:bank-account/include-in-reports` false, or nil when there are none. + + The GraphQL ledger page has always dropped these (see + auto-ap.datomic.ledger/graphql-results), so the register has to agree -- + otherwise the same entry is visible on one ledger page and missing from the + other. Resolved up front off VAET, which is ~150 entries db-wide and costs + ~2ms; excluding them inside the query with a not-join instead measured ~10x + the cost of the whole scan on a wide date range." + [db clients] + (when-let [off-reports (seq (dc/q '[:find [?ba ...] + + :in $ [?client ...] + + :where + [?client :client/bank-accounts ?ba] + [?ba :bank-account/include-in-reports false]] + db clients))] + (set (dc/q '[:find [?e ...] + + :in $ [?ba ...] + + :where + [?li :journal-entry-line/account ?ba] + [?e :journal-entry/line-items ?li]] + db off-reports)))) + +(defn- apply-include-in-reports [db clients results] + (if-let [hidden (entries-off-reports db clients)] + (remove (comp hidden last) results) + results)) + ;; TODO ;; 1. Sorting in investigate dialog ;; 2. actual date range filtering in investigate dialog @@ -375,6 +408,7 @@ (merge-query {:query {:find ['?sort-default '?e]}})))] (->> (observable-query query) + (apply-include-in-reports db valid-clients) (apply-sort-4 (assoc query-params :default-asc? true)) (apply-only-unbalanced query-params) (apply-pagination query-params)))) diff --git a/test/clj/auto_ap/ssr/ledger_test.clj b/test/clj/auto_ap/ssr/ledger_test.clj index 7454c00e..13974993 100644 --- a/test/clj/auto_ap/ssr/ledger_test.clj +++ b/test/clj/auto_ap/ssr/ledger_test.clj @@ -797,3 +797,85 @@ [:journal-entry/external-id (external-id "ext-good-1")])] (is (= 100.0 (:journal-entry/amount entry))) (is (= 2 (count (:journal-entry/line-items entry)))))))))))) + +(deftest fetch-ids-include-in-reports-test + (testing "Should leave an entry out of the register when a line posts to a bank account off reports" + (let [{:strs [reports-client shown-entry]} + (setup-test-data [(test-bank-account :db/id "shown-bank" + :bank-account/name "Shown Bank" + :bank-account/numeric-code 11000 + :bank-account/include-in-reports true) + (test-bank-account :db/id "hidden-bank" + :bank-account/name "Hidden Bank" + :bank-account/numeric-code 11001 + :bank-account/include-in-reports false) + (test-client :db/id "reports-client" + :client/code "REPORTS-TEST" + :client/locations ["HQ"] + :client/bank-accounts ["shown-bank" "hidden-bank"]) + {:db/id "reports-expense" + :account/name "Expense" + :account/numeric-code 60000 + :account/account-set "default"} + {:db/id "shown-entry" + :journal-entry/client "reports-client" + :journal-entry/date #inst "2024-03-01" + :journal-entry/amount 100.0 + :journal-entry/source "transaction" + :journal-entry/line-items + [{:journal-entry-line/account "shown-bank" + :journal-entry-line/credit 100.0} + {:journal-entry-line/account "reports-expense" + :journal-entry-line/debit 100.0}]} + {:db/id "hidden-entry" + :journal-entry/client "reports-client" + :journal-entry/date #inst "2024-03-02" + :journal-entry/amount 50.0 + :journal-entry/source "transaction" + :journal-entry/line-items + [{:journal-entry-line/account "hidden-bank" + :journal-entry-line/credit 50.0} + {:journal-entry-line/account "reports-expense" + :journal-entry-line/debit 50.0}]}]) + request {:client-id reports-client + :clients [reports-client] + :route-params {} + :query-params {}} + result (common/fetch-ids (dc/db conn) request)] + + (is (= [shown-entry] (vec (:all-ids result)))) + (is (= [shown-entry] (vec (:ids result)))) + (testing "and counts it out of the total, so the page is not silently short" + (is (= 1 (:count result)))))) + + (testing "Should keep every entry when no bank account is off reports" + (let [{:strs [open-client open-entry]} + (setup-test-data [(test-bank-account :db/id "open-bank" + :bank-account/name "Open Bank" + :bank-account/numeric-code 11002 + :bank-account/include-in-reports true) + (test-client :db/id "open-client" + :client/code "REPORTS-OPEN" + :client/locations ["HQ"] + :client/bank-accounts ["open-bank"]) + {:db/id "open-expense" + :account/name "Expense" + :account/numeric-code 60001 + :account/account-set "default"} + {:db/id "open-entry" + :journal-entry/client "open-client" + :journal-entry/date #inst "2024-03-01" + :journal-entry/amount 100.0 + :journal-entry/source "transaction" + :journal-entry/line-items + [{:journal-entry-line/account "open-bank" + :journal-entry-line/credit 100.0} + {:journal-entry-line/account "open-expense" + :journal-entry-line/debit 100.0}]}]) + result (common/fetch-ids (dc/db conn) {:client-id open-client + :clients [open-client] + :route-params {} + :query-params {}})] + + (is (= [open-entry] (vec (:all-ids result)))) + (is (= 1 (:count result)))))) -- 2.49.1 From 8cc3dc8bd7bb7f25e0a2b13217f683a553f1c63d Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 16 Aug 2026 08:01:01 -0700 Subject: [PATCH 2/3] Revert "fix(ledger): honor include-in-reports in the register" This reverts commit 6e7f66a78f3fe3f1baaa65acdc4ff84ce17db062. --- src/clj/auto_ap/ssr/ledger/common.clj | 34 ----------- test/clj/auto_ap/ssr/ledger_test.clj | 82 --------------------------- 2 files changed, 116 deletions(-) diff --git a/src/clj/auto_ap/ssr/ledger/common.clj b/src/clj/auto_ap/ssr/ledger/common.clj index 1bebd5cd..62863789 100644 --- a/src/clj/auto_ap/ssr/ledger/common.clj +++ b/src/clj/auto_ap/ssr/ledger/common.clj @@ -203,39 +203,6 @@ result)) results)) -(defn- entries-off-reports - "Journal entries with a line posted to one of `clients`' bank accounts flagged - `:bank-account/include-in-reports` false, or nil when there are none. - - The GraphQL ledger page has always dropped these (see - auto-ap.datomic.ledger/graphql-results), so the register has to agree -- - otherwise the same entry is visible on one ledger page and missing from the - other. Resolved up front off VAET, which is ~150 entries db-wide and costs - ~2ms; excluding them inside the query with a not-join instead measured ~10x - the cost of the whole scan on a wide date range." - [db clients] - (when-let [off-reports (seq (dc/q '[:find [?ba ...] - - :in $ [?client ...] - - :where - [?client :client/bank-accounts ?ba] - [?ba :bank-account/include-in-reports false]] - db clients))] - (set (dc/q '[:find [?e ...] - - :in $ [?ba ...] - - :where - [?li :journal-entry-line/account ?ba] - [?e :journal-entry/line-items ?li]] - db off-reports)))) - -(defn- apply-include-in-reports [db clients results] - (if-let [hidden (entries-off-reports db clients)] - (remove (comp hidden last) results) - results)) - ;; TODO ;; 1. Sorting in investigate dialog ;; 2. actual date range filtering in investigate dialog @@ -408,7 +375,6 @@ (merge-query {:query {:find ['?sort-default '?e]}})))] (->> (observable-query query) - (apply-include-in-reports db valid-clients) (apply-sort-4 (assoc query-params :default-asc? true)) (apply-only-unbalanced query-params) (apply-pagination query-params)))) diff --git a/test/clj/auto_ap/ssr/ledger_test.clj b/test/clj/auto_ap/ssr/ledger_test.clj index 13974993..7454c00e 100644 --- a/test/clj/auto_ap/ssr/ledger_test.clj +++ b/test/clj/auto_ap/ssr/ledger_test.clj @@ -797,85 +797,3 @@ [:journal-entry/external-id (external-id "ext-good-1")])] (is (= 100.0 (:journal-entry/amount entry))) (is (= 2 (count (:journal-entry/line-items entry)))))))))))) - -(deftest fetch-ids-include-in-reports-test - (testing "Should leave an entry out of the register when a line posts to a bank account off reports" - (let [{:strs [reports-client shown-entry]} - (setup-test-data [(test-bank-account :db/id "shown-bank" - :bank-account/name "Shown Bank" - :bank-account/numeric-code 11000 - :bank-account/include-in-reports true) - (test-bank-account :db/id "hidden-bank" - :bank-account/name "Hidden Bank" - :bank-account/numeric-code 11001 - :bank-account/include-in-reports false) - (test-client :db/id "reports-client" - :client/code "REPORTS-TEST" - :client/locations ["HQ"] - :client/bank-accounts ["shown-bank" "hidden-bank"]) - {:db/id "reports-expense" - :account/name "Expense" - :account/numeric-code 60000 - :account/account-set "default"} - {:db/id "shown-entry" - :journal-entry/client "reports-client" - :journal-entry/date #inst "2024-03-01" - :journal-entry/amount 100.0 - :journal-entry/source "transaction" - :journal-entry/line-items - [{:journal-entry-line/account "shown-bank" - :journal-entry-line/credit 100.0} - {:journal-entry-line/account "reports-expense" - :journal-entry-line/debit 100.0}]} - {:db/id "hidden-entry" - :journal-entry/client "reports-client" - :journal-entry/date #inst "2024-03-02" - :journal-entry/amount 50.0 - :journal-entry/source "transaction" - :journal-entry/line-items - [{:journal-entry-line/account "hidden-bank" - :journal-entry-line/credit 50.0} - {:journal-entry-line/account "reports-expense" - :journal-entry-line/debit 50.0}]}]) - request {:client-id reports-client - :clients [reports-client] - :route-params {} - :query-params {}} - result (common/fetch-ids (dc/db conn) request)] - - (is (= [shown-entry] (vec (:all-ids result)))) - (is (= [shown-entry] (vec (:ids result)))) - (testing "and counts it out of the total, so the page is not silently short" - (is (= 1 (:count result)))))) - - (testing "Should keep every entry when no bank account is off reports" - (let [{:strs [open-client open-entry]} - (setup-test-data [(test-bank-account :db/id "open-bank" - :bank-account/name "Open Bank" - :bank-account/numeric-code 11002 - :bank-account/include-in-reports true) - (test-client :db/id "open-client" - :client/code "REPORTS-OPEN" - :client/locations ["HQ"] - :client/bank-accounts ["open-bank"]) - {:db/id "open-expense" - :account/name "Expense" - :account/numeric-code 60001 - :account/account-set "default"} - {:db/id "open-entry" - :journal-entry/client "open-client" - :journal-entry/date #inst "2024-03-01" - :journal-entry/amount 100.0 - :journal-entry/source "transaction" - :journal-entry/line-items - [{:journal-entry-line/account "open-bank" - :journal-entry-line/credit 100.0} - {:journal-entry-line/account "open-expense" - :journal-entry-line/debit 100.0}]}]) - result (common/fetch-ids (dc/db conn) {:client-id open-client - :clients [open-client] - :route-params {} - :query-params {}})] - - (is (= [open-entry] (vec (:all-ids result)))) - (is (= 1 (:count result)))))) -- 2.49.1 From 0c17000065e5814205edc0f200fc381c86b1a53a Mon Sep 17 00:00:00 2001 From: Bryce Date: Sun, 16 Aug 2026 08:09:12 -0700 Subject: [PATCH 3/3] fix(transactions): drop suppressed transactions from the transactions page The SSR transactions page listed transactions flagged :transaction-approval-status/suppressed, which the GraphQL/CLJS page has always excluded (auto-ap.datomic.transactions, the true branch of its cond->>), as do the ledger queries in auto-ap.ledger. Suppressing a transaction is the user's way of saying 'stop showing me this', so the same transaction stayed visible on the page it was suppressed from. The scan-transactions ion does no status filtering of its own, so nothing downstream was dropping them: for the worst-affected client a five-year range returned 14,736 rows against 3,641 real ones -- the other 11,095 were suppressed. 22,813 suppressed transactions exist db-wide. Filtered inside the query rather than after it so that the row count and the amount total, both derived from fetch-ids, match the rows rendered. Measured 6ms -> 29ms on that worst-case client and range. The three status routes (approved, unapproved, requires-feedback) constrain the status themselves and there is no suppressed route, so no page loses its own rows. Co-Authored-By: Claude Opus 5 --- src/clj/auto_ap/ssr/transaction/common.clj | 8 ++- .../auto_ap/ssr/transaction/common_test.clj | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 test/clj/auto_ap/ssr/transaction/common_test.clj diff --git a/src/clj/auto_ap/ssr/transaction/common.clj b/src/clj/auto_ap/ssr/transaction/common.clj index 20d8df71..39041111 100644 --- a/src/clj/auto_ap/ssr/transaction/common.clj +++ b/src/clj/auto_ap/ssr/transaction/common.clj @@ -132,7 +132,13 @@ valid-clients]} (cond-> {:query {:find [] :in ['$ '[?clients ?start ?end]] - :where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]} + ;; Suppressed transactions are never listed -- the GraphQL page has + ;; always dropped them (auto-ap.datomic.transactions/graphql-results), + ;; as do the ledger queries. The three status routes (approved, + ;; unapproved, requires-feedback) constrain the status themselves, and + ;; there is no suppressed route, so this never hides a page's own rows. + :where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]] + (not [?e :transaction/approval-status :transaction-approval-status/suppressed])]} :args [db [valid-clients (some-> (:start-date query-params) coerce/to-date) diff --git a/test/clj/auto_ap/ssr/transaction/common_test.clj b/test/clj/auto_ap/ssr/transaction/common_test.clj new file mode 100644 index 00000000..be439d07 --- /dev/null +++ b/test/clj/auto_ap/ssr/transaction/common_test.clj @@ -0,0 +1,68 @@ +(ns auto-ap.ssr.transaction.common-test + (:require + [auto-ap.datomic :refer [conn]] + [auto-ap.integration.util :refer [setup-test-data test-bank-account test-client + test-transaction wrap-setup]] + [auto-ap.ssr.transaction.common :as sut] + [clojure.test :refer [deftest is testing use-fixtures]] + [datomic.api :as dc])) + +(use-fixtures :each wrap-setup) + +(defn- fetch + "Runs the transactions page query the way the page does, over a range wide + enough to cover the fixture data." + [client-id & {:keys [route-params query-params]}] + (sut/fetch-ids (dc/db conn) + {:clients [client-id] + :route-params (or route-params {}) + :query-params (merge {:start-date "2021-01-01" + :end-date "2023-12-31"} + query-params)})) + +(deftest fetch-ids-suppressed-test + (let [{:strs [suppressed-client kept-tx suppressed-tx]} + (setup-test-data + [(test-bank-account :db/id "suppressed-bank") + (test-client :db/id "suppressed-client" + :client/bank-accounts ["suppressed-bank"]) + (test-transaction :db/id "kept-tx" + :transaction/client "suppressed-client" + :transaction/bank-account "suppressed-bank" + :transaction/date #inst "2022-06-01" + :transaction/amount 100.0 + :transaction/approval-status :transaction-approval-status/approved) + (test-transaction :db/id "suppressed-tx" + :transaction/client "suppressed-client" + :transaction/bank-account "suppressed-bank" + :transaction/date #inst "2022-06-02" + :transaction/amount 250.0 + :transaction/approval-status :transaction-approval-status/suppressed)])] + + (testing "Should leave a suppressed transaction out of the transactions page" + (let [{:keys [ids all-ids count]} (fetch suppressed-client)] + (is (= [kept-tx] (vec ids))) + (is (not (contains? (set all-ids) suppressed-tx)) + "a suppressed transaction must not reach the amount total either") + (is (= 1 count) + "the row count has to match what the page renders, not the unfiltered scan"))) + + (testing "Should still list a transaction on an explicit status route" + (let [{:keys [ids]} (fetch suppressed-client + :route-params {:status :transaction-approval-status/approved})] + (is (= [kept-tx] (vec ids))))) + + (testing "Should return nothing for a client whose transactions are all suppressed" + (let [{:strs [empty-client]} + (setup-test-data + [(test-bank-account :db/id "empty-bank") + (test-client :db/id "empty-client" + :client/bank-accounts ["empty-bank"]) + (test-transaction :db/id "only-tx" + :transaction/client "empty-client" + :transaction/bank-account "empty-bank" + :transaction/date #inst "2022-06-03" + :transaction/approval-status :transaction-approval-status/suppressed)]) + {:keys [ids count]} (fetch empty-client)] + (is (empty? ids)) + (is (= 0 count)))))) -- 2.49.1