completed invetory updates
This commit is contained in:
+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` |
|
||||
|
||||
Reference in New Issue
Block a user