# BMJ PayloadCMS Taxonomy Final

Status: Final  
Project: `bungamekarsari`  
Target platform: PayloadCMS with `@payloadcms/plugin-ecommerce`

## Purpose

This document is the maintained domain reference for the BMJ product taxonomy as implemented for this PayloadCMS project.

It resolves the prior mismatch between older taxonomy planning artifacts. Those historical artifacts are no longer maintained in the active `docs/` directory.

## Source Of Truth Order

Use this priority when documents disagree:

1. Current Payload collection schemas and query code.
2. This document.
3. Current tests covering taxonomy and shop filtering.

The source code is the executable contract. This document explains the intended business taxonomy and should be updated whenever taxonomy behavior changes.

## Final Domain Decisions

1. The SVG taxonomy is kept as-is.
2. Categories support cross-segment membership.
3. Products still have exactly one active `segment` and one effective `category`.
4. `attribute` filters use AND logic.
5. `usecase` filters use AND logic.
6. `size` filters use OR logic across variants.
7. When the user changes `segment`, reset `category` and `usecase`.
8. Variant `unit` stays as a controlled vocabulary for v1:
   - `plant`
   - `pot`
   - `polybag`
   - `stem`
   - `square_meter`
   - `linear_meter`
   - `lot`
9. Cross-segment categories such as `Bambu`, `Tanaman air`, and `Rumput` do not have a default segment.
10. A product assigned to a cross-segment category must still have exactly one active `segment`, chosen explicitly by the editor.

## Canonical Vocabulary

### Segments

- `Proyek / lansekap`
- `Hias / retail`

### Categories

Project / landscape categories:

- `Pohon & pelindung`
- `Palem`
- `Semak & perdu`
- `Ground cover`

Retail / ornamental categories:

- `Hias daun`
- `Rambat`
- `Bonsai`
- `Sukulen & kaktus`
- `T. buah produktif`

Cross-segment categories:

- `Bambu`
- `Tanaman air`
- `Rumput`

Cross-segment rule:

- these categories may be paired with any allowed segment
- they do not infer a default segment automatically
- the editor must choose the active product segment explicitly

### Attributes

- `Berbunga`
- `Indoor`
- `Outdoor`
- `Tahan panas`
- `Gantung`
- `Low maintenance`

### Use Cases

- `Pagar hidup`
- `Border`
- `Median & penghijauan`
- `Focal point`
- `Taman koleksi`

### Sizes

- `Bibit / biji`
- `Ukuran kecil`
- `Ukuran sedang`
- `Ukuran besar`
- `Borongan / proyek`

## Final PayloadCMS Data Model

## Collection: `segments`

Purpose:
- controlled vocabulary for product segment

Canonical domain fields:
- `title`

Implementation support fields:
- `slug`
- `sortOrder`
- `description`

Notes:
- exactly 2 records in BMJ taxonomy

## Collection: `categories`

Purpose:
- primary product category vocabulary

Canonical domain fields:
- `title`
- `segments[]`

Implementation support fields:
- `slug`
- `sortOrder`
- `description`

Important:
- `segments[]` is final
- this is required to support cross-segment categories from the SVG
- `category.segmentId` as a single FK is no longer valid for the final model

## Collection: `productAttributes`

Purpose:
- reusable descriptive tags attached to products

Canonical domain fields:
- `title`

Implementation support fields:
- `slug`
- `sortOrder`
- `description`

Rules:
- many-to-many with products
- not free text

## Collection: `productUseCases`

Purpose:
- reusable context-of-use tags attached to products

Canonical domain fields:
- `title`

Implementation support fields:
- `slug`
- `sortOrder`
- `description`

Rules:
- many-to-many with products
- not free text

## Collection: `productSizes`

Purpose:
- controlled size / selling-condition vocabulary used by variants

Canonical domain fields:
- `label`

Implementation support fields:
- `slug`
- `sortOrder`
- `notes`

Rules:
- used by `variants.size`
- not free text

## Collection: `products`

Purpose:
- main ecommerce product record

Final taxonomy fields:
- `segment` -> relationship to `segments`
- `category` -> relationship to `categories`
- `attributes[]` -> relationship to `productAttributes`
- `useCases[]` -> relationship to `productUseCases`

Compatibility field kept:
- `categories[]`

Meaning of compatibility field:
- `categories[]` is a legacy mirror of `category`
- it must not be treated as the new canonical taxonomy field

Existing ecommerce fields retained:
- all plugin ecommerce product fields remain in place
- current content fields remain in place
- cart, order, checkout, and payment flows are unchanged

## Collection: `variants`

Purpose:
- sellable product variants from the ecommerce plugin

Final added fields:
- `size` -> relationship to `productSizes`
- `minOrder` -> numeric minimum order quantity
- `unit` -> controlled vocabulary

Notes:
- `size` is the storefront taxonomy size filter
- `size` is separate from plugin `variantOptions`
- plugin `variantTypes` and `variantOptions` remain available for SKU-specific option logic when needed

## Final Business Rules

### Product-level rules

1. Each product has exactly one effective category.
2. Each product has exactly one active segment.
3. The selected product category must belong to the selected product segment.
4. `products.categories[]` must be synchronized from `products.category`.
5. `attributes[]` may contain multiple values.
6. `useCases[]` may contain multiple values.
7. For single-segment categories, `segment` may be inferred automatically.
8. For cross-segment categories, `segment` must be selected explicitly and is never auto-defaulted.

### Variant-level rules

1. `size` is a controlled relationship to `productSizes`.
2. `minOrder` is numeric and defaults to `1`.
3. `unit` must come from the controlled unit vocabulary.

### Storefront filter rules

1. `attribute` is repeatable and uses AND logic.
2. `usecase` is repeatable and uses AND logic.
3. `size` is repeatable and uses OR logic across variants.
4. `category` is single-select.
5. `segment` is single-select.

## Final `/shop` Query Contract

Supported params:

- `q`
- `segment`
- `category`
- `attribute`
- `usecase`
- `size`
- `sort`
- `page`

### Param semantics

`q`
- free-text search over product search fields

`segment`
- single value
- filters products by active segment

`category`
- single value
- filters products by effective category

`attribute`
- repeatable
- AND filter
- product must contain all selected attributes

`usecase`
- repeatable
- AND filter
- product must contain all selected use cases

`size`
- repeatable
- OR filter
- product matches if any sellable variant matches any selected size

`sort`
- allowed values:
  - `title`
  - `price_asc`
  - `price_desc`
  - `newest`

`page`
- integer
- default `1`

### Query preservation rules

When user changes `sort`:
- preserve `q`
- preserve `segment`
- preserve `category`
- preserve `attribute`
- preserve `usecase`
- preserve `size`

When user changes `segment`:
- reset `category`
- reset `usecase`
- preserve `q`
- preserve `sort`
- preserve `attribute`
- preserve `size`

When user changes `category`:
- preserve all other params

When user toggles `attribute`, `usecase`, or `size`:
- preserve all other params

## ERD Revision Required

The old ERD must be revised in the following way.

### Remove

- `categories.segmentId` as a single mandatory FK

### Replace with

Either:

- relational ERD form:
  - `categories`
  - `segments`
  - junction table `category_segments`

Or:

- PayloadCMS form:
  - `categories.segments[]`

### Keep

- `products.segmentId`
- `products.categoryId`
- `variants.sizeId`
- `variants.unit`
- `variants.minOrder`
- product-to-attributes many-to-many
- product-to-useCases many-to-many

## PayloadCMS Project Alignment

This final model has been verified against the actual architecture of this project.

### Architecture facts

- `categories` is a local collection and can safely be extended.
- `products` is provided by `@payloadcms/plugin-ecommerce` and must be extended through collection override.
- `variants` is provided by `@payloadcms/plugin-ecommerce` and must be extended through variant collection override.
- the storefront shop page is already query-param driven, so the new contract fits the current routing model.
- cart, disabled checkout routes, orders, transactions, and legacy payment data are isolated from taxonomy changes.

### Consequence

This taxonomy design is compatible with the current project structure and does not require:

- ecommerce plugin fork
- payment flow redesign
- cart schema redesign
- order schema redesign

## Implementation Notes

The current implementation in this repo already aligns with this final document in these areas:

- new taxonomy collections exist
- products have `segment`, `category`, `attributes`, `useCases`
- variants have `size`, `minOrder`, `unit`
- category-to-segment is modeled as multi-segment capable
- `/shop` query support and preservation are implemented

This document is the canonical business specification for those choices.

## Operational Tools

The project now includes an admin-only route handler for taxonomy operations:

- `GET /next/bmj-taxonomy`
  - returns a taxonomy alignment report
  - includes controlled vocabulary counts
  - includes product issues such as missing `segment`, missing `category`, or category-segment mismatch
  - includes variant issues such as missing `size`, invalid `minOrder`, or invalid `unit`

- `POST /next/bmj-taxonomy`
  - accepts JSON body with:
    - `{ "action": "upsert" }`
    - `{ "action": "backfill-safe" }`
    - `{ "action": "upsert-and-backfill-safe" }`
  - default action is `upsert-and-backfill-safe`

### Safe Backfill Scope

`backfill-safe` only performs deterministic updates:

- product `category` is inferred from legacy `categories[]` only when unambiguous
- product `segment` is inferred from category only when the category belongs to exactly one segment
- legacy `categories[]` is synchronized from canonical `category`
- variant `minOrder` defaults to `1` when missing or invalid
- variant `unit` defaults to `plant` when missing or invalid

It intentionally does **not** guess `variants.size` when no reliable mapping exists.

## Out Of Scope

This document does not define:

- final seeded BMJ production data import
- final product-by-product backfill mapping
- final variant-by-variant backfill mapping
- future additions to unit vocabulary beyond the v1 set
- UX copy or visual layout decisions outside the `/shop` filtering contract

## Required Next Steps

1. Revise the external ERD to replace single category-to-segment FK with cross-segment support.
2. Populate the 5 controlled vocabulary collections with final BMJ records.
3. Backfill existing products:
   - `segment`
   - `category`
   - `attributes`
   - `useCases`
4. Backfill existing variants:
   - `size`
   - `minOrder`
   - `unit`
5. Validate storefront results against real BMJ catalog data.

## Final Summary

For BMJ in PayloadCMS:

- the SVG taxonomy is final
- the query contract is final
- the ERD must be revised to support cross-segment categories
- products keep one segment and one effective category
- categories may belong to multiple segments
- attributes and use cases are controlled multi-value vocabularies
- sizes are controlled variant-level vocabularies
- taxonomy changes must not alter cart, order, or payment architecture
