sku and item fix
This commit is contained in:
@@ -264,7 +264,7 @@ Costing: FIFO · Multi-warehouse · Single-tenant. Legend: **PK** primary key ·
|
||||
CATEGORY(category_id PK, name, status) -- top level; no self-nesting
|
||||
SUBCATEGORY(subcategory_id PK, category_id FK→CATEGORY, name, status)
|
||||
BRAND(brand_id PK, name, status)
|
||||
ITEM_TYPE(item_type_id PK, name, status) -- Color, Size, Material — standalone
|
||||
ITEM_TYPE(item_type_id PK, name, is_measurable, status) -- Color, Size, Material — standalone
|
||||
UOM(uom_id PK, name)
|
||||
ITEM(item_id PK, sku, name, category_id FK→CATEGORY, subcategory_id FK→SUBCATEGORY [nullable],
|
||||
brand_id FK→BRAND [nullable], base_uom_id FK→UOM,
|
||||
@@ -365,7 +365,8 @@ USER(..., role_id FK→ROLE [nullable]) -- added to the existing USER shadow (s
|
||||
Note: `USER_ROLE` from the original placeholder sketch was dropped — a user has at most one role (`USER.role_id`), matching AuthHex's own `User.RoleId` being a single scalar FK, not a many-to-many.
|
||||
|
||||
## C.9 Modeling notes (load-bearing)
|
||||
- **Item types are a dropdown, not a relationship.** `ITEM_TYPE` (Color, Size, Material) exists **only** to populate the frontend item-builder's dropdown via `GET /item-types`. Nothing references it and it references nothing — there is no value table and no join to `ITEM`. The builder cross-products the checked types into **one standalone item per combination**; the chosen values (Red, S, M) are encoded by the **client** into the generated SKU (`BL-0002` for one type, `BL-100-0003` for two) and the server only checks that SKU for uniqueness. **The item list is the record of what was built.** This is not a product-variation model: there is no parent-product entity and no variant hierarchy.
|
||||
- **Item types are a dropdown, not a relationship.** `ITEM_TYPE` (Color, Size, Material) populates the frontend item-builder's dimension list via `GET /item-types`. Nothing references it and it references nothing — there is no value table and no join to `ITEM`. The builder cross-products the checked types into **one standalone item per combination**; the chosen values (Red, S, M) are encoded by the **client** into the generated SKU (`BL-0002` for one type, `BL-100-0003` for two) and the server only checks that SKU for uniqueness. **The item list is the record of what was built.** This is not a product-variation model: there is no parent-product entity and no variant hierarchy.
|
||||
- *One exception, added 2026-08-11:* the master carries a single semantic the client acts on — **`is_measurable`**. A dimension flagged measurable has its values entered as a number + unit (500 ml, 1 L) instead of free text, and that pair is written to each generated item's `content_qty`/`content_unit` (FR-MD-02). So "exists only to feed a dropdown" is no longer accurate; "is unreferenced by `ITEM`" still is, and the trade-off below is unchanged. The flag is what lets an apparel `Size` (S/M/L) stay plain text while a `Volume` dimension carries units. The server does not read it when writing an item — each item's content pair is validated and normalised on its own.
|
||||
- *Accepted trade-off (a decision, not an oversight):* the backend cannot answer "list all blue items", cannot filter or report by colour/size, and cannot validate that a SKU's segments correspond to real item types. Renaming an item type (`Color` → `Colour`) does **not** touch existing SKUs, which keep their old segments — the two are permanently decoupled the moment an item is created. If value-level querying is ever needed, an `ITEM_TYPE_VALUE` table plus a link table can be added additively, but existing SKUs will not be back-fillable without parsing them by hand.
|
||||
- **Sale price is a per-item scalar, not a variant/price table.** Because each "variant" is its own `ITEM` row (above), the optional selling price lives directly on `ITEM.sale_price` (nullable). `NULL` means "use stock value" — Sales values the item at its FIFO stock cost at sale time (FR-STK-04 / `STOCK_LAYER`); a value is a fixed selling price. It is **Sales-only**: it never participates in GRN, FIFO layering, or the stock ledger, so receipt/costing behaviour is identical whether the item is fixed-priced or not. The create-time "fixed price vs use stock value" choice is a **frontend UX toggle** — the contract is simply the nullable column, and the item builder requires a price on every generated variant when the user picks fixed pricing.
|
||||
- **Two-level categories.** `CATEGORY` no longer self-nests; `SUBCATEGORY` is the single optional level below it. An item stores both FKs rather than pointing only at the deepest node, so the parent is never inferred or lost. A subcategory cannot be reparented (it would silently invalidate the category of every item referencing it) — deactivate and recreate instead.
|
||||
|
||||
@@ -359,26 +359,39 @@ Requires `If-Match`. → **200 OK**; `412` on mismatch; `409` on duplicate name.
|
||||
Query: `q`, `status` (`Active|Inactive`), + paging. Pass `status=Active` for selectable rows.
|
||||
**200 OK**
|
||||
```json
|
||||
{ "items": [ { "itemTypeId": 1, "name": "Color", "status": "Active",
|
||||
{ "items": [ { "itemTypeId": 1, "name": "Color", "isMeasurable": false, "status": "Active",
|
||||
"createdAt": "2026-07-16T09:00:00Z", "updatedAt": null },
|
||||
{ "itemTypeId": 2, "name": "Size", "status": "Active",
|
||||
{ "itemTypeId": 2, "name": "Size", "isMeasurable": false, "status": "Active",
|
||||
"createdAt": "2026-07-16T09:00:00Z", "updatedAt": null } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 2, "totalPages": 1 } }
|
||||
```
|
||||
`Color` and `Size` are seeded on first start; users add their own (e.g. `Material`).
|
||||
`Color` and `Size` are seeded on first start, both with `isMeasurable: false`; users add their own
|
||||
(e.g. `Material`, or a `Pack Size` with `isMeasurable: true`).
|
||||
|
||||
**`isMeasurable`** (added 2026-08-11) marks a dimension whose values are content *measurements*
|
||||
(500 ml, 1 L) rather than plain labels. The item builder then captures a number + unit per value
|
||||
and writes that pair to each generated item's `contentQty`/`contentUnit`, instead of copying one
|
||||
form-level pair into every variant — which is what makes "Coca-Cola in 500 ml / 1 L / 250 ml"
|
||||
three correctly sized items. It is why an apparel `Size` (S/M/L) can stay plain text while a
|
||||
`Volume` dimension carries units. The server never reads it when writing an item: each item's
|
||||
pair is still validated and normalised on its own.
|
||||
|
||||
#### `GET /item-types/{itemTypeId}` → **200 OK** (+ `ETag`); `404` if absent.
|
||||
|
||||
#### `POST /item-types`
|
||||
```json
|
||||
{ "name": "Material" }
|
||||
{ "name": "Pack Size", "isMeasurable": true }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/item-types/3` → the `ItemTypeDto`. `409` if the name exists.
|
||||
Callable from the item builder's inline "+" as well as the admin screen.
|
||||
`isMeasurable` is optional and defaults to `false`. Callable from the item builder's inline "+"
|
||||
as well as the admin screen.
|
||||
|
||||
#### `PUT /item-types/{itemTypeId}`
|
||||
Requires `If-Match`. → **200 OK**; `412` on mismatch; `409` on duplicate name.
|
||||
**Renaming does not touch existing items** — nothing joins back to this row.
|
||||
`isMeasurable` is **nullable in the request body and preserved when omitted**: a plain `bool`
|
||||
would bind an absent property as `false`, so a name-only PUT — which is what the admin screen
|
||||
used to send — would clear the flag on every rename.
|
||||
|
||||
#### `PATCH /item-types/{itemTypeId}/status` → **204 No Content**. Deactivate, never delete (FR-MD-08).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user