You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Sales: audit payloads snapshot productId but not the product name, so history re-renders under today's name #747
#722's mockup (docs/images/discount-mockups/AuditRow.png) draws the Details column as:
Medium Eggs $5.40 → $4.32 (list $5.40)
Large Eggs ×240 at list $0.45
That renders the product name. With only a Guid stored, the renderer (#745) has to join to Products — and it gets today's name. Rename a product and every historical audit row re-renders under a name the seller never saw.
This is the exact hazard #720 exists to prevent, one field over.#720's whole argument for snapshotting the list price was that computing it live against today's catalogue means "raise a product's price next month and every past sale retroactively becomes a discount". The same reasoning applies to the name: an audit row is a record of what happened, and resolving any part of it live re-interprets history.
The house idiom already does this
Sibling handlers snapshot the name rather than the id:
SetProductActiveHandler.cs:24 — details: new { product.Name }
#722's design diffed its block against CreateProductHandler field by field and matched its transaction shape, while missing that it snapshots a name.
Scope
Add productName to both sales-line payloads, snapshotted at write time. AddOrderItemHandler already holds product. UpdateOrderItemHandler holds only the order and its items — SalesOrderItem carries ProductId but no name, so that path needs either a product read or a name snapshot on the line itself. Decide which before implementing; a product read in that handler is a new dependency.
Keep productId — it is the stable join key; the name is the human-readable snapshot beside it.
The renderer (#745), which consumes this. Filing them separately because this one changes what is written and that one changes what is read; the renderer is not blocked on it — it can show the id until the name exists.
Found by reading #722's mockup artboard after that slice merged. A gap in what #722 shipped, not in the renderer that will consume it.
What
The payloads #722 added (
97c866f) carryproductIdand no name:Why it matters
#722's mockup (
docs/images/discount-mockups/AuditRow.png) draws the Details column as:That renders the product name. With only a Guid stored, the renderer (#745) has to join to
Products— and it gets today's name. Rename a product and every historical audit row re-renders under a name the seller never saw.This is the exact hazard #720 exists to prevent, one field over. #720's whole argument for snapshotting the list price was that computing it live against today's catalogue means "raise a product's price next month and every past sale retroactively becomes a discount". The same reasoning applies to the name: an audit row is a record of what happened, and resolving any part of it live re-interprets history.
The house idiom already does this
Sibling handlers snapshot the name rather than the id:
CreateProductHandler.cs:65—details: new { product.Name, product.ProductType, EggGrade = grade.Name }UpdateProductHandler.cs:78—product.Name, product.DefaultUnit, ...SetProductActiveHandler.cs:24—details: new { product.Name }#722's design diffed its block against
CreateProductHandlerfield by field and matched its transaction shape, while missing that it snapshots a name.Scope
productNameto both sales-line payloads, snapshotted at write time.AddOrderItemHandleralready holdsproduct.UpdateOrderItemHandlerholds only the order and its items —SalesOrderItemcarriesProductIdbut no name, so that path needs either a product read or a name snapshot on the line itself. Decide which before implementing; a product read in that handler is a new dependency.productId— it is the stable join key; the name is the human-readable snapshot beside it.Not in scope
The renderer (#745), which consumes this. Filing them separately because this one changes what is written and that one changes what is read; the renderer is not blocked on it — it can show the id until the name exists.