completed invetory updates
This commit is contained in:
+1
-1
@@ -311,7 +311,7 @@ dotnet run
|
||||
- Swagger UI at `https://localhost:<port>/swagger`
|
||||
- Health at `https://localhost:<port>/health` → `Healthy`
|
||||
|
||||
Entity/DbContext modeling (the 38 entities, configurations, enums) is specified in `10-BACKEND-PHASE1.md`. Do not invent the schema here — follow that file.
|
||||
Entity/DbContext modeling (the 42 entities, configurations, enums) is specified in `10-BACKEND-PHASE1.md`. Do not invent the schema here — follow that file.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+32
-10
@@ -1,6 +1,6 @@
|
||||
# 10 · BACKEND — Phase 1 Spec (Inventory & Supply Chain)
|
||||
|
||||
> **Authoritative for:** backend architecture, business rules, and the data model (the 38-entity schema).
|
||||
> **Authoritative for:** backend architecture, business rules, and the data model (the 42-entity schema).
|
||||
> **Navigation:** you arrived here from `00-CORE.md`. API request/response contracts are in `11-BACKEND-PHASE1.md`. Frontend rules are in `20-FRONTEND.md`. Record work in `Backend/PROGRESS.md`.
|
||||
> **Scope basis:** SRS v1.1. Costing = FIFO · Multi-warehouse · Single-tenant · RBAC deferred (user identity stamped) · approvals auto/config-gated · vendor invoice + 3-way match deferred to Accounting.
|
||||
> **Authentication:** identity is owned by the **external AuthHex identity provider** (separate service), but as of 2026-07-16 the frontend no longer calls AuthHex directly — all login/registration/recovery/2FA/session traffic is proxied through ERPCore's own `AuthController` (`Controllers/AuthController.cs`, `Services/Auth/*`), which forwards to AuthHex and delivers the resulting session as httpOnly Secure cookies (docs/02-SECURITY.md §B.2). ERPCore still does not mint or sign tokens itself — it only forwards to and validates AuthHex's RS256 JWTs. See A.4 (Authentication / Audit actor) and `11-BACKEND-PHASE1.md §2.0` for the endpoint list. RBAC (per-endpoint) still deferred.
|
||||
@@ -118,14 +118,17 @@ One base currency; invoicing/3-way match in Accounting (GRN carries data); users
|
||||
### B.3.1 Master Data (FR-MD)
|
||||
| ID | Requirement | Pri |
|
||||
|---|---|---|
|
||||
| FR-MD-01 | Maintain **Item master**: SKU (unique), name, description, category, item type (Stocked/NonStocked/Service), tracking mode (None/Batch/Serial), status, tax class, default vendor. | M |
|
||||
| FR-MD-01 | Maintain **Item master**: SKU (unique), name, description, category (+ optional subcategory), optional brand, **stock nature** (Stocked/NonStocked/Service), tracking mode (None/Batch/Serial), status, tax class, default vendor. | M |
|
||||
| FR-MD-02 | Maintain **UOM master** with base UOM per item and **conversion factors** (purchase→stock→base). | M |
|
||||
| FR-MD-03 | Convert quantities between UOMs on every transaction; store base-UOM quantity in the ledger. | M |
|
||||
| FR-MD-04 | Maintain **hierarchical item categories**. | S |
|
||||
| FR-MD-04 | Maintain **item categories with one optional subcategory level**. An item references a category (required) and a subcategory (optional) that must belong to it. Deeper nesting is not supported. | S |
|
||||
| FR-MD-05 | Hold **reorder point** and **reorder quantity** per item, optionally per warehouse. | M |
|
||||
| FR-MD-06 | Maintain **Vendor master**: code, name, contact, terms, tax reg, status, currency. | M |
|
||||
| FR-MD-07 | Maintain **Warehouse master** and, within each, a **bin/location** structure. | M |
|
||||
| FR-MD-08 | Prevent deletion of any master referenced by a transaction; deactivate instead. | M |
|
||||
| FR-MD-09 | Maintain **Brand master** (name, status); optionally referenced by an item. | S |
|
||||
| FR-MD-10 | Maintain **Item Type master** (name, status — e.g. Color, Size, Material) as a **selectable list only**: it feeds the item builder's dropdown and is **not** referenced by any item. Chosen values are encoded into the client-generated SKU, not stored (Part C.9). No product-variation model. | S |
|
||||
| FR-MD-11 | Maintain a singleton **Product Configuration** gating optional features. `subcategoriesEnabled`/`brandsEnabled` are **enforced server-side** — an item write carrying a gated field while its flag is off is rejected (`CONFIG_DISABLED`). `itemTypesEnabled` is **advisory** (frontend-honoured) since items hold no item-type reference. Reads are never gated. | S |
|
||||
|
||||
### B.3.2 Procurement (FR-PROC)
|
||||
| ID | Requirement | Pri |
|
||||
@@ -191,7 +194,7 @@ One base currency; invoicing/3-way match in Accounting (GRN carries data); users
|
||||
| FR-X-05 | No editing/deleting confirmed/posted transactions; corrections via reversing entries. | M |
|
||||
|
||||
## B.4 Data Model (summary)
|
||||
Key entities and relationships are enumerated in **Part C**. The commitment: Item, UOM/UOMConversion, Category, Vendor, Warehouse/Bin, ItemReorder, Requisition(+Line), RFQ(+Line)/VendorQuotation, PurchaseOrder(+Line), GRN(+Line), PurchaseReturn(+Line), StockLayer (FIFO), StockLedger (immutable), Batch, Serial, StockTransfer(+Line), StockAdjustment(+Line), StockCount(+Line), User, ReasonCode, NumberSequence, AuditLog, JournalEntryStub, and reserved RBAC (Role, Permission, UserRole, RolePermission).
|
||||
Key entities and relationships are enumerated in **Part C**. The commitment: Item, UOM/UOMConversion, Category, SubCategory, Brand, ItemType, ProductConfig, Vendor, Warehouse/Bin, ItemReorder, Requisition(+Line), RFQ(+Line)/VendorQuotation, PurchaseOrder(+Line), GRN(+Line), PurchaseReturn(+Line), StockLayer (FIFO), StockLedger (immutable), Batch, Serial, StockTransfer(+Line), StockAdjustment(+Line), StockCount(+Line), User, ReasonCode, NumberSequence, AuditLog, JournalEntryStub, and reserved RBAC (Role, Permission, UserRole, RolePermission).
|
||||
|
||||
## B.5 External Interfaces
|
||||
UI: responsive; count/pick screens handheld-friendly; status badges; mandatory-field validation. Hardware: barcode/QR (designed-for). Software: relational DB with transactional integrity + row locking; internal service interfaces/events for Phase-2+ modules.
|
||||
@@ -244,26 +247,41 @@ Adjustment: Damage, Theft/Loss, Count Variance, Expiry Write-off, System Correct
|
||||
| 8 | Costing method | **Resolved:** FIFO |
|
||||
| 9 | Tenancy | **Resolved:** single-tenant |
|
||||
| 10 | Authentication | **Resolved:** external **AuthHex** IdP (RS256; ERPCore validates only), **shadow-user** provisioning (`auth_user_id` GUID → local `int`), door-gated by ERP `UserType`/`Role`; per-endpoint RBAC deferred. *Open sub-item:* exact ERP `UserTypeCode`/`RoleCode` + RSA-key rotation process. |
|
||||
| 11 | Category hierarchy depth | **Resolved:** dedicated `SUBCATEGORY` table, exactly two levels; `CATEGORY.parent_id` dropped. Item carries both FKs (subcategory nullable). Arbitrary nesting is not coming back. |
|
||||
| 12 | Item types / variants | **Resolved:** the `ItemType` **enum** was replaced by an **unreferenced master list**; Stocked/NonStocked/Service survives as `stock_nature`. Values are **SKU-encoded only** — no value table, no item link, no product-variation model (Part C.9 records the accepted trade-off). |
|
||||
| 13 | Product-config authorization | **Open:** `PUT /product-config` is gated by the door policy only, like every other endpoint. A `CONFIG_MANAGE` permission is reserved for when per-endpoint RBAC lands (decision #6). Until then any ERP-admitted user can flip the flags. |
|
||||
|
||||
---
|
||||
|
||||
# Part C — ER Model (38 entities)
|
||||
# Part C — ER Model (42 entities)
|
||||
|
||||
Costing: FIFO · Multi-warehouse · Single-tenant. Legend: **PK** primary key · **FK** foreign key. Companion visual diagrams (Mermaid / draw.io ERD) accompany this repo; this part is the authoritative textual model.
|
||||
|
||||
## C.1 Master Data
|
||||
```
|
||||
CATEGORY(category_id PK, parent_id FK→CATEGORY, name)
|
||||
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
|
||||
UOM(uom_id PK, name)
|
||||
UOM_CONVERSION(conversion_id PK, item_id FK→ITEM, from_uom FK→UOM, to_uom FK→UOM, factor)
|
||||
ITEM(item_id PK, sku, name, category_id FK→CATEGORY, base_uom_id FK→UOM,
|
||||
default_vendor_id FK→VENDOR, item_type, tracking_mode, tax_class, status)
|
||||
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,
|
||||
default_vendor_id FK→VENDOR, stock_nature, tracking_mode, tax_class, status)
|
||||
ITEM_REORDER(reorder_id PK, item_id FK→ITEM, warehouse_id FK→WAREHOUSE, reorder_point, reorder_qty)
|
||||
VENDOR(vendor_id PK, code, name, terms, tax_reg, currency, status)
|
||||
WAREHOUSE(warehouse_id PK, code, name)
|
||||
BIN(bin_id PK, warehouse_id FK→WAREHOUSE, code, bin_type)
|
||||
PRODUCT_CONFIG(config_id PK [singleton = 1], subcategories_enabled, brands_enabled,
|
||||
item_types_enabled, updated_at, updated_by FK→USER) -- FR-MD-11 feature gate
|
||||
```
|
||||
|
||||
**Category hierarchy is exactly two levels.** `CATEGORY.parent_id` was removed (migration `AddBrandsSubcategoriesItemTypesAndProductConfig`); the optional level below a category is now `SUBCATEGORY`. An item carries **both** FKs — `category_id` required, `subcategory_id` nullable — and the service rejects a subcategory that does not belong to the given category (422).
|
||||
|
||||
**`ITEM_TYPE` is deliberately unreferenced** — see C.9.
|
||||
|
||||
**`stock_nature`** (Stocked/NonStocked/Service) is the former `item_type` column, renamed so the name could be taken by the `ITEM_TYPE` master. The two are unrelated concepts.
|
||||
|
||||
## C.2 Procurement
|
||||
```
|
||||
REQUISITION(requisition_id PK, doc_no, requested_by FK→USER, status, created_at)
|
||||
@@ -339,6 +357,10 @@ ROLE_PERMISSION(role_id FK→ROLE, permission_id FK→PERMISSION)
|
||||
```
|
||||
|
||||
## 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.
|
||||
- *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.
|
||||
- **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.
|
||||
- **Product config is a singleton, and only two of its flags are enforceable.** `subcategories_enabled` / `brands_enabled` gate item writes (`CONFIG_DISABLED`, 422). `item_types_enabled` is **advisory only** — since items carry no item-type reference, there is nothing on a write to reject; the frontend honours it by hiding the builder's type section. Reads are never gated, so existing data stays readable after a flag is switched off.
|
||||
- **FIFO = two structures.** `STOCK_LAYER` answers valuation ("what's on hand and at what cost"); `STOCK_LEDGER` answers history ("what moved, when, by whom"). Layers are keyed per item **per warehouse**.
|
||||
- **Polymorphic source.** `STOCK_LEDGER.source_doc_type/source_doc_id` (and `AUDIT_LOG`, `JOURNAL_ENTRY_STUB`) reference the originating document without a hard FK per type — new transaction types (Sales, Manufacturing) write to the ledger without a schema change.
|
||||
- **In-transit + cost-preserving transfer.** `STOCK_TRANSFER` holds `src`/`dest` warehouse; dispatch consumes source layers into in-transit, receive creates the destination layer at the **inherited** source cost.
|
||||
@@ -348,10 +370,10 @@ ROLE_PERMISSION(role_id FK→ROLE, permission_id FK→PERMISSION)
|
||||
- **Reorder alerts are a query**, not an entity — computed from `ITEM_REORDER` vs available. Add a table only if alert history is required.
|
||||
|
||||
## C.10 Entity → implementation mapping
|
||||
- Entities → `Domain/Entities`; enums (`ItemType`, `TrackingMode`, `HoldStatus`, `Direction`, `*Status`, `CountType`) → `Domain/Enums`.
|
||||
- Entities → `Domain/Entities`; enums (`StockNature`, `TrackingMode`, `HoldStatus`, `Direction`, `*Status`, `CountType`) → `Domain/Enums`. **Note:** `ItemType` in `Domain/Entities` is the master entity; the old `ItemType` **enum** is now `StockNature` — there is no enum by that name.
|
||||
- EF configurations (`IEntityTypeConfiguration<T>`, one per entity) → `Infra/Persistence/Configurations`.
|
||||
- FIFO logic → `Services/Stock/FifoCostingService` (Part A.2). Ledger writes only via stock services inside the UoW transaction.
|
||||
- `RowVersion` (concurrency) on mutable aggregates: Item, Vendor, PurchaseOrder, GRN, transfers/adjustments/counts headers.
|
||||
- `RowVersion` (concurrency) on mutable aggregates: Item, Category, SubCategory, Brand, ItemType, ProductConfig, Vendor, PurchaseOrder, GRN, transfers/adjustments/counts headers.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+149
-14
@@ -105,38 +105,45 @@ session-issuing responses omit `AccessToken`/`RefreshToken` (cookie-delivered in
|
||||
`erp_rt` cookie rather than the request body.
|
||||
|
||||
### 2.1 Items
|
||||
> **`itemType` → `stockNature` (2026-07-16).** The Stocked/NonStocked/Service field was renamed so the name `itemType` could be taken by the new Item Type master (§2.7) — an unrelated concept. Items gained `subCategoryId` and `brandId` (both nullable). Items carry **no** item-type reference: the values chosen in the builder are encoded into the client-generated SKU (docs/10 Part C.9).
|
||||
|
||||
#### `GET /items`
|
||||
Query: `q`, `status` (`Active|Inactive`), `categoryId`, `trackingMode` (`None|Batch|Serial`), + paging.
|
||||
Query: `q`, `status` (`Active|Inactive`), `categoryId`, `subCategoryId`, `brandId`, `trackingMode` (`None|Batch|Serial`), + paging.
|
||||
**200 OK**
|
||||
```json
|
||||
{ "items": [ { "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
|
||||
"categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD", "status": "Active" } ],
|
||||
"categoryId": 12, "subCategoryId": 30, "brandId": 2, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"stockNature": "Stocked", "trackingMode": "Batch", "taxClass": "STD", "status": "Active" } ],
|
||||
"pagination": { "page": 1, "pageSize": 20, "totalItems": 1, "totalPages": 1 } }
|
||||
```
|
||||
|
||||
#### `GET /items/{itemId}` → **200 OK** (header `ETag: "AAAAAAAAB9E="`)
|
||||
```json
|
||||
{ "itemId": 1001, "sku": "ITM-1001", "name": "Steel Bolt M8x40",
|
||||
"description": "Grade 8.8 zinc-plated hex bolt", "categoryId": 12, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
|
||||
"description": "Grade 8.8 zinc-plated hex bolt", "categoryId": 12, "subCategoryId": 30,
|
||||
"brandId": 2, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "stockNature": "Stocked", "trackingMode": "Batch", "taxClass": "STD",
|
||||
"status": "Active", "reorder": [ { "warehouseId": 1, "reorderPoint": 500, "reorderQty": 2000 } ],
|
||||
"createdAt": "2026-06-01T08:00:00Z", "updatedAt": "2026-07-01T10:15:00Z" }
|
||||
```
|
||||
|
||||
#### `POST /items`
|
||||
The `sku` is **generated by the client** (it encodes the chosen item-type values, e.g. `BL-100-0003`); the server only enforces uniqueness. `subCategoryId`/`brandId` are optional.
|
||||
```json
|
||||
{ "sku": "ITM-1002", "name": "Steel Nut M8", "description": "Grade 8 zinc-plated hex nut",
|
||||
"categoryId": 12, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"itemType": "Stocked", "trackingMode": "None", "taxClass": "STD" }
|
||||
"categoryId": 12, "subCategoryId": 30, "brandId": 2, "baseUomId": 1, "defaultVendorId": 5,
|
||||
"stockNature": "Stocked", "trackingMode": "None", "taxClass": "STD" }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/items/1002`
|
||||
```json
|
||||
{ "itemId": 1002, "sku": "ITM-1002", "name": "Steel Nut M8", "categoryId": 12, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "itemType": "Stocked", "trackingMode": "None", "taxClass": "STD",
|
||||
{ "itemId": 1002, "sku": "ITM-1002", "name": "Steel Nut M8", "categoryId": 12,
|
||||
"subCategoryId": 30, "brandId": 2, "baseUomId": 1,
|
||||
"defaultVendorId": 5, "stockNature": "Stocked", "trackingMode": "None", "taxClass": "STD",
|
||||
"status": "Active", "createdAt": "2026-07-07T09:30:00Z" }
|
||||
```
|
||||
`400` → `code: SKU_DUPLICATE` if SKU exists.
|
||||
`422` → `code: CONFIG_DISABLED` if `subCategoryId` is sent while subcategories are disabled, or `brandId` while brands are disabled (§2.8).
|
||||
`422` → validation error if the subcategory does not belong to `categoryId`, or if a referenced subcategory/brand/vendor is missing or inactive.
|
||||
|
||||
#### `PUT /items/{itemId}`
|
||||
Full update; requires `If-Match`. → **200 OK** updated resource; `412` on ETag mismatch.
|
||||
@@ -171,13 +178,58 @@ Full update; requires `If-Match`. → **200 OK** updated resource; `412` on ETag
|
||||
"conversions": [ { "conversionId": 33, "fromUom": 7, "toUom": 1, "factor": 12 } ] }
|
||||
```
|
||||
|
||||
### 2.3 Categories
|
||||
### 2.3 Categories & Subcategories
|
||||
> **Two-level hierarchy (2026-07-16).** Categories no longer self-nest: `parentId` and `GET /categories?tree=true` are **gone**, replaced by a dedicated Subcategory resource one level below. Categories also gained `status` + an `ETag` (they previously had neither, so there was no update path at all).
|
||||
|
||||
#### `GET /categories`
|
||||
Query: `q`, `status` (`Active|Inactive`), + paging. **200 OK** → list envelope of `CategoryDto`.
|
||||
|
||||
#### `GET /categories/{categoryId}` → **200 OK** (+ `ETag`); `404` if absent.
|
||||
```json
|
||||
{ "categoryId": 12, "name": "Fasteners", "status": "Active",
|
||||
"createdAt": "2026-06-01T08:00:00Z", "updatedAt": null }
|
||||
```
|
||||
|
||||
#### `POST /categories`
|
||||
```json
|
||||
{ "name": "Fasteners", "parentId": 3 }
|
||||
{ "name": "Fasteners" }
|
||||
```
|
||||
**201 Created** → `{ "categoryId": 12, "name": "Fasteners", "parentId": 3 }`
|
||||
`GET /categories?tree=true` returns a nested tree.
|
||||
**201 Created** → `{ "categoryId": 12, "name": "Fasteners", "status": "Active", "createdAt": "...", "updatedAt": null }`
|
||||
`409` if the name already exists (names are unique, case-insensitive).
|
||||
|
||||
#### `PUT /categories/{categoryId}`
|
||||
Requires `If-Match`. → **200 OK**; `412` on ETag mismatch; `409` on duplicate name.
|
||||
|
||||
#### `PATCH /categories/{categoryId}/status`
|
||||
```json
|
||||
{ "status": "Inactive" }
|
||||
```
|
||||
**204 No Content**. Deactivate, never delete (FR-MD-08).
|
||||
|
||||
#### `GET /categories/{categoryId}/subcategories`
|
||||
Query: `q`, `status`, + paging. **200 OK** → list envelope of `SubCategoryDto`; `404` if the category itself is absent.
|
||||
|
||||
#### `POST /categories/{categoryId}/subcategories`
|
||||
The parent comes from the route.
|
||||
```json
|
||||
{ "name": "Hex Bolts" }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/subcategories/30`
|
||||
```json
|
||||
{ "subCategoryId": 30, "categoryId": 12, "name": "Hex Bolts", "status": "Active",
|
||||
"createdAt": "2026-07-16T09:00:00Z", "updatedAt": null }
|
||||
```
|
||||
`404` if the category does not exist · `422` if it is inactive · `409` if the name already exists **within that category** (names need only be unique per parent).
|
||||
|
||||
#### `GET /subcategories/{subCategoryId}` → **200 OK** (+ `ETag`); `404` if absent.
|
||||
|
||||
#### `PUT /subcategories/{subCategoryId}`
|
||||
Requires `If-Match`. **Name only** — a subcategory cannot be moved to another category, since that would silently invalidate the `categoryId` of every item referencing it. → **200 OK**; `412` on mismatch.
|
||||
```json
|
||||
{ "name": "Hex Bolts (metric)" }
|
||||
```
|
||||
|
||||
#### `PATCH /subcategories/{subCategoryId}/status` → **204 No Content**.
|
||||
|
||||
### 2.4 Vendors
|
||||
#### `POST /vendors`
|
||||
@@ -206,6 +258,88 @@ Full update; requires `If-Match`. → **200 OK** updated resource; `412` on ETag
|
||||
**201 Created** → `{ "binId": 45, "warehouseId": 1, "code": "A-01-01", "binType": "Shelf" }`
|
||||
`GET /warehouses/{warehouseId}/bins` lists bins.
|
||||
|
||||
### 2.6 Brands
|
||||
Referenced optionally by `Item.brandId`. Rejected on item writes when brands are disabled (§2.8).
|
||||
|
||||
#### `GET /brands`
|
||||
Query: `q`, `status` (`Active|Inactive`), + paging. **200 OK** → list envelope of `BrandDto`.
|
||||
|
||||
#### `GET /brands/{brandId}` → **200 OK** (+ `ETag`); `404` if absent.
|
||||
|
||||
#### `POST /brands`
|
||||
```json
|
||||
{ "name": "Bosch" }
|
||||
```
|
||||
**201 Created** — `Location: /api/v1/brands/2`
|
||||
```json
|
||||
{ "brandId": 2, "name": "Bosch", "status": "Active",
|
||||
"createdAt": "2026-07-16T09:00:00Z", "updatedAt": null }
|
||||
```
|
||||
`409` if the name already exists (unique, case-insensitive).
|
||||
|
||||
#### `PUT /brands/{brandId}`
|
||||
Requires `If-Match`. → **200 OK**; `412` on mismatch; `409` on duplicate name.
|
||||
|
||||
#### `PATCH /brands/{brandId}/status` → **204 No Content**. Deactivate, never delete (FR-MD-08).
|
||||
|
||||
### 2.7 Item Types
|
||||
> **Read this before assuming a relationship exists.** An item type is a *dimension name* (Color, Size, Material) and nothing more. **No item references an item type**, and there is no value resource: the values chosen in the frontend builder (Red, S, M) are encoded into the **client-generated SKU** — `BL-0002` for one type, `BL-100-0003` for two — and are never stored or parsed server-side. `GET /item-types` exists to populate the builder's dropdown; that is the entire purpose of this master. Consequently the API cannot filter items by colour/size, and renaming an item type does not alter any existing SKU. See docs/10 Part C.9 for the recorded trade-off. Not to be confused with `stockNature` (§2.1), which is what the old `itemType` enum became.
|
||||
|
||||
#### `GET /item-types`
|
||||
Query: `q`, `status` (`Active|Inactive`), + paging. Pass `status=Active` for selectable rows.
|
||||
**200 OK**
|
||||
```json
|
||||
{ "items": [ { "itemTypeId": 1, "name": "Color", "status": "Active",
|
||||
"createdAt": "2026-07-16T09:00:00Z", "updatedAt": null },
|
||||
{ "itemTypeId": 2, "name": "Size", "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`).
|
||||
|
||||
#### `GET /item-types/{itemTypeId}` → **200 OK** (+ `ETag`); `404` if absent.
|
||||
|
||||
#### `POST /item-types`
|
||||
```json
|
||||
{ "name": "Material" }
|
||||
```
|
||||
**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.
|
||||
|
||||
#### `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.
|
||||
|
||||
#### `PATCH /item-types/{itemTypeId}/status` → **204 No Content**. Deactivate, never delete (FR-MD-08).
|
||||
|
||||
### 2.8 Product Configuration
|
||||
A **singleton** feature gate (FR-MD-11), seeded with every flag `true`.
|
||||
|
||||
**Enforcement is not uniform, by design:**
|
||||
|
||||
| Flag | Enforced? | Effect when `false` |
|
||||
|---|---|---|
|
||||
| `subcategoriesEnabled` | **Server-side** | `POST`/`PUT /items` with a non-null `subCategoryId` → `422 CONFIG_DISABLED` |
|
||||
| `brandsEnabled` | **Server-side** | `POST`/`PUT /items` with a non-null `brandId` → `422 CONFIG_DISABLED` |
|
||||
| `itemTypesEnabled` | **Advisory only** | Nothing server-side. Items carry no item-type reference (§2.7), so there is nothing on a write to reject — the frontend honours it by hiding the builder's type section. |
|
||||
|
||||
Reads are **never** gated: switching a flag off leaves existing items readable with their subcategory/brand intact.
|
||||
|
||||
#### `GET /product-config` → **200 OK** (+ `ETag`)
|
||||
```json
|
||||
{ "subcategoriesEnabled": true, "brandsEnabled": true, "itemTypesEnabled": true,
|
||||
"updatedAt": "2026-07-16T10:00:00Z", "updatedBy": 17 }
|
||||
```
|
||||
|
||||
#### `PUT /product-config`
|
||||
Requires `If-Match`. All three flags are **required** — a partial body is a `400`, so a feature can never be switched off by omission. `updatedBy` is derived from the token, never posted.
|
||||
```json
|
||||
{ "subcategoriesEnabled": false, "brandsEnabled": true, "itemTypesEnabled": true }
|
||||
```
|
||||
**200 OK** → the updated resource; `412` on ETag mismatch.
|
||||
|
||||
> **Authorization:** writes are admitted by the ERP door policy only — any authenticated ERP user may flip these flags. A `CONFIG_MANAGE` permission is **reserved** for when per-endpoint RBAC lands (FR-X-01, deferred); no schema or route change will be needed to enable it. Tracked as open decision #13 in docs/10 §B.8.4.
|
||||
|
||||
---
|
||||
|
||||
## 3. Procurement
|
||||
@@ -471,6 +605,7 @@ Items at/below ROP (FR-STK-10); computed on read, no stored entity.
|
||||
| `EXPIRED_BATCH_BLOCKED` | 409 | Issue/pick of an expired batch. |
|
||||
| `ONHOLD_NOT_ISSUABLE` | 409 | Issue against on-hold/quarantined stock. |
|
||||
| `REASON_CODE_REQUIRED` | 400 | Adjustment/return without a reason code. |
|
||||
| `CONFIG_DISABLED` | 422 | An item write carries a field whose feature is switched off in the product configuration (`subCategoryId` with subcategories disabled, `brandId` with brands disabled). See §2.8. |
|
||||
| `CONCURRENCY_CONFLICT` | 412 | ETag / RowVersion mismatch. |
|
||||
| `IDEMPOTENCY_REPLAY` | 200 | Duplicate `Idempotency-Key`; original result returned. |
|
||||
|
||||
@@ -488,7 +623,7 @@ Example (`409`, `application/problem+json`):
|
||||
## 8. Enumerations
|
||||
| Enum | Values |
|
||||
|---|---|
|
||||
| `itemType` | `Stocked`, `NonStocked`, `Service` |
|
||||
| `stockNature` | `Stocked`, `NonStocked`, `Service` — **renamed from `itemType`** (2026-07-16). Item *types* (Color/Size/Material) are now master **data**, not an enum: see §2.7. |
|
||||
| `trackingMode` | `None`, `Batch`, `Serial` |
|
||||
| `holdStatus` | `Available`, `OnHold`, `Rejected` |
|
||||
| `direction` (ledger) | `In`, `Out` |
|
||||
|
||||
+16
-3
@@ -126,9 +126,22 @@ Each screen calls the endpoints in `11-BACKEND-PHASE1.md`. System steps (blue) a
|
||||
| Count | Count | `POST /stock-counts`, `PUT /stock-counts/{id}/counts`, `POST /stock-counts/{id}/post` |
|
||||
|
||||
### 2.2 Master data screens (supporting, outside the core flow)
|
||||
Vendors, Items, Categories, UOM, Warehouses, Brands, and Variant Categories are supporting master-data CRUD screens the flow above depends on but doesn't itself route through, so they're intentionally absent from the diagram/table. List screens follow one pagination convention: `page`/`pageSize`/`q`/`sortOrder` params, page size 5, debounced search, Previous/Next controls.
|
||||
Vendors, Items, Categories, Subcategories, UOM, Warehouses, Brands, and Item Types are supporting master-data CRUD screens the flow above depends on but doesn't itself route through, so they're intentionally absent from the diagram/table. List screens follow one pagination convention: `page`/`pageSize`/`q`/`sortOrder` params, page size 5, debounced search, Previous/Next controls.
|
||||
|
||||
**Brand** (`app/dashboard/products/brands`) and **Variant Category** (`app/dashboard/products/variants`) are UI-only additions with no corresponding endpoint in `11-BACKEND-PHASE1.md` — Item's `brandId` is built the same way. The Item variant builder on `/dashboard/products/new` reads the Variant Category list live: checking a category (Color, Size, or any custom one added inline from that same page) reveals a value-entry section for it, and one Item is auto-created per combination across however many categories are checked, with an auto-generated SKU. Flag Brand/Variant Category to whoever owns the backend contract if they should become real entities rather than staying frontend-only; see `Frontend/PROGRESS.md` (2026-07-15 entries) for the full rationale and discarded design iterations.
|
||||
> **2026-07-16 — these are real backend entities now; the UI has NOT caught up.** Brand, Subcategory, Item Type (the frontend's "Variant Categories") and a Product Configuration gate were built on the backend (`docs/11 §2.3/2.6/2.7/2.8`). The screens below still run on `lib/api/mock-data.ts` and do not call any of it. Reconciling them is outstanding frontend work — the contract drift is listed in §2.2.1.
|
||||
|
||||
**Brand** (`app/dashboard/products/brands`) and **Variant Category** (`app/dashboard/products/variants`) began as UI-only additions with no backend. The Item variant builder on `/dashboard/products/new` reads the Variant Category list live: checking a category (Color, Size, or any custom one added inline from that same page) reveals a value-entry section for it, and one Item is auto-created per combination across however many categories are checked, with an auto-generated SKU. See `Frontend/PROGRESS.md` (2026-07-15 entries) for the full rationale and discarded design iterations.
|
||||
|
||||
#### 2.2.1 Contract drift to reconcile (backend is authoritative — §1)
|
||||
- **`variantCategoriesApi` → `GET /item-types`.** Same shape (name-only list), new name. `variantCategoryId` → `itemTypeId`.
|
||||
- **`Item.itemType` → `stockNature`.** The `Stocked|NonStocked|Service` field was renamed. `itemType` now means something else entirely (Color/Size), so this rename is not cosmetic — read `docs/11 §2.7` before touching it.
|
||||
- **Send both category FKs.** `effectiveCategoryId = subCategoryId ?? categoryId` must become `categoryId` **and** `subCategoryId`; the server rejects a subcategory that doesn't belong to the category (422). Subcategories are their own resource now, not `Category.parentId`, and **`GET /categories?tree=true` no longer exists**.
|
||||
- **Colour hex-packing stays frontend-only.** There is no value table, so `"Red|#EF4444"`, `encodeColorValue`/`decodeColorValue`/`isColorCategory` have nothing to reconcile against — keep them.
|
||||
- **SKU generation stays client-side** (`buildVariantSku`) and is now the *only* record of which colour/size an item is; the server only uniqueness-checks it. Nothing can query items by colour.
|
||||
- **`remove()` must become `PATCH /{id}/status`.** There are no `DELETE` endpoints on any master (FR-MD-08) — the mock's unconditional delete has no backend equivalent.
|
||||
- **`initialQty` remains unbacked** — no Stock Core wiring; still informational-only.
|
||||
- **New: Product Configuration** (`GET`/`PUT /product-config`) gates subcategories/brands/item-types. This is the backend for the toggle screen; note **only 3 of that design's ~13 toggles exist**, and `itemTypesEnabled` is advisory — the frontend is what honours it (`docs/11 §2.8`). No `Switch` primitive exists in `components/ui/` yet.
|
||||
- **Non-transactional create loop:** the builder's per-row `itemsApi.create()` has no transaction — a `SKU_DUPLICATE` on row 7 of 12 leaves 6 items created. Real HTTP calls will make this failure mode visible in a way mock data never did.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,7 +155,7 @@ Vendors, Items, Categories, UOM, Warehouses, Brands, and Variant Categories are
|
||||
- Format: SKU pattern, numeric fields numeric, date format, positive integers.
|
||||
- Range/bounds: `qty > 0`, `unitPrice >= 0`, `factor > 0`.
|
||||
- Simple cross-field input rules: transfer `destWarehouseId != srcWarehouseId`.
|
||||
- Enum membership via constrained dropdowns (`itemType`, `trackingMode`, `countType`, `holdStatus`).
|
||||
- Enum membership via constrained dropdowns (`stockNature` — ex-`itemType`, `trackingMode`, `countType`, `holdStatus`). Note the **Item Type** dropdown is *not* in this category: it's server data (`GET /item-types`), not an enum.
|
||||
|
||||
**Server-authoritative (client MUST NOT assume — only the server can judge):** anything depending on current server state.
|
||||
- **Stock availability / negative-stock block** (depends on live ledger) — `STOCK_NEGATIVE_BLOCKED`.
|
||||
|
||||
Reference in New Issue
Block a user