Skip to content

Sales: snapshot the list price on the order line (discount foundation) #720

Description

@mforce

Part of #719.

What

Add ListUnitPriceMinorUnits to SalesOrderItem, captured from the product's
DefaultPriceMinorUnits at line creation. Discount is then derived as
ListUnitPrice − UnitPrice and is stable forever.

Why this blocks everything else in #719

The line already snapshots ProductTypeSnapshot, EggGradeId, BaseUnitFactor
and UnitPrice (spec §10.5) but not the list price it was sold against, so
today there is no stored fact saying a sale was discounted.

Without the snapshot, "was this discounted?" would be computed live against
today's catalog price: raise a product's price next month and every past sale
retroactively becomes a discount; drop it and real discounts vanish. The order
screen, the history badge and the report would each drift, and they would drift
differently. This is the same rule already written into §10.5 for
BaseUnitFactor — redefining a carton must not reinterpret recorded orders.

Scope

  • ListUnitPriceMinorUnits (nullable long) on SalesOrderItem, set in
    SalesOrderItem.Create from the product read that AddOrderItemHandler
    already performs. Nullable on purpose — a product with no default price
    has no list price, and that is a different fact from "sold at list".
  • Never re-resolved on SalesOrderItem.Update: editing quantity or price does
    not change what the list price was when the line was written.
  • Exposed on the order item API response so the read surfaces can use it.
  • One new migration (feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407). Existing rows stay NULL.
  • specs/product/specs.md §10.5 updated to name the fourth snapshot alongside
    the existing three.
  • Regenerated docs/schema/ (chore(schema): generate PostgreSQL schema documentation #417) in the same PR.

Decide before writing code

  1. Null list price. Does an egg product now require a
    DefaultPriceMinorUnits, or do we keep it optional and render those lines as
    no list price? Either is fine; silently showing them as 0% off is not — that
    is the version that lets a discount hide.
  2. Pre-migration orders have no snapshot and must read as unknown, not as
    zero discount, in every consumer. Worth asserting in a test now so a later
    slice cannot regress it into a ?? 0.

Acceptance

  • A line added against a priced product stores that product's price at that
    moment.
  • Changing the product's default price afterwards does not change any stored
    line's ListUnitPriceMinorUnits. This is the test that matters.
  • A line added against a product with no default price stores NULL.
  • UpdateItem leaves ListUnitPriceMinorUnits untouched.
  • Rows written before the migration read as NULL.

Repo rules

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerarea:domainDomain layerepic-719Discount visibility and control on sales (epic #719)priority:criticalBlocks the vertical slicesize:MA day or two; migration or a multi-state UIsliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions