# Shop Commerce Data Readiness TODO

Date: 2026-06-07

Scope: prepare the remaining Stitch-inspired commerce data features without disrupting the Payload ecommerce plugin, current `/shop` query contract, cart, orders, transactions, or product taxonomy.

References:

- Official Payload Ecommerce Plugin docs: https://payloadcms.com/docs/ecommerce/plugin
- Local plugin source confirms product/order overrides and currency-generated price fields.
- Current project source confirms `/shop` uses `queryShopProducts`, `shopParams`, ecommerce `products`, ecommerce `variants`, and ecommerce `orders`.
- Dedicated IDR migration plan: `docs/shop-idr-currency-migration-todo.md`

## Current Baseline

Already ready:

- Dynamic taxonomy values exist for segment, category, attributes, use cases, and sizes.
- Shop sidebar builder can render search, filter groups, and sort from `ShopArchive.sidebarLayout`.
- Shop sidebar builder can split Attribute filters by admin-managed `productAttributeGroups` without changing the public `attribute` query contract.
- Product grid reads product data from ecommerce `products` and `variants`.
- Product/card price display now uses active-currency-aware `ProductPrice` for product cards, product detail, carousel labels, and three-item grid labels.
- Product filters currently support `q`, `segment`, `category`, `attribute`, `usecase`, `size`, `minPrice`, `maxPrice`, `sort`, and `page`.
- Current sort values are `title`, `newest`, `price_asc`, and `price_desc`.
- Current currency schema supports IDR, USD, EUR, SGD, and MYR. Storefront runtime defaults to IDR, active Product/Variant prices have been backfilled into supported currencies, and historical USD orders/carts/transactions are preserved.
- Product display metadata now supports optional `commonName`, manual `badge`, and admin-managed `ratingSummary`.

No-touch baseline:

- Do not change ecommerce plugin internals.
- Do not change cart, order, transaction, or checkout behavior while building display/filter readiness.
- Do not hardcode Stitch sample values in UI.
- Do not make faceted/filter URLs indexable.
- Do not mix unrelated schema migration with visual styling.

## Gap Classification

### 1. Price Range

Current fact:

- Product and variant prices exist through plugin-generated currency fields. Current shop query/filter runtime reads the default storefront currency through shared pricing helpers, currently IDR.
- Product/card display components now choose visible price amounts from the active ecommerce provider currency, avoiding the dangerous pattern of formatting an IDR amount as USD/EUR/SGD/MYR.
- `queryShopProducts` already computes an effective price for sort, including variant prices.
- `shopParams` now supports `minPrice` / `maxPrice` parsing, building, active chips, and URL updates.
- `ShopArchive.sidebarLayout` now supports a `priceRangeControl`.

Risk:

- Completed medium-risk slice with targeted test coverage. This changed query/filter behavior and faceted URL state.

Recommended slice:

- Done: added `minPrice` and `maxPrice` parsing to `shopParams`.
- Done: filter by effective price after existing taxonomy filtering.
- Done: active chips and clear behavior.
- Done: sidebar `priceRangeControl` added after query behavior was proven.
- Keep filtered price URLs under existing noindex policy.

Acceptance criteria:

- Passed: `/shop?minPrice=...&maxPrice=...` returns only matching effective product prices.
- Passed: invalid price params are ignored or normalized safely.
- Passed: pagination resets when price filters change through `updateShopSearchParams`.
- Passed: price chips can be removed.
- Passed: existing filters and sort keep working.

### 2. Facet Counts

Current fact:

- Taxonomy relationships exist.
- Count aggregation now exists for segment/category/attribute/usecase/size options.
- `queryShopProducts` returns `facetCounts` for the current final result set before pagination.
- `ShopFilters` renders count metadata next to taxonomy options when counts are available.

Risk:

- Completed medium-risk first slice using Count mode A. Counts can still become more complex if Count mode B is needed later.

Decision needed:

- Done for first slice: Count mode A, count options inside the current result set.
- Count mode B: count each facet while excluding the current facet's own selection.

Recommended slice:

- Done: started with count mode A because it is simpler and safer.
- Done: counts are computed from resolved product docs after filtering and before pagination.
- Done: variant data is hydrated once so size counts use real variant size relationships.
- Done: counts render as optional metadata on filter items.

Acceptance criteria:

- Passed: counts match current result context.
- Passed: counts update when filters/search change because they are derived from `queryShopProducts`.
- Passed: empty options render with `0` in the current result context instead of being removed.
- Passed: query performance is acceptable on local seed data with targeted integration tests.

### 3. Popular Sort

Current fact:

- Orders have `items[]`, `product`, `variant`, `quantity`, and status values.
- Existing order statuses include `processing`, `completed`, `cancelled`, and `refunded`.
- Product-level editorial `popularityScore` now exists.
- `sort=popular` is part of the shop URL contract.
- Popular sorting uses `popularityScore` descending with title fallback for deterministic output.
- No order-derived popularity scan is performed at request time.

Risk:

- High if computed from orders at request time.
- Low for the implemented editorial score slice.

Decision needed:

- Done for first slice: editorial popularity via product `popularityScore`.
- Deferred: order-derived popularity from non-cancelled/non-refunded orders.
- Deferred: hybrid cached order-derived score with admin override.

Recommended slice:

- Done: added product-level `popularityScore`.
- Done: added `popular` to the sort contract and UI sort list.
- Done: selected product data includes `popularityScore`.
- Keep order-derived analytics deferred until an aggregation/cache strategy exists.

Acceptance criteria:

- Passed: `sort=popular` has a deterministic editorial metric.
- Passed: missing scores fall back safely to `0`.
- Passed: equal scores fall back to title sorting.
- Passed: existing sort values do not regress in targeted query tests.
- Passed: no per-request expensive order scan is introduced.

### 4. Rating / Review

Current fact:

- No review collection exists.
- Product now has admin-managed `ratingSummary.average` and `ratingSummary.count`.
- Product card can render rating summary only when trusted admin data exists.

Risk:

- High if implemented as a full public review workflow.
- Low to medium if implemented as admin-managed summary fields only.

Decision needed:

- Admin-managed rating summary only: `ratingAverage`, `ratingCount`.
- Full review collection: `productReviews` with moderation/status.
- Hybrid: full reviews later, summary fields now.

Recommended slice:

- Done for the safe first slice: admin-managed summary fields exist on products.
- Keep public review submission out of scope until moderation/spam policy exists.

Acceptance criteria:

- Product card can show rating only when data exists.
- Empty rating does not render fake stars.
- Future full review collection can replace or feed the summary fields.

### 5. Product Common Name

Current fact:

- Product has `title` and optional `commonName`.
- Stitch uses a secondary plant/common name such as `Swiss Cheese Plant`.

Risk:

- Low. This is a product metadata field.

Recommended slice:

- Done: optional `commonName` was added to product details.
- Done: `commonName` is included in `defaultPopulate`, shop query select, and product card render only when present.

Acceptance criteria:

- Admin can fill common name.
- Product card/detail can render common name without fallback fake text.
- Existing products remain valid with `commonName = null`.

### 6. Badge / Terlaris

Current fact:

- Product now has optional manual `badge.label` and `badge.tone`.
- Stitch badge `Terlaris` could be editorial, campaign-based, or sales-derived.

Risk:

- Low if manual.
- Medium to high if derived from sales/order analytics.

Decision needed:

- Product-level manual badge text.
- Relationship to reusable `productBadges`.
- Derived badge from popularity/order metrics.

Recommended slice:

- Done for the safe first slice: product-level optional badge group exists.
- Prefer reusable collection if future badges include color/icon/sort/display rules.
- Do not derive `Terlaris` from sales until popular metric is ready.

Acceptance criteria:

- Admin controls badge label.
- No hardcoded `Terlaris` in UI.
- Product card shows badge only when assigned.

### 7. IDR / Rupiah Currency

Current fact:

- Project ecommerce currency config now supports IDR, USD, EUR, SGD, and MYR.
- Local plugin source generates price fields from `supportedCurrencies`, e.g. `priceInUSD`.
- Currency migration introduced `priceInIDR`, `priceInEUR`, `priceInSGD`, `priceInMYR`, and matching enabled fields alongside USD.
- Storefront runtime reads IDR by default through `DEFAULT_STOREFRONT_CURRENCY` and shared pricing helpers.
- Product/card price presentation is active-currency-aware through `ProductPrice`, and the public currency selector is now implemented through `docs/shop-multicurrency-public-selector-todo.md`.
- Orders currently type `currency?: 'IDR' | 'USD' | 'EUR' | 'SGD' | 'MYR' | null`. Existing historical rows are still USD and were not rewritten.

Risk:

- High. This is a migration and ecommerce data compatibility task.

Decision needed:

- IDR-first store with USD/EUR/SGD/MYR supported in schema.
- Public currency selector completed with URL-backed `/shop` currency state and no hidden server-side persistence.
- Transitional USD data with Rupiah display only is not recommended because it lies about stored money.

Recommended slice:

- Completed IDR migration as a dedicated phase.
- Follow `docs/shop-idr-currency-migration-todo.md` for audit details and future currency decisions.
- Keep historical USD rows preserved unless a separate historical-data migration is approved.
- Regenerate Payload types after any future currency config changes.

Acceptance criteria:

- Passed: Product and Variant admin forms expose IDR, USD, EUR, SGD, and MYR price fields.
- Passed: Storefront price reads use IDR by default.
- Passed: Product/card price presentation no longer formats IDR-only amounts as other currencies when the active ecommerce provider currency changes later.
- Passed: Public currency selector supports IDR, USD, EUR, SGD, and MYR while keeping `/shop` server behavior controlled by explicit URL state.
- Passed: Seed/test fixture data now includes IDR for current shop paths.
- Passed: Orders and transactions were not rewritten during the product-price migration.

## Recommended Execution Order

1. Product display metadata:

- Add `commonName`.
- Add badge model or badge field.
- Optionally add admin-managed rating summary if approved.

Why first:

- Lowest blast radius.
- Pure CMS data readiness.
- Enables product card structure without touching filters or currency.

2. Price range filtering:

- Extend URL/query contract.
- Add query tests.
- Add sidebar control after query tests pass.

Why second:

- It uses existing price data and can be contained in shop query/runtime.

3. Facet counts:

- Add count metadata after price/filter behavior is stable.

Why third:

- Counts depend on the final filter behavior.

4. Popular sort:

- Completed first slice with editorial product `popularityScore`.

Why fourth:

- Order-derived popularity requires careful aggregation; editorial score is safer but needs business approval.

5. Rating/review system:

- Admin summary first, full reviews later if needed.

Why fifth:

- A real review system needs moderation/spam/security planning.

6. IDR currency migration:

- Completed as a separate migration task, not mixed with sidebar/product-card polishing.

Why last:

- Highest risk because it changes ecommerce generated fields and order currency semantics. Keep historical-row cleanup as a separate task.

## Next Slice Candidate

Recommended first executable slice:

- Add optional product display metadata only:
- `commonName`
- manual/reusable badge support
- optional rating summary only if explicitly approved

Status:

- Completed safe first slice with product-level optional `commonName`, `badge`, and admin-managed `ratingSummary`.
- Completed popular sort first slice with product-level optional `popularityScore` and `sort=popular`.
- Completed shop facet expansion Phase 5 with admin-managed `productAttributeGroups` and optional grouped Attribute sidebar controls.
- Created dedicated IDR/Rupiah migration TODO after auditing official docs, local plugin source, frontend provider wiring, USD dependency map, and local DB aggregate counts.
- Completed IDR-first runtime switch with multi-currency schema support for IDR/USD/EUR/SGD/MYR, product/variant backfill, and preserved historical USD rows.
- Verified with Payload type generation, TypeScript, targeted integration tests, and shop/product smoke checks listed in the currency TODO.

Files likely touched:

- `src/collections/Products/index.ts`
- `src/payload-types.ts` after type generation
- `src/lib/shop/queryProducts.ts`
- `src/components/ProductGridItem/index.tsx`
- Product seed files only if sample data is approved
- Tests for product metadata rendering/query select

Do not touch in this first slice:

- `shopParams`
- price range
- facet count aggregation
- popular sort
- currency config
- orders
- transactions
- checkout

## Execution Rule

Before each slice:

- Re-read this document.
- Re-check `git status --short`.
- Identify exact files to touch.
- Keep unrelated dirty files untouched.
- Define acceptance criteria for the slice.
- Run TypeScript and relevant tests after the slice.
