Update pricing and store implementation plans

This commit is contained in:
2026-08-20 17:29:11 +05:30
parent 63bc6e9cd8
commit b2681d1236
2 changed files with 326 additions and 42 deletions
+309 -26
View File
@@ -1,7 +1,7 @@
# 14 — Pricing & Supplier Master Data # 14 — Pricing & Supplier Master Data
**Status**: Not Started **Status**: Done — migration applied to the database
**Depends On**: 01 — Item Catalog **Depends On**: 01 — Item Catalog (Done)
--- ---
@@ -15,56 +15,339 @@ records. `createProduct`/`deactivateSku` live in 01; this step is everything els
## Background / Context ## Background / Context
`Item.BasePrice` (from [01_item_catalog.md](./01_item_catalog.md)) is a single flat price. There is `Item.SalePrice` (from [01_item_catalog.md](./01_item_catalog.md)) is a single flat, optional price
no concept of a region- or date-scoped price list, and no `Supplier` entity anywhere in the schema. — `OrderItem.UnitPrice` is captured from it at order-creation time, or supplied manually if
`SalePrice` is null. There is no concept of a region- or date-scoped price list, and no `Supplier`
entity anywhere in the schema.
`applyPricingAndDiscounts` ([mainPlan.md §1](./mainPlan.md#1-order-management-lifecycle)) is the
function that resolves the actual price for an order line; this step implements the price-list half
of it (region + date scoping). The discount/scheme half stays out of scope until
[17_promotions_schemes.md](./17_promotions_schemes.md) lands — until then `applyPricingAndDiscounts`
only walks price lists, never scheme discounts.
Resolved in planning discussion (2026-08-20): price lists are **region-only in v1**, not
segment-scoped. `Store.Segment` ([12_store_management.md](./12_store_management.md)) is not
consulted by pricing. Segment-based pricing can be added as a later step without breaking this
model, since it would only add an optional new scoping column.
### Negotiable price range (Min/Max)
An item's price within a price list can float in a band instead of being fixed: the same actor who
creates the price list (`pricing.create`/`pricing.update`, AreaManager and above) sets both a
**ceiling** (`Price` — the list/default price) and an optional **floor** (`MinPrice`) below which no
order for that item may be priced. The sales rep/driver placing the order can then supply any
`UnitPrice` within `[MinPrice, Price]` on `OrderItemRequest` instead of always taking the resolved
default — the same negotiation-boundary pattern already used for discount limits in
[08_sales_agent_controls.md](./08_sales_agent_controls.md).
- `PriceListItem.MinPrice (decimal?)` — `null` = no negotiation, the item is fixed at `Price`.
- `PUT /api/price-lists/{id}/items/{sku}` accepts `{ "price": ..., "minPrice": ... }`; rejected with
409 if `MinPrice > Price`.
- `OrderService.CreateAsync` validates any caller-supplied `OrderItemRequest.UnitPrice` against
`[MinPrice ?? Price, Price]` for that item's resolved price (region-scoped list → global list;
the `Item.SalePrice` fallback has no `MinPrice` and is always fixed). The order is rejected
(400, via a null resolution) if the requested price falls outside the band, or if no price can be
resolved at all.
--- ---
## Data Model Changes ## Data Model Changes
### Price List ### PriceList
```
PriceList
├── Id (Guid) — PK, from Template
├── Name (string)
├── FK_RegionId (Guid?) — nullable, null = global (applies to all regions)
├── EffectiveFrom (DateTime)
├── EffectiveTo (DateTime)
├── IsActive, CreatedAt — from Template
```
`Name`, `RegionId` (nullable — null means global), `EffectiveFrom`, `EffectiveTo`. ### PriceListItem
```
### Price List Item PriceListItem
├── Id (Guid) — PK, from Template
`PriceListId`, `ItemId`, `Price` — overrides `Item.BasePrice` when an applicable, currently- ├── FK_PriceListId (Guid) — required
effective price list exists for the order's region. ├── FK_ItemId (Guid) — required
├── Price (decimal) — ceiling/list price, overrides Item.SalePrice for this item while
│ the parent PriceList is active and applicable to the order's region
├── MinPrice (decimal?) — nullable floor for negotiated pricing; null = Price is fixed.
│ Must be <= Price when set.
├── IsActive, CreatedAt — from Template
```
Unique constraint on `(PriceListId, ItemId)` — one price per item per price list.
### Supplier ### Supplier
```
Supplier
├── Id (Guid) — PK, from Template
├── Name (string)
├── ContactInfo (string?) — free text (phone/email/address), matches ContactInfo pattern
│ used elsewhere rather than splitting into separate columns
├── PerformanceScore (decimal?) — nullable, populated by 15_procurement.md's evaluateSupplier;
│ this step only defines the column and leaves it null-only
├── IsActive, CreatedAt — from Template
```
`Name`, `ContactInfo`, `PerformanceScore` (nullable, populated by ---
[15_procurement.md](./15_procurement.md)'s `evaluateSupplier`).
## Migrations
One migration needed:
1. **AddPricingAndSuppliers** — creates `PriceLists`, `PriceListItems`, `Suppliers` tables.
--- ---
## API Endpoints ## API Endpoints
| Function reference | Endpoint intent | > Permission column names the target policy for each route; enforcement is **not implemented yet**
|---|---| > — see [Permissions to Add to Seed](#permissions-to-add-to-seed). Endpoints require
| `createPriceList(name, region, effectiveDates)` | `POST /api/price-lists` | > authentication only until seeding is turned on.
| `updateProductPricing(sku, priceListId, price)` | `PUT /api/price-lists/{id}/items/{sku}` |
| `createSupplierMaster(supplierData)` | `POST /api/suppliers` | ### Price Lists
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/price-lists | pricing.view | Filterable by region, active-on-date |
| GET | /api/price-lists/{id} | pricing.view | Includes items |
| POST | /api/price-lists | pricing.create | `createPriceList(name, region, effectiveDates)` |
| PUT | /api/price-lists/{id} | pricing.update | Rejects if new dates overlap another price list for any shared `(ItemId, RegionId)` pair |
| DELETE | /api/price-lists/{id} | pricing.delete | Soft delete |
| PUT | /api/price-lists/{id}/items/{sku} | pricing.update | `updateProductPricing(sku, priceListId, price)` — upserts the `PriceListItem` row |
| DELETE | /api/price-lists/{id}/items/{sku} | pricing.update | Removes an item's override from the price list |
### Suppliers
| Method | Route | Permission | Notes |
|---|---|---|---|
| GET | /api/suppliers | suppliers.view | |
| GET | /api/suppliers/{id} | suppliers.view | |
| POST | /api/suppliers | suppliers.create | `createSupplierMaster(supplierData)` |
| PUT | /api/suppliers/{id} | suppliers.update | Cannot set `PerformanceScore` directly — that field is read-only via this endpoint, only 15_procurement.md's `evaluateSupplier` writes it |
| DELETE | /api/suppliers/{id} | suppliers.delete | Soft delete, blocked if any PurchaseOrder references it (enforced once 15_procurement.md exists; no-op check until then) |
---
## Request / Response Shapes
### POST `/api/price-lists`
```json
{ "name": "North Region Q3", "regionId": "...", "effectiveFrom": "2026-09-01", "effectiveTo": "2026-09-30" }
```
`regionId` omitted or null = global price list.
### PUT `/api/price-lists/{id}/items/{sku}`
```json
{ "price": 125.50, "minPrice": 115.00 }
```
Resolves `sku` to `ItemId`, upserts the `PriceListItem` row. `minPrice` is optional — omit or send
`null` for a fixed, non-negotiable price. Rejected with 409 if `minPrice > price`.
### POST `/api/suppliers`
```json
{ "name": "Acme Distributors", "contactInfo": "acme@example.com, +91-98765-43210" }
```
--- ---
## Business Rules ## Business Rules
- `applyPricingAndDiscounts` (§1, used in [01_item_catalog.md](./01_item_catalog.md) and 1. `applyPricingAndDiscounts` resolves the unit price for an order line in this order:
[06_sale_ride.md](./06_sale_ride.md)) resolves price in this order: region-scoped active price region-scoped active price list → global active price list → `Item.SalePrice` → reject if all
list → global active price list → `Item.BasePrice`. three are absent (order creation requires an explicit price, per rule 10 of
- Two price lists cannot have overlapping `EffectiveFrom`/`EffectiveTo` ranges for the same [01_item_catalog.md](./01_item_catalog.md)).
`(ItemId, RegionId)` pair. - "Active" means `EffectiveFrom <= orderDate <= EffectiveTo` and `IsActive = true`.
- "Region-scoped" means `PriceList.RegionId` equals the order's store's region.
- If more than one region-scoped price list is active for the same region (shouldn't happen
given rule 2, but defensively), the most recently created one wins.
2. Two active price lists cannot have overlapping `EffectiveFrom`/`EffectiveTo` ranges for the same
`(ItemId, RegionId)` pair — checked at the `PriceListItem` level (a global list and a region list
with the same item may coexist since region-scoped always wins in resolution, so the overlap
check only compares price lists with the *same* `RegionId` value, including both being null).
3. A `PriceList` cannot be deleted (soft delete) while any `PriceListItem` under it is still active
— delete/deactivate the items first, or cascade-deactivate them in the same transaction (cascade
chosen: deleting a price list deactivates all its `PriceListItem` rows in the same
unit-of-work).
4. `Supplier.PerformanceScore` is null until [15_procurement.md](./15_procurement.md)'s
`evaluateSupplier` sets it; no endpoint in this step writes a non-null value.
5. A `Supplier` cannot be hard-deleted, ever — soft delete only, matching the `Template.IsActive`
convention used across the codebase.
6. `PriceListItem.MinPrice`, when set, must be `<= Price` — enforced on every write via
`PUT /api/price-lists/{id}/items/{sku}` (409 if violated).
7. At order time, a caller-supplied `OrderItemRequest.UnitPrice` must fall within
`[MinPrice ?? Price, Price]` for the item's resolved `PriceListItem` (region-scoped, then
global). The `Item.SalePrice` fallback has no `MinPrice` — always fixed, no negotiation. An
out-of-band price, or no resolvable price at all, rejects the whole order (400).
---
## Permissions to Add to Seed
**Status: Not implemented for now.** Endpoints in this step are built without permission
enforcement (no `[Authorize(Policy = ...)]` gate) until explicitly requested. The codes and role
assignments below are the target design to wire in later — keep them here as the reference, not as
something to seed yet.
Target permission codes (not yet in `RoleAndPermissionSeed.cs`):
```
pricing.view, pricing.create, pricing.update, pricing.delete
suppliers.view, suppliers.create, suppliers.update, suppliers.delete
```
Target role assignments (mirrors the [01_item_catalog.md](./01_item_catalog.md) items/categories
pattern), for when seeding is turned on:
- **SalesRep (10)**: pricing.view
- **Supervisor (20)**: pricing.view
- **AreaManager (30)**: pricing.view, pricing.create, pricing.update, suppliers.view
- **AreaSupervisor (40)**: all pricing + supplier permissions
- **RegionalManager (50)**: all pricing + supplier permissions
- **RegionalAgency (60)**: all pricing + supplier permissions
--- ---
## Open Questions ## Open Questions
- Are price lists customer/segment-specific as well as region-specific (ties into `Store.Segment` - None. Resolved in planning discussion (2026-08-20): price lists are region-only in v1
from [12_store_management.md](./12_store_management.md)), or region-only in v1? (no `Store.Segment` scoping); deleting a price list cascades to deactivate its items rather than
being blocked by them.
--- ---
## Implementation Notes ## Implementation Notes
> Fill this section in during planning discussion before writing any code. ### File structure created / touched
```
Features/
PriceList/
PriceListController.cs
PriceListDTOs.cs
PriceListService.cs (ResolveUnitPriceAsync — the price-list half of applyPricingAndDiscounts)
IPriceListRepository.cs
PriceListRepository.cs
Supplier/
SupplierController.cs
SupplierDTOs.cs
SupplierService.cs
ISupplierRepository.cs
SupplierRepository.cs
Order/
OrderDTOs.cs (extended — OrderItemRequest gained an optional UnitPrice override
for the SalePrice-is-null / no-price-list-match case; OrderItemResponse
now echoes the captured UnitPrice)
OrderService.cs (extended — CreateAsync resolves each line's price via a private
ResolveUnitPriceAsync that calls PriceListService, using the request's
UnitPrice override if supplied; rejects the order (returns null) if no
price can be resolved at all)
Item/
IItemRepository.cs / ItemRepository.cs (extended — GetBySkuAsync, needed to resolve the
{sku} route segment on the price-list item endpoints)
Store/
IStoreRepository.cs / StoreRepository.cs (extended — GetByIdWithRegionAsync, includes
Store.Route.Region so OrderService can read the
store's RegionId for price resolution)
Models/
PriceList.cs (new)
PriceListItem.cs (new)
Supplier.cs (new)
Data/AppDBContext.cs
add PriceLists, PriceListItems, Suppliers DbSets; unique index on
PriceListItem(FK_PriceListId, FK_ItemId)
Migrations/
AddPricingAndSuppliers (applied)
Program.cs
register IPriceListRepository/PriceListRepository, ISupplierRepository/SupplierRepository,
PriceListService, SupplierService
Data/Seed/
RoleAndPermissionSeed.cs — untouched. Permission enforcement is intentionally deferred (see
Permissions to Add to Seed above); controllers use plain [Authorize]
with no policy for now.
```
### Price resolution logic (pseudo)
```
ResolveUnitPriceAsync(itemId, storeId, orderDate):
regionId = store.RegionId
regionList = active PriceListItem where ItemId = itemId
and PriceList.RegionId = regionId
and PriceList.EffectiveFrom <= orderDate <= PriceList.EffectiveTo
and PriceList.IsActive and PriceListItem.IsActive
order by PriceList.CreatedAt desc, take 1
if regionList found: return regionList.Price
globalList = same query with PriceList.RegionId = null
if globalList found: return globalList.Price
if item.SalePrice != null: return item.SalePrice
return null // caller (OrderService) rejects with 400 if no explicit price was supplied
```
### Overlap validation on price list write (pseudo)
```
On PUT /api/price-lists/{id}/items/{sku} or POST /api/price-lists:
for each ItemId affected, check existing PriceListItem rows joined to PriceList
where PriceList.RegionId = this.RegionId (both null counts as a match)
and PriceList.Id != this.Id
and date ranges overlap (existing.EffectiveFrom <= new.EffectiveTo
and existing.EffectiveTo >= new.EffectiveFrom)
→ reject with 400 if any match found
```
---
## Worked Example
Item `COKE-330` has `SalePrice = 100`. Store `S1` is in the North region.
**Set up a negotiable region price list:**
```
POST /api/price-lists
{ "name": "North Region September", "regionId": "<northRegionId>",
"effectiveFrom": "2026-09-01", "effectiveTo": "2026-09-30" }
→ PriceList A (RegionId = North)
PUT /api/price-lists/{A.Id}/items/COKE-330
{ "price": 90, "minPrice": 80 }
```
This publishes a band for `COKE-330` in the North region: ceiling 90, floor 80.
**Order at the default (no negotiation):**
```
POST /api/orders
{ "storeId": "S1", "orderDate": "2026-09-15", "items": [{ "itemId": "COKE-330", "quantity": 10 }] }
```
No `unitPrice` supplied → resolves to the ceiling, **unit price = 90**.
**Order with a negotiated price inside the band:**
```
POST /api/orders
{ "storeId": "S1", "orderDate": "2026-09-15",
"items": [{ "itemId": "COKE-330", "quantity": 10, "unitPrice": 83 }] }
```
83 is within `[80, 90]` → **accepted, unit price = 83**.
**Order with a price outside the band:**
```
POST /api/orders
{ "storeId": "S1", "orderDate": "2026-09-15",
"items": [{ "itemId": "COKE-330", "quantity": 10, "unitPrice": 75 }] }
```
75 is below `MinPrice = 80` → **the whole order is rejected (400)**, same as if no price could be
resolved at all.
A store in the South region (no region price list, so it falls through to the global list or
`Item.SalePrice = 100`) has no `MinPrice` unless a global `PriceListItem` also sets one — an
override there is rejected unless it equals the fixed price exactly.
**Supplier example** (unrelated to pricing resolution, same step):
```
POST /api/suppliers
{ "name": "Acme Distributors", "contactInfo": "acme@example.com, +91-98765-43210" }
→ Supplier { Id, Name: "Acme Distributors", PerformanceScore: null }
```
`PerformanceScore` stays null until [15_procurement.md](./15_procurement.md)'s `evaluateSupplier`
runs against this supplier.
+17 -16
View File
@@ -13,6 +13,7 @@
| Order CRUD | Done | Scope-aware (user only sees their assigned stores) | | Order CRUD | Done | Scope-aware (user only sees their assigned stores) |
| Unit of Work + Repositories | Done | Transaction support throughout | | Unit of Work + Repositories | Done | Transaction support throughout |
| Soft Deletes | Done | IsActive pattern on all entities | | Soft Deletes | Done | IsActive pattern on all entities |
| Price Lists / Suppliers | Done | Region-scoped price lists + supplier master; see [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) |
## Known Gaps in Existing System (Pre-Work) ## Known Gaps in Existing System (Pre-Work)
@@ -46,9 +47,9 @@ They must be resolved before new feature steps begin because later steps depend
| 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) | | 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) |
| 10 | Region Management (Beyond CRUD) | Not Started | 00 | [10_region_management.md](./10_region_management.md) | | 10 | Region Management (Beyond CRUD) | Not Started | 00 | [10_region_management.md](./10_region_management.md) |
| 11 | Route & Sales Visit Management | Not Started | 00 | [11_route_sales_management.md](./11_route_sales_management.md) | | 11 | Route & Sales Visit Management | Not Started | 00 | [11_route_sales_management.md](./11_route_sales_management.md) |
| 12 | Store Management (Beyond CRUD) | Not Started | 00 | [12_store_management.md](./12_store_management.md) | | 12 | Store Management (Beyond CRUD) | In Progress | 00 | [12_store_management.md](./12_store_management.md) |
| 13 | Fleet Management (Beyond CRUD) | Not Started | 00 | [13_fleet_management.md](./13_fleet_management.md) | | 13 | Fleet Management (Beyond CRUD) | Not Started | 00 | [13_fleet_management.md](./13_fleet_management.md) |
| 14 | Pricing & Supplier Master Data | Not Started | 01 | [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) | | 14 | Pricing & Supplier Master Data | Done | 01 | [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) |
| 15 | Procurement / Purchasing | Not Started | 01, 14 | [15_procurement.md](./15_procurement.md) | | 15 | Procurement / Purchasing | Not Started | 01, 14 | [15_procurement.md](./15_procurement.md) |
| 16 | Sales Force Automation / CRM | Not Started | 12 | [16_sfa_crm.md](./16_sfa_crm.md) | | 16 | Sales Force Automation / CRM | Not Started | 12 | [16_sfa_crm.md](./16_sfa_crm.md) |
| 17 | Promotions & Trade Scheme Management | Not Started | 01, 14 | [17_promotions_schemes.md](./17_promotions_schemes.md) | | 17 | Promotions & Trade Scheme Management | Not Started | 01, 14 | [17_promotions_schemes.md](./17_promotions_schemes.md) |
@@ -79,10 +80,10 @@ here — see `SRS_User_Management_Module.pdf`.
| Function | What it does | Status | | Function | What it does | Status |
|---|---|---| |---|---|---|
| `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Partially implemented — `OrderService.CreateAsync` exists but has no credit/stock validation or state machine yet. | | `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Partially implemented - `OrderService.CreateAsync` exists, enforces store scope, blocks inactive stores, and applies the unverified-KYC order threshold from config. It still has no stock validation, credit-limit validation, approval flow, or order state machine. |
| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented — no credit limit field on `Store` yet. | | `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented - `Store` now has `CreditLimit` and `OutstandingBalance`, but there is no dedicated validator or order-time credit-limit enforcement yet. |
| `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). | | `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
| `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Not implemented — depends on §7 Master Data and §11 Promotions below. | | `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Partially implemented — `OrderService.CreateAsync` resolves the price-list half via `PriceListService.ResolveUnitPriceAsync` (region-scoped → global → `Item.SalePrice`, or a caller-supplied `UnitPrice`); the scheme-discount half still depends on §11 Promotions below. |
| `submitOrderForApproval(orderId)` | Routes the order to the correct approver based on its value/discount threshold. | Not implemented — depends on [08_sales_agent_controls.md](./08_sales_agent_controls.md). | | `submitOrderForApproval(orderId)` | Routes the order to the correct approver based on its value/discount threshold. | Not implemented — depends on [08_sales_agent_controls.md](./08_sales_agent_controls.md). |
| `approveOrder(orderId, approverId)` / `rejectOrder(orderId, reason)` | Moves order to `approved` or `rejected`, logging the actor. | Not implemented. | | `approveOrder(orderId, approverId)` / `rejectOrder(orderId, reason)` | Moves order to `approved` or `rejected`, logging the actor. | Not implemented. |
| `updateOrderStatus(orderId, status)` | Transitions order through `pending → approved → picked → dispatched → delivered → invoiced`. | Not implemented — `Order` has no status field or state machine today. | | `updateOrderStatus(orderId, status)` | Transitions order through `pending → approved → picked → dispatched → delivered → invoiced`. | Not implemented — `Order` has no status field or state machine today. |
@@ -124,15 +125,15 @@ here — see `SRS_User_Management_Module.pdf`.
| Function | What it does | Status | | Function | What it does | Status |
|---|---|---| |---|---|---|
| `createStore(storeData, kycDocs)` | Onboards a new outlet with required KYC. | Partially implemented — `StoreController` create exists, no KYC document handling. | | `createStore(storeData, kycDocs)` | Onboards a new outlet with required KYC. | Partially implemented - store create exists, and KYC documents can now be uploaded/listed via separate `/api/stores/{id}/kyc-documents` endpoints rather than inline on create. |
| `verifyStoreKyc(storeId)` | Marks KYC as verified/rejected by an approver. | Not implemented. | | `verifyStoreKyc(storeId)` | Marks KYC as verified/rejected by an approver. | Implemented - `PUT /api/stores/{id}/kyc/verify` updates `Store.KycStatus`. |
| `updateStoreCreditLimit(storeId, newLimit, approverId)` | Changes a store's credit limit, subject to approval authority. | Not implemented — no credit limit field on `Store`. | | `updateStoreCreditLimit(storeId, newLimit, approverId)` | Changes a store's credit limit, subject to approval authority. | Implemented - `PUT /api/stores/{id}/credit-limit` updates `Store.CreditLimit` and requires the `stores.credit.update` permission plus role level >= 20. |
| `getStoreOutstandingBalance(storeId)` | Returns current receivables against the store. | Not implemented — depends on §13 Finance & Accounting. | | `getStoreOutstandingBalance(storeId)` | Returns current receivables against the store. | Partially implemented - `GET /api/stores/{id}/outstanding-balance` returns the cached `OutstandingBalance`, `CreditLimit`, and available credit. It is not yet driven by Finance events or invoice/payment records. |
| `classifyStore(storeId, segment)` | Sets/updates the store's segment (key account, general trade, etc.). | Not implemented — no segment field on `Store`. | | `classifyStore(storeId, segment)` | Sets/updates the store's segment (key account, general trade, etc.). | Implemented - `PUT /api/stores/{id}/segment` updates the free-text `Store.Segment` field. |
| `deactivateStore(storeId, reason)` | Soft-deletes a store, blocking new orders against it. | Partially implemented — `DELETE /api/stores/{id}` soft-deletes via `IsActive`, no reason code captured. | | `deactivateStore(storeId, reason)` | Soft-deletes a store, blocking new orders against it. | Partially implemented — `DELETE /api/stores/{id}` soft-deletes via `IsActive`, no reason code captured. |
| `getStoreVisitHistory(storeId)` | Returns visit compliance history from Route Management. | Not implemented — depends on route check-in/check-out (§3). | | `getStoreVisitHistory(storeId)` | Returns visit compliance history from Route Management. | Placeholder only - endpoint exists (`GET /api/stores/{id}/visit-history`) but currently returns an empty list until Route/Sales Visit Management lands. |
| `checkStoreSchemeEligibility(storeId, schemeId)` | Evaluates whether a store qualifies for a given promotion. | Not implemented — depends on §11 Promotions. | | `checkStoreSchemeEligibility(storeId, schemeId)` | Evaluates whether a store qualifies for a given promotion. | Placeholder only - endpoint exists (`GET /api/stores/{id}/schemes/{schemeId}/eligibility`) but currently returns a default eligible response until Promotions is implemented. |
| `getStoreSalesDashboard(storeId, period)` | Store-level sales and outstanding summary. | Not implemented. | | `getStoreSalesDashboard(storeId, period)` | Store-level sales and outstanding summary. | Partially implemented - `GET /api/stores/{id}/dashboard` aggregates order count, total order value, and last order date for the selected month. It does not yet include full sales/outstanding analytics from Finance. |
### 5. Vehicle / Fleet Management ### 5. Vehicle / Fleet Management
@@ -173,9 +174,9 @@ here — see `SRS_User_Management_Module.pdf`.
|---|---|---| |---|---|---|
| `createProduct(skuData)` | Creates a SKU with category, UOM, barcode, pack hierarchy. | Not implemented — `Item` model exists but has no properties; see [01_item_catalog.md](./01_item_catalog.md). | | `createProduct(skuData)` | Creates a SKU with category, UOM, barcode, pack hierarchy. | Not implemented — `Item` model exists but has no properties; see [01_item_catalog.md](./01_item_catalog.md). |
| `deactivateSku(sku, reason)` | Retires a SKU from active ordering. | Not implemented. | | `deactivateSku(sku, reason)` | Retires a SKU from active ordering. | Not implemented. |
| `createPriceList(name, region, effectiveDates)` | Defines a new price list scoped to a region/date range. | Not implemented — no price list entity exists. | | `createPriceList(name, region, effectiveDates)` | Defines a new price list scoped to a region/date range. | Implemented — `POST /api/price-lists`, see [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md). |
| `updateProductPricing(sku, priceListId, price)` | Sets/updates the price of a SKU within a price list. | Not implemented. | | `updateProductPricing(sku, priceListId, price)` | Sets/updates the price of a SKU within a price list. | Implemented — `PUT /api/price-lists/{id}/items/{sku}`. |
| `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Not implemented — no `Supplier` entity exists. | | `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Implemented — `POST /api/suppliers`. `PerformanceScore` stays null until [15_procurement.md](./15_procurement.md)'s `evaluateSupplier` exists. |
### 8. Enterprise-wide Inventory & Stock ### 8. Enterprise-wide Inventory & Stock