Skip to content

QE-06: the server prices a configured item from the stored options - #266

Open
futurebuildai wants to merge 6 commits into
refactor/v1from
refactor/qe-configured-items
Open

futurebuildai wants to merge 6 commits into
refactor/v1from
refactor/qe-configured-items

Conversation

@futurebuildai

@futurebuildai futurebuildai commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A configured item is a door or a millwork piece the dealer configures rather
than picks off the shelf. Until now the configurator only made a name for it
and the client priced it: the desk computed a price in the browser and sent
it with the line, so the price on the quote was the dealer's number, not the
business's.

This PR makes the server own the price of a configured item.

The catalog carries the money. millwork_options gains
base_price_cents, base_cost_cents and cost_adjustment_cents. The base is
what an option contributes when it names the item (the door type row carries
the door's base price and base cost, the species row the board's); the
adjustments are what it adds on top. The server prices a configuration as the
sum over its selected options of base plus adjustment, in integer cents, and
answers the wire in ten-thousandths. The client sends the choices and reads
the money; a price the client sends is read and ignored.

The configured item is a product row of its own, marked non-stock. The SKU
is NS- plus the product type's short name plus the hash of the canonical
configuration, so the same configuration always gives the same SKU and the
same row, and two different configurations never collide. A non-stock row is
left out of the stock counts, the replenishment and the counter's stocked
search: a configured item is never counted, never reordered and never offered
at the counter.

The configuration travels with the item. It is stored on the quote, order
and invoice lines, and the documents show the item as a short description
rather than the raw configuration.

A configured item is a special order. The dealer holds no stock of a door
made to a configuration, so the order line it becomes is a special order line
and the convert raises the vendor purchase order for it. That purchase order
line carries the short description: the vendor reads the SKU and the
description, not the configuration.

The desk gains what the dealer asked for. A "Configure a door" entry in
the Quote Builder opens the configurator beside the quote. Both
configurators carry a live "Add to Quote" and "Add to Order": they open a
picker that says where the item goes (the draft quotes or the editable
orders, and a way to start a new draft quote), the dealer picks, and the item
lands on that document's line at the server's price with its configuration.
The configurator hosted by the Quote Builder keeps its old way: the item goes
straight onto the quote the builder is holding. A Desk option catalog screen
is where the dealer sets the money the server prices from.

Routes

Route What it does
POST /api/v1/millwork/estimate Prices a configuration from the stored options and answers the SKU, the description, the product row and the breakdown.
PUT /api/v1/millwork/options/{id} Updates an option's money and names, guarded by the revision (If-Match).
POST /api/v1/configurator/build-sku Now answers the server's price, cost, configuration and product row beside the SKU.

Component(s) touched

  • core/migrations/ (162 and its down sibling), core/internal/millwork/,
    core/internal/configurator/, core/internal/product/, core/internal/quote/,
    core/internal/order/, core/internal/invoice/, core/internal/salesdoc/,
    core/internal/pos/, core/internal/inventory/, core/internal/dashboard/,
    core/internal/purchase_order/, core/internal/app/serve/,
    core/internal/app/seed/, core/api/fragments/
  • web/apps/desk/ (the configurators, the Quote Builder, the option catalog
    screen, the services and the types), web/packages/api-client/ (the
    generated types), web/e2e/

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Database migration
  • Other (describe):

Pre-flight checklist

Run the gates locally before pushing — CI runs them too, but failing locally is
faster.

Backend (cd core):

  • go build ./...
  • go vet ./...
  • go test ./...

Frontend (cd web/apps/desk):

  • npx tsc --noEmit
  • npm run lint
  • npm run build

General:

  • This PR targets staging, not main. (It targets refactor/v1, the
    branch this lane is cut from, like every other slice PR.)
  • Commits are focused (one logical change each) with clear messages.
  • New DB columns follow the conventions (UUID PKs, DECIMAL(19,4) for
    quantities, money-as-cents in app code, every quantity paired with a UOM).
  • New endpoints are under the correct prefix and wired into
    RegisterRoutes in core/internal/app/serve/serve.go.
  • I have read CONTRIBUTING.md and agree to license my contribution under
    the OpenLBM Standard license governing the file(s) I touched (via the CLA).
  • No secrets, credentials, or live hostnames are committed.
  • AUTH_MODE=dev is not introduced into any reachable/production config.

Migration notes

Migration 162 adds:

  • products.is_non_stock (boolean, defaults to FALSE, with its own index):
    the mark that keeps a configured row out of the stock reads.
  • millwork_options.base_price_cents, base_cost_cents,
    cost_adjustment_cents (bigint, default 0): the money the server prices
    from.
  • quote_lines.configuration_json, order_lines.configuration_json and
    invoice_lines.configuration_json (jsonb): the configuration on the line.

Every existing row keeps its present meaning: an option with no money
contributes nothing, and a row with no mark is a stocked product. The down
sibling is in core/migrations/down/162_configured_items_down.sql.

The purchase order line carries no configuration column on purpose: a purchase
order is written to the vendor, and its line already shows the configured item
as a short description.

Screenshots / notes for reviewers

The money never leaves the server. The desk's client-side price calculation is
gone: the configurator asks the server for the price and shows what it
answers. A dealer who changes an option's base price sees the configured
item's price change with it.

The tests:

  • core/internal/configurator/qe06_test.go and core/internal/quote/qe06_test.go
    carry the TestQE06_* names: TestQE06_BuildSKUServerPrice (the estimate
    and the build-sku answer the stored options' sum to the cent),
    TestQE06_SameConfigSameProduct, TestQE06_ConcurrentBuildSKUOneRow (pool
    size 4, three contenders), TestQE06_ClientPriceIgnored,
    TestQE06_ConfiguredLineConvertsToSpecialOrderPO and
    TestQE06_NonStockNotInStockCounts.
  • web/apps/desk/src/lib/configured-item.test.ts,
    web/apps/desk/src/components/configured/ConfiguredTargetPicker.test.ts,
    web/apps/desk/src/pages/millwork/DoorConfigurator.test.ts and
    web/apps/desk/src/pages/QuoteBuilder.configured.test.ts cover the
    handoff, the picker, the server price and the quote line.
  • web/e2e/configured-items.spec.ts drives the real stack: the server's
    price, the same configuration's same row, the configured line priced by the
    server, the desk's Configure a door flow beside the quote, and the
    configurator's own path: Add to Quote, the picker, a new draft quote, the
    line at the server's price, the convert, and the vendor purchase order the
    configured line raised.

Built by MiniMax M3, the coder lane of the QE section lead.

… (QE-06)

A configured item is a door or a millwork piece the dealer configures
rather than picks off the shelf. Until now the configurator only made a
name for it and the client priced it: the desk computed a price in the
browser and sent it with the line, so the price on the quote was the
dealerer's number, not the business's.

The option catalog now carries the money. base_price_cents and
base_cost_cents are what an option contributes when it names the item
(the door type row carries the door's base, the species row the board's),
price_adjustment and cost_adjustment_cents are what it adds on top. The
server prices a configuration as the sum over its selected options of
those two, in integer cents, and answers the wire in ten-thousandths.
The client sends the choices and reads the money; a price the client
sends is read and ignored.

The configured item is a product row of its own, marked non-stock: the
SKU is NS- plus the product type's short name plus the hash of the
canonical configuration, so the same configuration always gives the same
SKU and the same row, and two different ones never collide. A non-stock
row is left out of the stock reads, the replenishment and the counter's
stocked search: a configured item is never counted and never reordered.

The configuration travels with the item. It is stored on the quote, order
and invoice lines, and the documents show the item as a short description
rather than the raw configuration. The purchase order line carries the
description only: it is written to the vendor, and the vendor reads the
SKU and the description.

The desk gains what the dealer asked for: a Configure a door entry in the
Quote Builder that opens the configurator beside the quote, a live Add to
Quote on the configured item, an Add to Order on the priced door, and a
Desk option catalog screen where the dealer sets the money the server
prices from.

Red first: the build-sku and estimate routes answer the stored options'
sum to the cent, the same configuration gives one SKU and one product
row, a quote line that carries a configuration is priced by the server
whatever the client sent, and the configured row stays out of the stock
counts.

Signed-off-by: colton <colton@futurebuild.ai>
The branch bound key's route allowlist is deny by default, and the census
test holds it against every machine key reachable route in both
directions. The configured item's estimate route and the option update
route arrived without a decision, so the gate refused them and both
gates failed by name.

The estimate is a stated limit, like its twin the configurator's
build-sku: it prices the dealer wide option catalogue and carries no
branch fact. The option update is refused, like the option create: the
catalogue is dealer wide and a write to it is not branch scoped.

Signed-off-by: colton <colton@futurebuild.ai>
…QE-06)

A configured item is made to the configuration, so the dealer holds no
stock of it and it is bought from the vendor. The order line it became
now says so: the convert raises the purchase order for it, and the
purchase order line names the item the way the vendor reads it, the SKU
and the short description.

An ordinary stocked line is untouched: it is served from the stock the
dealer allocated and is never a special order line.

The tests carry the names the brief names: TestQE06_BuildSKUServerPrice,
TestQE06_SameConfigSameProduct, TestQE06_ConcurrentBuildSKUOneRow (pool
size 4, three contenders), TestQE06_ClientPriceIgnored,
TestQE06_ConfiguredLineConvertsToSpecialOrderPO and
TestQE06_NonStockNotInStockCounts.

Signed-off-by: colton <colton@futurebuild.ai>
The configurators had the buttons live but no way to say which document
the item belongs on: Add to Quote and Add to Order offered the item and
left the dealer to find it.

Both configurators now open a picker: the draft quotes or the editable
orders, and a way to start a new draft quote. The dealer picks, the item
is handed over and the dealer walks to that document, where the item
lands on the line at the server's price with its configuration. The order
page takes the item too: a draft order gets the line and is saved with
it.

The configurator hosted by the quote builder keeps its old way: the item
goes straight onto the quote the builder is holding, and no picker opens.

Red first: the picker lists the drafts and says which one was picked, the
door configurator shows the server's price and keeps both buttons live,
and the quote builder's configurator adds the item to the quote it holds.

Signed-off-by: colton <colton@futurebuild.ai>
The walk now covers the dealer's own path: the door configurator, Add to
Quote, the picker, a new draft quote, the line at the server's price with
its configuration, the convert, and the vendor purchase order the
configured line raised.

Signed-off-by: colton <colton@futurebuild.ai>
…QE-06)

The catalog's category was folded in SQL and the configuration's in Go,
and the two folds disagreed on an upper case letter: SQL replaced the
letter with a separator before it lower cased the name, while Go lower
cased first and kept the letter. A dealer whose catalog spelled a
category with capitals priced every configured item at zero, with no
refusal to show for it.

Both sides fold the same way now: lower case first, then every run that
is neither a letter nor a digit becomes one underscore.

Red first: the estimate wire test prices a configuration whose catalog
spells its categories with capitals, and it priced at zero.

The wire test also covers the two new routes: the estimate answers the
stored options' sum to the cent, and the option update sets the money
the server prices from on the revision it names.

Signed-off-by: colton <colton@futurebuild.ai>
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.

1 participant