420 lines
42 KiB
Markdown
420 lines
42 KiB
Markdown
# ERP Distribution — Master Plan
|
||
|
||
## What Already Exists
|
||
|
||
| Area | Status | Notes |
|
||
|---|---|---|
|
||
| JWT Authentication | Done | Token generation, claims, 120-min expiry |
|
||
| Permission-based Authorization | Done | Dynamic policy provider, role → permission mapping |
|
||
| User Management | Done | CRUD, role assignment, scope assignment (region/route/store) |
|
||
| Role Management | Done (read-only) | 6 roles seeded, no create/update via API |
|
||
| Region / Route / Vehicle CRUD | Done | Full API surface with permission checks |
|
||
| Store / Warehouse CRUD | Done | Full API surface with permission checks |
|
||
| Store Management (Beyond CRUD) | Done | Credit limit, KYC, segment, dashboard, scheme eligibility; see [12_store_management.md](./12_store_management.md) |
|
||
| Order CRUD | Done | Scope-aware (user only sees their assigned stores); an `Order` is now created only as a side effect of `Sale` creation, not standalone — see §1 Order Management notes below |
|
||
| Sale & Payment | Done | `Features/Sale` creates a `Sale`+linked `Order` together (pricing, KYC threshold, immediate-handover stock check); `Features/Payment` records collections against a `Sale` and drives its `PaymentStatus` |
|
||
| 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) |
|
||
| Promotions & Trade Schemes | Done | BuyXGetY/FreeBundle/SlabDiscount/FlatPercent schemes, free-goods fulfillment, spend/ROI tracking; see [17_promotions_schemes.md](./17_promotions_schemes.md) |
|
||
| Procurement / Purchasing | Done | Purchase Order → Goods Receipt → Purchase Return, supplier evaluation; see [15_procurement.md](./15_procurement.md) |
|
||
| Returns & Claims Management | Done | Sale returns, claims, supplier claims; see [18_returns_claims.md](./18_returns_claims.md) |
|
||
|
||
## Known Gaps in Existing System (Pre-Work) — Resolved
|
||
|
||
Step 00 closed these. Kept here for history; all are fixed in the current codebase.
|
||
|
||
| Gap | Impact | Resolution |
|
||
|---|---|---|
|
||
| Route DTOs missing RegionId | Can't link routes to regions via API | Fixed — `CreateRouteRequest`/`UpdateRouteRequest`/`RouteResponse` all carry `RegionId` (`Features/Route/RouteDTOs.cs`). |
|
||
| Store DTOs missing RouteId | Can't link stores to routes via API | Fixed — all Store DTOs carry `RouteId` (`Features/Store/StoreDTOs.cs`). |
|
||
| Role endpoints have no [Authorize] | Anyone can read roles without authentication | Fixed — `[Authorize]` on `RoleController` (`Features/Role/RoleController.cs`). |
|
||
| Item model is empty (no properties) | OrderItems reference items that have no data | Fixed — `Item` has `Sku`/`Name`/`Description`/category/brand/UOM/pricing fields (`Models/Item.cs`). |
|
||
|
||
### Still open (not a step-00 blocker, tracked separately)
|
||
|
||
| Gap | Impact |
|
||
|---|---|
|
||
| JWT key & DB credentials in appsettings.json | Security risk in any shared/production environment — `appsettings.json` still has a literal DB password and placeholder JWT key. |
|
||
| No input validation on any DTO | Bad data can enter the system silently. |
|
||
|
||
---
|
||
|
||
## Feature Steps
|
||
|
||
| # | Feature | Status | Depends On | Detail File |
|
||
|---|---|---|---|---|
|
||
| 00 | Existing Gap Fixes | Done | — | [00_existing_gaps.md](./00_existing_gaps.md) |
|
||
| 01 | Item Catalog | Done | 00 | [01_item_catalog.md](./01_item_catalog.md) |
|
||
| 02 | Stock Ledger | Done | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
|
||
| 03 | Ride Lifecycle | Done | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
|
||
| 04 | Loading Event | Done | 03 | [04_loading_event.md](./04_loading_event.md) |
|
||
| 05 | Delivery Ride Execution | Done | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
|
||
| 06 | Sale Ride Execution | Done | 04 | [06_sale_ride.md](./06_sale_ride.md) |
|
||
| 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) |
|
||
| 08 | Sales Agent Controls | Not Started | 07 | [08_sales_agent_controls.md](./08_sales_agent_controls.md) |
|
||
| 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) |
|
||
| 10 | Region Management (Beyond CRUD) | Done | 00 | [10_region_management.md](./10_region_management.md) |
|
||
| 11 | Route & Sales Visit Management | Partially Implemented | 00 | [11_route_sales_management.md](./11_route_sales_management.md) |
|
||
| 12 | Store Management (Beyond CRUD) | Done | 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 | Done | 01 | [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) |
|
||
| 15 | Procurement / Purchasing | Done | 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 | Done | 01, 14 | [17_promotions_schemes.md](./17_promotions_schemes.md) |
|
||
| 18 | Returns & Claims Management | Done | 05, 06 | [18_returns_claims.md](./18_returns_claims.md) |
|
||
| 19 | Finance & Accounting | Not Started | 05, 06, 18 | [19_finance_accounting.md](./19_finance_accounting.md) |
|
||
| 20 | Reporting, Analytics & BI | Not Started | all prior | [20_reporting_analytics.md](./20_reporting_analytics.md) |
|
||
| 21 | Notifications & Alerts | Not Started | 02, 12, 08 | [21_notifications_alerts.md](./21_notifications_alerts.md) |
|
||
| 22 | Integration & API Layer | Not Started | 19, 13 | [22_integration_api.md](./22_integration_api.md) |
|
||
| 23 | Mobile & Offline Capability | Not Started | 05, 06 | [23_mobile_offline.md](./23_mobile_offline.md) |
|
||
|
||
Steps 00–09 are the original ride/stock-ledger roadmap and keep their strict dependency order.
|
||
Steps 10–23 fill in every remaining area from the [Function Reference by Feature](#function-reference-by-feature)
|
||
section above that wasn't already covered by 00–09; they depend only on the specific earlier steps
|
||
noted above, not on the full 00–09 chain, and can be resequenced relative to each other as
|
||
priorities become clearer.
|
||
|
||
---
|
||
|
||
## Function Reference by Feature
|
||
|
||
Reference only, not a build plan — background material to consult while planning/implementing the
|
||
steps above and below. Service-layer function names and what each should do when implemented.
|
||
Names are illustrative (camelCase) — adapt to your language/framework conventions. None of the
|
||
functions below are implemented yet unless noted. User Management/RBAC functions aren't repeated
|
||
here — see `SRS_User_Management_Module.pdf`.
|
||
|
||
### 1. Order Management
|
||
|
||
> **Design change since this section was first written**: `Order` is no longer created directly
|
||
> from `storeId/items/salesRepId`. Order creation was folded into `Features/Sale` — every `Order`
|
||
> is now a 1:1 side effect of `SaleService.CreateAsync` (see §6 Sale Ride Execution). `POST
|
||
> /api/orders` (`OrderService.CreateAsync`) is now just a lookup — it throws
|
||
> `"Orders are created automatically from sales"` if called for a sale that doesn't already have
|
||
> one, rather than creating a standalone order. All the validation described below (stock check,
|
||
> KYC threshold, pricing) now happens in `SaleService.CreateAsync`
|
||
> (`Features/Sale/SaleService.cs`), not `OrderService`.
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Superseded — see design-change note above. The equivalent entry point is `SaleService.CreateAsync` (`createSale`, §6), which creates the `Sale` and its linked `Order` together. `OrderService.CreateAsync` itself only fetches the existing order for a given `SaleId`. |
|
||
| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented - `Store` has `CreditLimit` and `OutstandingBalance`, but there is no dedicated validator or sale/order-time credit-limit enforcement yet. |
|
||
| `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Partially implemented — `SaleService.CreateAsync` checks `StockBalance` at the ride's vehicle, but **only** when `FulfillImmediately = true` and a `RideId` is supplied (immediate van-sale handover); a deferred/pending sale does no stock check at create time at all. Not warehouse-wide, not reserved — a failed check simply returns `null` (400) rather than a typed error. |
|
||
| `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Partially implemented — `SaleService.CreateAsync` resolves the price-list half via `ResolveUnitPriceAsync` → `PriceListService.ResolvePriceAsync` (region-scoped via the store's route → `Item.SalePrice` fallback, or a caller-supplied `UnitPrice` validated against `[MinPrice, Price]`). The scheme-discount half is a separate call — `SchemeService.EvaluateEligibilityAsync`/`ApplySchemeAsync` (§11) — not auto-invoked from `createSale`; the caller applies a scheme explicitly via `POST /api/orders/{id}/apply-scheme` after the order exists. |
|
||
| `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`. | Implemented, with a smaller state machine than described — `Models/Enums.cs` defines `OrderStatus { Pending, PartiallyFulfilled, Fulfilled, Cancelled }`; `OrderService.UpdateAsync` (`PUT /api/orders/{id}`) sets `Order.Status` directly (any status accepted, no transition guard) and mirrors it onto the linked `Sale.Status`/`CompletedAt`. No `approved/picked/dispatched/invoiced` states exist. |
|
||
| `convertOrderToInvoice(orderId)` | Generates the invoice once the order is dispatched/delivered. | Not implemented — depends on §13 Finance & Accounting. |
|
||
| `createBackorder(orderId, unfulfilledItems[])` | Splits unfulfillable quantity into a linked backorder. | Not implemented. |
|
||
| `cancelOrder(orderId, reason)` | Cancels an order pre-dispatch and releases any reserved stock. | Partially implemented — `OrderService.DeleteAsync` (`DELETE /api/orders/{id}`) soft-deletes the order and cascades `Sale.Status = Cancelled`, but takes no reason code and releases no reserved stock (nothing is reserved at create time to release). |
|
||
| `getOrdersByStore(storeId)` / `getOrdersBySalesRep(salesRepId)` | Scoped order listings. | `getOrdersByStore` implemented (`GET /api/orders/store/{storeId}`). `getOrdersBySalesRep` not implemented. |
|
||
| `getOrderAnalytics(filters)` | Returns fill rate, average order value, rejection rate over a period. | Not implemented. |
|
||
|
||
### 2. Region Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createRegion(name, boundaries)` | Creates a new region record. | Partially implemented — `RegionController` supports create with `Name` only, no `boundaries` field. |
|
||
| `assignAreaToRegion(areaId, regionId)` | Links an existing area under a region. | Not implemented — no `Area` entity exists in the schema (see [00_existing_gaps.md](./00_existing_gaps.md#9-no-area-entity-between-region-and-route)). As a stand-in, a route (the next tier down) can be assigned/reassigned to a region directly via `PUT /api/routes/{id}/region`. |
|
||
| `setRegionalTarget(regionId, period, targetValue)` | Sets the sales target for a region for a given period. | Implemented — `PUT /api/regions/{id}/manage` (consolidated with the three rows below), backed by the `RegionalTarget` entity. Role-level restriction (`RegionalManager`+ with region scope, or `RegionalAgency` across any region) is temporarily disabled — see `10_region_management.md`. |
|
||
| `setRegionalBudget(regionId, period, budgetAmount)` | Allocates budget at the regional level. | Implemented — `PUT /api/regions/{id}/manage` (consolidated), backed by the `RegionalBudget` entity. Same endpoint/access note as `setRegionalTarget`. |
|
||
| `mapAgencyToRegion(agencyId, regionId)` | Assigns a Regional Agency's contract scope to a region. | Implemented — `PUT /api/regions/{id}/manage` (consolidated), backed by the `RegionAgencyMap` entity. `RegionalAgency` is still a `Role`, not a distinct agency entity — `agencyId` is the `Guid` of an existing `User` holding that role (consistent with `Claim.FK_AgencyUserId`, §18); the endpoint rejects a user who doesn't hold the role (this check remains active even with the role gate disabled). |
|
||
| `setRegionalTaxRule(regionId, ruleConfig)` | Overrides tax/pricing rules for a specific region. | Implemented — `PUT /api/regions/{id}/manage` (consolidated), backed by the `RegionalTaxRule` entity (`RuleConfig` stored as a raw JSON string, not yet consumed by pricing/tax logic elsewhere). |
|
||
| `getRegionHierarchy(regionId)` | Returns all areas/territories nested under a region. | Partially implemented — `GET /api/regions/{id}/hierarchy` returns the region's routes and each route's store ids; no `Area` level since that entity doesn't exist yet. |
|
||
| `getRegionDashboard(regionId, period)` | Aggregated KPIs: sales, fill rate, agency performance for the region. | Partially implemented — `GET /api/regions/{id}/dashboard?period=...` returns real-time order count, actual sales, target, and budget for the period; no fill-rate or agency-performance metrics yet. |
|
||
|
||
### 3. Route Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createRoute(name, areaId)` | Creates a new sales visit route within an area. | Partially implemented — `RouteController` supports create, but by `RegionId` not `areaId` (no `Area` entity exists). A route's region can also be changed after creation via `PUT /api/routes/{id}/region`. |
|
||
| `assignOutletsToRoute(routeId, outletIds[])` | Maps a set of stores to a route. | Implemented — `POST /api/routes/{id}/stores` bulk-replaces `FK_RouteId` for the given store ids (all-or-nothing; 404 if any id doesn't exist). |
|
||
| `assignRouteToSalesRep(routeId, salesRepId)` | Assigns ownership of a route to an SR. | Implemented — `PUT /api/routes/{id}/owner`, backed by the new `Route.FK_OwnerUserId` field, distinct from the existing `UserRouteScope` grant. |
|
||
| `optimizeRouteSequence(routeId)` | Reorders the visit sequence for efficiency (distance/priority-based). | Stubbed — `POST /api/routes/{id}/optimize` always returns `501 Not Implemented`; no distance/priority computation exists yet (see open question in [11_route_sales_management.md](./11_route_sales_management.md)). |
|
||
| `recordRouteCheckIn(routeId, outletId, gpsCoords, timestamp)` | Logs an SR's arrival at an outlet. | Implemented — `POST /api/routes/{id}/visits/check-in`, backed by the new `RouteVisitLog` entity. Sets `IsDeviationFlagged` via a basic Haversine distance check (>500m) against the store's `Latitude`/`Longitude`; does not yet call `sendRouteDeviationAlert` (§9, unimplemented). |
|
||
| `recordRouteCheckOut(routeId, outletId, timestamp)` | Logs departure and closes out the visit. | Implemented — `POST /api/routes/{id}/visits/check-out`, closes the matching open `RouteVisitLog` row (404 if none). |
|
||
| `getRouteAdherence(routeId, date)` | Compares planned vs. actual visits for a route/day. | Not implemented — metrics deferred, out of scope for the current pass. |
|
||
| `reassignRoute(routeId, newSalesRepId)` | Moves a route to a different SR. | Implemented — same endpoint as `assignRouteToSalesRep` (`PUT /api/routes/{id}/owner`); the doc treats these as one action, distinguished only by caller intent. |
|
||
| `getRoutePerformance(routeId, period)` | Sales per route, strike rate, missed-visit count. | Not implemented — metrics deferred, out of scope for the current pass. |
|
||
| `planDeliveryRoute(warehouseId, orderIds[])` | Builds the warehouse-to-outlet dispatch route (separate from the sales visit route). | Not implemented — this is the `DeliveryRun` concept; see [03_ride_lifecycle.md](./03_ride_lifecycle.md). |
|
||
|
||
### 4. Store (Outlet) Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `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/{storeId:guid}/kyc-documents` endpoints rather than inline on create. |
|
||
| `uploadStoreKycDocument(storeId, documentType, file)` | Appends a KYC document to a store. | Implemented — `POST /api/stores/{storeId:guid}/kyc-documents`. |
|
||
| `listStoreKycDocuments(storeId)` | Lists KYC documents for a store. | Implemented — `GET /api/stores/{storeId:guid}/kyc-documents`. |
|
||
| `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. |
|
||
| `updateStoreOutstandingBalance(storeId, amount, reason?)` | Manually adjusts the cached outstanding balance. | Implemented — `PUT /api/stores/{id}/outstanding-balance`. |
|
||
| `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. | Implemented — `GET /api/stores/{id}/visit-history` now reads from the `RouteVisitLog` table (§3), returning each visit's date, rep, and outcome (`In Progress`/`Completed`). |
|
||
| `checkStoreSchemeEligibility(storeId, schemeId)` | Evaluates whether a store qualifies for a given promotion. | Implemented — `GET /api/stores/{id}/schemes/{schemeId}/eligibility` now calls `SchemeService.CheckStoreScopeEligibilityAsync` (region/segment/date-range scope check, no order context). For full order-line rule evaluation against a specific order, use `evaluateSchemeEligibility(storeId, orderId)` instead (`GET /api/schemes/eligible?storeId=...&orderId=...`, §11). |
|
||
| `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
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `registerVehicle(vehicleData)` | Creates the vehicle master record (registration, capacity, ownership). | Partially implemented — `VehicleController` create exists with `RegNumber`/`NumberPlate` only, no capacity/ownership fields. |
|
||
| `assignVehicleToRoute(vehicleId, routeId)` | Assigns a vehicle to a delivery/sales route. | Not implemented — `Vehicle` has no route link; only `DeliveryRun.FK_RouteId`/`FK_VehicleId` model this, and `DeliveryRun` is unwired (see [03_ride_lifecycle.md](./03_ride_lifecycle.md)). |
|
||
| `assignDriverToVehicle(vehicleId, driverId)` | Links a driver to a vehicle, checking license validity. | Not implemented — no `Driver` entity exists yet (`DeliveryRun.FK_DriverId` is commented out in the model). |
|
||
| `scheduleMaintenance(vehicleId, maintenanceType, dueAt)` | Sets up a preventive maintenance task (mileage/calendar-based). | Not implemented. |
|
||
| `logFuelExpense(vehicleId, amount, liters, date)` | Records a fuel transaction against the vehicle. | Not implemented. |
|
||
| `recordVehicleTelemetry(vehicleId, gpsCoords, speed, timestamp)` | Ingests telematics data (location, driver behavior). | Not implemented — see §16 Integration. |
|
||
| `checkDriverCompliance(driverId)` | Validates license/permit status before dispatch. | Not implemented — depends on a `Driver` entity. |
|
||
| `calculateLoadPlan(vehicleId, orderIds[])` | Matches order volume/weight to available vehicle capacity. | Not implemented — depends on [04_loading_event.md](./04_loading_event.md). |
|
||
| `getVehicleUtilization(vehicleId, period)` | Utilization and cost-per-delivery metrics. | Not implemented. |
|
||
| `retireVehicle(vehicleId, reason)` | Removes a vehicle from active fleet, retaining history. | Partially implemented — `DELETE /api/vehicles/{id}` soft-deletes via `IsActive`, no reason code captured. |
|
||
|
||
### 6. Warehouse Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createGoodsReceipt(poId, receivedItems[])` | Records inbound stock against a purchase order (GRN). | Implemented — `POST /api/purchase-orders/{id}/goods-receipts`, see [15_procurement.md](./15_procurement.md). Credits `StockBalance`/`StockMovement` for accepted quantities and transitions the PO's status. |
|
||
| `inspectGoodsReceipt(grnId, inspectionResult)` | Logs quality-check outcome before putaway. | Not implemented. |
|
||
| `putawayStock(grnId, locationAssignments[])` | Assigns received stock to warehouse locations (zone/ABC-based). | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
|
||
| `getStockByLocation(warehouseId, sku)` | Returns real-time quantity and location for a SKU. | Not implemented — depends on [02_stock_ledger.md](./02_stock_ledger.md). |
|
||
| `generatePickList(orderIds[])` | Creates a pick list/task for one or more orders. | Not implemented — overlaps with [04_loading_event.md](./04_loading_event.md). |
|
||
| `confirmPicking(pickListId, pickedItems[])` | Marks items as picked, decrementing available stock. | Not implemented. |
|
||
| `packOrder(orderId, packageDetails)` | Records packing completion and package details. | Not implemented. |
|
||
| `createCrossDockTransfer(inboundId, outboundOrderId)` | Routes inbound stock directly to an outbound order, skipping storage. | Not implemented. |
|
||
| `runCycleCount(warehouseId, zoneId)` | Initiates a scheduled partial stock count. | Not implemented — overlaps with [07_ride_reconciliation.md](./07_ride_reconciliation.md) physical count concept. |
|
||
| `reconcileStock(warehouseId, countResults)` | Adjusts system stock to match physical count results. | Not implemented — see [07_ride_reconciliation.md](./07_ride_reconciliation.md). |
|
||
| `buildDispatchLoad(vehicleId, orderIds[])` | Groups orders into a dispatch load for a specific vehicle/route. | Not implemented — see [04_loading_event.md](./04_loading_event.md). |
|
||
| `receiveReturn(returnId, items[], condition)` | Processes returned goods back into the warehouse. | Not implemented — see §12 Returns & Claims. |
|
||
| `getWarehouseKpis(warehouseId, period)` | Dock-to-stock time, pick accuracy, turnover rate. | Not implemented. |
|
||
|
||
### 7. Master Data Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `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. |
|
||
| `listPriceLists(regionId?)` | Lists price lists, optionally filtered by region. | Implemented — `GET /api/price-lists`. |
|
||
| `getPriceList(priceListId)` | Fetches a price list and its items. | Implemented — `GET /api/price-lists/{id}`. |
|
||
| `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). |
|
||
| `updatePriceList(priceListId, name, region, effectiveDates)` | Updates price-list metadata. | Implemented — `PUT /api/price-lists/{id}`. |
|
||
| `deletePriceList(priceListId)` | Soft-deletes a price list. | Implemented — `DELETE /api/price-lists/{id}`. |
|
||
| `updateProductPricing(sku, priceListId, price, minPrice?)` | Sets/updates the price of a SKU within a price list. | Implemented — `PUT /api/price-lists/{id}/items/{sku}`. |
|
||
| `removeProductPricing(sku, priceListId)` | Removes a SKU override from a price list. | Implemented — `DELETE /api/price-lists/{id}/items/{sku}`. |
|
||
| `listSuppliers()` | Lists suppliers. | Implemented — `GET /api/suppliers`. |
|
||
| `getSupplier(supplierId)` | Fetches a supplier by ID. | Implemented — `GET /api/suppliers/{id}`. |
|
||
| `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Implemented — `POST /api/suppliers`. `PerformanceScore` stays null until [15_procurement.md](./15_procurement.md)'s `evaluateSupplier` exists. |
|
||
| `updateSupplier(supplierId, supplierData)` | Updates supplier master data. | Implemented — `PUT /api/suppliers/{id}`. `PerformanceScore` remains read-only here. |
|
||
| `deleteSupplier(supplierId)` | Soft-deletes a supplier. | Implemented — `DELETE /api/suppliers/{id}`. Blocked if an active PurchaseOrder references the supplier. |
|
||
|
||
### 8. Enterprise-wide Inventory & Stock
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `getMultiLocationStock(sku)` | Returns stock levels for a SKU across all warehouses/agencies. | Not implemented — see [02_stock_ledger.md](./02_stock_ledger.md). |
|
||
| `setReorderPoint(sku, warehouseId, threshold)` | Configures the auto-replenishment trigger level. | Not implemented. |
|
||
| `createStockTransfer(fromWarehouseId, toWarehouseId, items[])` | Initiates an inter-warehouse stock movement. | Not implemented. |
|
||
| `trackBatchExpiry(sku, batchNo, expiryDate)` | Registers batch/expiry data for FEFO enforcement. | Not implemented. |
|
||
| `calculateStockValuation(warehouseId, method)` | Computes stock value using FIFO/weighted-average. | Not implemented. |
|
||
|
||
### 9. Procurement / Purchasing
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createPurchaseRequisition(items[], requestedBy)` | Raises an internal request to purchase stock. | Not implemented — no procurement entities exist. |
|
||
| `createPurchaseOrder(supplierId, items[])` | Issues a PO to a supplier. | Implemented — `POST /api/purchase-orders`, see [15_procurement.md](./15_procurement.md). Also has `updatePurchaseOrder`, `closePurchaseOrder`, `cancelPurchaseOrder`, `cancelPurchaseOrderLine`. |
|
||
| `getPurchaseOrder(poId)` | Fetches a purchase order. | Implemented — `GET /api/purchase-orders/{id}`. |
|
||
| `listPurchaseOrders(filters)` | Lists purchase orders. | Implemented — `GET /api/purchase-orders`. |
|
||
| `updatePurchaseOrder(poId, items[])` | Updates an open purchase order. | Implemented — `PUT /api/purchase-orders/{id}`. |
|
||
| `closePurchaseOrder(poId)` | Closes a received purchase order. | Implemented — `POST /api/purchase-orders/{id}/close`. |
|
||
| `cancelPurchaseOrder(poId, reason)` | Cancels a purchase order before receipts exist. | Implemented — `POST /api/purchase-orders/{id}/cancel`. |
|
||
| `cancelPurchaseOrderLine(poId, poItemId, reason)` | Cancels a single PO line. | Implemented — `POST /api/purchase-orders/{id}/items/{itemId}/cancel`. |
|
||
| `matchInvoiceToPO(invoiceId, poId, grnId)` | Performs the 3-way match (PO–GRN–Invoice) before payment. | Partially implemented — `POST /api/purchase-orders/{id}/match-invoice` reconciles PurchaseOrderItem vs GoodsReceiptItem quantities/cost only; full 3-way match activates once Finance's `Invoice` entity (§13, not built) exists. `resolveInvoiceVariance` records the mismatch and its resolution. |
|
||
| `resolveInvoiceVariance(poId, variance, resolution)` | Records how a mismatch should be handled. | Implemented — `POST /api/purchase-orders/{id}/resolve-variance`. |
|
||
| `getGoodsReceipt(grnId)` | Fetches a goods receipt. | Implemented — `GET /api/goods-receipts/{id}`. |
|
||
| `listGoodsReceipts(poId)` | Lists goods receipts for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/goods-receipts`. |
|
||
| `createGoodsReceipt(poId, warehouseId, items[])` | Records inbound stock against a purchase order. | Implemented — `POST /api/purchase-orders/{id}/goods-receipts`. |
|
||
| `getPurchaseReturn(returnId)` | Fetches a purchase return. | Implemented — `GET /api/purchase-returns/{id}`. |
|
||
| `listPurchaseReturns(poId)` | Lists purchase returns for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/returns`. |
|
||
| `createPurchaseReturn(poId, items[], reason)` | Initiates a return of received goods to the supplier. | Implemented — `POST /api/purchase-orders/{id}/returns`, see [15_procurement.md](./15_procurement.md). Debits `StockBalance`/`StockMovement` at the PO's receiving warehouse. |
|
||
| `evaluateSupplier(supplierId, criteria)` | Scores a supplier on delivery/quality/price performance. | Implemented — `POST /api/suppliers/{id}/evaluate`, the only writer of `Supplier.PerformanceScore`. Scored from accepted-vs-rejected quantity ratio and cancelled-vs-fulfilled line ratio across that supplier's PurchaseOrders/GoodsReceipts. |
|
||
|
||
### 10. Sales Force Automation (SFA) / CRM
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `logCustomerInteraction(storeId, notes)` | Records a visit note or customer interaction. | Not implemented. |
|
||
| `trackTargetVsAchievement(userId, period)` | Compares a user's sales target to actuals. | Not implemented. |
|
||
| `calculateIncentive(userId, period)` | Computes commission/incentive payout based on performance. | Not implemented. |
|
||
| `createLead(prospectData)` | Captures a prospective new outlet. | Not implemented — no `Lead` entity exists. |
|
||
| `convertLeadToStore(leadId)` | Converts a qualified lead into an active store record. | Not implemented. |
|
||
|
||
### 11. Promotions & Trade Scheme Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createScheme(schemeRules, eligibility, dateRange)` | Defines a promotion (buy-X-get-Y, slab discount, etc.). | Implemented — `POST /api/schemes`. Supports `BuyXGetY`, `FreeBundle`, `SlabDiscount`, `FlatPercent`; free-goods trigger/payout and bundle components can be supplied inline in the same call. See [17_promotions_schemes.md](./17_promotions_schemes.md). |
|
||
| `evaluateSchemeEligibility(storeId, orderId)` | Checks whether an order qualifies for a scheme. | Implemented — `GET /api/schemes/eligible?storeId=...&orderId=...`, returns eligibility (and computed discount/free-goods payout) for every in-scope scheme, not just one. |
|
||
| `applySchemeDiscount(orderId, schemeId)` | Applies the scheme's discount to order pricing. | Implemented — `POST /api/orders/{id}/apply-scheme`. For free-goods schemes this adds a zero-price `OrderItem` line and checks `StockBalance` availability first; not yet auto-invoked from `createOrder`/`applyPricingAndDiscounts` (§1) — caller applies it explicitly after order creation. |
|
||
| `trackSchemeSpend(schemeId)` | Aggregates total discount value given under a scheme. | Implemented — `GET /api/schemes/{id}/spend`, sums `DiscountAmount` plus free-goods cost (`FreeItemCostValue × FreeQty`) across redemptions. |
|
||
| `getSchemeROI(schemeId)` | Compares scheme spend against incremental sales generated. | Implemented — `GET /api/schemes/{id}/roi`, using a manually-entered `BaselineExpectedSales` per scheme (no trailing-period auto-baseline yet). |
|
||
|
||
### 12. Returns & Claims Management
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createSaleReturn(orderId, mode, items[])` | Records a customer return against a delivered order. | Implemented — `POST /api/orders/{id}/returns`, settles immediately and credits the ride's vehicle. No approval step. |
|
||
| `getSaleReturn(returnId)` | Fetches a sale return by ID. | Implemented — `GET /api/returns/{id}`. |
|
||
| `listSaleReturns(orderId)` | Lists sale returns for an order. | Implemented — `GET /api/orders/{id}/returns`. |
|
||
| `createClaim(agencyUserId, details)` | Raises a claim against a Regional Agency user. | Implemented — `POST /api/claims`. |
|
||
| `getClaim(claimId)` | Fetches a claim by ID. | Implemented — `GET /api/claims/{id}`. |
|
||
| `settleClaim(claimId, settlementAmount)` | Closes out a claim with a final settlement amount. | Implemented — `POST /api/claims/{id}/settle`, requires `claims.settle`. |
|
||
| `createSupplierClaim(poId, grnId?, items[], details, evidenceUrl?)` | Raises a supplier claim against a purchase order. | Implemented — `POST /api/purchase-orders/{id}/claims`. |
|
||
| `getSupplierClaim(claimId)` | Fetches a supplier claim by ID. | Implemented — `GET /api/supplier-claims/{id}`. |
|
||
| `listSupplierClaims(poId)` | Lists supplier claims for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/claims`. |
|
||
| `approveSupplierClaim(claimId, resolutionType, resolutionAmount?, notes?)` | Approves a supplier claim. | Implemented — `POST /api/supplier-claims/{id}/approve`, requires `suppliers.claims.approve`. |
|
||
| `rejectSupplierClaim(claimId, notes?)` | Rejects a supplier claim. | Implemented — `POST /api/supplier-claims/{id}/reject`, requires `suppliers.claims.approve`. |
|
||
|
||
### 13. Finance & Accounting
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `createInvoice(orderId)` | Generates a formal invoice from a fulfilled order. | Not implemented — no `Invoice` entity exists; `Sale`/`Order` remain the billable record. |
|
||
| `recordPayment(invoiceId, amount, method)` | Logs a payment/collection against an invoice. | Implemented against `Sale` rather than an invoice — `POST /api/sales/{saleId:guid}/payments` (`Features/Payment`). Each payment sums against `Sale.TotalAmount` and updates `Sale.PaymentStatus` (`Pending`/`PartiallyPaid`/`Paid`); rejects (400) if the sale is `Cancelled`. `Method` is `Cash` or `Credit` (`Models/Enums.cs`). |
|
||
| `postToGeneralLedger(transactionId)` | Posts a financial transaction to the GL. | Not implemented — no GL/ledger entity exists. |
|
||
| `generateTaxInvoice(invoiceId, jurisdiction)` | Produces a tax-compliant invoice (GST/VAT e-invoicing). | Not implemented. |
|
||
| `getFinancialReport(entityId, period, reportType)` | Returns P&L/cash-flow reports scoped to a region/area. | Not implemented. |
|
||
|
||
### 14. Reporting, Analytics & BI
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `getDashboardData(role, scope)` | Returns the role-appropriate dashboard payload. | Not implemented. |
|
||
| `generateKpiReport(kpiType, filters)` | Produces a specific KPI report (fill rate, OOS%, credit risk, etc.). | Not implemented. |
|
||
| `getPredictiveAlert(type, scope)` | Returns AI-flagged issues, e.g. a region's declining fill rate. | Not implemented. |
|
||
|
||
### 15. Notifications & Alerts
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `sendLowStockAlert(warehouseId, sku)` | Notifies relevant roles when stock drops below the reorder point. | Not implemented. |
|
||
| `sendCreditLimitBreachAlert(storeId)` | Notifies approvers when a store exceeds its credit limit. | Not implemented. |
|
||
| `sendApprovalPendingNotification(approverId, requestId)` | Notifies an approver of a pending action. | Not implemented. |
|
||
| `sendRouteDeviationAlert(routeId, salesRepId)` | Flags when an SR deviates materially from the planned route. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md) geofencing check. |
|
||
|
||
### 16. Integration & API Layer
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `syncToAccountingSystem(transactionId, targetSystem)` | Pushes a transaction to an external accounting platform (QuickBooks/SAP/Xero). | Not implemented. |
|
||
| `syncTelematicsData(vehicleId, provider)` | Pulls GPS/telematics data from a third-party provider. | Not implemented. |
|
||
| `pushOrderToDistributorPortal(orderId)` | Syncs order status to a distributor-facing B2B portal. | Not implemented. |
|
||
|
||
### 17. Mobile & Offline Capability
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `queueOfflineOrder(orderData)` | Stores an order locally on the device when offline. | Not implemented — no mobile client in this repository. |
|
||
| `syncOfflineQueue(deviceId)` | Pushes queued offline actions to the server once connectivity returns. | Not implemented. |
|
||
| `scanBarcode(barcodeValue)` | Resolves a scanned barcode to a SKU or warehouse location. | Not implemented. |
|
||
|
||
### 18. Compliance & Audit
|
||
|
||
| Function | What it does | Status |
|
||
|---|---|---|
|
||
| `logAuditEvent(userId, action, entityType, entityId)` | Writes an immutable audit trail entry for a state-changing action. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md), which scopes an audit log as part of Fraud Controls. |
|
||
| `checkLicenseCompliance(entityId, licenseType)` | Validates a required license/permit is current (e.g., regulated goods). | Not implemented. |
|
||
| `generateComplianceReport(period)` | Produces a report of compliance status across the org for a period. | Not implemented. |
|
||
|
||
---
|
||
|
||
## Feature Summaries
|
||
|
||
### 00 — Existing Gap Fixes
|
||
Fix all known holes in the current codebase before new features are layered on top.
|
||
Includes DTO field gaps, missing authorization on role endpoints, secrets management,
|
||
and adding input validation across all existing endpoints.
|
||
**Gate**: All existing API endpoints behave correctly and securely before step 01 begins.
|
||
|
||
### 01 — Item Catalog
|
||
Define the Item entity with real properties (name, SKU, unit of measure, category, price tiers).
|
||
Items are the unit of stock — nothing in the stock ledger makes sense without them.
|
||
This step also covers the Item CRUD API and how items relate to orders.
|
||
**Gate**: An item can be created, updated, and referenced in an order with quantity and price.
|
||
|
||
### 02 — Stock Ledger
|
||
Introduce the concept of stock per location. Every warehouse (fixed or vehicle) holds a quantity
|
||
of each item. Stock is never edited directly — all changes happen through movement transactions
|
||
(load, sale, return, damage, adjustment). The current balance is always the running sum.
|
||
This is the single most important architectural decision for the whole system.
|
||
For now, the shop is treated as a business destination, not a tracked stock location in this
|
||
system. That means warehouse-to-vehicle and vehicle-to-warehouse movements update tracked stock
|
||
balances, while vehicle-to-shop handover reduces vehicle stock and updates sale/order fulfillment
|
||
state without creating a shop stock balance.
|
||
**Gate**: Stock balance for any item at any location can be queried. Every change is a traceable transaction.
|
||
|
||
### 03 — Ride Lifecycle
|
||
A ride is a session that turns a vehicle into a temporary warehouse. It has a state machine:
|
||
Draft → Open → Active → Closed. A vehicle can only be on one active ride at a time.
|
||
Two ride types exist: Delivery (pre-orders) and Sale (van sales). The ride tracks who is driving,
|
||
which route, which vehicle, and when it started and ended. For now, one ride uses one route.
|
||
**Gate**: A ride can be created, opened, and closed. A vehicle's stock ledger is created when a ride opens.
|
||
|
||
### 04 — Loading Event
|
||
When a ride opens, stock must be physically loaded from a source warehouse onto the vehicle.
|
||
This is a two-party event: warehouse manager confirms what was dispatched, driver confirms what was received.
|
||
Discrepancies between the two are flagged immediately and require resolution before the ride goes active.
|
||
The loading event creates stock movement transactions that debit the warehouse and credit the vehicle.
|
||
**Gate**: Stock moves from warehouse to vehicle via a confirmed loading event. Any discrepancy is logged.
|
||
|
||
### 05 — Delivery Ride Execution
|
||
On a delivery ride, the driver fulfills pre-confirmed orders. Each order becomes a delivery task.
|
||
The driver marks each as delivered, partially delivered, or failed. Partially delivered orders
|
||
generate a credit note for the undelivered quantity. Customer confirmation (signature or OTP)
|
||
is required to mark a delivery complete. Stock movements are created per delivery event.
|
||
Vehicle-to-shop handover reduces vehicle stock only; the shop does not yet exist as a tracked stock
|
||
location in the ledger.
|
||
**Gate**: A delivery ride can execute all orders on its route and produce a delivery report.
|
||
|
||
### 06 — Sale Ride Execution
|
||
On a sale ride, the sales agent visits customers and creates sales in the field. Every sale creates
|
||
an order. If items are handed over immediately, the order is fulfilled immediately and the sale is
|
||
completed immediately. If items are not handed over immediately, the order stays pending for later
|
||
fulfillment. Payment is collected based on the final sale flow. The vehicle's stock ledger decreases
|
||
only for quantities actually handed over from the vehicle. The shop is not yet a tracked stock
|
||
location in the ledger.
|
||
**Gate**: A sale ride can create sales in the field, create linked orders, and reduce vehicle stock for immediate handover quantities.
|
||
|
||
### 07 — Ride Close & Reconciliation
|
||
When a ride ends, the system performs a full reconciliation. The equation must balance:
|
||
Opening Stock − Sales − Damages + Returns = Closing Stock (physical count)
|
||
Cash Collected + Credit Extended = Total Sales − Returns
|
||
Any variance must be approved by a supervisor with a reason code before the ride can close.
|
||
Unresolved variances are logged as discrepancies against the agent's record.
|
||
**Gate**: A ride cannot close with an unexplained variance. All discrepancies are permanently recorded.
|
||
|
||
### 08 — Sales Agent Controls
|
||
Agents operate within boundaries set by their role level. This step wires role levels to business rules:
|
||
discount limits per agent level, credit limits per customer, maximum single-transaction value,
|
||
and approval chains for exceptions. Supervisors can grant one-time overrides logged against their account.
|
||
**Gate**: An agent cannot exceed their limits without a logged, authorized override.
|
||
|
||
### 09 — Fraud Controls & Audit Trail
|
||
A dedicated audit log records every sensitive event (who, what, when, from where).
|
||
Automated checks flag: returns without a matching prior sale, sales outside the geofenced route,
|
||
agents with consistently high variance rates, and cash collected below invoiced amounts.
|
||
Return-related processing is handled in §12 as an immediate-settlement flow; this step focuses on
|
||
audit logging and anomaly detection around those events. Anomaly reports are surfaced to area
|
||
managers on a scheduled basis.
|
||
**Gate**: Every sensitive event is logged immutably. Automated flags surface to the right level of management.
|
||
|
||
---
|
||
|
||
## Rules
|
||
|
||
1. A feature file must be filled in and reviewed before implementation begins on that step.
|
||
2. Status must be updated in this file whenever a step transitions.
|
||
3. If implementation reveals a design change, the feature file is updated and this plan is revised.
|
||
4. Steps must be done in dependency order — no skipping.
|