# Distribution ERP — Backend API Reference > **Stack:** ASP.NET Core (.NET 9) · MySQL (Entity Framework Core) > **Base URL:** `https://` (configure per environment) > **Swagger UI:** available at `/swagger` in Development mode > **Content-Type:** `application/json` for all requests unless noted (`multipart/form-data` for KYC uploads) --- ## Table of Contents 1. [Authentication & JWT](#1-authentication--jwt) 2. [Authorization Model](#2-authorization-model) 3. [Global Response Conventions](#3-global-response-conventions) 4. [Enumerations Reference](#4-enumerations-reference) 5. [Pagination](#5-pagination) 6. [Modules](#6-modules) - [Auth](#61-auth) - [Users](#62-users) - [Roles](#63-roles) - [Authorization Checks](#64-authorization-checks) - [Regions](#65-regions) - [Routes](#66-routes) - [Vehicles](#67-vehicles) - [Warehouses](#68-warehouses) - [Stores](#69-stores) - [Store KYC Documents](#610-store-kyc-documents) - [Categories & Subcategories](#611-categories--subcategories) - [Brands](#612-brands) - [Units of Measure (UOMs)](#613-units-of-measure-uoms) - [Items](#614-items) - [Price Lists](#615-price-lists) - [Schemes (Promotions)](#616-schemes-promotions) - [Suppliers](#617-suppliers) - [Purchase Orders (PO)](#618-purchase-orders-po) - [Goods Receipt Notes (GRN)](#619-goods-receipt-notes-grn) - [Purchase Returns](#620-purchase-returns) - [Stock](#621-stock) - [Driver Profiles](#622-driver-profiles) - [Driver Compliance Config](#623-driver-compliance-config) - [Rides](#624-rides) - [Loading Sheets](#625-loading-sheets) - [Delivery Tasks](#626-delivery-tasks) - [Sales](#627-sales) - [Orders](#628-orders) - [Payments](#629-payments) - [Sync (ERP Import)](#630-sync-erp-import) 7. [Business Flows](#7-business-flows) - [Region → Route → Store Setup](#71-region--route--store-setup) - [Sale Flow](#72-sale-flow-sales-run) - [Delivery Flow](#73-delivery-flow-end-to-end-ride-lifecycle) - [Ride Cancellation Flow](#74-ride-cancellation-flow) - [Payment Flow](#75-payment-flow) - [Purchase Order Flow](#76-purchase-order-flow-procurement) - [Loading Sheet Flow](#77-loading-sheet-flow-detail) - [Stock Flow](#78-stock-flow-how-stock-moves) - [Scheme / Promotion Flow](#79-scheme--promotion-flow) --- ## 1. Authentication > ⚠️ **YET TO IMPLEMENT** > > Authentication is handled by a **dedicated external Auth Service** (separate microservice). This backend acts as a **connector/proxy** — the Next.js frontend never calls the Auth Service directly; all auth traffic flows through this backend's `/api/auth/*` endpoints. > > The exact contract (token format, claims, endpoints, session strategy) is not yet finalized. The `/api/auth` routes are scaffolded in the backend as pass-through stubs pending Auth Service integration. > > What is decided: > - The frontend calls **this backend** for all auth operations, not the Auth Service directly. > - This backend forwards the request to the Auth Service and relays the response. > - All other endpoints on this backend will validate whatever credential the Auth Service issues (token format TBD). > > **Do not implement auth in the frontend until this section is updated.** --- ## 2. Authorization Model The system uses two orthogonal authorization concepts: ### 2.1 Permission-Based Access Control Every protected endpoint is guarded by a **policy string** (e.g. `"orders.view"`). The JWT's `role_id` claim is used to look up the role's granted permissions in the database. The frontend should gracefully hide/disable UI elements the user lacks permission for (use the `/api/authorization/permissions/{code}` endpoint to check at runtime). #### Full Permission Code List | Permission Code | What it Controls | |---|---| | `items.view` | Read items, brands, UOMs | | `items.create` | Create items, brands, UOMs; also gate for Sync endpoints | | `items.update` | Update items, brands, UOMs | | `items.delete` | Delete items, brands, UOMs | | `categories.view` | Read categories/subcategories | | `categories.create` | Create categories/subcategories | | `categories.update` | Update categories/subcategories | | `categories.delete` | Delete categories/subcategories | | `regions.view` | Read regions | | `regions.create` | Create regions | | `regions.update` | Update regions | | `regions.delete` | Delete regions | | `routes.view` | Read routes | | `routes.create` | Create routes | | `routes.update` | Update routes | | `routes.delete` | Delete routes | | `vehicles.view` | Read vehicles | | `vehicles.create` | Create vehicles | | `vehicles.update` | Update vehicles | | `vehicles.delete` | Delete vehicles | | `warehouses.view` | Read warehouses | | `warehouses.create` | Create warehouses | | `warehouses.update` | Update warehouses | | `warehouses.delete` | Delete warehouses | | `stores.view` | Read stores, visit history, dashboard | | `stores.create` | Create stores | | `stores.update` | Update stores, upload KYC docs | | `stores.delete` | Delete stores | | `stores.credit.update` | Update credit limit / outstanding balance | | `stores.kyc.verify` | Change a store's KYC status | | `orders.view` | Read orders, sales | | `orders.create` | Create orders, sales | | `orders.update` | Update orders | | `orders.delete` | Delete orders | | `users.view` | Read users | | `users.create` | Create users | | `users.update` | Update users | | `users.assign_role` | Change a user's role | | `users.assign_scope` | Set a user's region/route/store scope | | `users.deactivate` | Deactivate users | | `driver_profiles.view` | Read driver profiles | | `driver_profiles.create` | Create driver profiles | | `driver_profiles.update` | Update driver profiles | | `driver_profiles.deactivate` | Deactivate driver profiles | | `compliance.manage` | Read/update driver compliance config | | `rides.view` | Read rides, cancel requests | | `rides.create` | Create rides | | `rides.manage` | Assign driver, open, activate, close rides | | `rides.cancel.request` | Raise a cancel request on a ride | | `rides.cancel.approve` | Approve or reject a cancel request | | `loading.view` | Read loading sheets | | `loading.dispatch` | Create loading sheets, set items, confirm dispatch | | `loading.receive` | Set receipt items, confirm receipt | | `loading.resolve` | Resolve discrepancy on a loading sheet | ### 2.2 Scope-Based Data Filtering Users can be restricted to specific **regions**, **routes**, or **stores**. Certain read operations (orders/sales by store, etc.) enforce this scope. Use the scope-check endpoints to determine what data a user can access: - `GET /api/authorization/scopes/stores/{storeId}` — can the current user access this store? - `GET /api/authorization/scopes/routes/{routeId}` — can the current user access this route? - `GET /api/authorization/scopes/regions/{regionId}` — can the current user access this region? - `GET /api/authorization/scopes/stores` — list all store IDs the current user can access --- ## 3. Global Response Conventions | HTTP Status | Meaning | |---|---| | `200 OK` | Success (read / update / action) | | `201 Created` | Resource created; `Location` header + body returned | | `204 No Content` | Delete successful | | `400 Bad Request` | Validation error or invalid state; body: `{ "error": "message" }` | | `401 Unauthorized` | Missing or invalid JWT | | `403 Forbidden` | Authenticated but access denied (scope) | | `404 Not Found` | Resource not found | | `409 Conflict` | Business rule violation (duplicate, already exists, etc.) | All timestamps are **ISO 8601 UTC** strings (e.g. `"2024-01-15T09:30:00Z"`). All IDs are **GUIDs** in the format `"3fa85f64-5717-4562-b3fc-2c963f66afa6"`. --- ## 4. Enumerations Reference ### `KycStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Awaiting review | | `1` / `"Verified"` | KYC approved | | `2` / `"Rejected"` | KYC rejected | ### `LocationType` | Value | Description | |---|---| | `0` / `"Warehouse"` | Stock held in a warehouse | | `1` / `"Vehicle"` | Stock loaded on a vehicle | ### `MovementType` | Value | Description | |---|---| | `0` / `"Sale"` | Stock reduced by a sale | | `1` / `"CustomerReturn"` | Stock returned by customer | | `2` / `"Transfer"` | Moved between locations | | `3` / `"Damage"` | Written off as damaged | | `4` / `"Adjustment"` | Manual stock correction | | `5` / `"Load"` | Vehicle loading event | | `6` / `"PurchaseReceipt"` | GRN stock addition | | `7` / `"PurchaseReturn"` | Returned to supplier | ### `LoadingSheetStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Created, not yet dispatched | | `1` / `"DispatchConfirmed"` | Warehouse confirmed dispatch | | `2` / `"ReceiptConfirmed"` | Driver confirmed receipt | | `3` / `"DiscrepancyFlagged"` | Quantities differ; needs resolution | | `4` / `"Resolved"` | Discrepancy settled | ### `RideType` | Value | Description | |---|---| | `0` / `"Delivery"` | Delivery-only run | | `1` / `"Sale"` | Sales run (driver can take orders) | ### `RideStatus` | Value | Description | |---|---| | `0` / `"Draft"` | Created, not yet started | | `1` / `"Open"` | Route opened, ready to activate | | `2` / `"Active"` | Currently in progress | | `3` / `"CancelRequested"` | Cancel requested, pending approval | | `4` / `"Cancelled"` | Cancelled | | `5` / `"Closed"` | Completed | ### `CancelRequestType` | Value | Description | |---|---| | `0` / `"VehicleBreakdown"` | Vehicle broke down | | `1` / `"DriverUnavailable"` | Driver unavailable | | `2` / `"RouteIssue"` | Route-related issue | | `3` / `"Other"` | Other reason | ### `CancelRequestStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Awaiting approval decision | | `1` / `"Approved"` | Cancel request approved | | `2` / `"Rejected"` | Cancel request rejected | ### `PurchaseOrderStatus` | Value | Description | |---|---| | `0` / `"Issued"` | PO created, not yet received | | `1` / `"PartiallyReceived"` | Some items received via GRN | | `2` / `"Received"` | All items received | | `3` / `"Closed"` | Manually closed | | `4` / `"Cancelled"` | PO cancelled | ### `SaleStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Sale not yet completed | | `1` / `"Completed"` | Sale fulfilled | | `2` / `"Cancelled"` | Sale cancelled | ### `OrderStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Order awaiting fulfilment | | `1` / `"PartiallyFulfilled"` | Some items fulfilled | | `2` / `"Fulfilled"` | Fully fulfilled | | `3` / `"Cancelled"` | Order cancelled | ### `PaymentStatus` | Value | Description | |---|---| | `0` / `"Pending"` | No payment received | | `1` / `"PartiallyPaid"` | Partial payment received | | `2` / `"Paid"` | Fully paid | ### `PaymentMethod` | Value | Description | |---|---| | `0` / `"Cash"` | Cash payment | | `1` / `"Credit"` | Credit payment | ### `DeliveryTaskStatus` | Value | Description | |---|---| | `0` / `"Pending"` | Not yet actioned | | `1` / `"Delivered"` | Fully delivered | | `2` / `"PartiallyDelivered"` | Partially delivered | | `3` / `"Failed"` | Delivery failed | ### `VehicleType` | Value | Description | |---|---| | `0` / `"Car"` | Car | | `1` / `"Truck"` | Truck | | `2` / `"Van"` | Van | | `3` / `"Motorcycle"` | Motorcycle | | `4` / `"Bicycle"` | Bicycle | ### `WareHouseType` | Value | Description | |---|---| | `0` / `"DistributionCenter"` | Distribution center | | `1` / `"FulfillmentCenter"` | Fulfilment center | | `2` / `"CrossDocking"` | Cross-docking facility | | `3` / `"ColdStorage"` | Cold storage | | `4` / `"BondedWarehouse"` | Bonded warehouse | | `5` / `"AutomatedWarehouse"` | Automated warehouse | ### `SchemeRuleType` | Value | Description | |---|---| | `0` / `"BuyXGetY"` | Buy X quantity of trigger item, get Y free | | `1` / `"SlabDiscount"` | Tiered order-value discount | | `2` / `"FlatPercent"` | Flat percentage discount | | `3` / `"FreeBundle"` | Buy a bundle of required items, get free item | ### `SyncSource` (on Items) | Value | Description | |---|---| | `0` / `"Manual"` | Created directly in this system | | `1` / `"Erp"` | Imported via the Sync API | --- ## 5. Pagination Endpoints that support pagination accept these query parameters: | Parameter | Type | Default | Description | |---|---|---|---| | `page` | `int` | `1` | 1-based page number | | `pageSize` | `int` | `20` | Records per page | Paginated responses use this wrapper: ```json { "page": 1, "pageSize": 20, "total": 150, "items": [ ... ] } ``` **Endpoints with pagination:** Driver Profiles, Rides, Loading Sheets, Stock Balances, Stock Movements, Ride Cancel Requests. All other list endpoints return a plain array — **implement client-side pagination or virtual scrolling for large datasets**. --- ## 6. Modules --- ### 6.1 Auth > ⚠️ **YET TO IMPLEMENT** — See [Section 1](#1-authentication) for context. > > The `/api/auth` routes are stubs. Shape of requests and responses will be defined once the Auth Service contract is agreed. **Base path:** `/api/auth` --- ### 6.2 Users **Base path:** `/api/users` **Required:** JWT token | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/users` | `users.view` | List all users | | `GET` | `/api/users/{id}` | `users.view` | Get user by ID | | `POST` | `/api/users` | `users.create` | Create user | | `PUT` | `/api/users/{id}` | `users.update` | Update user full name | | `PUT` | `/api/users/{id}/role` | `users.assign_role` | Assign role to user | | `PUT` | `/api/users/{id}/scope` | `users.assign_scope` | Set user's data scope | | `DELETE` | `/api/users/{id}` | `users.deactivate` | Deactivate user | #### Create User — `POST /api/users` ```json { "username": "string", // required, unique login name "password": "string", // required, plaintext (hashed server-side) "fullName": "string", // required "roleId": "guid", // required "reportsToUserId": "guid" // optional, parent user in hierarchy } ``` #### Update User — `PUT /api/users/{id}` ```json { "fullName": "string" } ``` #### Assign Role — `PUT /api/users/{id}/role` ```json { "roleId": "guid" } ``` #### Assign Scope — `PUT /api/users/{id}/scope` ```json { "regionIds": ["guid", ...], // list (can be empty) "routeIds": ["guid", ...], "storeIds": ["guid", ...] } ``` #### User Response Object ```json { "id": "guid", "username": "string", "fullName": "string", "roleId": "guid", "roleName": "string", "reportsToUserId": "guid | null", "isActive": true, "createdAt": "datetime", "updatedAt": "datetime | null" } ``` --- ### 6.3 Roles **Base path:** `/api/roles` **Required:** JWT token (no specific permission — any authenticated user) | Method | Path | Description | |---|---|---| | `GET` | `/api/roles` | List all roles | | `GET` | `/api/roles/{id}` | Get role by ID | | `GET` | `/api/roles/{id}/permissions` | Get permissions assigned to role | #### Role Response ```json { "id": "guid", "name": "string", "level": 1, // integer hierarchy level; lower = more authority "isActive": true, "createdAt": "datetime" } ``` #### Role Permissions Response Array of: ```json { "permissionId": "guid", "code": "orders.view", // use this code for Policy checks "description": "string" } ``` --- ### 6.4 Authorization Checks **Base path:** `/api/authorization` **Required:** JWT token | Method | Path | Description | |---|---|---| | `GET` | `/api/authorization/permissions/{permissionCode}` | Check if current user's role has a permission | | `GET` | `/api/authorization/scopes/stores/{storeId}` | Check if current user can access a store | | `GET` | `/api/authorization/scopes/routes/{routeId}` | Check if current user can access a route | | `GET` | `/api/authorization/scopes/regions/{regionId}` | Check if current user can access a region | | `GET` | `/api/authorization/scopes/stores` | Get all store IDs accessible to current user | **Permission check response:** ```json { "hasPermission": true } ``` **Scope check responses:** ```json { "hasScope": true } // stores/routes/regions { "storeIds": ["guid", "guid", ...] } // accessible stores list ``` --- ### 6.5 Regions **Base path:** `/api/regions` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/regions` | `regions.view` | List all regions | | `GET` | `/api/regions/{id}` | `regions.view` | Get region by ID | | `POST` | `/api/regions` | `regions.create` | Create region | | `PUT` | `/api/regions/{id}` | `regions.update` | Update region | | `DELETE` | `/api/regions/{id}` | `regions.delete` | Delete region | **Create/Update request:** ```json { "name": "string" } ``` **Response:** ```json { "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" } ``` **Beyond CRUD** — targets, budgets, agency mapping, tax rules, hierarchy, and dashboard. These are not gated by the `regions.*` permission codes above; access is checked by role level in `RegionService`: `RegionalManager` (level 50) can act only on a region they have scope over (via `UserRegionScope`), `RegionalAgency` (level 60) can act on any region. | Method | Path | Description | |---|---|---| | `PUT` | `/api/regions/{id}/targets/{period}` | Set the sales target for a region for a period (`period` = `yyyy-MM`) | | `PUT` | `/api/regions/{id}/budgets/{period}` | Set the budget for a region for a period | | `POST` | `/api/regions/{id}/agencies` | Map an existing user (holding the `RegionalAgency` role) to this region | | `PUT` | `/api/regions/{id}/tax-rule` | Set/replace the region's tax rule config | | `GET` | `/api/regions/{id}/hierarchy` | Region's routes and each route's store ids | | `GET` | `/api/regions/{id}/dashboard?period=yyyy-MM` | Real-time order count/sales/target/budget for the period | **Set target request / response:** ```json // PUT /api/regions/{id}/targets/{period} { "targetValue": 500000 } ``` ```json { "regionId": "guid", "period": "2026-08", "targetValue": 500000 } ``` **Set budget request / response:** ```json // PUT /api/regions/{id}/budgets/{period} { "budgetAmount": 200000 } ``` ```json { "regionId": "guid", "period": "2026-08", "budgetAmount": 200000 } ``` **Map agency request / response:** ```json // POST /api/regions/{id}/agencies { "agencyUserId": "guid" } // must be a User with the RegionalAgency role ``` ```json { "regionId": "guid", "agencyUserId": "guid" } ``` **Set tax rule request / response:** ```json // PUT /api/regions/{id}/tax-rule { "ruleConfig": "{\"rate\": 0.15}" } // arbitrary JSON stored as a string; not yet consumed by pricing ``` ```json { "regionId": "guid", "ruleConfig": "{\"rate\": 0.15}" } ``` **Hierarchy response:** ```json { "regionId": "guid", "regionName": "string", "routes": [ { "routeId": "guid", "routeName": "string", "storeIds": ["guid", "..."] } ] } ``` **Dashboard response:** ```json { "regionId": "guid", "period": "2026-08", "targetValue": 500000, // null if no target set for the period "budgetAmount": 200000, // null if no budget set for the period "orderCount": 42, "actualSales": 187500.00, "lastOrderDate": "datetime | null" } ``` All four of these endpoints return `404` if the region doesn't exist, the caller lacks the required region scope, or (for agency mapping) the target user doesn't hold the `RegionalAgency` role — the response body carries no distinction between "not found" and "forbidden". No EF Core migration for `RegionalTarget`/`RegionalBudget`/`RegionAgencyMap`/`RegionalTaxRule` has been applied yet — these endpoints will 500 against a database that hasn't run the pending migration. --- ### 6.6 Routes **Base path:** `/api/routes` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/routes` | `routes.view` | List all routes | | `GET` | `/api/routes/{id}` | `routes.view` | Get route by ID | | `POST` | `/api/routes` | `routes.create` | Create route | | `PUT` | `/api/routes/{id}` | `routes.update` | Update route | | `DELETE` | `/api/routes/{id}` | `routes.delete` | Delete route | **Create/Update request:** ```json { "regionId": "guid", // required "routeName": "string", // required "startPoint": "string", // required "endPoint": "string", // required "length": 12.5 // decimal (kilometers) } ``` **Response:** ```json { "id": "guid", "regionId": "guid", "routeName": "string", "startPoint": "string", "endPoint": "string", "length": 12.5, "isActive": true, "createdAt": "datetime" } ``` --- ### 6.7 Vehicles **Base path:** `/api/vehicles` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/vehicles` | `vehicles.view` | List all vehicles | | `GET` | `/api/vehicles/{id}` | `vehicles.view` | Get vehicle by ID | | `POST` | `/api/vehicles` | `vehicles.create` | Create vehicle | | `PUT` | `/api/vehicles/{id}` | `vehicles.update` | Update vehicle | | `DELETE` | `/api/vehicles/{id}` | `vehicles.delete` | Delete vehicle | **Create/Update request:** ```json { "regNumber": "string", // registration number "numberPlate": "string", // display plate "type": "Van" // VehicleType enum } ``` **Response:** ```json { "id": "guid", "regNumber": "string", "numberPlate": "string", "type": "Van", "isActive": true, "createdAt": "datetime" } ``` --- ### 6.8 Warehouses **Base path:** `/api/warehouses` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/warehouses` | `warehouses.view` | List all warehouses | | `GET` | `/api/warehouses/{id}` | `warehouses.view` | Get warehouse by ID | | `POST` | `/api/warehouses` | `warehouses.create` | Create warehouse | | `PUT` | `/api/warehouses/{id}` | `warehouses.update` | Update warehouse | | `DELETE` | `/api/warehouses/{id}` | `warehouses.delete` | Delete warehouse | **Create/Update request:** ```json { "name": "string", "location": "string", "type": "DistributionCenter", // WareHouseType enum "vehicleId": "guid | null" // optional, link to a vehicle } ``` **Response:** ```json { "id": "guid", "name": "string", "location": "string", "type": "DistributionCenter", "vehicleId": "guid | null", "isActive": true, "createdAt": "datetime" } ``` --- ### 6.9 Stores **Base path:** `/api/stores` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/stores` | `stores.view` | List all stores | | `GET` | `/api/stores/{id}` | `stores.view` | Get store by ID | | `POST` | `/api/stores` | `stores.create` | Create store | | `PUT` | `/api/stores/{id}` | `stores.update` | Update store | | `DELETE` | `/api/stores/{id}` | `stores.delete` | Delete store | | `PUT` | `/api/stores/{id}/credit-limit` | `stores.credit.update` | Update credit limit | | `PUT` | `/api/stores/{id}/outstanding-balance` | `stores.credit.update` | Update outstanding balance | | `GET` | `/api/stores/{id}/outstanding-balance` | `stores.view` | Get outstanding balance details | | `PUT` | `/api/stores/{id}/kyc/verify` | `stores.kyc.verify` | Verify or reject KYC | | `PUT` | `/api/stores/{id}/segment` | `stores.update` | Classify store segment | | `GET` | `/api/stores/{id}/visit-history` | `stores.view` | Get store visit history | | `GET` | `/api/stores/{id}/schemes/{schemeId}/eligibility` | `stores.view` | Check scheme eligibility | | `GET` | `/api/stores/{id}/dashboard` | `stores.view` | Get store dashboard metrics | #### Create/Update Store ```json { "routeId": "guid", "ownerName": "string", "storeName": "string", "storeAddress": "string", "latitude": 6.927079, "longitude": 79.861244, "oneTimeAmount": 0.00, // one-time registration/setup fee "creditAmount": 0.00 // assigned credit amount } ``` #### Store Response ```json { "id": "guid", "routeId": "guid", "ownerName": "string", "storeName": "string", "storeAddress": "string", "latitude": 6.927079, "longitude": 79.861244, "oneTimeAmount": 0.00, "creditAmount": 0.00, "creditLimit": 50000.00, "outstandingBalance": 12500.00, "segment": "Gold | null", // Gold, Silver, Key Account, High Risk, etc. "kycStatus": "Pending", "isActive": true, "createdAt": "datetime" } ``` #### Update Credit Limit — `PUT /api/stores/{id}/credit-limit` ```json { "newLimit": 75000.00, "approverId": "guid" // user ID of the approver } ``` #### Update Outstanding Balance — `PUT /api/stores/{id}/outstanding-balance` ```json { "newBalance": 10000.00 } ``` #### Outstanding Balance Response — `GET /api/stores/{id}/outstanding-balance` ```json { "storeId": "guid", "creditLimit": 50000.00, "outstandingBalance": 12500.00, "availableCredit": 37500.00 } ``` #### Verify KYC — `PUT /api/stores/{id}/kyc/verify` ```json { "status": "Verified", // KycStatus: Verified or Rejected "notes": "string | null" } ``` #### Classify Segment — `PUT /api/stores/{id}/segment` ```json { "segment": "Gold" } ``` #### Visit History Response — `GET /api/stores/{id}/visit-history` Array of: ```json { "visitId": "guid", "visitDate": "datetime", "repUserId": "guid", "outcome": "string" } ``` #### Scheme Eligibility — `GET /api/stores/{id}/schemes/{schemeId}/eligibility` ```json { "eligible": true, "reason": "string | null" } ``` #### Store Dashboard — `GET /api/stores/{id}/dashboard?period=monthly` `period` query param is a string (e.g. `"daily"`, `"weekly"`, `"monthly"`). ```json { "storeId": "guid", "period": "monthly", "orderCount": 12, "totalOrderValue": 145000.00, "lastOrderDate": "datetime | null" } ``` --- ### 6.10 Store KYC Documents **Base path:** `/api/stores/{storeId}/kyc-documents` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/stores/{storeId}/kyc-documents` | `stores.view` | Get all KYC documents for a store | | `POST` | `/api/stores/{storeId}/kyc-documents` | `stores.update` | Upload a KYC document | #### Upload KYC Document — `POST /api/stores/{storeId}/kyc-documents` > **Content-Type:** `multipart/form-data` | Form field | Type | Required | Description | |---|---|---|---| | `documentType` | `string` | Yes | Document type label (e.g. `"NIC"`, `"Business Registration"`) | | `file` | `File` | Yes | The document file | **Response:** ```json { "id": "guid", "storeId": "guid", "documentType": "NIC", "fileReference": "string", // server-side file reference/path "uploadedAt": "datetime" } ``` --- ### 6.11 Categories & Subcategories | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/categories` | `categories.view` | List all categories (with subcategories) | | `GET` | `/api/categories/{id}` | `categories.view` | Get category by ID | | `POST` | `/api/categories` | `categories.create` | Create category | | `PUT` | `/api/categories/{id}` | `categories.update` | Update category | | `DELETE` | `/api/categories/{id}` | `categories.delete` | Delete category | | `POST` | `/api/categories/{categoryId}/subcategories` | `categories.create` | Create subcategory | | `PUT` | `/api/subcategories/{id}` | `categories.update` | Update subcategory | | `DELETE` | `/api/subcategories/{id}` | `categories.delete` | Delete subcategory | **Create/Update category or subcategory:** ```json { "name": "string" } ``` **Category detail response (GET by ID):** ```json { "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime", "subCategories": [ { "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" } ] } ``` **Category list response (GET all):** Array of above objects. --- ### 6.12 Brands **Base path:** `/api/brands` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/brands` | `items.view` | List all brands | | `GET` | `/api/brands/{id}` | `items.view` | Get brand by ID | | `POST` | `/api/brands` | `items.create` | Create brand | | `PUT` | `/api/brands/{id}` | `items.update` | Update brand | | `DELETE` | `/api/brands/{id}` | `items.delete` | Delete brand | **Create/Update:** `{ "name": "string" }` **Response:** `{ "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" }` --- ### 6.13 Units of Measure (UOMs) **Base path:** `/api/uoms` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/uoms` | `items.view` | List all UOMs | | `GET` | `/api/uoms/{id}` | `items.view` | Get UOM by ID | | `POST` | `/api/uoms` | `items.create` | Create UOM | | `PUT` | `/api/uoms/{id}` | `items.update` | Update UOM | | `DELETE` | `/api/uoms/{id}` | `items.delete` | Delete UOM | **Create/Update:** `{ "name": "string" }` **Response:** `{ "id": "guid", "name": "string", "isActive": true, "createdAt": "datetime" }` --- ### 6.14 Items **Base path:** `/api/items` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/items` | `items.view` | List items (filterable) | | `GET` | `/api/items/{id}` | `items.view` | Get item detail | | `POST` | `/api/items` | `items.create` | Create item | | `PUT` | `/api/items/{id}` | `items.update` | Update item | | `DELETE` | `/api/items/{id}` | `items.delete` | Delete item | | `PUT` | `/api/items/{id}/uom-conversions` | `items.update` | Replace all UOM conversions | #### List Items — `GET /api/items` Optional query filters: | Param | Type | Description | |---|---|---| | `categoryId` | `guid` | Filter by category | | `subCategoryId` | `guid` | Filter by subcategory | | `brandId` | `guid` | Filter by brand | #### Create Item — `POST /api/items` ```json { "sku": "string", // required, unique SKU "name": "string", // required "description": "string", // optional "categoryId": "guid", // required "subCategoryId": "guid", // optional "brandId": "guid", // optional "baseUomId": "guid", // required, base unit of measure "salePrice": 150.00, // optional default sale price "conversions": [ // optional UOM conversion rules { "fromUomId": "guid", "toUomId": "guid", "factor": 12.0 // 1 fromUom = factor toUom } ] } ``` Returns `409 Conflict` if SKU already exists. #### Update Item — `PUT /api/items/{id}` ```json { "name": "string", "description": "string | null", "categoryId": "guid", "subCategoryId": "guid | null", "brandId": "guid | null", "baseUomId": "guid", "salePrice": 150.00 } ``` #### Replace UOM Conversions — `PUT /api/items/{id}/uom-conversions` Replaces **all** existing conversions for the item. ```json { "conversions": [ { "fromUomId": "guid", "toUomId": "guid", "factor": 12.0 } ] } ``` #### Item Summary Response (list) ```json { "id": "guid", "sku": "string", "name": "string", "categoryId": "guid", "categoryName": "string", "subCategoryId": "guid | null", "subCategoryName": "string | null", "brandId": "guid | null", "brandName": "string | null", "baseUomId": "guid", "baseUomName": "string", "salePrice": 150.00, "syncSource": "Manual | Erp", "isActive": true, "createdAt": "datetime" } ``` #### Item Detail Response (single) Same as summary plus: ```json { "description": "string | null", "conversions": [ { "id": "guid", "fromUomId": "guid", "fromUomName": "string", "toUomId": "guid", "toUomName": "string", "factor": 12.0 } ] } ``` --- ### 6.15 Price Lists **Base path:** `/api/price-lists` **Required:** JWT token (no specific permission policy — any authenticated user) | Method | Path | Description | |---|---|---| | `GET` | `/api/price-lists` | List price lists (filterable by region) | | `GET` | `/api/price-lists/{id}` | Get price list with all items | | `POST` | `/api/price-lists` | Create price list | | `PUT` | `/api/price-lists/{id}` | Update price list metadata | | `DELETE` | `/api/price-lists/{id}` | Delete price list | | `PUT` | `/api/price-lists/{id}/items/{sku}` | Upsert an item's price in this list | | `DELETE` | `/api/price-lists/{id}/items/{sku}` | Remove an item from this list | #### List Price Lists — `GET /api/price-lists` Optional query: `?regionId={guid}` #### Create/Update Price List ```json { "name": "string", "regionId": "guid | null", // null = applies to all regions "effectiveFrom": "datetime", "effectiveTo": "datetime" } ``` Returns `409 Conflict` if overlapping active price list for the same region exists. #### Upsert Item Price — `PUT /api/price-lists/{id}/items/{sku}` `sku` is the item's SKU string in the URL path. ```json { "price": 150.00, "minPrice": 130.00 // optional, null = price is non-negotiable } ``` #### Price List Response ```json { "id": "guid", "name": "string", "regionId": "guid | null", "regionName": "string | null", "effectiveFrom": "datetime", "effectiveTo": "datetime", "isActive": true, "createdAt": "datetime", "items": [ { "id": "guid", "itemId": "guid", "sku": "string", "itemName": "string", "price": 150.00, "minPrice": 130.00 } ] } ``` --- ### 6.16 Schemes (Promotions) **Base path:** `/api/schemes` **Required:** JWT token (no specific permission policy) | Method | Path | Description | |---|---|---| | `GET` | `/api/schemes` | List schemes (optional `?regionId={guid}`) | | `GET` | `/api/schemes/{id}` | Get scheme detail | | `POST` | `/api/schemes` | Create scheme | | `PUT` | `/api/schemes/{id}` | Update scheme metadata | | `DELETE` | `/api/schemes/{id}` | Delete scheme | | `PUT` | `/api/schemes/{id}/free-goods-rule` | Set free-goods rule | | `DELETE` | `/api/schemes/{id}/free-goods-rule` | Clear free-goods rule | | `PUT` | `/api/schemes/{id}/bundle-components/{itemId}` | Upsert bundle component | | `DELETE` | `/api/schemes/{id}/bundle-components/{itemId}` | Remove bundle component | | `GET` | `/api/schemes/eligible?storeId={guid}&orderId={guid}` | Get eligible schemes for a store+order | | `GET` | `/api/schemes/{id}/spend` | Get scheme spend analytics | | `GET` | `/api/schemes/{id}/roi` | Get scheme ROI analytics | | `POST` | `/api/orders/{id}/apply-scheme` | Apply a scheme to an order | #### Scheme Rule Types - **`BuyXGetY`**: Store buys `triggerQty` of `triggerItemId`, gets `freeQty` of `freeItemId` for free. - **`SlabDiscount`**: Tiered discount based on order value. `ruleConfig` JSON: `{ "tiers": [ { "minOrderAmount": 10000, "discountPercent": 5.0 }, ... ] }` (ordered ascending). - **`FlatPercent`**: Flat percentage off. `ruleConfig` JSON: `{ "discountPercent": 10.0 }`. - **`FreeBundle`**: Store buys all required bundle components, gets `freeQty` of `freeItemId` free. #### Create Scheme — `POST /api/schemes` ```json { "name": "string", "ruleType": "SlabDiscount", // SchemeRuleType enum "ruleConfig": "{...}", // JSON string; required for SlabDiscount/FlatPercent, null for BuyXGetY/FreeBundle "regionId": "guid | null", // null = all regions "eligibleSegment": "Gold | null", // null = all segments "dateRangeFrom": "datetime", "dateRangeTo": "datetime", "agentLimitExempt": false, // if true, exempt from agent discount ceiling "baselineExpectedSales": 500000.00, // optional, used for ROI calc // Optional one-shot fields (can also use separate endpoints later): "triggerItemId": "guid | null", // BuyXGetY: item store must buy "triggerQty": 10, // BuyXGetY: quantity trigger "freeItemId": "guid | null", // BuyXGetY/FreeBundle: item given free "freeQty": 2.0, // quantity of free item "bundleComponents": [ // FreeBundle: required purchase items { "itemId": "guid", "qty": 5 } ] } ``` #### Update Scheme — `PUT /api/schemes/{id}` ```json { "name": "string", "ruleConfig": "{...} | null", "regionId": "guid | null", "eligibleSegment": "string | null", "dateRangeFrom": "datetime", "dateRangeTo": "datetime", "agentLimitExempt": false, "baselineExpectedSales": 500000.00 } ``` #### Set Free Goods Rule — `PUT /api/schemes/{id}/free-goods-rule` ```json { "triggerItemId": "guid | null", // required for BuyXGetY; ignored for FreeBundle "triggerQty": 10, // required for BuyXGetY; ignored for FreeBundle "freeItemId": "guid", // required "freeQty": 2.0 // required } ``` #### Upsert Bundle Component — `PUT /api/schemes/{id}/bundle-components/{itemId}` ```json { "qty": 5 } ``` #### Apply Scheme to Order — `POST /api/orders/{id}/apply-scheme` ```json { "schemeId": "guid" } ``` #### Scheme Response ```json { "id": "guid", "name": "string", "ruleType": "BuyXGetY", "ruleConfig": "string | null", "triggerItemId": "guid | null", "triggerItemName": "string | null", "triggerQty": 10, "freeItemId": "guid | null", "freeItemName": "string | null", "freeQty": 2.0, "bundleComponents": [ { "itemId": "guid", "sku": "string", "itemName": "string", "qty": 5 } ], "regionId": "guid | null", "regionName": "string | null", "eligibleSegment": "Gold | null", "dateRangeFrom": "datetime", "dateRangeTo": "datetime", "agentLimitExempt": false, "baselineExpectedSales": 500000.00, "isActive": true, "createdAt": "datetime" } ``` #### Eligible Schemes Response — `GET /api/schemes/eligible?storeId=...&orderId=...` Array of: ```json { "schemeId": "guid", "schemeName": "string", "ruleType": "SlabDiscount", "eligible": true, "reason": "string | null", "discountAmount": 1500.00, "freeItemId": "guid | null", "freeQty": 2.0 } ``` #### Scheme Spend — `GET /api/schemes/{id}/spend` ```json { "schemeId": "guid", "totalDiscountSpend": 15000.00, "totalFreeGoodsCostSpend": 3000.00, "totalSpend": 18000.00, "redemptionCount": 45 } ``` #### Scheme ROI — `GET /api/schemes/{id}/roi` ```json { "schemeId": "guid", "baselineExpectedSales": 500000.00, "actualSales": 650000.00, "incrementalSales": 150000.00, "totalSpend": 18000.00, "roi": 7.33 // (incrementalSales - totalSpend) / totalSpend; null if baseline not set } ``` --- ### 6.17 Suppliers **Base path:** `/api/suppliers` **Required:** JWT token (no specific permission policy) | Method | Path | Description | |---|---|---| | `GET` | `/api/suppliers` | List all suppliers | | `GET` | `/api/suppliers/{id}` | Get supplier by ID | | `POST` | `/api/suppliers` | Create supplier | | `PUT` | `/api/suppliers/{id}` | Update supplier | | `DELETE` | `/api/suppliers/{id}` | Delete supplier (returns `409` if has active POs) | | `POST` | `/api/suppliers/{id}/evaluate` | Trigger supplier performance evaluation | **Create/Update:** ```json { "name": "string", "contactInfo": "string | null" } ``` **Response:** ```json { "id": "guid", "name": "string", "contactInfo": "string | null", "performanceScore": 4.5, // decimal | null; computed by evaluate "isActive": true, "createdAt": "datetime" } ``` --- ### 6.18 Purchase Orders (PO) **Base path:** `/api/purchase-orders` **Required:** JWT token (no specific permission policy) | Method | Path | Description | |---|---|---| | `GET` | `/api/purchase-orders` | List POs (optional filters) | | `GET` | `/api/purchase-orders/{id}` | Get PO detail | | `POST` | `/api/purchase-orders` | Create PO | | `PUT` | `/api/purchase-orders/{id}` | Update PO lines (while Issued) | | `POST` | `/api/purchase-orders/{id}/close` | Manually close PO | | `POST` | `/api/purchase-orders/{id}/cancel` | Cancel entire PO | | `POST` | `/api/purchase-orders/{id}/items/{itemId}/cancel` | Cancel a single PO line | | `POST` | `/api/purchase-orders/{id}/match-invoice` | Match PO to GRN/Invoice | | `POST` | `/api/purchase-orders/{id}/resolve-variance` | Resolve invoice variance | #### List POs — `GET /api/purchase-orders` Optional query filters: | Param | Type | Description | |---|---|---| | `supplierId` | `guid` | Filter by supplier | | `status` | `PurchaseOrderStatus` | Filter by status | #### Create PO — `POST /api/purchase-orders` ```json { "supplierId": "guid", "items": [ { "itemId": "guid", "quantity": 100, "unitCost": 85.00 } ] } ``` #### Update PO — `PUT /api/purchase-orders/{id}` Only valid while status is `Issued`. ```json { "items": [ { "purchaseOrderItemId": "guid | null", // null = new line, guid = update existing "itemId": "guid", "quantity": 120, "unitCost": 83.00 } ] } ``` #### Cancel PO — `POST /api/purchase-orders/{id}/cancel` ```json { "reason": "string" } ``` #### Cancel PO Line — `POST /api/purchase-orders/{id}/items/{itemId}/cancel` `itemId` here is the `PurchaseOrderItem.Id` (not the item's SKU ID). ```json { "reason": "string | null" } ``` #### Match Invoice — `POST /api/purchase-orders/{id}/match-invoice` Query params: `?invoiceId={guid}&grnId={guid}` (both optional) **Response:** ```json { "purchaseOrderId": "guid", "goodsReceiptId": "guid | null", "invoiceId": "guid | null", "isMatched": true, "lines": [ { "purchaseOrderItemId": "guid", "itemId": "guid", "orderedQuantity": 100, "acceptedQuantity": 98, "unitCost": 85.00, "quantityMatches": false } ], "note": "string" } ``` #### Resolve Variance — `POST /api/purchase-orders/{id}/resolve-variance` ```json { "variance": "string", // description of the variance "resolution": "string", // resolution action taken "adjustedUnitCost": 84.00, // optional adjusted cost "purchaseOrderItemId": "guid" // optional, which line was adjusted } ``` **Response:** ```json { "purchaseOrderId": "guid", "variance": "string", "resolution": "string", "resolvedAt": "datetime" } ``` #### PO Response ```json { "id": "guid", "supplierId": "guid", "supplierName": "string", "createdByUserId": "guid", "status": "Issued", "cancelReason": "string | null", "isActive": true, "createdAt": "datetime", "items": [ { "id": "guid", "itemId": "guid", "sku": "string", "itemName": "string", "quantity": 100, "unitCost": 85.00, "receivedQuantity": 50, "cancelledQuantity": 0, "isCancelled": false } ] } ``` --- ### 6.19 Goods Receipt Notes (GRN) | Method | Path | Description | |---|---|---| | `GET` | `/api/goods-receipts/{id}` | Get GRN by ID | | `GET` | `/api/purchase-orders/{id}/goods-receipts` | List GRNs for a PO | | `POST` | `/api/purchase-orders/{id}/goods-receipts` | Create GRN for PO | #### Create GRN — `POST /api/purchase-orders/{id}/goods-receipts` Returns `409 Conflict` if the PO is not in a receivable status. ```json { "warehouseId": "guid", "items": [ { "purchaseOrderItemId": "guid", // PO line being received "acceptedQuantity": 98, "rejectedQuantity": 2 } ] } ``` #### GRN Response ```json { "id": "guid", "purchaseOrderId": "guid", "receivedByUserId": "guid", "warehouseId": "guid", "warehouseName": "string", "receivedAt": "datetime", "createdAt": "datetime", "items": [ { "id": "guid", "purchaseOrderItemId": "guid", "itemId": "guid", "sku": "string", "itemName": "string", "acceptedQuantity": 98, "rejectedQuantity": 2 } ] } ``` --- ### 6.20 Purchase Returns | Method | Path | Description | |---|---|---| | `GET` | `/api/purchase-returns/{id}` | Get purchase return by ID | | `GET` | `/api/purchase-orders/{id}/returns` | List returns for a PO | | `POST` | `/api/purchase-orders/{id}/returns` | Create purchase return for PO | #### Create Purchase Return — `POST /api/purchase-orders/{id}/returns` Returns `409 Conflict` on business rule violations. ```json { "reason": "string", "items": [ { "itemId": "guid", "quantity": 5 } ] } ``` #### Purchase Return Response ```json { "id": "guid", "purchaseOrderId": "guid", "reason": "string", "isActive": true, "createdAt": "datetime", "items": [ { "id": "guid", "itemId": "guid", "sku": "string", "itemName": "string", "quantity": 5 } ] } ``` --- ### 6.21 Stock **Base path:** `/api/stock` **Required:** JWT token (no specific permission policy) | Method | Path | Description | |---|---|---| | `GET` | `/api/stock/balances` | Get stock balances (paginated, filterable) | | `GET` | `/api/stock/balances/{itemId}/total` | Get total stock across all locations for item | | `GET` | `/api/stock/movements` | Get stock movement history (paginated, filterable) | | `POST` | `/api/stock/transfer` | Transfer stock between locations | | `POST` | `/api/stock/damage` | Record damaged stock write-off | | `POST` | `/api/stock/customer-return` | Record customer return to vehicle | | `POST` | `/api/stock/adjustment` | Record stock adjustment | #### Get Stock Balances — `GET /api/stock/balances` Query params (all optional): | Param | Type | Description | |---|---|---| | `locationId` | `guid` | Filter by specific location (warehouse or vehicle ID) | | `locationType` | `LocationType` | Filter by location type (`Warehouse` or `Vehicle`) | | `itemId` | `guid` | Filter by item | | `page` | `int` | Default: 1 | | `pageSize` | `int` | Default: 20 | **Response:** Paginated list of `StockBalanceResponse`: ```json { "page": 1, "pageSize": 20, "total": 100, "items": [ { "itemId": "guid", "itemName": "string", "locationId": "guid", "locationType": "Warehouse", "quantity": 250.0, "updatedAt": "datetime" } ] } ``` #### Get Item Total — `GET /api/stock/balances/{itemId}/total` ```json { "itemId": "guid", "itemName": "string", "totalQuantity": 500.0, "breakdown": [ { "itemId": "guid", "itemName": "string", "locationId": "guid", "locationType": "Vehicle", "quantity": 250.0, "updatedAt": "datetime" } ] } ``` #### Get Stock Movements — `GET /api/stock/movements` Query params (all optional): | Param | Type | Description | |---|---|---| | `locationId` | `guid` | Filter by location | | `itemId` | `guid` | Filter by item | | `type` | `MovementType` | Filter by movement type | | `from` | `datetime` | Start date filter | | `to` | `datetime` | End date filter | | `page` | `int` | Default: 1 | | `pageSize` | `int` | Default: 20 | **Response:** Paginated list of `StockMovementResponse`: ```json { "items": [ { "id": "guid", "itemId": "guid", "itemName": "string", "fromLocationId": "guid | null", "fromLocationType": "Warehouse | null", "toLocationId": "guid | null", "toLocationType": "Vehicle | null", "quantity": 100.0, "type": "Transfer", "reasonCode": "string | null", "referenceId": "guid | null", // e.g. SaleId, RideId, etc. "createdAt": "datetime", "createdBy": "guid" } ] } ``` #### Transfer Stock — `POST /api/stock/transfer` ```json { "itemId": "guid", "fromLocationId": "guid", "fromLocationType": "Warehouse", "toLocationId": "guid", "toLocationType": "Vehicle", "quantity": 50.0, "referenceId": "guid | null" // e.g. loading sheet ID } ``` #### Record Damage — `POST /api/stock/damage` ```json { "itemId": "guid", "locationId": "guid", "locationType": "Warehouse", "quantity": 3.0, "reasonCode": "string" // e.g. "WaterDamage", "ExpiredGoods" } ``` #### Record Customer Return — `POST /api/stock/customer-return` ```json { "itemId": "guid", "vehicleId": "guid", // returns go to vehicle stock "quantity": 2.0, "referenceId": "guid | null" } ``` #### Record Adjustment — `POST /api/stock/adjustment` ```json { "itemId": "guid", "locationId": "guid", "locationType": "Warehouse", "quantity": -5.0, // negative = reduce stock, positive = add stock "reasonCode": "string" } ``` --- ### 6.22 Driver Profiles **Base path:** `/api/driver-profiles` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/driver-profiles` | `driver_profiles.view` | List driver profiles (paginated) | | `GET` | `/api/driver-profiles/{id}` | `driver_profiles.view` | Get driver profile by ID | | `POST` | `/api/driver-profiles` | `driver_profiles.create` | Create driver profile | | `PUT` | `/api/driver-profiles/{id}` | `driver_profiles.update` | Update driver profile | | `DELETE` | `/api/driver-profiles/{id}` | `driver_profiles.deactivate` | Deactivate driver profile | #### List — `GET /api/driver-profiles?page=1&pageSize=20` #### Create Driver Profile — `POST /api/driver-profiles` ```json { "fK_UserId": "guid", // user to link this profile to "licenseNumber": "string", "licenseExpiry": "datetime", "medicalClearanceExpiry": "datetime" } ``` #### Update Driver Profile — `PUT /api/driver-profiles/{id}` ```json { "licenseNumber": "string", "licenseExpiry": "datetime", "medicalClearanceExpiry": "datetime" } ``` #### Driver Profile Response ```json { "id": "guid", "userId": "guid", "userFullName": "string", "licenseNumber": "string", "licenseExpiry": "datetime", "medicalClearanceExpiry": "datetime", "isActive": true, "createdAt": "datetime" } ``` --- ### 6.23 Driver Compliance Config **Base path:** `/api/driver-compliance-config` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/driver-compliance-config` | `compliance.manage` | Get current config | | `PUT` | `/api/driver-compliance-config` | `compliance.manage` | Create or update config | This is a singleton resource — only one config record exists system-wide. **GET response** (returns `204 No Content` if not yet configured): ```json { "id": "guid", "enforceLicenseExpiry": true, "enforceMedicalClearance": true, "updatedAt": "datetime", "updatedBy": "guid" } ``` **PUT request:** ```json { "enforceLicenseExpiry": true, "enforceMedicalClearance": false } ``` --- ### 6.24 Rides **Base path:** `/api/rides` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/rides` | `rides.view` | List rides (paginated, filterable) | | `GET` | `/api/rides/{id}` | `rides.view` | Get ride detail | | `POST` | `/api/rides` | `rides.create` | Create ride | | `PUT` | `/api/rides/{id}/driver` | `rides.manage` | Assign/change driver | | `POST` | `/api/rides/{id}/open` | `rides.manage` | Transition to Open status | | `POST` | `/api/rides/{id}/activate` | `rides.manage` | Activate ride (record meter start) | | `POST` | `/api/rides/{id}/close` | `rides.manage` | Close ride (record meter end) | | `POST` | `/api/rides/{id}/cancel-request` | `rides.cancel.request` | Raise a cancel request | | `GET` | `/api/rides/cancel-requests` | `rides.view` | List all cancel requests | | `POST` | `/api/rides/cancel-requests/{id}/approve` | `rides.cancel.approve` | Approve cancel request | | `POST` | `/api/rides/cancel-requests/{id}/reject` | `rides.cancel.approve` | Reject cancel request | | `GET` | `/api/rides/{id}/cancel-request` | `rides.view` | Get cancel request for a ride | | `GET` | `/api/rides/breakdown-assist/{rideId}` | `rides.view` | Get breakdown assist data | #### Ride Lifecycle ``` Draft → Open → Active → Closed ↓ CancelRequested → Cancelled ``` #### List Rides — `GET /api/rides` Query params (all optional): | Param | Type | Description | |---|---|---| | `status` | `RideStatus` | Filter by status | | `type` | `RideType` | Filter by ride type | | `driverProfileId` | `guid` | Filter by driver | | `vehicleId` | `guid` | Filter by vehicle | | `page` | `int` | Default: 1 | | `pageSize` | `int` | Default: 20 | #### Create Ride — `POST /api/rides` ```json { "rideType": "Delivery", "fK_VehicleId": "guid", "fK_DriverProfileId": "guid", "fK_RouteId": "guid", "fK_SourceWarehouseId": "guid | null" } ``` #### Assign Driver — `PUT /api/rides/{id}/driver` ```json { "fK_DriverProfileId": "guid" } ``` #### Open Ride — `POST /api/rides/{id}/open` No body required. #### Activate Ride — `POST /api/rides/{id}/activate` ```json { "meterstart": 12345 } // odometer reading at start (integer) ``` #### Close Ride — `POST /api/rides/{id}/close` ```json { "meterend": 12567 } // odometer reading at end (integer) ``` #### Raise Cancel Request — `POST /api/rides/{id}/cancel-request` ```json { "typeCode": "VehicleBreakdown", // CancelRequestType enum "reason": "string" } ``` #### List Cancel Requests — `GET /api/rides/cancel-requests` Query params: `?status={CancelRequestStatus}&page=1&pageSize=20` #### Ride Response ```json { "id": "guid", "rideType": "Delivery", "status": "Active", "vehicleId": "guid", "vehicleNumberPlate": "ABC-1234", "driverProfileId": "guid", "driverFullName": "string", "routeId": "guid", "routeName": "string", "sourceWarehouseId": "guid", "sourceWarehouseName": "string", "meterstart": 12345, "meterend": 0, "startedAt": "datetime | null", "activatedAt": "datetime | null", "closedAt": "datetime | null", "createdAt": "datetime" } ``` #### Cancel Request Response ```json { "id": "guid", "rideId": "guid", "typeCode": "VehicleBreakdown", "reason": "string", "requestedBy": "guid", "requestedAt": "datetime", "status": "Pending", "approvedBy": "guid | null", "approvedAt": "datetime | null" } ``` #### Breakdown Assist Response — `GET /api/rides/breakdown-assist/{rideId}` Returns active rides on the same route with stock levels, and the open orders for the broken-down ride. ```json { "rideId": "guid", "activeRides": [ { "rideId": "guid", "vehicleId": "guid", "vehicleNumberPlate": "string", "driverProfileId": "guid", "driverFullName": "string", "routeId": "guid", "routeName": "string", "stockLevels": [ { "itemId": "guid", "itemName": "string", "locationId": "guid", "locationType": "Vehicle", "quantity": 50.0, "updatedAt": "datetime" } ] } ], "openOrders": [ { "orderId": "guid", "storeId": "guid", "storeName": "string", "orderDate": "datetime", "totalAmount": 25000.00 } ] } ``` --- ### 6.25 Loading Sheets **Base path:** `/api/loading-sheets` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/loading-sheets` | `loading.view` | List loading sheets (paginated, filterable) | | `GET` | `/api/loading-sheets/{id}` | `loading.view` | Get loading sheet detail | | `POST` | `/api/loading-sheets` | `loading.dispatch` | Create loading sheet for a ride | | `PUT` | `/api/loading-sheets/{id}/items` | `loading.dispatch` | Set dispatched items | | `POST` | `/api/loading-sheets/{id}/confirm-dispatch` | `loading.dispatch` | Confirm dispatch from warehouse | | `PUT` | `/api/loading-sheets/{id}/receipt-items` | `loading.receive` | Set received item quantities | | `POST` | `/api/loading-sheets/{id}/confirm-receipt` | `loading.receive` | Driver confirms receipt | | `POST` | `/api/loading-sheets/{id}/resolve` | `loading.resolve` | Resolve quantity discrepancy | #### Loading Sheet Lifecycle ``` Pending → DispatchConfirmed → ReceiptConfirmed ↓ (if quantities differ) DiscrepancyFlagged → Resolved ``` #### List Loading Sheets — `GET /api/loading-sheets` Query params (all optional): | Param | Type | Description | |---|---|---| | `rideId` | `guid` | Filter by ride | | `status` | `LoadingSheetStatus` | Filter by status | | `page` | `int` | Default: 1 | | `pageSize` | `int` | Default: 20 | #### Create Loading Sheet — `POST /api/loading-sheets` ```json { "fK_RideId": "guid" } ``` #### Set Dispatched Items — `PUT /api/loading-sheets/{id}/items` Replaces all items on the loading sheet. ```json { "items": [ { "fK_ItemId": "guid", "quantity": 100.0 } ] } ``` #### Confirm Dispatch — `POST /api/loading-sheets/{id}/confirm-dispatch` No body required. Transitions status to `DispatchConfirmed`. #### Set Receipt Items — `PUT /api/loading-sheets/{id}/receipt-items` ```json { "items": [ { "fK_ItemId": "guid", "quantityReceived": 98.0 } ] } ``` #### Confirm Receipt — `POST /api/loading-sheets/{id}/confirm-receipt` ```json { "meterstart": 12345 } // driver's vehicle odometer at receipt ``` #### Resolve Discrepancy — `POST /api/loading-sheets/{id}/resolve` ```json { "items": [ { "fK_ItemId": "guid", "quantityResolved": 98.0 } ], "meterstart": 12345 } ``` #### Loading Sheet Response ```json { "id": "guid", "fK_RideId": "guid", "fK_SourceWarehouseId": "guid", "warehouseName": "string", "status": "Pending", "dispatchConfirmedAt": "datetime | null", "dispatchConfirmedBy": "guid | null", "receiptConfirmedAt": "datetime | null", "receiptConfirmedBy": "guid | null", "createdAt": "datetime", "items": [ { "id": "guid", "fK_ItemId": "guid", "itemName": "string", "quantityDispatched": 100.0, "quantityReceived": 98.0, "quantityResolved": 98.0 } ] } ``` --- ### 6.26 Delivery Tasks **Base path:** `/api` (mixed routes — see below) | Method | Path | Description | |---|---|---| | `POST` | `/api/rides/{rideId}/tasks/generate` | Auto-generate delivery tasks from ride's orders | | `GET` | `/api/rides/{rideId}/tasks` | List all tasks for a ride | | `GET` | `/api/delivery-tasks/{id}` | Get delivery task by ID | | `PUT` | `/api/delivery-tasks/{id}/quantities` | Set fulfilled quantities per item | | `POST` | `/api/delivery-tasks/{id}/confirm` | Mark task as delivered | | `POST` | `/api/delivery-tasks/{id}/fail` | Mark task as failed | #### Generate Tasks — `POST /api/rides/{rideId}/tasks/generate` No body. Generates one `DeliveryTask` per store/order on the ride. Returns `400 Bad Request` if ride is not in a valid state, `404 Not Found` if ride not found. **Response:** Array of `DeliveryTaskResponse`. #### Set Quantities — `PUT /api/delivery-tasks/{id}/quantities` ```json { "items": [ { "itemId": "guid", "quantityFulfilled": 8 } ] } ``` #### Fail Task — `POST /api/delivery-tasks/{id}/fail` ```json { "note": "string" } // reason for failure ``` #### Delivery Task Response ```json { "id": "guid", "fK_RideId": "guid", "fK_OrderId": "guid", "storeId": "guid", "storeName": "string", "sequence": 1, "status": "Pending", "customerConfirmed": false, "confirmedAt": "datetime | null", "note": "string | null", "items": [ { "itemId": "guid", "itemName": "string", "quantity": 10, "quantityFulfilled": 8, "unitPrice": 150.00 } ], "createdAt": "datetime" } ``` --- ### 6.27 Sales **Base path:** `/api/sales` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/sales` | `orders.view` | List all sales | | `GET` | `/api/sales/{id}` | `orders.view` | Get sale detail | | `GET` | `/api/sales/store/{storeId}` | `orders.view` | Get sales for a store (scope-enforced) | | `POST` | `/api/sales` | `orders.create` | Create a new sale | > `GET /api/sales/store/{storeId}` returns `403 Forbidden` if current user's scope does not include that store. #### Create Sale — `POST /api/sales` ```json { "storeId": "guid", "saleDate": "datetime", "totalAmount": 45000.00, "fulfillImmediately": false, // if true, immediately fulfills the linked order "rideId": "guid | null", // associate with a ride (for mobile sales) "items": [ { "itemId": "guid", "quantity": 10, "unitPrice": null // null = use price list; decimal = override price } ] } ``` **Returns `403 Forbidden`** if current user's scope doesn't include the store. #### Sale Response ```json { "id": "guid", "saleDate": "datetime", "storeId": "guid", "createdByUserId": "guid", "rideId": "guid | null", "totalAmount": 45000.00, "status": "Pending", "paymentStatus": "Pending", "fulfillImmediately": false, "completedAt": "datetime | null", "orderId": "guid", // the linked Order record "orderStatus": "Pending", "orderFulfilledAt": "datetime | null", "items": [ { "itemId": "guid", "quantity": 10, "quantityFulfilled": 0, "unitPrice": 150.00 } ], "isActive": true, "createdAt": "datetime" } ``` --- ### 6.28 Orders **Base path:** `/api/orders` | Method | Path | Permission | Description | |---|---|---|---| | `GET` | `/api/orders` | `orders.view` | List all orders | | `GET` | `/api/orders/{id}` | `orders.view` | Get order by ID | | `GET` | `/api/orders/store/{storeId}` | `orders.view` | Get orders for a store (scope-enforced) | | `POST` | `/api/orders` | `orders.create` | Create order (linked to a sale) | | `PUT` | `/api/orders/{id}` | `orders.update` | Update order status | | `DELETE` | `/api/orders/{id}` | `orders.delete` | Delete order | | `POST` | `/api/orders/{id}/apply-scheme` | — | Apply a scheme to order | > `GET /api/orders/store/{storeId}` and `POST /api/orders` return `403 Forbidden` if scope is not satisfied. #### Create Order — `POST /api/orders` ```json { "saleId": "guid" } ``` #### Update Order — `PUT /api/orders/{id}` ```json { "status": "Fulfilled" } // OrderStatus enum ``` #### Order Response ```json { "id": "guid", "saleId": "guid", "orderDate": "datetime", "storeId": "guid", "totalAmount": 45000.00, "status": "Pending", "fulfilledAt": "datetime | null", "items": [ { "itemId": "guid", "quantity": 10, "quantityFulfilled": 0, "unitPrice": 150.00 } ], "isActive": true, "createdAt": "datetime" } ``` --- ### 6.29 Payments **Base path:** `/api/sales/{saleId}/payments` | Method | Path | Description | |---|---|---| | `GET` | `/api/sales/{saleId}/payments` | Get all payments for a sale | | `POST` | `/api/sales/{saleId}/payments` | Record a payment for a sale | > `GET` returns `404 Not Found` if the sale doesn't exist. > `POST` returns `404 Not Found` if the sale doesn't exist, `400 Bad Request` if payment already complete. #### Create Payment — `POST /api/sales/{saleId}/payments` ```json { "amount": 25000.00, "method": "Cash" // PaymentMethod enum } ``` #### Payment Response ```json { "id": "guid", "saleId": "guid", "storeId": "guid", "amount": 25000.00, "method": "Cash", "collectedAt": "datetime", "collectedBy": "guid", // user who recorded the payment "createdAt": "datetime" } ``` --- ### 6.30 Sync (ERP Import) **Base path:** `/api/sync` **Required permission:** `items.create` These endpoints are for bulk import from an external ERP system. Each payload is idempotent — existing records (matched by `erpId`) are updated; new records are inserted. | Method | Path | Description | |---|---|---| | `POST` | `/api/sync/uoms` | Bulk upsert units of measure | | `POST` | `/api/sync/categories` | Bulk upsert categories + subcategories | | `POST` | `/api/sync/brands` | Bulk upsert brands | | `POST` | `/api/sync/items` | Bulk upsert items | #### Sync UOMs — `POST /api/sync/uoms` ```json { "items": [ { "erpId": 101, "name": "Piece" } ] } ``` #### Sync Categories — `POST /api/sync/categories` ```json { "items": [ { "erpId": 10, "name": "Beverages", "subCategories": [ { "erpId": 101, "name": "Soft Drinks", "parentErpId": 10 } ] } ] } ``` #### Sync Brands — `POST /api/sync/brands` ```json { "items": [ { "erpId": 201, "name": "Coca-Cola" } ] } ``` #### Sync Items — `POST /api/sync/items` ```json { "items": [ { "erpId": 5001, "sku": "CC-330ML", "name": "Coca-Cola 330ml", "description": "string | null", "categoryErpId": 10, "subCategoryErpId": 101, "brandErpId": 201, "baseUomErpId": 101, "salePrice": 65.00, "isActive": true } ] } ``` #### Sync Result Response (all sync endpoints) ```json { "inserted": 5, "updated": 12, "skipped": 0 } ``` --- ## 7. Business Flows End-to-end sequences showing which API calls to make, in order, for each major workflow. Every step maps directly to an endpoint documented in Section 6. --- ### 7.1 Region → Route → Store Setup This is the master data foundation. Everything else (rides, sales, delivery) depends on it existing first. ``` 1. POST /api/regions → creates a region (geographic area) 2. POST /api/routes → creates a route inside that region (requires regionId) 3. POST /api/stores → creates a store on that route (requires routeId) 4. POST /api/stores/{id}/kyc-documents → upload KYC docs for the store 5. PUT /api/stores/{id}/kyc/verify → approve/reject KYC (kycStatus: Verified unlocks full ordering) ``` **Key rules:** - A store belongs to exactly one route; a route belongs to exactly one region. - Stores with `KycStatus = Pending` can only place orders up to the configured threshold (default 0 — meaning blocked). Verify KYC before the store can transact. - Users are scoped to regions/routes/stores via `PUT /api/users/{id}/scope`. Scoped users only see and operate on data within their assigned scope. --- ### 7.2 Sale Flow (Sales Run) Used when a sales rep visits a store and records a sale on the spot. The sale and its linked order are created atomically. ``` 1. POST /api/sales body: { storeId, saleDate, totalAmount, fulfillImmediately, rideId?, items[] } → creates Sale + Order in one call → Sale.status = Pending, Order.status = Pending (if fulfillImmediately: false) → Sale.status = Completed, Order.status = Fulfilled (if fulfillImmediately: true) 2. (Optional) GET /api/schemes/eligible?storeId=...&orderId=... → find promotions the store qualifies for 3. (Optional) POST /api/orders/{orderId}/apply-scheme body: { schemeId } → applies discount or free-goods scheme to the order 4. POST /api/sales/{saleId}/payments body: { amount, method: "Cash"|"Credit" } → records a payment; Sale.paymentStatus updates automatically: Pending → PartiallyPaid → Paid (based on cumulative total vs totalAmount) → repeat this step for partial payments ``` **Price resolution (automatic on sale creation):** - If `items[].unitPrice` is `null`, the system looks up the active price list for the store's region and uses that price. - If `unitPrice` is supplied, it must fall within `[minPrice, price]` from the price list. Prices outside this range cause the sale to be rejected (`null` response → treat as validation error). **Stock deduction (when `fulfillImmediately: true` + `rideId` supplied):** - The system checks that the vehicle has sufficient stock for every item. - Stock is deducted from the vehicle immediately and a `Sale` movement is recorded. - If any item lacks stock, the entire sale creation fails. **Scope enforcement:** - The creating user must have scope access to the store, otherwise `403 Forbidden` is returned. --- ### 7.3 Delivery Flow (End-to-End Ride Lifecycle) The full sequence for a delivery run — from creating the ride to closing it out. #### Step 1 — Set up the ride (Draft) ``` POST /api/rides body: { rideType: "Delivery", fK_VehicleId, fK_DriverProfileId, fK_RouteId, fK_SourceWarehouseId? } → Ride.status = Draft PUT /api/rides/{id}/driver ← optional, change driver while still Draft body: { fK_DriverProfileId } ``` #### Step 2 — Open the ride (Draft → Open) ``` POST /api/rides/{id}/open → Ride.status = Open ``` **Blocked if:** - The vehicle already has another Open or Active ride. - Driver compliance is enforced and the driver's license or medical clearance has expired. #### Step 3 — Create & confirm the loading sheet (Open ride required) ``` POST /api/loading-sheets body: { fK_RideId } → LoadingSheet.status = Pending (For Delivery rides: items are auto-populated from pending orders on the route. For Sale rides: items must be added manually in the next step.) PUT /api/loading-sheets/{id}/items ← set/adjust dispatched quantities body: { items: [{ fK_ItemId, quantity }] } POST /api/loading-sheets/{id}/confirm-dispatch → LoadingSheet.status = DispatchConfirmed (Warehouse side confirms the goods left the warehouse) ``` #### Step 4 — Driver receives stock & ride goes Active ``` PUT /api/loading-sheets/{id}/receipt-items ← driver records what they actually received body: { items: [{ fK_ItemId, quantityReceived }] } POST /api/loading-sheets/{id}/confirm-receipt body: { meterstart: 12345 } → if quantities match dispatched: LoadingSheet.status = ReceiptConfirmed Stock transfers from Warehouse → Vehicle (Load movement recorded) Ride.status = Active, Ride.meterstart set → if quantities differ: LoadingSheet.status = DiscrepancyFlagged (Ride stays Open; must resolve before activation) POST /api/loading-sheets/{id}/resolve ← if DiscrepancyFlagged body: { items: [{ fK_ItemId, quantityResolved }], meterstart } → LoadingSheet.status = Resolved Stock transfers using resolved quantities Ride.status = Active ``` #### Step 5 — Generate delivery tasks & execute deliveries (Active ride, Delivery type only) ``` POST /api/rides/{rideId}/tasks/generate → creates one DeliveryTask per pending order on the route (idempotent — skips orders that already have a task for this ride) GET /api/rides/{rideId}/tasks ← list tasks to display the delivery sequence For each task: PUT /api/delivery-tasks/{id}/quantities body: { items: [{ itemId, quantityFulfilled }] } → set how much was actually delivered per item POST /api/delivery-tasks/{id}/confirm → DeliveryTask.status = Delivered (all qty) or PartiallyDelivered Order.status = Fulfilled or PartiallyFulfilled Sale.status = Completed (if fully delivered) Stock deducted from Vehicle (Sale movement recorded) — OR — POST /api/delivery-tasks/{id}/fail body: { note: "reason" } → DeliveryTask.status = Failed (stock not deducted) ``` #### Step 6 — Close the ride (Active → Closed) ``` POST /api/rides/{id}/close body: { meterend: 12567 } → Ride.status = Closed ``` **Blocked if:** The vehicle still has stock with quantity > 0. All stock must be returned (via transfer, damage, or customer-return endpoints) before closing. --- ### 7.4 Ride Cancellation Flow Applies when a ride is Active and something goes wrong (breakdown, driver issue, etc.). ``` POST /api/rides/{id}/cancel-request body: { typeCode: "VehicleBreakdown", reason: "string" } → Ride.status = CancelRequested RideCancelRequest.status = Pending GET /api/rides/cancel-requests?status=Pending ← supervisor polls for pending requests GET /api/rides/{id}/cancel-request ← check status for a specific ride POST /api/rides/cancel-requests/{requestId}/approve → RideCancelRequest.status = Approved Ride.status = Cancelled (Blocked if vehicle still has stock) — OR — POST /api/rides/cancel-requests/{requestId}/reject → RideCancelRequest.status = Rejected Ride.status = Active (restored) ``` **Breakdown Assist** — when a ride is cancelled mid-route, use this to find a replacement: ``` GET /api/rides/breakdown-assist/{cancelledRideId} → returns: other Active rides on the same route with their current vehicle stock levels + open orders from the cancelled ride's route ``` --- ### 7.5 Payment Flow Payments are recorded against a Sale after it is created. Multiple partial payments are supported. ``` 1. POST /api/sales ← sale created; paymentStatus = Pending 2. POST /api/sales/{saleId}/payments body: { amount: 25000.00, method: "Cash" } → payment recorded; sale.paymentStatus updated: sum(payments) == 0 → Pending sum(payments) < totalAmount → PartiallyPaid sum(payments) >= totalAmount → Paid 3. Repeat step 2 for additional instalments. 4. GET /api/sales/{saleId}/payments ← list all payments for the sale ``` **Rules:** - Cannot record payment against a `Cancelled` sale — returns `400 Bad Request`. - No upper limit enforced — overpayment is allowed (handle reconciliation in the UI). - `collectedBy` is automatically set from the authenticated user's ID. --- ### 7.6 Purchase Order Flow (Procurement) ``` 1. POST /api/purchase-orders body: { supplierId, items: [{ itemId, quantity, unitCost }] } → PO.status = Issued 2. (Optional) PUT /api/purchase-orders/{id} ← edit lines while Issued only body: { items: [{ purchaseOrderItemId?, itemId, quantity, unitCost }] } (purchaseOrderItemId null = add new line, guid = update existing) 3. POST /api/purchase-orders/{id}/goods-receipts ← goods arrive at warehouse body: { warehouseId, items: [{ purchaseOrderItemId, acceptedQuantity, rejectedQuantity }] } → GRN created; stock added to Warehouse (PurchaseReceipt movement) → PO.status = PartiallyReceived or Received (if all lines fully received) 4. POST /api/purchase-orders/{id}/match-invoice ← optional 3-way match query: ?grnId=...&invoiceId=... → returns line-by-line comparison of ordered vs received quantities 5. (if discrepancy) POST /api/purchase-orders/{id}/resolve-variance body: { variance, resolution, adjustedUnitCost?, purchaseOrderItemId? } 6. POST /api/purchase-orders/{id}/close ← manually close when done → PO.status = Closed — OR — POST /api/purchase-orders/{id}/cancel ← cancel entire PO (Issued only) body: { reason } POST /api/purchase-orders/{id}/items/{itemId}/cancel ← cancel one line (Issued only) body: { reason? } ``` **Returns:** `GET /api/purchase-orders/{id}/returns` / `POST /api/purchase-orders/{id}/returns` Record supplier return: `{ reason, items: [{ itemId, quantity }] }` → stock deducted from Warehouse (PurchaseReturn movement). --- ### 7.7 Loading Sheet Flow (Detail) The loading sheet controls the physical movement of stock from warehouse to vehicle. It sits between "ride opened" and "ride active". ``` Status machine: Pending ──► DispatchConfirmed ──► ReceiptConfirmed (clean) └──► DiscrepancyFlagged ──► Resolved ``` | Status | Who acts | What happens to stock | |---|---|---| | `Pending` | Warehouse staff | No stock movement yet; edit items freely | | `DispatchConfirmed` | Warehouse staff | Goods physically leave warehouse; no stock change yet | | `ReceiptConfirmed` | Driver | Stock transferred Warehouse → Vehicle; ride goes Active | | `DiscrepancyFlagged` | System | Quantities differ; ride stays Open; waiting for resolution | | `Resolved` | Supervisor | Resolved quantities transferred; ride goes Active | **Important:** confirming receipt (or resolving discrepancy) is what actually activates the ride and loads stock onto the vehicle. The `meterstart` odometer reading is recorded at this point. --- ### 7.8 Stock Flow (How Stock Moves) Stock is tracked per item per location (Warehouse or Vehicle). Every write operation creates an immutable `StockMovement` record. | Trigger | API call | Movement type | Direction | |---|---|---|---| | GRN received | `POST /api/purchase-orders/{id}/goods-receipts` | `PurchaseReceipt` | → Warehouse | | Loading sheet confirmed | `POST /api/loading-sheets/{id}/confirm-receipt` | `Load` | Warehouse → Vehicle | | Sale fulfilled (immediate) | `POST /api/sales` (fulfillImmediately: true) | `Sale` | Vehicle → (out) | | Delivery task confirmed | `POST /api/delivery-tasks/{id}/confirm` | `Sale` | Vehicle → (out) | | Customer returns goods | `POST /api/stock/customer-return` | `CustomerReturn` | → Vehicle | | Supplier return | `POST /api/purchase-orders/{id}/returns` | `PurchaseReturn` | Warehouse → (out) | | Manual transfer | `POST /api/stock/transfer` | `Transfer` | Any → Any | | Damage write-off | `POST /api/stock/damage` | `Damage` | Location → (out) | | Manual correction | `POST /api/stock/adjustment` | `Adjustment` | Location ± delta | **Closing a ride requires zero vehicle stock.** Any remaining stock must be transferred back to a warehouse, recorded as damage, or returned by the customer before `POST /api/rides/{id}/close` will succeed. --- ### 7.9 Scheme / Promotion Flow ``` 1. POST /api/schemes ← create a promotion body: { name, ruleType, ruleConfig?, regionId?, eligibleSegment?, dateRangeFrom, dateRangeTo, agentLimitExempt, baselineExpectedSales?, ...optional free-goods fields } 2. (if BuyXGetY or FreeBundle) PUT /api/schemes/{id}/free-goods-rule PUT /api/schemes/{id}/bundle-components/{itemId} 3. At order time: GET /api/schemes/eligible?storeId=...&orderId=... → returns which schemes the store qualifies for, with discount/free-goods amounts 4. POST /api/orders/{orderId}/apply-scheme body: { schemeId } → scheme applied to the order; SchemeRedemption recorded 5. Analytics: GET /api/schemes/{id}/spend ← total cost of scheme so far GET /api/schemes/{id}/roi ← ROI vs baseline expected sales ``` **Rule types quick reference:** | Type | `ruleConfig` JSON | Trigger | Payout | |---|---|---|---| | `FlatPercent` | `{"discountPercent": 10.0}` | Any order | % off total | | `SlabDiscount` | `{"tiers":[{"minOrderAmount":10000,"discountPercent":5},...]}` | Order value reaches slab | % off total | | `BuyXGetY` | `null` | Buy N of trigger item | Free qty of free item | | `FreeBundle` | `null` | Buy all bundle components | Free qty of free item | --- ## Appendix A — Common Business Rules 1. **Store KYC threshold:** A store with `KycStatus = Pending` has an order threshold of `0` (configurable via `StoreManagement:UnverifiedKycOrderThreshold`). Orders for unverified stores may be blocked or limited. 2. **Credit enforcement:** On sale creation, the system checks `OutstandingBalance + saleAmount ≤ CreditLimit`. Exceeding this will block the sale. 3. **Driver compliance:** When `EnforceLicenseExpiry = true`, a ride cannot be opened/activated if the assigned driver's license has expired. Same for `EnforceMedicalClearance`. 4. **Ride state machine:** Transitions are strictly enforced — calling `open` on a non-Draft ride, `activate` on a non-Open ride, etc. returns `400 Bad Request`. 5. **Loading sheet discrepancy:** When `ConfirmReceipt` is called and received quantities differ from dispatched quantities, the sheet automatically transitions to `DiscrepancyFlagged` instead of `ReceiptConfirmed`. 6. **PO line cancellation:** Individual lines can be cancelled only while the PO is `Issued`. The entire PO can only be cancelled while `Issued`. 7. **Scheme eligibility:** A scheme applies to a store only if the store's segment matches `EligibleSegment` (or the scheme has no segment restriction), the scheme is within its date range, and the region matches (or scheme has no region restriction). 8. **Agent discount ceiling:** A role's `Level` field controls the maximum discount an agent can apply. Schemes with `AgentLimitExempt = true` stack on top regardless of this ceiling. 9. **Scope enforcement:** Users with an empty scope list for a scope type (regions/routes/stores) are assumed to have **no restriction** for that type (system-wide access). Only explicitly listed IDs restrict access. --- ## Appendix B — Frontend Implementation Notes ### Token / Session Strategy > ⚠️ **YET TO IMPLEMENT** — Auth mechanism not yet decided. Do not implement token storage until Section 1 is finalized. ### Permission Gating Once auth is implemented, call `GET /api/roles/{roleId}/permissions` on login to pre-fetch all permissions for the session. Cache them client-side and use them to show/hide UI elements. The role ID will come from whatever credential the Auth Service issues. ### Enum Serialization The API serializes enums as **strings** by default in ASP.NET Core `System.Text.Json` — verify with your actual deployment. If numbers are returned, map them using the tables in Section 4. ### GUID Format Always send GUIDs as lowercase hyphenated strings: `"3fa85f64-5717-4562-b3fc-2c963f66afa6"`. ### Date Format Send all dates as ISO 8601 strings. For date-only fields (license expiry), include a time component: `"2026-12-31T00:00:00Z"`. ### Pagination For paginated endpoints, always implement `page` / `pageSize` query params in the UI. Default `pageSize` is `20`. ### File Upload KYC document upload uses `multipart/form-data`. Do not set `Content-Type` manually when using browser `FormData` — the browser sets it with the boundary automatically. ### Error Handling - `400` errors include `{ "error": "descriptive message" }` — surface this to the user. - `404` — show "not found" state. - `409` — show the conflict message from `{ "error": "..." }`. - `401` — redirect to login. - `403` — show "access denied" / hide the action.