completed invetory updates

This commit is contained in:
2026-07-17 00:23:33 +05:30
parent 7c5faabc2d
commit f72b24fcaa
47 changed files with 4849 additions and 136 deletions
+149 -14
View File
@@ -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` |