Skip to content

fix(audit): show the price a line sold for, not its list price - #759

Merged
mforce merged 1 commit into
mainfrom
fix/758-audit-sale-price
Sep 11, 2026
Merged

mforce merged 1 commit into
mainfrom
fix/758-audit-sale-price

Conversation

@mforce

@mforce mforce commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

TL;DR

The audit Details cell printed each sales line's list price as though it were the price the line sold for. AuditDetails read unitPriceMinorUnits, used it only as a null guard, then discarded it whenever a list price existed — so every SalesOrder.AddItem row rendered at list $<list>, whatever it actually sold for.

One branch in one component, plus tests. No API, payload, migration or i18n-key change.

The defect, on fixture order SO-1318252C

Line List Sold Sales screen Audit before Audit after
Sim Cracked Eggs $0.18 $0.15 Below list, 16.7% at list $0.18 ❌ at $0.15 (list $0.18)
Sim Dirty Eggs $0.22 $0.30 Above list at list $0.22 ❌ at $0.30 (list $0.22)
Sim Small Eggs $0.30 $0.30 at list at list $0.30 ✅ at list $0.30
Sim Large Eggs $0.45 $0.20 Below list, 55.6% at list $0.45 ❌ at $0.20 (list $0.45)

Three of four were wrong. The worst read as a line selling at $0.45 that sold at $0.20.

Introduced by d26d389 (#749, for #745) — the commit that built the Details column. It shipped this way; it did not drift. It survived review and every test because at list $0.45 is a plausible, correctly formatted number, and nothing on screen contradicted it until #721 began printing Discount reason: Volume directly beneath four rows all claiming to have sold at list.

Rendering contract

Case Renders Changed?
no list price (list === null) at $0.15 no
sold at list (unit === list) at list $0.30 no
sold below or above list at $0.20 (list $0.45) the fix

An off-list line gets no marker word in either direction. #719's open question 4 deliberately treats a markup as not a discount, so the cell states both facts and lets the reader see the direction rather than editorialising. That reasoning is a comment in the code, not just here.

No new translation strings. detailsAtPrice, detailsAtList and detailsListParen already existed in en, es and tl (3/3 each); detailsListParen is the same key the UpdateItem branch already renders in the same position, so per #688 each locale keeps its own existing wording for list price rather than a fresh translation.

Reviewer entry points

  • web/src/routes/AuditPage.tsx — the whole behaviour change is the SalesOrder.AddItem ternary, ~6 lines. Everything else in the diff is the comment above it.
  • web/src/routes/AuditPage.test.tsx — the four new cases.

One deletion to look at deliberately

The previous test, "renders an AddItem payload as the artboard's summary", asserted /\$0\.45/ against a payload with unitPriceMinorUnits: 40, listUnitPriceMinorUnits: 45. It pinned the defect, not the intended behaviour — it passed precisely because the wrong number was on screen. It is replaced rather than weakened, by four tests asserting the cell's literal full text (toBe, on the cell's textContent, which is what pins the order of the two prices — a regex on one number is what let this ship).

Mutation evidence

Every new test was watched fail before being claimed. Three mutations, each reverted; git grep -n -e MUTANT -e 'DEBUG-' -- src tests web returns nothing.

Mutation Tests red Observed
M1 restore the shipped defect (money(list) in the off-list branch) below-list, above-list expected 'Large Eggs ×240 at list $0.45' to be 'Large Eggs ×240 at $0.20 (list $0.45)'
M2 drop the unit === list collapse at-list expected 'Large Eggs ×240 at $0.30 (list $0.30)' to be 'Large Eggs ×240 at list $0.30'
M3 stop list === null short-circuiting no-list-price expected 'Large Eggs ×240 at $0.15 (list $0.00)' to be 'Large Eggs ×240 at $0.15' — M3 reproduces #720's zero-price failure mode exactly

All four tests are individually load-bearing: no mutation reddened a test that was not aimed at it.

Gates

cd web && npm run typecheck   # clean
cd web && npx vitest run      # 122 files, 2816 tests, all passing

Web-only — nothing outside web/ changes, so no .NET build and no integration suite.

Sibling check

grep -rn listUnitPriceMinorUnits web/src finds one other renderer, SalesPage.tsx, which reads the sale and list prices separately and is the screen the issue confirms was already correct. No sibling defect; no coverage added there.

Risk

Low. Read-only rendering of a payload field already present on every row, in one component, behind four literal-text tests. No backfill concern: the change is presentational, so historical rows render correctly the moment this ships. The at-list and no-list-price paths are byte-identical to before and pinned as regressions.

Closes #758

Summary by CodeRabbit

  • Bug Fixes
    • Audit details now consistently display the sold unit price for added sales order items.
    • When a list price is available, matching prices are labeled “at list,” while differing prices show both the sold and list prices without classifying the difference as a discount or markup.
    • Items without a list price continue to display their existing details.

The Details cell rendered every SalesOrder.AddItem row as "at list $X" with
X taken from listUnitPriceMinorUnits, so a line sold below, at, or above
list all read identically and none of them named the price that was
actually charged. On the simulation fixture's SO-1318252C, three of four
lines were wrong; the worst read "at list $0.45" for a line sold at $0.20.

The branch read unitPriceMinorUnits, used it only as a null guard, then
discarded it whenever a list price existed. It shipped this way in #745
and did not drift.

An off-list line now renders both numbers — "at $0.20 (list $0.45)" — and
no marker word in either direction, because #719's open question 4 treats
a markup as not a discount. At-list and no-list-price lines are unchanged.
All three keys already existed in en, es and tl; no new strings.

The previous AddItem test asserted /\$0\.45/ against that payload, which
pinned the defect rather than the behaviour. It is replaced by four tests
asserting the cell's literal text, each watched red under a mutation.

Closes #758
@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d16cebc4-7bef-4c95-8f93-5ba04185d0bf

📥 Commits

Reviewing files that changed from the base of the PR and between c7864be and 8c4ea41.

📒 Files selected for processing (2)
  • web/src/routes/AuditPage.test.tsx
  • web/src/routes/AuditPage.tsx

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ac1c7e6b-ba6b-4487-a13b-e9e6c6ed88e8

📥 Commits

Reviewing files that changed from the base of the PR and between c7864be and 8c4ea41.

📒 Files selected for processing (2)
  • web/src/routes/AuditPage.test.tsx
  • web/src/routes/AuditPage.tsx

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

SalesOrder.AddItem audit details now display the sold unit price. They also display the list price when it differs, retain “at list” for matching prices, and keep no-list-price output unchanged. Tests cover all four price cases.

Changes

Audit AddItem price rendering

Layer / File(s) Summary
Render sold and list prices
web/src/routes/AuditPage.tsx
SalesOrder.AddItem rows now display the sold unit price. Rows with a different list price display both prices. Matching prices retain the “at list” text.
Validate price-case output
web/src/routes/AuditPage.test.tsx
Tests assert exact details text for below-list, above-list, at-list, and null-list-price cases. The shared event and text helpers support these assertions.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 8c4ea

Audit details now display sold and list prices as intended for all supported price cases. No merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: showing the actual sale price instead of the list price. It also uses a conventional commit format.
Description check ✅ Passed The description provides complete context, rendering requirements, verification commands, mutation evidence, scope, risk, and the linked issue. It does not use the template headings exactly, but it in…
Linked Issues check ✅ Passed The implementation satisfies issue #758. It renders the actual sale price for below-list and above-list lines, preserves at-list and no-list-price behavior, and adds tests that detect regressions.
Out of Scope Changes check ✅ Passed The changes are limited to the Audit Details renderer and its tests. They match issue #758 and introduce no unrelated API, payload, migration, translation, or backend changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/758-audit-sale-price

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce

mforce commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

Before / after — captured from a stack rebuilt at each revision

Same viewport (1280×1741, deviceScaleFactor: 1, full page), same signed-in persona, same URL (/audit?action=SalesOrder.AddItem), and the same seeded database — only the app image was rebuilt between the two captures, so the frames differ only where the code does.

The four contract cases are the last five rows of each image.

Row (fixture) payload sold / list Before After
Sim Unpriced Eggs ×24 25 / null at $0.25 at $0.25 (unchanged)
Sim Small Eggs ×24 40 / 30 (above list) at list $0.30 ❌ at $0.40 (list $0.30)
Sim Large Eggs ×24 35 / 45 (below list) at list $0.45 ❌ at $0.35 (list $0.45)
Sim Medium Eggs ×24 38 / 38 (at list) at list $0.38 at list $0.38 (unchanged)

The two ❌ rows are the defect on a real screen: a line that sold at $0.35 was published as $0.45, and a line that sold above list at $0.40 was published as $0.30 — the audit trail understating it by 25% while claiming it sold at list. The 15 genuine at-list rows above them are byte-identical between the frames, which is the point of holding the fixture constant.

Before — every line reads "at list", whatever it sold for

After — the sale price, with the list price beside it only when they differ


This is a rendering defect only — the backend was already correct

Checked directly against the sim database rather than inferred. Across the fixture's 20 SalesOrder.AddItem audit payloads:

unitPriceMinorUnits listUnitPriceMinorUnits rows
45 45 15
38 38 2
40 30 1
35 45 1
25 null 1

The writer stores the sale price and the list price as separate, correct fields, and the fixture already exercises a markup, a discount and a null list price. AuditDetails had every number it needed and discarded the sale price at render time. No API, payload, handler or migration change is warranted, and none is in this PR.

Capture harness: a throwaway Playwright spec against the tools/simulation stack, deliberately not committed — it writes to /tmp, not docs/images/, so it cannot dirty the tracked screenshots that npm run screenshots owns.

Before — every line reads "at list", whatever it sold for

After — the sale price, with the list price beside it only when they differ

@mforce

mforce commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Context for the review: this is a one-branch rendering fix in web/src/routes/AuditPage.tsx. The SalesOrder.AddItem audit Details cell printed the list price as though it were the price the line sold for, discarding unitPriceMinorUnits whenever a list price existed.

Two things worth aiming at specifically:

  1. The replaced test. The previous AddItem test asserted /\$0\.45/ against a payload whose sale price was $0.40 — it pinned the defect. It is replaced by four literal-text assertions, each watched red under a mutation (M1–M3, quoted in the PR body). Please check the new tests actually constrain the behaviour rather than restating the implementation.
  2. The no-marker-word decision. An above-list line renders at $0.40 (list $0.30) with no word naming the direction, per EPIC: Discount visibility and control on sales #719's open question 4 (a markup is not a discount). That is deliberate, not an omission.

Backend was verified correct against the seeded database — the payload carries sale and list prices as separate, correct fields — so no API or handler change is in scope here.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== changed files ==='
git diff --name-status HEAD^ HEAD

echo '=== AuditPage diff ==='
git diff --unified=80 HEAD^ HEAD -- web/src/routes/AuditPage.tsx

echo '=== AuditPage test diff ==='
git diff --unified=100 HEAD^ HEAD -- web/src/routes/AuditPage.test.tsx

echo '=== AuditPage outline ==='
ast-grep outline web/src/routes/AuditPage.tsx --items all

echo '=== AuditPage test outline ==='
ast-grep outline web/src/routes/AuditPage.test.tsx --items all

echo '=== price translation keys and renderer references ==='
rg -n -C 4 'detailsAtPrice|detailsAtList|detailsListParen|unitPriceMinorUnits|listUnitPriceMinorUnits' web/src

Length of output: 82857


@​mforce No findings.

The SalesOrder.AddItem branch now implements the required contract:

  • A null list price renders the sale price only.
  • Equal sale and list prices render at list.
  • Different sale and list prices render the sale price first, then the parenthesized list price.
  • The off-list branch adds no direction marker. This matches the stated markup decision.

The replacement tests constrain the behavior. Each test uses different sale and list values. Each test asserts the full Details cell text. The assertions therefore detect a swapped value, a missing value, an incorrect price order, an incorrect at-list collapse, and the null-list zero-price regression. The above-list test also explicitly rejects direction-marker words.

The existing translation keys support this composition in all three locale files. No API or backend change is indicated.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mforce

mforce commented Sep 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Triggering the deep form deliberately, and this is the single deeper pass this PR will take — whatever it returns, the loop stops here.

Why this one earns it. The previous clean pass was CodeRabbit's incremental profile, and the diff touches the money class: it decides which of two prices an audit record publishes. An audit row is evidentiary and not re-derivable from anywhere else in the system, so a defect here is expensive to detect later — which is exactly the case the earlier round already proves, since the original bug shipped through review and a full green suite and only surfaced when #721 printed a discount reason under four rows all claiming to have sold at list.

Worth aiming at, in rough order of what would hurt most:

  1. The equality collapse. unit === list is an exact integer compare on minor units. Is there any payload shape where the two are equal in value but not by ===, or where collapsing to at list hides something a reader needs?
  2. String composition. The off-list case concatenates two translated strings with a literal space: ${t("detailsAtPrice")} ${t("detailsListParen")}. Check that this holds in es and tl, not just en — per i18n: catalogParity compares key SETS, so a translation that drifts in MEANING passes every gate #688 a locale must never disagree with itself about the same control.
  3. The null path. list === null must never render a zero price (Sales: snapshot the list price on the order line (discount foundation) #720). Mutation M3 reproduced exactly that failure (at $0.15 (list $0.00)), so the guard matters.
  4. Whether the four tests can pass against a wrong implementation other than the three mutations already run.

Previously verified and not in scope: the backend writes sale and list prices as separate correct fields (checked against the seeded DB — 20 AddItem payloads covering a markup, a discount and a null list), and the before/after screenshots were captured from stacks rebuilt at each revision over one unchanged fixture.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== PR diff: renderer and tests ==='
git diff --unified=100 HEAD^ HEAD -- web/src/routes/AuditPage.tsx web/src/routes/AuditPage.test.tsx

echo '=== Renderer outline ==='
ast-grep outline web/src/routes/AuditPage.tsx --items all

echo '=== Test outline ==='
ast-grep outline web/src/routes/AuditPage.test.tsx --items all

echo '=== Price fields: declarations and renderer/test use ==='
rg -n -C 5 --glob '*.{ts,tsx}' \
  'unitPriceMinorUnits|listUnitPriceMinorUnits|SalesOrder\.AddItem' \
  web/src

echo '=== Relevant translation keys in locale resources ==='
rg -n -C 4 \
  'detailsAtPrice|detailsAtList|detailsListParen' \
  web/src

echo '=== Tests that render AuditPage under non-English locales ==='
rg -n -C 5 --glob '*.{ts,tsx}' \
  'AuditPage|AuditDetails|locale|language|i18n' \
  web/src/routes/AuditPage.test.tsx web/src

Length of output: 201114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== changed hunks only ==='
git diff --unified=25 HEAD^ HEAD -- web/src/routes/AuditPage.tsx web/src/routes/AuditPage.test.tsx

echo '=== renderer: relevant lines ==='
rg -n -C 18 --glob 'AuditPage.tsx' \
  'SalesOrder\.AddItem|detailsAtPrice|detailsAtList|detailsListParen|unitPriceMinorUnits|listUnitPriceMinorUnits' \
  web/src/routes

echo '=== tests: relevant lines ==='
rg -n -C 22 --glob 'AuditPage.test.tsx' \
  'addItemEvent|detailsText|SalesOrder\.AddItem|at list|list \$|discount|markup|unitPriceMinorUnits|listUnitPriceMinorUnits' \
  web/src/routes

echo '=== API declarations: price field types ==='
rg -n -C 12 --glob 'cluckwork.ts' \
  'unitPriceMinorUnits|listUnitPriceMinorUnits|Audit' \
  web/src/api

echo '=== translation values ==='
for file in web/src/i18n/en.ts web/src/i18n/es.ts web/src/i18n/tl.ts; do
  echo "--- $file ---"
  rg -n -C 2 'detailsAtPrice|detailsAtList|detailsListParen' "$file"
done

echo '=== installed language resources ==='
rg -n -C 4 'RESOURCES|TRANSLATED_NAMESPACES|audit' \
  web/src/i18n/index.ts web/src/i18n/translations-status.ts 2>/dev/null || true

Length of output: 49093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== AuditPage localization tests ==='
rg -n -C 12 --glob 'AuditPage.test.tsx' \
  'changeLanguage|i18n\.|detailsAtPrice|detailsAtList|detailsListParen|es|tl' \
  web/src/routes

echo '=== Audit payload writer, schema, and contract references ==='
rg -n -C 10 \
  'SalesOrder\.AddItem|listUnitPriceMinorUnits|unitPriceMinorUnits' \
  --glob '*.{cs,json,ts,tsx}' \
  . \
  -g '!web/src/routes/AuditPage.tsx' \
  -g '!web/src/routes/AuditPage.test.tsx' \
  | head -n 1200

echo '=== Price value validation at order-write boundaries ==='
rg -n -C 8 \
  'unitPriceMinorUnits|expectedListUnitPriceMinorUnits|listUnitPriceMinorUnits' \
  --glob '*.cs' \
  . \
  | head -n 1000

Length of output: 201089


@​mforce No findings.

The equality check is correct for the accepted payload shape. The renderer accepts only numeric JSON values. It compares minor-unit numbers. There is no supported shape where equal accepted values are unequal with ===.

The null guard is correct. A null or non-numeric listUnitPriceMinorUnits takes the sale-price-only branch. It cannot call money(list).

The off-list composition is correct in all installed languages:

  • en: at {{amount}} (list {{amount}})
  • es: a {{amount}} (lista {{amount}})
  • tl: sa {{amount}} (listahan {{amount}})

The literal space produces the required separation in each case.

The four tests constrain the price contract. They reject a swapped field, a missing sale price, a missing list price, reversed price order, an incorrect at-list collapse, and null rendered as $0.00. The above-list test also rejects a direction marker.

The tests use English text. A future implementation that hardcodes the exact English strings could pass them. The reviewed implementation uses the existing translation keys, and the installed translations support the composition correctly.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 27 minutes.

@mforce
mforce merged commit e6b37d0 into main Sep 11, 2026
11 checks passed
@mforce
mforce deleted the fix/758-audit-sale-price branch September 11, 2026 02:40
@mforce mforce mentioned this pull request Sep 11, 2026
9 of 11 tasks
mforce pushed a commit that referenced this pull request Sep 12, 2026
🤖 I have created a release *beep* *boop*
---


## [0.1.0](v0.0.4...v0.1.0)
(2026-09-12)


### ⚠ BREAKING CHANGES

* log in by farm code, with per-account email identity
([#532](#532)) (#564)

### Features

* **accounts:** add Account.Slug (farm code), suspend/reactivate,
list-accounts verb
([#531](#531))
([3fe9754](3fe9754))
* **accounts:** provision additional farms
([#581](#581))
([006f298](006f298))
* add Aspire local development AppHost
([#567](#567))
([2c9e6b9](2c9e6b9))
* add configurable worker sale allocation
([#619](#619))
([0955095](0955095))
* add searchable entity pickers
([#642](#642))
([60d2053](60d2053))
* **api:** provision-account takes an optional --timezone at creation
([#603](#603))
([#694](#694))
([a0aee39](a0aee39))
* **audit:** show the sales-line audit payload as a readable Details
column ([#745](#745))
([#749](#749))
([d26d389](d26d389))
* **auth:** add ApplicationUser.StepUpLogoutEpoch column
([#338](#338))
([#554](#554))
([18306ee](18306ee))
* certify over-cap simulation fixture bands
([#633](#633))
([a67b2e1](a67b2e1)),
closes [#627](#627)
* **cli:** rename-account verb to change a farm code
([#732](#732))
([#733](#733))
([4b70559](4b70559))
* **customers:** edit existing customer details
([#625](#625))
([#626](#626))
([062a55c](062a55c))
* **jobs:** single-runner leader gate for the durable job worker
([#271](#271))
([#555](#555))
([4148f9b](4148f9b))
* let owners change user email addresses
([#605](#605))
([842347b](842347b))
* log in by farm code, with per-account email identity
([#532](#532))
([#564](#564))
([68adb62](68adb62))
* **ratelimit:** distributed IP-keyed auth limiters
([#544](#544))
([#558](#558))
([ec14972](ec14972))
* **ratelimit:** distributed per-account report concurrency cap with
local-ceiling fallback
([#545](#545))
([#559](#559))
([1522e4e](1522e4e))
* **sales:** mark discounted lines, total the discount, and show it in
the Orders list ([#723](#723),
[#724](#724))
([#741](#741))
([1a07441](1a07441))
* **sales:** record list, old and new price in the order-line audit
payload ([#722](#722))
([#742](#742))
([97c866f](97c866f))
* **sales:** refuse an over-ceiling confirm from a Sales user
([#727](#727))
([#766](#766))
([8c0792a](8c0792a))
* **sales:** show what each order still owes, and filter the list to
unpaid ([#771](#771))
([ca59d68](ca59d68))
* **sales:** snapshot the list price on the order line and show the
discount ([#734](#734))
([cffed5e](cffed5e))
* **sales:** snapshot the product name and unit in the order-line audit
payload ([#747](#747))
([#748](#748))
([0481c06](0481c06))
* scope Worker reads to assigned flocks
([#388](#388))
([#611](#611))
([5884a9a](5884a9a))
* shared-state ports with Redis + in-process fallback
([#543](#543))
([#552](#552))
([f767fa9](f767fa9))
* suspend-account / reactivate-account operator verbs
([#534](#534))
([#573](#573))
([d0be26c](d0be26c))
* **tenancy:** write-side tenant guard + single-assignment TenantContext
([#546](#546))
([#561](#561))
([f371f1d](f371f1d))
* **web:** dashboard rework — capture-status tiles, 14-day trend, stock
as a stacked bar
([#654](#654))
([396ba23](396ba23))
* **web:** date-range filters on audit and expenses, and the stock lot
filter gets its bounded toolbar
([#666](#666),
[#667](#667),
[#653](#653))
([94b188f](94b188f))
* **web:** elevation hierarchy and sentence-case labels
([#651](#651),
[#652](#652))
([#661](#661))
([28db4c7](28db4c7))
* **web:** Expenses and Audit keep a clear-filters control while rows
are still showing
([#679](#679))
([#697](#697))
([b859982](b859982))
* **web:** expenses filters by a date range like its sibling screens
([#667](#667))
([f13858f](f13858f))
* **web:** key the farm brand palette per farm
([#586](#586))
([#600](#600))
([7183a43](7183a43))
* **web:** let operators forget remembered farms
([#598](#598))
([577d94e](577d94e))
* **web:** one-line provenance, bounded date filters, and empty states
that invite action
([#653](#653),
[#655](#655))
([#668](#668))
([80b53f4](80b53f4))
* **web:** prefill the farm code from ?farm= and remember it
([#535](#535))
([#588](#588))
([b7f5cc6](b7f5cc6))
* **web:** split authenticated routes into lazy chunks
([#620](#620))
([5089271](5089271))
* **web:** the audit log filters by a date range, and says which window
is empty ([#666](#666))
([63027e0](63027e0))
* **web:** typeset numbers as numbers and refresh the Help glossary
([#650](#650),
[#657](#657))
([af4fe11](af4fe11))


### Bug fixes

* **api:** order same-instant audit events by a durable monotonic key
([#700](#700))
([8fcf084](8fcf084))
* **api:** print the farm code from bootstrap-admin
([#589](#589))
([#594](#594))
([34032ac](34032ac))
* **audit:** show the price a line sold for, not its list price
([#759](#759))
([e6b37d0](e6b37d0))
* **audit:** store catalog enums by name and guard the add-item
transaction shape
([#751](#751))
([23609ff](23609ff))
* **auth:** reject invalid account claims
([#622](#622))
([8d6c7fe](8d6c7fe))
* **auth:** require step-up for durable user access
([#360](#360))
([#607](#607))
([f767dce](f767dce))
* **ci:** bound the npm audit calls and give the web job room to finish
([#686](#686))
([153b7a8](153b7a8))
* **ci:** escalate the audit bound to SIGKILL, so it actually bounds
([#686](#686))
([a0c8f4e](a0c8f4e))
* **ci:** fail closed on invalid vulnerability config
([#621](#621))
([1690db8](1690db8))
* **ci:** lockfix covers the two AppHost lock files, derived from the
sln
([efb05e6](efb05e6))
* **ci:** lockfix covers the two AppHost lock files, derived from the
sln
([8986d77](8986d77))
* **ci:** remove invalid XML comment from nuget.lockfix.config
([#541](#541))
([5f1bc0a](5f1bc0a))
* **ci:** the advisory vuln gate no longer blocks on an unusable report
([#686](#686))
([aaf6934](aaf6934))
* **ci:** the advisory vuln gate no longer blocks on an unusable report
([#686](#686))
([64f1f53](64f1f53))
* **i18n:** tl help text names the saleable flag and unit-system setting
what their labels call them
([#688](#688))
([#696](#696))
([bfd24d7](bfd24d7))
* **infra:** AccountId must be a non-nullable Guid or both tenant write
layers refuse ([#673](#673))
([#695](#695))
([2470c4e](2470c4e))
* require step-up for flock scope changes
([#609](#609))
([4151f89](4151f89))
* **sales:** keep a line's discount markers agreeing while its price is
edited ([#752](#752))
([#753](#753))
([c159b4b](c159b4b))
* **sales:** say which kind of missing list price a line has
([#774](#774))
([489180e](489180e))
* scope legacy logout to selected farm
([#624](#624))
([fae8d82](fae8d82))
* **seed:** drain the daily-entry lock sweep so deep simulation fixtures
validate ([#644](#644))
([730fa23](730fa23)),
closes [#638](#638)
* **tenancy:** AccountId is a concurrency token, so the database refuses
a detached cross-tenant write
([#562](#562))
([4d1dfa3](4d1dfa3))
* **tenancy:** AspNetUserRoles carries a tenant column, so a role write
naming another farm's user is refused
([#670](#670))
([fc0552a](fc0552a))
* **tests:** bump the image-pin allow-list counts for the AppHost
LocalPorts tests
([#593](#593))
([58d3056](58d3056))
* **tests:** the OTLP collector survives a lost port race and ignores
traffic that is not an export
([#672](#672),
[#676](#676))
([#677](#677))
([965c737](965c737))
* **web:** a scoped audit view filtered to nothing names both the record
and the range ([#666](#666))
([41bbfe1](41bbfe1))
* **web:** an abandoned dialog attempt's success no longer hijacks the
replacement on Customers, Daily Entry, Flocks, Grades and Products
([#703](#703))
([#705](#705))
([85605db](85605db))
* **web:** an abandoned dialog attempt's success no longer hijacks the
replacement on Inventory, Expenses, History and Stock
([#703](#703))
([#706](#706))
([60a4997](60a4997))
* **web:** an abandoned edit's success no longer hijacks the dialog that
replaced it on Users
([#703](#703))
([#710](#710))
([778faab](778faab))
* **web:** an abandoned order attempt's success no longer hijacks the
dialog that replaced it
([#702](#702))
([522c699](522c699))
* **web:** capture screens open on the flock you last used, and
assigning one no longer guesses
([#646](#646))
([#699](#699))
([7f8f317](7f8f317))
* **web:** constrain dialog session helpers to declared scopes
([#715](#715))
([389e3c8](389e3c8))
* **web:** date validation gets one boundary table instead of one case
per review round
([#666](#666))
([215f830](215f830))
* **web:** keep a paged window and an item panel on the user's newest
intent ([#645](#645))
([d81bccf](d81bccf))
* **web:** keep Sales order panels closed after pending writes
([#711](#711))
([f0f7492](f0f7492))
* **web:** keep Sales panels closed after pending Open reads
([#716](#716))
([620411f](620411f))
* **web:** make login take the cross-tab cookie lock so a racing refresh
cannot restore the wrong session
([#648](#648))
([ff18beb](ff18beb))
* **web:** make the entity picker read as a search field and focus it on
open ([#736](#736))
([66ef667](66ef667)),
closes [#735](#735)
* **web:** page truncated customer and movement tables with usePagedList
([7cfe4d6](7cfe4d6))
* **web:** reconcile Sales line edits with refreshed orders
([#717](#717))
([d7dd2c9](d7dd2c9))
* **web:** the audit date filter accepts low-numbered years, and its
empty state covers every narrowing
([#666](#666))
([af52d25](af52d25))
* **web:** the audit date filter rejects impossible dates, and its
history guard actually guards
([#666](#666))
([8d51846](8d51846))
* **web:** the expense range bounds are not capped at today, which the
month-end default exceeds
([#667](#667))
([7e01864](7e01864))
* **web:** the help text calls the expiry field what the field calls
itself ([#666](#666))
([2fd1f3c](2fd1f3c))
* **web:** the stock lot date range sits in the bounded toolbar
([#653](#653))
([43dec5e](43dec5e))


### Refactoring

* **web:** extract SalesPage's dialog-write wrapper into a shared
useDialogAction hook
([#703](#703))
([#704](#704))
([60ee9d9](60ee9d9))


### Documentation

* add k6 preparation steps to the dev-database fixture runbook
([#643](#643))
([a4f1f09](a4f1f09))
* add runbook for loading the simulation fixture into a dev database
([#639](#639))
([2d143b8](2d143b8))
* **agents:** a PR closes its issue from the body, not the title
([#744](#744))
([39be13c](39be13c))
* **agents:** drop the commit and push gate, and require screenshots on
UI changes ([#757](#757))
([6225172](6225172))
* **agents:** find guards by grepping registry readers; amend issues a
PR overtakes ([#580](#580))
([fe3fde8](fe3fde8))
* **agents:** the Playwright specs have been in CI since 2026-08-08
([#768](#768))
([68ee612](68ee612))
* **aspire:** record the second local database and pin the AppHost
dashboard ports ([#623](#623))
([713b941](713b941))
* compress AGENTS.md to one paragraph per rule, and draw the two orders
that matter ([#551](#551))
([997ae8a](997ae8a))
* item 7 names each screen's actual initial filter value
([#666](#666))
([70a53d8](70a53d8))
* multi-farm tenancy decision record and AGENTS/GLOSSARY sync
([#537](#537))
([#601](#601))
([2c34771](2c34771))
* name the scoped filtered-empty key and state the
[#653](#653) relationship
plainly ([#666](#666))
([0e93dac](0e93dac))
* note that a PackageReference in Directory.Build.props is invisible to
the dependency graph
([4845724](4845724))
* **plans:** commit the
[#722](#722) and
[#745](#745) design records
([#754](#754))
([c942fcd](c942fcd))
* record [#579](#579) as
won't-fix — suspension is immediate for use, not issuance
([#582](#582))
([7a3be40](7a3be40))
* record the [#508](#508)
audit ordering key and the tracked-file guard lesson
([#701](#701))
([08964e9](08964e9))
* **runbooks:** add procedure to rename the default farm's code after
upgrade ([#731](#731))
([2f6e242](2f6e242))
* screenshots of the running SPA in the README
([#550](#550))
([711488a](711488a))
* **sim:** commit the dashboard screenshot, capture the palette matrix,
and record the
[#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652)
conventions ([#660](#660),
[#662](#662),
[#663](#663),
[#664](#664))
([#665](#665))
([930ea30](930ea30))
* specify searchable entity picker
([#641](#641))
([91d4300](91d4300))
* split the README into audience-scoped docs and adopt repo-template
scaffolding ([#548](#548))
([b3f3fcf](b3f3fcf))
* surface Aspire local development workflow
([#568](#568))
([a343baa](a343baa))
* **web:** record the per-screen idempotency-key policies and runWrite's
refresh contract
([#703](#703))
([#707](#707))
([8bee651](8bee651))
* **web:** the date-cap help text covers every stocked item, not only
feed ([#666](#666),
[#667](#667))
([c8433c5](c8433c5))
* **web:** the help text claims only what is true of recording, and says
nothing about filter caps
([#666](#666),
[#667](#667))
([e2f63d1](e2f63d1))
* **web:** the help text describes the date-range filters that shipped
([#666](#666),
[#667](#667))
([c3275b7](c3275b7))
* **web:** the help text stops describing a cap the filters no longer
have ([#666](#666),
[#667](#667))
([49654cd](49654cd))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit Details shows the list price as if it were the sale price on every order line

1 participant