Skip to content

[Plugin] A product with no title renders "active" in Pricing & inventory but fails every checkout #166

Description

@vedanshujain

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.titleTITLE_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):

  1. 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.
  2. 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.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions