diff --git a/Distribution/docs/14_pricing_and_suppliers.md b/Distribution/docs/14_pricing_and_suppliers.md index ea0ca15..4719ee1 100644 --- a/Distribution/docs/14_pricing_and_suppliers.md +++ b/Distribution/docs/14_pricing_and_suppliers.md @@ -1,7 +1,7 @@ # 14 — Pricing & Supplier Master Data -**Status**: Not Started -**Depends On**: 01 — Item Catalog +**Status**: Done — migration applied to the database +**Depends On**: 01 — Item Catalog (Done) --- @@ -15,56 +15,339 @@ records. `createProduct`/`deactivateSku` live in 01; this step is everything els ## Background / Context -`Item.BasePrice` (from [01_item_catalog.md](./01_item_catalog.md)) is a single flat price. There is -no concept of a region- or date-scoped price list, and no `Supplier` entity anywhere in the schema. +`Item.SalePrice` (from [01_item_catalog.md](./01_item_catalog.md)) is a single flat, optional price +— `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 -### 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`. - -### Price List Item - -`PriceListId`, `ItemId`, `Price` — overrides `Item.BasePrice` when an applicable, currently- -effective price list exists for the order's region. +### PriceListItem +``` +PriceListItem +├── Id (Guid) — PK, from Template +├── FK_PriceListId (Guid) — required +├── 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 +├── 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 -| Function reference | Endpoint intent | -|---|---| -| `createPriceList(name, region, effectiveDates)` | `POST /api/price-lists` | -| `updateProductPricing(sku, priceListId, price)` | `PUT /api/price-lists/{id}/items/{sku}` | -| `createSupplierMaster(supplierData)` | `POST /api/suppliers` | +> 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 +> authentication only until seeding is turned on. + +### 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 -- `applyPricingAndDiscounts` (§1, used in [01_item_catalog.md](./01_item_catalog.md) and - [06_sale_ride.md](./06_sale_ride.md)) resolves price in this order: region-scoped active price - list → global active price list → `Item.BasePrice`. -- Two price lists cannot have overlapping `EffectiveFrom`/`EffectiveTo` ranges for the same - `(ItemId, RegionId)` pair. +1. `applyPricingAndDiscounts` resolves the unit price for an order line in this order: + region-scoped active price list → global active price list → `Item.SalePrice` → reject if all + three are absent (order creation requires an explicit price, per rule 10 of + [01_item_catalog.md](./01_item_catalog.md)). + - "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 -- Are price lists customer/segment-specific as well as region-specific (ties into `Store.Segment` - from [12_store_management.md](./12_store_management.md)), or region-only in v1? +- None. Resolved in planning discussion (2026-08-20): price lists are 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 -> 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": "", + "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. diff --git a/Distribution/docs/mainPlan.md b/Distribution/docs/mainPlan.md index f6bb2fb..0d286f8 100644 --- a/Distribution/docs/mainPlan.md +++ b/Distribution/docs/mainPlan.md @@ -13,6 +13,7 @@ | Order CRUD | Done | Scope-aware (user only sees their assigned stores) | | Unit of Work + Repositories | Done | Transaction support throughout | | 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) @@ -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) | | 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) | -| 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) | -| 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) | | 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) | @@ -79,10 +80,10 @@ here — see `SRS_User_Management_Module.pdf`. | 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. | -| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented — no credit limit field on `Store` 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 - `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). | -| `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). | | `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. | @@ -124,15 +125,15 @@ here — see `SRS_User_Management_Module.pdf`. | Function | What it does | Status | |---|---|---| -| `createStore(storeData, kycDocs)` | Onboards a new outlet with required KYC. | Partially implemented — `StoreController` create exists, no KYC document handling. | -| `verifyStoreKyc(storeId)` | Marks KYC as verified/rejected by an approver. | Not implemented. | -| `updateStoreCreditLimit(storeId, newLimit, approverId)` | Changes a store's credit limit, subject to approval authority. | Not implemented — no credit limit field on `Store`. | -| `getStoreOutstandingBalance(storeId)` | Returns current receivables against the store. | Not implemented — depends on §13 Finance & Accounting. | -| `classifyStore(storeId, segment)` | Sets/updates the store's segment (key account, general trade, etc.). | Not implemented — no segment field on `Store`. | +| `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. | Implemented - `PUT /api/stores/{id}/kyc/verify` updates `Store.KycStatus`. | +| `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. | 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.). | 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. | -| `getStoreVisitHistory(storeId)` | Returns visit compliance history from Route Management. | Not implemented — depends on route check-in/check-out (§3). | -| `checkStoreSchemeEligibility(storeId, schemeId)` | Evaluates whether a store qualifies for a given promotion. | Not implemented — depends on §11 Promotions. | -| `getStoreSalesDashboard(storeId, period)` | Store-level sales and outstanding summary. | Not implemented. | +| `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. | 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. | 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 @@ -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). | | `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. | -| `updateProductPricing(sku, priceListId, price)` | Sets/updates the price of a SKU within a price list. | Not implemented. | -| `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Not implemented — no `Supplier` 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. | Implemented — `PUT /api/price-lists/{id}/items/{sku}`. | +| `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