Rendering the report against a restored production database turned up three things the tests could not have told me, and one they should have. **Payroll posts on a pay run, not on a week.** A journal entry carries the date it was posted, so a client paying every fortnight puts a fortnight of labour on a single day. Against weekly columns the trend read `—, 56.7%, —, 52.5%, -19.5%`, which is noise. Against fortnightly columns the same client reads 18% to 27%. Hence the period control: weekly, bi-weekly or four weeks, set to whatever the client's pay cycle actually is. **Zero is a claim, and it was the wrong one.** A period with no payroll posted used to print 0.0% labour. No restaurant runs a week on no labour; the pay run simply has not landed yet. That now reads as an em dash, the trend line joins across the gap rather than diving to the floor, and the footnote says what to do about it. **A detail line printed larger than its own subtotal.** Averages dropped empty periods from the denominator, so a band posting every second period averaged over four periods while the total it rolled into averaged over eight — $23,923 of "Payroll - General" inside $12,035 of "Payroll". Money now averages across every period and ratios still average only the periods they are known for, which is the distinction that was missing. The one the tests should have caught: the shared cell renderer calls `dollars-0?` on a value before nil-punning it, so any nil cell was an NPE at render time rather than a blank. Guarded, along with its `:percent` branch, which also printed `%27.2` — sign on the wrong side. Nothing else in the codebase reaches that branch. Also here, in service of it being read rather than merely correct: a summary strip with the latest labour percentage, what it usually runs at, the change between them and a trend sparkline; a % of sales column beside the average, as the workbook has; detail lines indented under their subtotals; and an opt-in `height` on the shared table so a short report stops reserving 70vh it does not use. 11 tests, 41 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
219 lines
12 KiB
Clojure
219 lines
12 KiB
Clojure
(ns auto-ap.ssr.ledger.prime-cost-test
|
|
(:require
|
|
[auto-ap.datomic :refer [conn]]
|
|
[auto-ap.integration.util :refer [setup-test-data wrap-setup]]
|
|
[auto-ap.ledger.reports :as l-reports]
|
|
[auto-ap.ssr.ledger.prime-cost :as sut]
|
|
[clj-time.coerce :as coerce]
|
|
[clj-time.core :as time]
|
|
[clojure.test :refer [deftest is testing use-fixtures]]
|
|
[datomic.api :as dc]))
|
|
|
|
(use-fixtures :each wrap-setup)
|
|
|
|
(def week-ending (time/date-time 2026 8 10))
|
|
(def in-week #inst "2026-08-05T12:00:00.000-00:00")
|
|
(def week-before #inst "2026-07-29T12:00:00.000-00:00")
|
|
|
|
(defn- account [tempid code]
|
|
{:db/id tempid :account/name (str "Account " code) :account/numeric-code code})
|
|
|
|
(defn- jel [client account-tempid date debit]
|
|
{:journal-entry-line/client client
|
|
:journal-entry-line/account account-tempid
|
|
:journal-entry-line/date date
|
|
:journal-entry-line/debit debit})
|
|
|
|
(deftest period-windows-tile-without-gaps
|
|
(testing "each period ends where the next begins, so no day is counted twice or missed"
|
|
(let [ws (sut/period-windows week-ending)]
|
|
(is (= sut/periods-shown (count ws)))
|
|
(is (= (coerce/to-date week-ending) (coerce/to-date (:ends (first ws))))
|
|
"the newest window ends at the requested date")
|
|
(is (every? (fn [[newer older]] (= (:starts newer) (:ends older)))
|
|
(partition 2 1 ws))
|
|
"windows abut")
|
|
(is (apply > (map (comp coerce/to-long :starts) ws))
|
|
"most recent first")))
|
|
(testing "a longer period widens each column without leaving a gap between them"
|
|
(let [ws (sut/period-windows week-ending 2)]
|
|
(is (= sut/periods-shown (count ws)))
|
|
(is (= 14 (time/in-days (time/interval (:starts (first ws)) (:ends (first ws))))))
|
|
(is (every? (fn [[newer older]] (= (:starts newer) (:ends older)))
|
|
(partition 2 1 ws))))))
|
|
|
|
(deftest period-length-select-maps-to-weeks
|
|
(testing "the query param the form submits resolves to a column width"
|
|
(is (= 1 (sut/weeks-per-period "1")))
|
|
(is (= 2 (sut/weeks-per-period "2")))
|
|
(is (= 1 (sut/weeks-per-period nil)) "an absent or unknown period falls back to weekly")))
|
|
|
|
(deftest payroll-bands-come-from-the-shared-ledger-groupings
|
|
(testing "the bands are the ledger's, not a second list that can drift from the P&L"
|
|
(is (= (:payroll l-reports/groupings) sut/payroll-bands)))
|
|
(testing "every payroll account code lands in exactly one band"
|
|
(is (some? (sut/band-for 60500)) "general payroll is not orphaned")
|
|
(is (some? (sut/band-for 61100)))
|
|
(is (some? (sut/band-for 62200)))
|
|
(is (some? (sut/band-for 63200)))
|
|
(is (some? (sut/band-for 65000)))
|
|
(is (some? (sut/band-for 69800)))
|
|
(is (nil? (sut/band-for 50000)) "food cost is not payroll")
|
|
(is (nil? (sut/band-for 70000)) "controllable costs are not payroll")))
|
|
|
|
(deftest payroll-is-debits-less-credits-within-the-week
|
|
(testing "a credit correction reduces the week rather than counting as more labour, and lines
|
|
outside the window are excluded"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "foh" 63200)
|
|
(account "boh" 62200)
|
|
(jel test-client-id "foh" in-week 1000.0)
|
|
(jel test-client-id "boh" in-week 400.0)
|
|
{:journal-entry-line/client test-client-id
|
|
:journal-entry-line/account "foh"
|
|
:journal-entry-line/date in-week
|
|
:journal-entry-line/credit 100.0}
|
|
(jel test-client-id "foh" week-before 999.0)])
|
|
(let [{:keys [total by-band]} (sut/payroll-in-window
|
|
(dc/db conn) [test-client-id]
|
|
(time/minus week-ending (time/weeks 1)) week-ending)]
|
|
(is (= 1300.0 total) "1000 + 400 - 100, and nothing from the prior week")
|
|
(is (= 900.0 (get by-band "63000-66000 Payroll - FOH")))
|
|
(is (= 400.0 (get by-band "62000 Payroll - BOH")))))))
|
|
|
|
(deftest payroll-excludes-non-payroll-accounts
|
|
(testing "only the 60000 block counts, so food cost never lands in labour"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "foh" 63200)
|
|
(account "food" 50000)
|
|
(jel test-client-id "foh" in-week 500.0)
|
|
(jel test-client-id "food" in-week 5000.0)])
|
|
(is (= 500.0 (:total (sut/payroll-in-window
|
|
(dc/db conn) [test-client-id]
|
|
(time/minus week-ending (time/weeks 1)) week-ending)))))))
|
|
|
|
(deftest sales-count-revenue-only-and-net-of-discounts
|
|
(testing "tender, tax and tip are not sales, and a debit against revenue reduces it"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "food-rev" 40111)
|
|
(account "discount" 41000)
|
|
(account "tax" 25700)
|
|
(account "card" 75460)
|
|
{:sales-summary/client test-client-id
|
|
:sales-summary/date in-week
|
|
:sales-summary/client+date [test-client-id in-week]
|
|
:sales-summary/items
|
|
[{:sales-summary-item/category "Gyros"
|
|
:ledger-mapped/amount 1000.0
|
|
:ledger-mapped/ledger-side :ledger-side/credit
|
|
:ledger-mapped/account "food-rev"}
|
|
{:sales-summary-item/category "Discounts"
|
|
:ledger-mapped/amount 100.0
|
|
:ledger-mapped/ledger-side :ledger-side/debit
|
|
:ledger-mapped/account "discount"}
|
|
{:sales-summary-item/category "Tax"
|
|
:ledger-mapped/amount 90.0
|
|
:ledger-mapped/ledger-side :ledger-side/credit
|
|
:ledger-mapped/account "tax"}
|
|
{:sales-summary-item/category "Card Payments"
|
|
:ledger-mapped/amount 990.0
|
|
:ledger-mapped/ledger-side :ledger-side/debit
|
|
:ledger-mapped/account "card"}]}])
|
|
(let [{:keys [total by-category]} (sut/sales-in-window
|
|
(dc/db conn) [test-client-id]
|
|
(time/minus week-ending (time/weeks 1)) week-ending)]
|
|
(is (= 900.0 total) "1000 of revenue less a 100 discount; tax and tender excluded")
|
|
(is (= 1000.0 (get by-category "Gyros")))
|
|
(is (= -100.0 (get by-category "Discounts")))
|
|
(is (nil? (get by-category "Tax")) "tax is money held for someone else, not a sale")
|
|
(is (nil? (get by-category "Card Payments")) "tender is the other side of the sale")))))
|
|
|
|
(deftest the-headline-ratio-is-payroll-over-sales
|
|
(testing "the number the report exists for"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "food-rev" 40111)
|
|
(account "foh" 63200)
|
|
(jel test-client-id "foh" in-week 250.0)
|
|
{:sales-summary/client test-client-id
|
|
:sales-summary/date in-week
|
|
:sales-summary/client+date [test-client-id in-week]
|
|
:sales-summary/items
|
|
[{:sales-summary-item/category "Gyros"
|
|
:ledger-mapped/amount 1000.0
|
|
:ledger-mapped/ledger-side :ledger-side/credit
|
|
:ledger-mapped/account "food-rev"}]}])
|
|
(let [columns (sut/get-report-data (dc/db conn) [test-client-id] week-ending)
|
|
current (first columns)]
|
|
(is (= sut/periods-shown (count columns)))
|
|
(is (= 0.25 (:labor-ratio current)) "250 of labour against 1000 of sales")
|
|
(is (nil? (:labor-ratio (second columns)))
|
|
"a week with no sales has no ratio rather than a divide-by-zero")))))
|
|
|
|
(deftest a-period-with-no-payroll-posted-has-no-ratio
|
|
(testing "payroll posted on a pay-period date leaves neighbouring weeks empty, and an empty week
|
|
must read as unknown rather than as 0% labour"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "food-rev" 40111)
|
|
{:sales-summary/client test-client-id
|
|
:sales-summary/date in-week
|
|
:sales-summary/client+date [test-client-id in-week]
|
|
:sales-summary/items
|
|
[{:sales-summary-item/category "Gyros"
|
|
:ledger-mapped/amount 1000.0
|
|
:ledger-mapped/ledger-side :ledger-side/credit
|
|
:ledger-mapped/account "food-rev"}]}])
|
|
(let [current (first (sut/get-report-data (dc/db conn) [test-client-id] week-ending))]
|
|
(is (= 1000.0 (get-in current [:sales :total])) "the sales are real")
|
|
(is (false? (get-in current [:payroll :posted?])))
|
|
(is (nil? (:labor-ratio current))
|
|
"no payroll line exists, so the ratio is unknown, not zero")))))
|
|
|
|
(deftest report-table-renders-a-column-per-week-plus-label-and-average
|
|
(testing "the table lines up with its header"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "food-rev" 40111)
|
|
{:sales-summary/client test-client-id
|
|
:sales-summary/date in-week
|
|
:sales-summary/client+date [test-client-id in-week]
|
|
:sales-summary/items
|
|
[{:sales-summary-item/category "Gyros"
|
|
:ledger-mapped/amount 1000.0
|
|
:ledger-mapped/ledger-side :ledger-side/credit
|
|
:ledger-mapped/account "food-rev"}]}])
|
|
(let [{:keys [header rows]} (sut/report-table
|
|
(sut/get-report-data (dc/db conn) [test-client-id] week-ending))
|
|
width (+ 3 sut/periods-shown)]
|
|
(is (= width (count (first header))))
|
|
(is (every? #(= width (count %)) (remove empty? rows))
|
|
"every populated row is the same width as the header, or the table skews")))))
|
|
|
|
(deftest the-trend-line-skips-periods-with-no-payroll
|
|
(testing "a period with no payroll posted contributes no point, so the line joins across the gap
|
|
rather than diving to the floor and drawing a week that never happened"
|
|
(let [columns [{:labor-ratio 0.30} {:labor-ratio nil} {:labor-ratio 0.20}]]
|
|
(is (= 2 (count (sut/spark-points columns))))))
|
|
(testing "a single readable period is not a trend"
|
|
(is (nil? (sut/spark-points [{:labor-ratio 0.30} {:labor-ratio nil}])))))
|
|
|
|
(deftest a-detail-line-never-averages-larger-than-its-subtotal
|
|
(testing "money averages over every period, so a band posting every second period is not divided
|
|
by a smaller denominator than the total it rolls up into"
|
|
(is (= 500.0 (sut/per-period [1000.0 nil 1000.0 nil]))
|
|
"the empty periods still count")
|
|
(is (= 1000.0 (sut/average [1000.0 nil 1000.0 nil]))
|
|
"a ratio, by contrast, only averages the periods it is known for"))
|
|
(testing "on real-shaped data the payroll bands sum to no more than the payroll total"
|
|
(let [{:strs [test-client-id]} (setup-test-data [])]
|
|
@(dc/transact conn [(account "foh" 63200)
|
|
(jel test-client-id "foh" in-week 1400.0)])
|
|
(let [{:keys [rows]} (sut/report-table
|
|
(sut/get-report-data (dc/db conn) [test-client-id] week-ending))
|
|
average-of (fn [label]
|
|
(->> rows
|
|
(filter #(= label (-> (str (:value (first %)))
|
|
(clojure.string/replace "\u00a0" "")
|
|
(clojure.string/trim))))
|
|
first second :value))]
|
|
(is (= (average-of "Payroll") (average-of "63000-66000 Payroll - FOH"))
|
|
"one band carrying all the payroll averages exactly what the total does")))))
|