item mng with sync

This commit is contained in:
Kalana B. Thilakarathna
2026-08-19 12:49:04 +05:30
parent 45f374133f
commit 397af8fd59
42 changed files with 5191 additions and 16 deletions
+235 -7
View File
@@ -1,52 +1,280 @@
# 01 — Item Catalog
**Status**: Not Started
**Depends On**: 00 — Existing Gap Fixes
**Status**: Ready
**Depends On**: 00 — Existing Gap Fixes (gaps 1 & 2 done)
---
## Goal
Define and implement a full item catalog with reference masters (Category, SubCategory, Brand, UOM)
and the Item entity itself. Support both standalone clients (full CRUD) and ERP-integrated clients
(sync endpoint that upserts from ERP). Everything downstream — stock ledger, orders, rides — reads
from the local item table regardless of how items got there.
---
## Background / Context
Two client types:
- **Standalone** — client manages their own catalog. Creates categories, brands, UOMs, and items
directly via CRUD APIs. `SyncSource = Manual`.
- **ERP + Distribution** — ERP pushes data into Distribution via sync endpoints. CRUD write
endpoints are blocked for synced records. `SyncSource = Erp`.
ERP uses `int` PKs. Distribution uses `Guid` PKs. Each reference master and item carries an
`ExternalErpId` (nullable int) so the sync can match and upsert by ERP ID and resolve local Guids.
Sync order is strict — ERP must push in this order:
1. UOMs
2. Categories + SubCategories
3. Brands
4. Items (sync resolves local Guid FKs via ExternalErpId lookups)
---
## Data Model Changes
### Item
### Category
```
Category
├── Id (Guid) — PK, from Template
├── Name (string)
├── ExternalErpId (int?) — null for manually created
├── IsActive, CreatedAt — from Template
```
### SubCategory
```
SubCategory
├── Id (Guid) — PK, from Template
├── Name (string)
├── FK_CategoryId (Guid) — required, parent Category
├── ExternalErpId (int?)
├── IsActive, CreatedAt — from Template
```
Rule: SubCategory must belong to the CategoryId it is assigned to. A subcategory cannot be
moved to a different category after creation.
### Brand
```
Brand
├── Id (Guid) — PK, from Template
├── Name (string)
├── ExternalErpId (int?)
├── IsActive, CreatedAt — from Template
```
### Related Changes to Existing Models
### Uom (Unit of Measure)
```
Uom
├── Id (Guid) — PK, from Template
├── Name (string) — e.g. "PC", "CASE", "KG", "BOTTLE"
├── ExternalErpId (int?)
├── IsActive, CreatedAt — from Template
```
### UomConversion
```
UomConversion
├── Id (Guid) — PK, from Template
├── FK_ItemId (Guid) — which item this conversion belongs to
├── FK_FromUomId (Guid) — source UOM
├── FK_ToUomId (Guid) — target UOM
├── Factor (decimal) — FromQty × Factor = ToQty
├── IsActive, CreatedAt — from Template
```
Example: Item = "Coca-Cola 330ml", FromUom = CASE, ToUom = BOTTLE, Factor = 24
→ 1 CASE × 24 = 24 BOTTLES
### Item (replaces empty Item.cs)
```
Item
├── Id (Guid) — PK, from Template
├── Sku (string, unique) — field identifier, unique across all items
├── Name (string)
├── Description (string?)
├── FK_CategoryId (Guid) — required
├── FK_SubCategoryId (Guid?) — optional
├── FK_BrandId (Guid?) — optional
├── FK_BaseUomId (Guid) — required, base unit for stock counting
├── SalePrice (decimal?) — NULL = price entered manually at order time
├── SyncSource (enum) — Manual | Erp
├── ExternalErpItemId (int?) — null for standalone items
├── LastSyncedAt (DateTime?) — null for standalone items
├── IsActive, CreatedAt — from Template
```
### OrderItem — add UnitPrice
```
OrderItem (existing, add one column)
├── UnitPrice (decimal) — price captured at time of order, not looked up live
```
This is a migration change on an existing table.
---
## Migrations
Two migrations needed:
1. **AddItemCatalogReferenceMasters** — creates Category, SubCategory, Brand, Uom, UomConversion tables
2. **UpdateItemAndOrderItem** — adds columns to existing Items table, adds UnitPrice to OrderItems
---
## API Endpoints
### Categories
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/categories | categories.view | Returns all active, includes subcategory count |
| GET | /api/categories/{id} | categories.view | Includes subcategories list |
| POST | /api/categories | categories.create | Standalone only |
| PUT | /api/categories/{id} | categories.update | Blocked if SyncSource = Erp |
| DELETE | /api/categories/{id} | categories.delete | Soft delete, blocked if items reference it |
### SubCategories
| Method | Route | Permission | Notes |
|---|---|---|---|
| POST | /api/categories/{categoryId}/subcategories | categories.create | Validates parent exists |
| PUT | /api/subcategories/{id} | categories.update | Blocked if SyncSource = Erp |
| DELETE | /api/subcategories/{id} | categories.delete | Blocked if items reference it |
### Brands
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/brands | items.view | |
| POST | /api/brands | items.create | Standalone only |
| PUT | /api/brands/{id} | items.update | Blocked if SyncSource = Erp |
| DELETE | /api/brands/{id} | items.delete | Blocked if items reference it |
### UOMs
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/uoms | items.view | |
| POST | /api/uoms | items.create | Standalone only |
| PUT | /api/uoms/{id} | items.update | Blocked if SyncSource = Erp |
| DELETE | /api/uoms/{id} | items.delete | Blocked if items reference it |
### Items
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/items | items.view | Filterable by category, brand, subcategory |
| GET | /api/items/{id} | items.view | Includes UOM conversions |
| POST | /api/items | items.create | Validates SKU unique, FK references exist |
| PUT | /api/items/{id} | items.update | Blocked if SyncSource = Erp |
| DELETE | /api/items/{id} | items.delete | Blocked if stock exists for item |
| PUT | /api/items/{id}/uom-conversions | items.update | Replace full conversion list |
### Sync (ERP → Distribution)
| Method | Route | Auth | Notes |
|---|---|---|---|
| POST | /api/sync/uoms | JWT (service user) | Upsert by ExternalErpId |
| POST | /api/sync/categories | JWT (service user) | Upsert by ExternalErpId, includes subcategories |
| POST | /api/sync/brands | JWT (service user) | Upsert by ExternalErpId |
| POST | /api/sync/items | JWT (service user) | Upsert by ExternalErpItemId, resolves local FK Guids via ExternalErpId |
---
## Business Rules
1. SKU must be unique across all items.
2. SubCategory must belong to the specified CategoryId — cannot assign a subcategory from a
different parent category.
3. Items with `SyncSource = Erp` cannot be modified via standard CRUD endpoints (PUT/DELETE).
Only the sync endpoint can update them.
4. Reference masters (Category, Brand, UOM) with `SyncSource = Erp` cannot be modified via
standard CRUD endpoints.
5. A Category cannot be deleted if any SubCategory or Item references it.
6. A SubCategory cannot be deleted if any Item references it.
7. A Brand cannot be deleted if any Item references it.
8. A UOM cannot be deleted if any Item uses it as BaseUom or if any UomConversion references it.
9. An Item cannot be deleted (soft delete blocked) if stock exists for it. Hard delete never
allowed.
10. `UnitPrice` on OrderItem is captured at the moment the order is created from `Item.SalePrice`.
If `SalePrice` is null, the caller must supply the price explicitly.
---
## Permissions to Add to Seed
New permission codes to add to the existing seed:
```
items.view, items.create, items.update, items.delete
categories.view, categories.create, categories.update, categories.delete
```
Role assignments:
- **SalesRep (10)**: items.view, categories.view
- **Supervisor (20)**: items.view, categories.view
- **AreaManager (30)**: items.view, categories.view, items.create, items.update
- **AreaSupervisor (40)**: all item + category permissions
- **RegionalManager (50)**: all item + category permissions
- **RegionalAgency (60)**: all item + category permissions
---
## Open Questions
- None. Resolved in planning discussion.
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
### File structure to create
```
Features/
Category/
CategoryController.cs
CategoryDTOs.cs
CategoryService.cs
ICategoryRepository.cs
CategoryRepository.cs
Brand/
BrandController.cs
BrandDTOs.cs
BrandService.cs
IBrandRepository.cs
BrandRepository.cs
Uom/
UomController.cs
UomDTOs.cs
UomService.cs
IUomRepository.cs
UomRepository.cs
Item/
ItemController.cs
ItemDTOs.cs
ItemService.cs
IItemRepository.cs
ItemRepository.cs
Sync/
SyncController.cs
SyncDTOs.cs
SyncService.cs
Models/
Category.cs (new)
SubCategory.cs (new)
Brand.cs (new)
Uom.cs (new)
UomConversion.cs (new)
Item.cs (replace empty)
Migrations/
AddItemCatalogReferenceMasters
UpdateItemAndOrderItem
```
### Sync upsert logic (pseudo)
```
POST /api/sync/items — for each item in payload:
1. Lookup local Category by ExternalErpId = item.categoryId → get local CategoryGuid
2. Lookup local SubCategory by ExternalErpId = item.subCategoryId (if present)
3. Lookup local Brand by ExternalErpId = item.brandId (if present)
4. Lookup local Uom by ExternalErpId = item.baseUomId → get local UomGuid
5. Find existing Item where ExternalErpItemId = item.itemId
→ if found: update fields, set LastSyncedAt = now
→ if not found: insert new, SyncSource = Erp
```
+1 -1
View File
@@ -35,7 +35,7 @@ They must be resolved before new feature steps begin because later steps depend
| # | Feature | Status | Depends On | Detail File |
|---|---|---|---|---|
| 00 | Existing Gap Fixes | Not Started | — | [00_existing_gaps.md](./00_existing_gaps.md) |
| 01 | Item Catalog | Not Started | 00 | [01_item_catalog.md](./01_item_catalog.md) |
| 01 | Item Catalog | Done | 00 | [01_item_catalog.md](./01_item_catalog.md) |
| 02 | Stock Ledger | Not Started | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
| 03 | Ride Lifecycle | Not Started | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
| 04 | Loading Event | Not Started | 03 | [04_loading_event.md](./04_loading_event.md) |