store: product detail — fitment, OEM numbers, availability #47

Closed
opened 2026-08-27 06:35:53 +00:00 by yuris · 1 comment
Owner

Blocked by #41 for the data. The #40 stock-count question is settled: no quantity is ever rendered — the chip shows Ready / Indent / Habis and nothing else. The raw stock integer stays server-side; do not thread it into the catalog payload to "improve" the badge.

Chip tokens land in #45: --ok (ready), --warn (indent), --off (habis), each with its own tinted background and border so the three states do not rely on hue alone.

apps/store/app/products/[slug]/page.tsx already renders price, brand, part number, a stock badge, Product+BreadcrumbList JSON-LD and a related grid. The mockup adds:

  • Kecocokan kendaraan — the fitment[] list, with the mockup's closing reassurance line ("Ragu cocok atau tidak? Kirim foto STNK atau nomor rangka lewat WhatsApp, kami cek dulu sebelum dikirim."). Omit the whole block when fitment is empty rather than rendering an empty heading.
  • OEM numbers beside partNumber in the spec table, mono like the existing part number.
  • Availability as the three states from #41: ready, indent, habis. indent is the one that changes behaviour — the item stays orderable, so add-to-cart.tsx must not reuse its current !inStock disabled branch for it. Label it plainly (barang dipesan dulu, bukan stok toko) so a buyer is not surprised, and carry the state into the cart line and the WhatsApp message so the shop sees it.
  • priceUpdatedAt rendered as the spec's "harga per " under the price, only when present.
  • Berat stays as-is from weightGrams; the mockup's volumetric figure is an #40 item 4 invention and is out of scope.

Product JSON-LD availability must map to the right schema.org value per state (InStock / BackOrder / OutOfStock), and the Offer still only appears when price !== null.

Acceptance

All three availability states render correctly and indent is genuinely addable to the cart while habis is not; the fitment and OEM blocks disappear cleanly when empty; JSON-LD validates for each state; formatIdr still used for price. Extend lib/checkout-message.test.ts for the indent annotation in the WhatsApp message.

**Blocked by #41** for the data. The #40 stock-count question is settled: **no quantity is ever rendered** — the chip shows `Ready` / `Indent` / `Habis` and nothing else. The raw `stock` integer stays server-side; do not thread it into the catalog payload to "improve" the badge. Chip tokens land in #45: `--ok` (ready), `--warn` (indent), `--off` (habis), each with its own tinted background and border so the three states do not rely on hue alone. `apps/store/app/products/[slug]/page.tsx` already renders price, brand, part number, a stock badge, `Product`+`BreadcrumbList` JSON-LD and a related grid. The mockup adds: - **Kecocokan kendaraan** — the `fitment[]` list, with the mockup's closing reassurance line (*"Ragu cocok atau tidak? Kirim foto STNK atau nomor rangka lewat WhatsApp, kami cek dulu sebelum dikirim."*). Omit the whole block when `fitment` is empty rather than rendering an empty heading. - **OEM numbers** beside `partNumber` in the spec table, mono like the existing part number. - **Availability** as the three states from #41: `ready`, `indent`, `habis`. `indent` is the one that changes behaviour — the item stays **orderable**, so `add-to-cart.tsx` must not reuse its current `!inStock` disabled branch for it. Label it plainly (barang dipesan dulu, bukan stok toko) so a buyer is not surprised, and carry the state into the cart line and the WhatsApp message so the shop sees it. - **`priceUpdatedAt`** rendered as the spec's *"harga per <tanggal>"* under the price, only when present. - **Berat** stays as-is from `weightGrams`; the mockup's volumetric figure is an #40 item 4 invention and is out of scope. `Product` JSON-LD `availability` must map to the right schema.org value per state (`InStock` / `BackOrder` / `OutOfStock`), and the `Offer` still only appears when `price !== null`. ### Acceptance All three availability states render correctly and `indent` is genuinely addable to the cart while `habis` is not; the fitment and OEM blocks disappear cleanly when empty; JSON-LD validates for each state; `formatIdr` still used for price. Extend `lib/checkout-message.test.ts` for the indent annotation in the WhatsApp message.
Author
Owner

Carried forward from PR #53 (schema v3, #41/#42) — read this before implementing the "harga per " line.

priceUpdatedAt reaches the storefront as of #53, but its precision is not what the field name implies, and #47 is what makes that visible to buyers for the first time.

apps/management/lib/inventory/actions.ts:L105 stamps price_updated_at whenever sell_price is present in the payload, not when it changes. So a writer re-sending the price it already has — an operator who just saved the edit form, or the MCP update_inventory tool re-asserting the same number — moves the public date without the price moving.

Reviewer53r1 raised this on #53 as a nit and I declined it there, deliberately:

  • It errs fresh, never stale, so no buyer is shown an out-of-date price as current.
  • lib/inventory/actions.ts has no test file at all, and the only DB-touching test (lib/catalog.test.ts:95) mocks @/lib/db — so the suggested fix (a sql CASE comparing against the existing column) would land with zero real coverage in a write path, on a branch whose merge publishes the management container to production.
  • Nothing rendered the field yet. priceUpdatedAt appeared nowhere in apps/store outside its own test fixture.

That third reason expires with this issue. Round 2's ruling on #53 named the direction risk precisely: an over-fresh date makes an older price look current, which is the less safe way to be wrong. Once "harga per 12 Agustus 2026" is on the page, the date is a claim to the buyer rather than dormant payload.

Two things worth doing here, neither large:

  1. Decide whether the semantics you render match the semantics stored. The current stamp honestly means "the last time someone affirmed this price", not "the last time it changed". If the rendered copy implies the latter, either change the copy or close the gap.
  2. If you close it, a sql CASE against the existing column avoids an extra read. actions.ts:109 already clears the date when the price goes to 0 or null, so that bound is in place and does not need redoing.

Not a blocker for #53, which merged with this recorded rather than dropped.

**Carried forward from PR #53 (schema v3, #41/#42) — read this before implementing the "harga per <tanggal>" line.** `priceUpdatedAt` reaches the storefront as of #53, but its **precision is not what the field name implies**, and #47 is what makes that visible to buyers for the first time. `apps/management/lib/inventory/actions.ts:L105` stamps `price_updated_at` whenever `sell_price` is *present in the payload*, not when it *changes*. So a writer re-sending the price it already has — an operator who just saved the edit form, or the MCP `update_inventory` tool re-asserting the same number — moves the public date without the price moving. Reviewer53r1 raised this on #53 as a nit and I declined it there, deliberately: - It errs **fresh, never stale**, so no buyer is shown an out-of-date price as current. - `lib/inventory/actions.ts` has no test file at all, and the only DB-touching test (`lib/catalog.test.ts:95`) mocks `@/lib/db` — so the suggested fix (a `sql` CASE comparing against the existing column) would land with zero real coverage in a write path, on a branch whose merge publishes the management container to production. - Nothing rendered the field yet. `priceUpdatedAt` appeared nowhere in `apps/store` outside its own test fixture. That third reason expires with this issue. Round 2's ruling on #53 named the direction risk precisely: an over-fresh date makes an older price look current, which is the less safe way to be wrong. Once "harga per 12 Agustus 2026" is on the page, the date is a claim to the buyer rather than dormant payload. Two things worth doing here, neither large: 1. Decide whether the semantics you render match the semantics stored. The current stamp honestly means *"the last time someone affirmed this price"*, not *"the last time it changed"*. If the rendered copy implies the latter, either change the copy or close the gap. 2. If you close it, a `sql` CASE against the existing column avoids an extra read. `actions.ts:109` already clears the date when the price goes to 0 or null, so that bound is in place and does not need redoing. Not a blocker for #53, which merged with this recorded rather than dropped.
yuris closed this issue 2026-08-27 14:30:24 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage#47
No description provided.