# Shop IDR / Rupiah Currency Migration TODO

Date: 2026-06-18

Status: Runtime storefront default is now IDR with multi-currency schema support for IDR, USD, EUR, SGD, and MYR. Product and Variant prices have been backfilled from the legacy USD minor-unit field into all supported currencies. Historical carts/orders/transactions remain historical data and were not rewritten.

Scope: migrate the shop currency model toward Rupiah without disrupting the Payload ecommerce plugin, product pricing, cart, orders, transactions, shop filters, checkout-disabled flow, or existing admin data.

References:

- Payload Ecommerce Overview: https://payloadcms.com/docs/ecommerce/overview
- Payload Ecommerce Plugin: https://payloadcms.com/docs/ecommerce/plugin
- Local plugin source: `node_modules/@payloadcms/plugin-ecommerce/dist`
- Current project config: `src/plugins/ecommerce/currenciesConfig.ts`

## Current Facts

- The installed ecommerce plugin version is `@payloadcms/plugin-ecommerce@3.84.1`.
- Payload documents that the ecommerce plugin supports multiple currencies and creates separate Product/Variant price fields per configured currency.
- Local plugin source confirms generated price field names are `priceIn${currency.code}` and `${field}Enabled`.
- The plugin exports predefined `USD`, `EUR`, and `GBP`, but its `Currency` type allows a project-defined `IDR` object.
- Current project currency config is multi-currency:
- `defaultCurrency: 'IDR'`
- `supportedCurrencies: [IDR, USD, EUR, SGD, MYR]`
- Current generated types expose `priceInIDR`, `priceInUSD`, `priceInEUR`, `priceInSGD`, `priceInMYR`, their enabled flags, and `currency?: 'IDR' | 'USD' | 'EUR' | 'SGD' | 'MYR' | null`.
- `EcommerceProvider` in `src/providers/index.tsx` now receives `currenciesConfig` and uses a new localStorage cart key `cart-idr`.
- `Price` formatting follows the active ecommerce provider currency.
- Storefront price reads now use shared pricing helpers instead of directly choosing `priceInUSD`.
- Product/card promotional price display now uses `ProductPrice`, which chooses the displayed amount from the active ecommerce provider currency instead of formatting an IDR-only amount as another currency.

## Local Data Snapshot

Pre-migration Postgres aggregate audit on 2026-06-18:

- Products: `18` total, `18` with USD price data.
- Variants: `159` total, `158` with USD price data.
- Orders: `4` rows, all `USD`.
- Transactions: `3` rows, all `USD`.
- Carts: `88` rows, all `USD`.

Implication:

- This is a real migration/backfill task, not a display-label task.
- Existing carts/orders/transactions were not rewritten automatically.
- A one-file currency change would have created mixed field/runtime behavior and was avoided.

Post-backfill audit on 2026-06-18:

- Products: `18` with USD price data, `18` with IDR/EUR/SGD/MYR price data.
- Variants: `158` with USD price data, `158` with IDR/EUR/SGD/MYR price data, `1` intentionally unpriced/null variant preserved.
- Product IDR range after conversion: `160200` to `26700000`.
- Variant IDR range after conversion: `204700` to `889822`.

Backfill rates used:

- `1 USD = 17800 IDR`
- `1 EUR = 20550 IDR`
- `1 SGD = 13850 IDR`
- `1 MYR = 4380 IDR`

Source rationale:

- USD/IDR was rounded from current public market references around June 18, 2026.
- EUR/IDR, SGD/IDR, and MYR/IDR were rounded from current public market/bank references around June 17-18, 2026.
- These are operational bootstrap rates, not a permanent FX-rate service.

## Main Decision

Recommended path: staged dual-currency migration first, IDR-default second, IDR-only last if still needed.

Reason:

- Adding IDR alongside USD preserves existing data while new `priceInIDR` fields are introduced.
- Backfill can happen while current storefront remains stable.
- Runtime can switch to IDR only after data and components are ready.
- Keeping USD temporarily avoids breaking historical USD carts/orders while the app transitions.

Rejected path:

- Do not simply format `priceInUSD` as Rupiah. That lies about stored money and will confuse orders, transactions, structured data, and admin.
- Do not remove USD from `supportedCurrencies` before dealing with existing USD rows.

## No-Touch Zones

- Do not fork or patch `@payloadcms/plugin-ecommerce`.
- Do not modify order/transaction semantics in the same slice as product display.
- Do not delete carts/orders/transactions without an explicit migration decision.
- Do not change checkout/payment behavior while checkout remains disabled.
- Do not mix currency migration with Stitch/sidebar styling.
- Do not fake Rupiah display while Product/Variant records still use only `priceInUSD`.

## USD Dependency Map

Backend/config:

- `src/plugins/ecommerce/currenciesConfig.ts`
- `src/plugins/index.ts`
- `src/payload-types.ts`

Storefront product pricing:

- `src/lib/shop/queryProducts.ts`
- `src/components/ProductGridItem/index.tsx`
- `src/components/ProductItem/index.tsx`
- `src/components/Cart/CartModal.tsx`
- `src/blocks/Carousel/Component.client.tsx`
- `src/blocks/ThreeItemGrid/Component.tsx`
- `src/app/(app)/products/[slug]/page.tsx`
- `src/components/product/ProductDescription.tsx`

Cart/order UI:

- `src/providers/index.tsx`
- `src/components/Price.tsx`
- `src/components/OrderItem/index.tsx`
- `src/app/(app)/(account)/orders/[id]/page.tsx`

Seed/test data:

- `src/endpoints/seed/index.ts`
- `src/endpoints/seed/product-hat.ts`
- `src/endpoints/seed/product-tshirt.ts`
- `tests/helpers/taxonomyFixtures.ts`
- `tests/int/queryShopProducts.int.spec.ts`
- `tests/e2e/frontend.e2e.spec.ts`
- `tests/e2e/taxonomy.e2e.spec.ts`

## Phase 0 - Preflight

Goal: freeze currency work into a controlled migration path.

Tasks:

- [x] Confirm official/plugin behavior around multi-currency.
- [x] Confirm current local currency config.
- [x] Confirm frontend provider currently defaults to USD.
- [x] Confirm current DB has USD products, variants, carts, orders, and transactions.
- [x] Create a database backup immediately before any currency schema change.
- [ ] Decide whether current `priceInUSD` numeric values are actually intended Rupiah nominal values.
- [ ] Decide whether active carts can be cleared, migrated, or isolated via a new localStorage key.

Acceptance criteria:

- No code schema changes happen before backup and runtime switch strategy are clear.
- We know whether USD values can be copied 1:1 into IDR.

Verification notes:

- Backup created before schema change: `backups/db/bungamekarsari-data-20260618-081243.sql`.
- Runtime switch strategy remains locked: Phase 1 keeps `defaultCurrency: USD.code`.
- 1:1 USD-to-IDR value decision remains open; no price data was copied.

## Phase 1 - Schema-Only Dual Currency

Goal: introduce IDR fields without changing public storefront behavior.

Candidate files:

- `src/plugins/ecommerce/currenciesConfig.ts`
- `src/payload-types.ts` after type generation
- `src/app/(payload)/admin/importMap.js` after import map/type generation if needed

Recommended implementation:

- Done: define local `IDR` currency object:
- `code: 'IDR'`
- `decimals: 0`
- `label: 'Indonesian Rupiah'`
- `symbol: 'Rp'`
- Done: set `supportedCurrencies: [USD, IDR]`.
- Done: keep `defaultCurrency: USD.code` in this phase.
- Done: run type generation.

Acceptance criteria:

- Passed by schema/config audit: Product and Variant configs include `priceInIDR` and `priceInIDREnabled`.
- Passed by DB audit: `products` and `variants` tables have `price_in_i_d_r` and `price_in_i_d_r_enabled`.
- Passed: existing `/shop` request returns 200 while default currency remains USD.
- Passed: generated types include both USD and IDR fields.
- Passed: TypeScript passes.

Rollback:

- Remove IDR from `supportedCurrencies` and regenerate types before any backfill if schema-only phase fails.

Verification notes:

- `pnpm run generate:types` passed.
- `pnpm exec tsc --noEmit --pretty false` passed.
- DB schema audit confirmed `products.price_in_i_d_r`, `products.price_in_i_d_r_enabled`, `variants.price_in_i_d_r`, and `variants.price_in_i_d_r_enabled`.
- Payload config audit confirmed `products` and `variants` include `priceInIDR` / `priceInIDREnabled`.
- `/shop?idrPhase1Smoke=1` returned HTTP 200.
- `pnpm exec vitest run tests/int/queryShopProducts.int.spec.ts --config ./vitest.config.mts --pool=threads` passed.
- Admin UI selector smoke for the nested price field was attempted but not used as final evidence because Payload admin nesting did not expose a stable `#field-priceInIDR` selector in the smoke script.

## Phase 2 - IDR Backfill

Goal: populate IDR price fields safely.

Decision required:

- If current USD values are actually Rupiah nominal values stored under the wrong currency, copy `priceInUSD` to `priceInIDR` 1:1.
- If current USD values are true USD minor-unit values, use a deliberate exchange-rate conversion instead.

Recommended implementation:

- Backfill `products.price_in_i_d_r = products.price_in_u_s_d` only after 1:1 decision is confirmed.
- Backfill `products.price_in_i_d_r_enabled = products.price_in_u_s_d_enabled`.
- Backfill equivalent variant columns.
- Leave orders, transactions, and carts unchanged in this phase.

Acceptance criteria:

- Every product with USD price has IDR price.
- Every variant with USD price has IDR price, except intentionally unpriced variants.
- No order, transaction, cart, or customer row is changed.
- A read-only audit confirms backfill counts.

## Phase 3 - Runtime IDR Switch

Goal: make storefront and future carts use IDR.

Candidate files:

- `src/plugins/ecommerce/currenciesConfig.ts`
- `src/providers/index.tsx`
- `src/lib/shop/queryProducts.ts`
- `src/components/ProductGridItem/index.tsx`
- `src/components/ProductItem/index.tsx`
- `src/components/Cart/CartModal.tsx`
- `src/blocks/Carousel/Component.client.tsx`
- `src/blocks/ThreeItemGrid/Component.tsx`
- `src/app/(app)/products/[slug]/page.tsx`
- `src/components/product/ProductDescription.tsx`
- seed and test fixtures

Recommended implementation:

- Change `defaultCurrency` to `IDR.code`.
- Pass `currenciesConfig={ecommerceCurrenciesConfig}` to `EcommerceProvider`.
- Consider changing ecommerce localStorage key, for example from `cart` to `cart-idr`, to avoid reusing stale USD carts.
- Update shop query select/effective price logic to use IDR fields.
- Update product cards/detail/cart UI to read IDR fields or a shared current-currency price helper.
- Update JSON-LD `priceCurrency` to `IDR`.
- Update seed/test data to create IDR prices.

Acceptance criteria:

- `/shop` shows Rupiah prices from `priceInIDR`.
- `/products/[slug]` shows Rupiah prices from `priceInIDR`.
- New carts are created with `currency: 'IDR'`.
- Old USD orders remain readable.
- Existing shop filters, price range, sort, and cart UI still pass targeted tests.
- Structured data uses `IDR`, not `usd`.

## Phase 4 - Historical Data Policy

Goal: decide what happens to existing USD carts/orders/transactions.

Options:

- Keep USD supported indefinitely for historical rows.
- Archive or delete old carts only, while preserving orders/transactions.
- Convert historical carts/orders/transactions to IDR only if amounts are confirmed to be Rupiah nominal values.

Implemented first step:

- Preserve orders and transactions as historical records.
- Isolate old browser carts by changing storefront localStorage from `cart` to `cart-idr`.
- Keep USD in `supportedCurrencies` for historical rows and future multi-currency support.

Acceptance criteria:

- Admin order history remains readable.
- No stale browser USD cart is silently used as an IDR cart through the storefront provider.
- Removing USD, if ever done, has its own DB enum/data migration.

## Phase 5 - Optional IDR-Only Cleanup

Goal: remove USD only if business/product policy requires it.

Do not start until:

- New storefront is stable on IDR.
- Orders and transactions policy is approved.
- There are no active USD carts, or cart migration/deletion is approved.
- Type and DB enum migrations are planned.

Candidate work:

- Set `supportedCurrencies: [IDR]`.
- Remove or ignore USD fields in runtime.
- Run schema migration/type generation.
- Update tests so future data is IDR-only.

Risk:

- High. Removing USD can invalidate existing enum values and historical data.

## Verification Matrix

Schema:

- `pnpm run generate:types`
- `pnpm exec tsc --noEmit --pretty false`

Query/runtime:

- `pnpm exec vitest run tests/int/queryShopProducts.int.spec.ts --config ./vitest.config.mts --pool=threads`
- Price range and sort tests must pass on IDR data.

Frontend:

- `/shop`
- `/shop?minPrice=...&maxPrice=...`
- `/shop?sort=popular`
- `/products/[slug]`
- cart add/remove smoke

Admin:

- Product admin exposes IDR price fields.
- Variant admin exposes IDR price fields.
- Orders remain readable.

SEO:

- Product JSON-LD uses `priceCurrency: 'IDR'`.
- Shop query URL noindex policy remains unchanged.

Latest verification notes:

- `pnpm exec tsc --noEmit --pretty false` passed after the IDR-first runtime switch.
- `pnpm exec vitest run tests/int/queryShopProducts.int.spec.ts tests/int/shopSeedData.int.spec.ts tests/int/shopSidebarLayout.int.spec.ts --config ./vitest.config.mts --pool=threads` passed: 3 files, 14 tests.
- `/shop?currencySmoke=1` returned HTTP 200 and rendered visible Rupiah prices, for example `Rp 26.700.000`, `Rp 445.000`, and `Rp 222.500`.
- `/shop?minPrice=100000&maxPrice=900000&sort=price_asc&currencySmoke=1` returned HTTP 200, rendered Rupiah prices, and preserved `noindex`.
- `/products/no-inventory-product?currencySmoke=1` returned HTTP 200, rendered Rupiah, emitted JSON-LD `priceCurrency: 'IDR'`, and did not emit USD product schema.
- After adding `ProductPrice`, `pnpm exec tsc --noEmit --pretty false` still passed.
- After adding `ProductPrice`, the same targeted Vitest command passed: 3 files, 14 tests.
- `/shop?productPriceSmoke=1` returned HTTP 200 and rendered Rupiah through active-currency-aware product price components.
- `/shop?minPrice=100000&maxPrice=900000&sort=price_asc&productPriceSmoke=1` returned HTTP 200, rendered Rupiah, and preserved `noindex`.
- `/products/no-inventory-product?productPriceSmoke=1` returned HTTP 200, rendered Rupiah, emitted JSON-LD `priceCurrency: 'IDR'`, and did not emit USD product schema.
- Public currency selector was implemented later in `docs/shop-multicurrency-public-selector-todo.md`.
- Selector verification passed for URL-backed `/shop` currency state, product detail display-only currency state, non-USD currency query coverage, and the existing `noindex` policy for non-default shop URLs.

## Current Recommendation

The public store has now been switched to IDR-first runtime after schema generation, DB backup, product/variant backfill, shared pricing helper updates, and targeted verification.

Next executable slice, if approved:

- Add a business-owned FX-rate management model if rates should be editable from admin instead of fixed product/variant price fields.
- Review historical cart/order policy before any cleanup of old USD rows.

The storefront now runs IDR-first while preserving multi-currency schema support and a controlled public selector.
