c0432a448f
Notes the new PUT /api/routes/{id}/region endpoint as a stand-in for
the still-blocked assignAreaToRegion, and removes the stale "migration
pending" caveat now that AddRegionManagement has been applied.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2689 lines
79 KiB
Markdown
2689 lines
79 KiB
Markdown
# Distribution ERP — Backend API Reference
|
|
|
|
> **Stack:** ASP.NET Core (.NET 9) · MySQL (Entity Framework Core)
|
|
> **Base URL:** `https://<host>` (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".
|
|
|
|
---
|
|
|
|
### 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 |
|
|
| `PUT` | `/api/routes/{id}/region` | `routes.update` | Assign/reassign the route to a region |
|
|
| `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"
|
|
}
|
|
```
|
|
|
|
**Assign to region request:**
|
|
```json
|
|
// PUT /api/routes/{id}/region
|
|
{ "regionId": "guid" }
|
|
```
|
|
Returns the same `RouteResponse` shape as above. `404` if the route or the target region doesn't
|
|
exist (or either is inactive).
|
|
|
|
---
|
|
|
|
### 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.
|