The bug
Pricing & inventory's status column reports a title-less product as a green "active", and every
checkout of that product 409s with PRODUCT_NOT_PRICED. The console says sellable; the store says
no. Nothing in the admin UI hints at the cause.
Why
statusLabel in packages/plugin/src/admin/products-page.ts decides sellability with:
const sellable = p.sku !== null && p.priceCents !== null && p.currency !== null;
return sellable ? "active" : "active (not priced)";
title is absent from that predicate. It mirrors the service's commerce-complete filter in
packages/store-postgres/src/kysely-product-commerce-store.ts (listCommerceByIds: sku is not null, price_cents is not null, price_currency is not null) — which has the same omission. But
createOrderFromCart (packages/domain/src/orders/create-order-from-cart.ts) does reject a
null-title line with PRODUCT_NOT_PRICED. So the two predicates disagree with the one that
actually gates a sale.
How a product ends up title-less
The content sync reads the title from content.data.title — TITLE_FIELD is the literal
"title" (packages/plugin/src/sync/hooks.ts). A products collection whose title field is named
anything else (name, heading, productTitle) yields nothing on every save: the upsert body
omits the key, product_commerce.title stays NULL, and the product is permanently unbuyable.
The only signal today is a service log line:
[urumi] content:afterSave: product_id=… synced WITHOUT a title (…). The product cannot be ordered until it has one — checkout rejects an untitled product with PRODUCT_NOT_PRICED.
Pre-existing, but PR 1c removed the workaround
Before the "one home per field" PR 1c a merchant could type a title into the console's Title
input and it stuck. 1c made the CMS content sync the sole writer of
product_commerce.title (ADR-0013, adr/0013-product-title-is-cms-owned.md), so
there is now no merchant-side repair at all — only a CMS schema change. That is recorded as an
accepted cost in ADR-0013's Consequences, which references this issue.
This issue does not re-open that decision. It fixes the reporting, so the state is at least
visible instead of being a green badge and a failed checkout.
Fix shape
Add title !== null to both predicates, so a title-less product renders active (not priced):
statusLabel in packages/plugin/src/admin/products-page.ts — widen the parameter to carry
title (it is already on ProductSummaryWire) and add the clause. Its doc comment states the
label mirrors the service filter exactly, so both must move together or the comment becomes
false.
- The commerce-complete filter in
listCommerceByIds
(packages/store-postgres/src/kysely-product-commerce-store.ts) and its in-memory twin, plus a
contract case — this is the one that keeps an unbuyable product out of the catalog wire.
Consider also surfacing the cause rather than only the symptom: a context line on the detail when
title === null, naming the CMS field the sync reads (data.title), since a merchant cannot
otherwise get from "active (not priced)" to "your collection's title field has the wrong name".
Related
These three are the same missing predicate seen from three surfaces; whoever picks one should
probably take all three.
The bug
Pricing & inventory's status column reports a title-less product as a green "active", and every
checkout of that product 409s with
PRODUCT_NOT_PRICED. The console says sellable; the store saysno. Nothing in the admin UI hints at the cause.
Why
statusLabelinpackages/plugin/src/admin/products-page.tsdecides sellability with:titleis absent from that predicate. It mirrors the service's commerce-complete filter inpackages/store-postgres/src/kysely-product-commerce-store.ts(listCommerceByIds:sku is not null,price_cents is not null,price_currency is not null) — which has the same omission. ButcreateOrderFromCart(packages/domain/src/orders/create-order-from-cart.ts) does reject anull-title line with
PRODUCT_NOT_PRICED. So the two predicates disagree with the one thatactually gates a sale.
How a product ends up title-less
The content sync reads the title from
content.data.title—TITLE_FIELDis the literal"title"(packages/plugin/src/sync/hooks.ts). A products collection whose title field is namedanything else (
name,heading,productTitle) yields nothing on every save: the upsert bodyomits the key,
product_commerce.titlestaysNULL, and the product is permanently unbuyable.The only signal today is a service log line:
Pre-existing, but PR 1c removed the workaround
Before the "one home per field" PR 1c a merchant could type a title into the console's Title
input and it stuck. 1c made the CMS content sync the sole writer of
product_commerce.title(ADR-0013,adr/0013-product-title-is-cms-owned.md), sothere is now no merchant-side repair at all — only a CMS schema change. That is recorded as an
accepted cost in ADR-0013's Consequences, which references this issue.
This issue does not re-open that decision. It fixes the reporting, so the state is at least
visible instead of being a green badge and a failed checkout.
Fix shape
Add
title !== nullto both predicates, so a title-less product rendersactive (not priced):statusLabelinpackages/plugin/src/admin/products-page.ts— widen the parameter to carrytitle(it is already onProductSummaryWire) and add the clause. Its doc comment states thelabel mirrors the service filter exactly, so both must move together or the comment becomes
false.
listCommerceByIds(
packages/store-postgres/src/kysely-product-commerce-store.ts) and its in-memory twin, plus acontract case — this is the one that keeps an unbuyable product out of the catalog wire.
Consider also surfacing the cause rather than only the symptom: a
contextline on the detail whentitle === null, naming the CMS field the sync reads (data.title), since a merchant cannototherwise get from "active (not priced)" to "your collection's title field has the wrong name".
Related
/checkout/quoteand/checkout/ordersdisagree on a priced-but-untitled product.These three are the same missing predicate seen from three surfaces; whoever picks one should
probably take all three.