name: CI/CD - ERP Core Monorepo
on:
pull_request:
branches:
- production
types:
- closed
jobs:
backend-build-and-deploy:
name: Build & Deploy ERP Core Backend
runs-on: linux
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Restore backend
run: dotnet restore Backend/ERPCore.sln
- name: Publish backend
run: |
rm -rf /tmp/erpcore-deploy
mkdir -p /tmp/erpcore-deploy
dotnet publish Backend/ERPCore/ERPCore.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o /tmp/erpcore-deploy
- name: Stop backend service
run: systemctl stop erpcore.service
- name: Copy backend to deploy folder
run: |
mkdir -p /home/erp/core/publish
rm -rf /home/erp/core/publish/*
cp -r /tmp/erpcore-deploy/* /home/erp/core/publish/
- name: Fix backend ownership and permissions
run: |
chown -R erpuser:erpuser /home/erp/core/publish
find /home/erp/core/publish -type d -exec chmod 755 {} +
find /home/erp/core/publish -type f -exec chmod 644 {} +
chmod +x /home/erp/core/publish/ERPCore
- name: Restart backend service
run: systemctl restart erpcore.service
- name: Verify backend
run: |
systemctl is-active erpcore.service && echo "Backend service running" || echo "Backend service failed"
frontend-build-and-deploy:
name: Build & Deploy ERP Core Frontend
runs-on: linux
needs: backend-build-and-deploy
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch Production \
https://oauth2:${{ secrets.ERPCORE }}@gitea.hexdive.com/New_REP_SYSTEM/ERP-core.git \
.
- name: Build frontend Docker image
run: docker build -t erpcore-frontend:latest Frontend/erp-system
- name: Stop & remove existing frontend container
run: |
docker stop erpcore-frontend 2>/dev/null || true
docker rm erpcore-frontend 2>/dev/null || true
- name: Start frontend container
run: |
docker run -d \
--name erpcore-frontend \
-p 6702:6702 \
--add-host=host.docker.internal:host-gateway \
-e BACKEND_ORIGIN=http://host.docker.internal:6701 \
--restart unless-stopped \
--memory="512m" \
--cpus="1.0" \
--cap-drop ALL \
--security-opt no-new-privileges \
erpcore-frontend:latest
- name: Remove old frontend images
run: docker image prune -f
- name: Verify frontend
run: |
docker ps --filter name=erpcore-frontend --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \
| grep erpcore-frontend && echo "Frontend container running" || echo "Frontend container failed"
This commit is contained in:
@@ -684,15 +684,33 @@ appsettings.Development.json
|
||||
|
||||
---
|
||||
|
||||
## Open Items (must resolve before coding the relevant phase)
|
||||
## Open Items
|
||||
|
||||
### Resolved during implementation
|
||||
|
||||
| # | Item | Resolution |
|
||||
|---|---|---|
|
||||
| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Cancel is rejected (400) if a linked Invoice exists. Invoice must be voided separately before the ShopPO can be cancelled. No automatic cascade. |
|
||||
| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Cancel is rejected (400) if the ShopPO appears in any Planned LoadingSheet. Operator must remove it from the sheet first. |
|
||||
| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Deferred to a later pass. v1 `POST /api/invoices/{id}/finalize` takes no body and locks the invoice as-is. |
|
||||
| 5 | Who can confirm a ShopPO? | No role restriction in v1 — any user can confirm. Gating deferred until the auth layer is implemented. |
|
||||
|
||||
### Still open
|
||||
|
||||
| # | Item | Blocks |
|
||||
|---|---|---|
|
||||
| 1 | Who holds `loading.confirm` permission? | Phase 9 — ConfirmAsync auth guard |
|
||||
| 2 | Cascade when Confirmed ShopPO is cancelled and Invoice already exists | Phase 7 — CancelAsync |
|
||||
| 3 | Cascade when Confirmed ShopPO is cancelled and it's in a LoadingSheet | Phase 7 — CancelAsync |
|
||||
| 4 | `FinalizeInvoiceRequest` payload — free issues field shape | Phase 6 — FinalizeAsync |
|
||||
| 5 | Who can confirm a ShopPO? (agent, office, either?) | Phase 7 — ConfirmAsync permission |
|
||||
| 1 | Who holds `loading.confirm` permission? | Permission seeding (`loading.populate`, `loading.confirm`) — explicitly noted as **Pending** in the API reference. Must decide role before seeding. |
|
||||
|
||||
### New items — identified during frontend refactor (2026-09-17)
|
||||
|
||||
| # | Item | Area |
|
||||
|---|---|---|
|
||||
| 6 | `order-return-edit-dialog.tsx` — file not verified; client imports it but it was not reviewed during the refactor pass. Needs to be checked for old `orderId`/`OrderResponse` references. | Returns |
|
||||
| 7 | `sales-visits/` components — any components in this section that interact with sales/orders need auditing against the new ShopPO/Invoice types. | Sales Visits |
|
||||
| 8 | SysConfig management page not built — the backend has a `SysConfig` model and the `InvoiceCreationMode` config key is read by `ShopPOController`. A UI to view and toggle system config is missing. | Config |
|
||||
| 9 | InvoiceCreationMode (ModeA vs ModeB) UI branching — the ShopPO confirm button calls `confirm` or `confirmAndCreateInvoice` depending on mode, but the mode is not currently read from the backend. The UI should fetch `GET /api/sys-config` and gate the action accordingly. | Sales / Invoice |
|
||||
| 10 | Loading Sheet item population UI — `PUT /api/loading-sheets/{id}/items` exists in the API layer but no UI for populating/editing items on a Planned sheet has been built. | Dispatch |
|
||||
| 11 | `PaymentMethod` enum is missing `Cheque` — per `invoice_collection.md`, the final design includes `Cash`, `Cheque`, `Credit`. The frontend only has `Cash` and `Credit`. Cheque-specific fields (`ChequeNumber`, `ChequeDate`) and `EntryMode` (`RealTime`, `DayEndManual`) are also unimplemented. | Payments |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,978 @@
|
||||
# Frontend API Reference — ERP Distribution (Post-Refactor)
|
||||
|
||||
> Last updated: 2026-09-17
|
||||
> Base URL: `https://<host>/api`
|
||||
> All responses are JSON. Enum fields are serialized as **strings** (e.g. `"Pending"`, `"ModeB"`).
|
||||
> All `Guid` fields are UUID v4 strings. All `DateTime` fields are ISO 8601.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Pagination](#pagination)
|
||||
2. [Enums Reference](#enums-reference)
|
||||
3. [ShopPO](#shoppo)
|
||||
4. [Invoice](#invoice)
|
||||
5. [Payment](#payment)
|
||||
6. [Ride (DeliveryRun)](#ride-deliveryrun)
|
||||
7. [Loading Sheet](#loading-sheet)
|
||||
8. [Delivery Tasks](#delivery-tasks)
|
||||
9. [Returns](#returns)
|
||||
10. [Schemes](#schemes)
|
||||
11. [System Config](#system-config)
|
||||
12. [Key Relationships & Flows](#key-relationships--flows)
|
||||
|
||||
---
|
||||
|
||||
## Pagination
|
||||
|
||||
All list endpoints return a `PagedResponse<T>`:
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 84,
|
||||
"items": [ /* T[] */ ]
|
||||
}
|
||||
```
|
||||
|
||||
Default page size is `20`. Pass `?page=2&pageSize=50` to paginate.
|
||||
|
||||
---
|
||||
|
||||
## Enums Reference
|
||||
|
||||
| Enum | Values |
|
||||
|------|--------|
|
||||
| `ShopPOStatus` | `Pending`, `PartiallyFulfilled`, `Fulfilled`, `Cancelled` |
|
||||
| `InvoiceStatus` | `Draft`, `Printed`, `Finalized` |
|
||||
| `PaymentStatus` | `Pending`, `PartiallyPaid`, `Paid` |
|
||||
| `PaymentMethod` | `Cash`, `Credit` |
|
||||
| `RideStatus` | `Open`, `Cancelled`, `Closed` |
|
||||
| `LoadingSheetStatus` | `Planned`, `Confirmed` |
|
||||
| `DeliveryTaskStatus` | `Pending`, `Delivered`, `PartiallyDelivered`, `Failed` |
|
||||
| `ReturnMode` | `CreditDeduction`, `ItemExchange` |
|
||||
| `ReturnReasonCode` | `Expired`, `Damaged`, `OrderError`, `Other` |
|
||||
| `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` |
|
||||
| `InvoiceCreationMode` | `ModeA`, `ModeB` |
|
||||
| `SchemeRuleType` | `SlabDiscount`, `FlatPercent`, `BuyXGetY`, `FreeBundle` |
|
||||
|
||||
---
|
||||
|
||||
## ShopPO
|
||||
|
||||
A ShopPO is a shop-facing purchase order raised by a sales agent on behalf of a store.
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
Pending → PartiallyFulfilled → Fulfilled
|
||||
↘ Cancelled
|
||||
```
|
||||
|
||||
### `GET /api/shop-pos`
|
||||
|
||||
List all ShopPOs with optional filters.
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `agentId` | `Guid?` | Filter by the agent who raised the PO |
|
||||
| `routeId` | `Guid?` | Filter by the store's route |
|
||||
| `shopId` | `Guid?` | Filter by store |
|
||||
| `confirmed` | `bool?` | `true` = confirmed only, `false` = unconfirmed only |
|
||||
| `page` | `int` | Default `1` |
|
||||
| `pageSize` | `int` | Default `20` |
|
||||
|
||||
**Response:** `PagedResponse<ShopPOResponse>`
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 12,
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"storeId": "uuid",
|
||||
"storeName": "Colombo Mart",
|
||||
"raisedById": "uuid",
|
||||
"invoiceId": "uuid | null",
|
||||
"totalAmount": 15000.00,
|
||||
"status": "Pending",
|
||||
"confirmedAt": "2026-09-17T10:00:00 | null",
|
||||
"confirmedBy": "uuid | null",
|
||||
"fulfilledAt": "2026-09-17T14:00:00 | null",
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"itemId": "uuid",
|
||||
"itemName": "Milo 400g",
|
||||
"quantity": 10,
|
||||
"quantityFulfilled": 0,
|
||||
"unitPrice": 850.00,
|
||||
"isActive": true
|
||||
}
|
||||
],
|
||||
"isActive": true,
|
||||
"createdAt": "2026-09-17T08:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/shop-pos/{id}`
|
||||
|
||||
**Response:** `ShopPOResponse` (same shape as above, single object) or `404`.
|
||||
|
||||
---
|
||||
|
||||
## Invoice
|
||||
|
||||
An Invoice is the billing document generated from a confirmed ShopPO. There is at most one Invoice per ShopPO.
|
||||
|
||||
**InvoiceCreationMode** (system config) controls whether the invoice is:
|
||||
- **ModeA** — auto-created when the ShopPO is confirmed
|
||||
- **ModeB** — created as a separate explicit step (default)
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
Draft → Printed → Finalized
|
||||
```
|
||||
|
||||
Once Finalized, the invoice is locked. Payments can be collected at any status.
|
||||
|
||||
### `GET /api/invoices`
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `agentId` | `Guid?` | Agent who raised the ShopPO |
|
||||
| `routeId` | `Guid?` | Store's route |
|
||||
| `shopId` | `Guid?` | Store |
|
||||
| `status` | `InvoiceStatus?` | `Draft`, `Printed`, `Finalized` |
|
||||
| `page` | `int` | Default `1` |
|
||||
| `pageSize` | `int` | Default `20` |
|
||||
|
||||
**Response:** `PagedResponse<InvoiceResponse>`
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 5,
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"shopPOId": "uuid",
|
||||
"storeName": "Colombo Mart",
|
||||
"status": "Draft",
|
||||
"paymentStatus": "Pending",
|
||||
"createdBy": "uuid",
|
||||
"printedAt": null,
|
||||
"printedBy": null,
|
||||
"finalizedAt": null,
|
||||
"finalizedBy": null,
|
||||
"totalAmount": 15000.00,
|
||||
"isActive": true,
|
||||
"createdAt": "2026-09-17T08:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/invoices/{id}`
|
||||
|
||||
**Response:** `InvoiceResponse` (single) or `404`.
|
||||
|
||||
> **Note:** The detail response from the repository also loads `Payments`. The current `InvoiceResponse` DTO does not yet expose payments — use `GET /api/invoices/{id}/payments` for that.
|
||||
|
||||
---
|
||||
|
||||
## Payment
|
||||
|
||||
Payments are collected against an Invoice. Multiple payments can be made on one invoice (e.g. partial cash collection).
|
||||
|
||||
### `GET /api/invoices/{invoiceId}/payments`
|
||||
|
||||
Returns all payments for the invoice.
|
||||
|
||||
**Response:** `PaymentResponse[]`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"invoiceId": "uuid",
|
||||
"amount": 5000.00,
|
||||
"method": "Cash",
|
||||
"collectedAt": "2026-09-17T15:00:00",
|
||||
"collectedBy": "uuid",
|
||||
"createdAt": "2026-09-17T15:00:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Returns `404` if the invoice does not exist.
|
||||
|
||||
### `POST /api/invoices/{invoiceId}/payments`
|
||||
|
||||
Record a payment against an invoice.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"amount": 5000.00,
|
||||
"method": "Cash",
|
||||
"collectedByUserId": "uuid | null"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `amount` | `decimal` | Yes | Must be > 0 |
|
||||
| `method` | `PaymentMethod` | Yes | `Cash` or `Credit` |
|
||||
| `collectedByUserId` | `Guid?` | No | Defaults to authenticated user |
|
||||
|
||||
**Response:** `PaymentResponse` or `404` if invoice not found, `400` on business rule violation.
|
||||
|
||||
---
|
||||
|
||||
## Ride (DeliveryRun)
|
||||
|
||||
A Ride represents a vehicle dispatch for a day. The driver moves along a route making deliveries.
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
Open → Closed
|
||||
↘ Cancelled
|
||||
```
|
||||
|
||||
### `GET /api/rides`
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `status` | `RideStatus?` | `Open`, `Cancelled`, `Closed` |
|
||||
| `driverProfileId` | `Guid?` | Filter by driver |
|
||||
| `vehicleId` | `Guid?` | Filter by vehicle |
|
||||
| `page` | `int` | Default `1` |
|
||||
| `pageSize` | `int` | Default `20` |
|
||||
|
||||
**Response:** `PagedResponse<RideResponse>`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"status": "Open",
|
||||
"vehicleId": "uuid",
|
||||
"vehicleNumberPlate": "WP CAB 1234",
|
||||
"driverProfileId": "uuid",
|
||||
"driverFullName": "Kamal Perera",
|
||||
"meterstart": 12450,
|
||||
"meterend": 0,
|
||||
"startedAt": null,
|
||||
"closedAt": null,
|
||||
"createdAt": "2026-09-17T06:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/rides/{id}`
|
||||
|
||||
**Response:** `RideResponse` or `404`.
|
||||
|
||||
### `POST /api/rides`
|
||||
|
||||
Create a new ride.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"vehicleId": "uuid",
|
||||
"driverProfileId": "uuid",
|
||||
"meterstart": 12450
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created` with `RideResponse`. `400` on validation error.
|
||||
|
||||
### `PUT /api/rides/{id}/driver`
|
||||
|
||||
Re-assign the driver on an Open ride.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"driverProfileId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `RideResponse`. `404` if ride not found. `400` if ride is not Open.
|
||||
|
||||
### `POST /api/rides/{id}/close`
|
||||
|
||||
Close a ride and record the ending odometer.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"meterend": 12890
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `RideResponse`. `400` if ride is not Open.
|
||||
|
||||
### `POST /api/rides/{id}/cancel`
|
||||
|
||||
Cancel a ride.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"typeCode": "VehicleBreakdown",
|
||||
"reason": "Engine overheating"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `typeCode` | `CancelRequestType` | `VehicleBreakdown`, `DriverUnavailable`, `RouteIssue`, `Other` |
|
||||
| `reason` | `string` | Free text reason |
|
||||
|
||||
**Response:** `RideResponse`. `400` if ride is not Open.
|
||||
|
||||
---
|
||||
|
||||
## Loading Sheet
|
||||
|
||||
A Loading Sheet records the items loaded onto a vehicle before a ride. It belongs to a Ride.
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
Planned → Confirmed
|
||||
```
|
||||
|
||||
Items can only be edited while the sheet is `Planned`. Confirming the sheet commits stock movements (Warehouse → Vehicle).
|
||||
|
||||
### `GET /api/loading-sheets`
|
||||
|
||||
**Query params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `rideId` | `Guid?` | Filter by ride |
|
||||
| `status` | `LoadingSheetStatus?` | `Planned`, `Confirmed` |
|
||||
| `page` | `int` | Default `1` |
|
||||
| `pageSize` | `int` | Default `20` |
|
||||
|
||||
**Response:** `PagedResponse<LoadingSheetResponse>`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"fK_RideId": "uuid",
|
||||
"status": "Planned",
|
||||
"populatedBy": "uuid",
|
||||
"populatedAt": "2026-09-17T07:00:00",
|
||||
"confirmedAt": null,
|
||||
"confirmedBy": null,
|
||||
"createdAt": "2026-09-17T07:00:00",
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"fK_ItemId": "uuid",
|
||||
"itemName": "Milo 400g",
|
||||
"fK_WarehouseId": "uuid",
|
||||
"warehouseName": "Colombo Warehouse",
|
||||
"quantityDispatched": 24.0,
|
||||
"fK_ShopPOItemId": "uuid | null"
|
||||
}
|
||||
],
|
||||
"orders": [
|
||||
{ "shopPOId": "uuid" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/loading-sheets/{id}`
|
||||
|
||||
**Response:** `LoadingSheetResponse` or `404`.
|
||||
|
||||
### `POST /api/loading-sheets`
|
||||
|
||||
Create a new loading sheet for a ride. The ride must exist. Only one `Planned` sheet per ride is allowed.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"fK_RideId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created` with `LoadingSheetResponse`. `400` if ride already has a planned sheet.
|
||||
|
||||
### `PUT /api/loading-sheets/{id}/items`
|
||||
|
||||
Replace all items on a `Planned` loading sheet. This is a full replace — any items not included are removed.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"fK_ItemId": "uuid",
|
||||
"fK_WarehouseId": "uuid",
|
||||
"quantity": 24.0,
|
||||
"fK_ShopPOItemId": "uuid | null"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `fK_ItemId` | `Guid` | Yes | The item being loaded |
|
||||
| `fK_WarehouseId` | `Guid` | Yes | Source warehouse for this item |
|
||||
| `quantity` | `decimal` | Yes | Units to load |
|
||||
| `fK_ShopPOItemId` | `Guid?` | No | Links the load line to a specific ShopPO line item |
|
||||
|
||||
**Response:** `LoadingSheetResponse`. `400` if sheet is not `Planned`.
|
||||
|
||||
### `POST /api/loading-sheets/{id}/confirm`
|
||||
|
||||
Confirm the loading sheet. Commits stock: moves each item from its specified warehouse to the vehicle. Sheet becomes `Confirmed` and can no longer be edited.
|
||||
|
||||
**Request body:** _(none)_
|
||||
|
||||
**Response:** `LoadingSheetResponse`. `400` if already confirmed.
|
||||
|
||||
---
|
||||
|
||||
## Delivery Tasks
|
||||
|
||||
Delivery Tasks are the per-stop delivery jobs attached to a Ride. Each task maps to one ShopPO.
|
||||
|
||||
> **Note:** Auto-generation of delivery tasks from route/ShopPO assignments is deferred and not yet implemented. Tasks must be created manually or via a future dispatch endpoint.
|
||||
|
||||
### Status Flow
|
||||
|
||||
```
|
||||
Pending → Delivered
|
||||
↘ PartiallyDelivered
|
||||
↘ Failed
|
||||
```
|
||||
|
||||
### `GET /api/rides/{rideId}/tasks`
|
||||
|
||||
List all delivery tasks for a ride, ordered by `Sequence`.
|
||||
|
||||
**Response:** `DeliveryTaskResponse[]`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"fK_RideId": "uuid",
|
||||
"fK_ShopPOId": "uuid",
|
||||
"storeId": "uuid",
|
||||
"storeName": "Galle Mart",
|
||||
"sequence": 1,
|
||||
"status": "Pending",
|
||||
"customerConfirmed": false,
|
||||
"confirmedAt": null,
|
||||
"note": null,
|
||||
"items": [
|
||||
{
|
||||
"itemId": "uuid",
|
||||
"itemName": "Milo 400g",
|
||||
"quantity": 10,
|
||||
"quantityFulfilled": 0,
|
||||
"unitPrice": 850.00
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-09-17T08:00:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `GET /api/delivery-tasks/{id}`
|
||||
|
||||
**Response:** `DeliveryTaskResponse` or `404`.
|
||||
|
||||
### `PUT /api/delivery-tasks/{id}/quantities`
|
||||
|
||||
Set the fulfilled quantity for each item in the task. Call this before confirming.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{ "itemId": "uuid", "quantityFulfilled": 8 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `DeliveryTaskResponse`. `400` if task is already closed.
|
||||
|
||||
### `POST /api/delivery-tasks/{id}/confirm`
|
||||
|
||||
Mark the task as Delivered or PartiallyDelivered based on quantities set. Updates ShopPO fulfillment, creates stock movement (Vehicle → External), and records payment if applicable.
|
||||
|
||||
**Request body:** _(none)_
|
||||
|
||||
**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`.
|
||||
|
||||
### `POST /api/delivery-tasks/{id}/fail`
|
||||
|
||||
Mark the task as Failed with a reason note.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"note": "Store was closed"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `DeliveryTaskResponse`. `400` if task is not `Pending`.
|
||||
|
||||
---
|
||||
|
||||
## Returns
|
||||
|
||||
Returns are recorded against a ShopPO during the same store visit. Effects (credit deduction or item exchange) are applied immediately — no approval step.
|
||||
|
||||
> **Note:** The Create endpoint (`POST /api/shop-pos/{id}/returns`) is currently stubbed (`501 Not Implemented`) while the return flow is being redesigned. `GET` and `PATCH` endpoints are fully operational.
|
||||
|
||||
### `GET /api/returns`
|
||||
|
||||
List all returns.
|
||||
|
||||
**Response:** `SaleReturnResponse[]`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"shopPOId": "uuid",
|
||||
"mode": "CreditDeduction",
|
||||
"totalCreditAmount": 1700.00,
|
||||
"orderTotalAmountAfter": 13300.00,
|
||||
"isActive": true,
|
||||
"createdAt": "2026-09-17T11:00:00",
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"itemId": "uuid",
|
||||
"sku": "MILO400",
|
||||
"itemName": "Milo 400g",
|
||||
"quantity": 2.0,
|
||||
"reasonCode": "Damaged",
|
||||
"unitPrice": 850.00
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `GET /api/returns/{id}`
|
||||
|
||||
**Response:** `SaleReturnResponse` or `404`.
|
||||
|
||||
### `GET /api/shop-pos/{id}/returns`
|
||||
|
||||
List all returns for a specific ShopPO.
|
||||
|
||||
**Response:** `SaleReturnResponse[]`
|
||||
|
||||
### `POST /api/shop-pos/{id}/returns`
|
||||
|
||||
> **501 Not Implemented** — return flow under redesign.
|
||||
|
||||
**Request body (for future reference):**
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "CreditDeduction",
|
||||
"items": [
|
||||
{ "itemId": "uuid", "quantity": 2.0, "reasonCode": "Damaged" }
|
||||
],
|
||||
"createdByUserId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
| `mode` | Effect |
|
||||
|--------|--------|
|
||||
| `CreditDeduction` | `totalCreditAmount` is netted off the ShopPO's `TotalAmount` |
|
||||
| `ItemExchange` | Items are swapped; no monetary credit applied |
|
||||
|
||||
### `PATCH /api/returns/{id}/reasons`
|
||||
|
||||
Update the reason code on individual return line items. Only reason codes can be edited post-creation.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{ "itemId": "uuid", "reasonCode": "Expired" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `SaleReturnResponse`. `409` if return is no longer active.
|
||||
|
||||
---
|
||||
|
||||
## Schemes
|
||||
|
||||
Schemes are promotional rules applied to ShopPOs (formerly Orders).
|
||||
|
||||
### Scheme Rule Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `SlabDiscount` | Tiered % discount based on order amount. Config: `SlabDiscountConfig` |
|
||||
| `FlatPercent` | Flat % discount on order total. Config: `FlatPercentConfig` |
|
||||
| `BuyXGetY` | Buy N of trigger item → get free payout item |
|
||||
| `FreeBundle` | Buy a bundle of items → get a free payout item |
|
||||
|
||||
### `GET /api/schemes`
|
||||
|
||||
List all schemes (paginated).
|
||||
|
||||
**Response:** `PagedResponse<SchemeResponse>`
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Q3 Volume Boost",
|
||||
"ruleType": "SlabDiscount",
|
||||
"ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}",
|
||||
"triggerItemId": null,
|
||||
"triggerItemName": null,
|
||||
"triggerQty": null,
|
||||
"freeItemId": null,
|
||||
"freeItemName": null,
|
||||
"freeQty": null,
|
||||
"bundleComponents": [],
|
||||
"regionId": "uuid | null",
|
||||
"regionName": "Western Province | null",
|
||||
"eligibleSegment": null,
|
||||
"dateRangeFrom": "2026-07-01T00:00:00",
|
||||
"dateRangeTo": "2026-09-30T00:00:00",
|
||||
"agentLimitExempt": false,
|
||||
"baselineExpectedSales": 500000.00,
|
||||
"isActive": true,
|
||||
"createdAt": "2026-06-15T09:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/schemes/{id}`
|
||||
|
||||
**Response:** `SchemeResponse` or `404`.
|
||||
|
||||
### `POST /api/schemes`
|
||||
|
||||
Create a scheme. For discount-only schemes (`SlabDiscount`, `FlatPercent`), free-goods fields can be omitted. For `BuyXGetY` / `FreeBundle`, you can supply the free-goods config inline here or set it later via `PUT .../free-goods-rule`.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Q3 Volume Boost",
|
||||
"ruleType": "SlabDiscount",
|
||||
"ruleConfig": "{\"Tiers\":[{\"MinOrderAmount\":10000,\"DiscountPercent\":5}]}",
|
||||
"regionId": null,
|
||||
"eligibleSegment": null,
|
||||
"dateRangeFrom": "2026-07-01T00:00:00",
|
||||
"dateRangeTo": "2026-09-30T00:00:00",
|
||||
"agentLimitExempt": false,
|
||||
"baselineExpectedSales": 500000.00
|
||||
}
|
||||
```
|
||||
|
||||
#### `ruleConfig` shapes
|
||||
|
||||
**SlabDiscount:**
|
||||
```json
|
||||
{ "Tiers": [ { "MinOrderAmount": 10000, "DiscountPercent": 5 }, { "MinOrderAmount": 20000, "DiscountPercent": 8 } ] }
|
||||
```
|
||||
|
||||
**FlatPercent:**
|
||||
```json
|
||||
{ "DiscountPercent": 3.5 }
|
||||
```
|
||||
|
||||
**BuyXGetY / FreeBundle:** `ruleConfig` is `null` — the payout is set via `PUT .../free-goods-rule`.
|
||||
|
||||
### `PUT /api/schemes/{id}`
|
||||
|
||||
Update scheme metadata. Cannot change `ruleType`.
|
||||
|
||||
### `DELETE /api/schemes/{id}`
|
||||
|
||||
Soft-delete (sets `IsActive = false`).
|
||||
|
||||
### `PUT /api/schemes/{id}/free-goods-rule`
|
||||
|
||||
Set the free-goods payout for `BuyXGetY` or `FreeBundle` schemes.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"triggerItemId": "uuid | null",
|
||||
"triggerQty": 6,
|
||||
"freeItemId": "uuid",
|
||||
"freeQty": 1.0
|
||||
}
|
||||
```
|
||||
|
||||
- `BuyXGetY`: `triggerItemId` + `triggerQty` required (what store must buy).
|
||||
- `FreeBundle`: `triggerItemId` / `triggerQty` ignored — trigger is the bundle components.
|
||||
|
||||
### `PUT /api/schemes/{id}/bundle-components/{itemId}`
|
||||
|
||||
Add or update a component in a `FreeBundle` scheme.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{ "qty": 4 }
|
||||
```
|
||||
|
||||
### `DELETE /api/schemes/{id}/bundle-components/{itemId}`
|
||||
|
||||
Remove a bundle component.
|
||||
|
||||
### `GET /api/shop-pos/{shopPOId}/scheme-eligibility`
|
||||
|
||||
Check which active schemes the ShopPO qualifies for.
|
||||
|
||||
**Response:** `SchemeEligibilityResponse[]`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"schemeId": "uuid",
|
||||
"schemeName": "Q3 Volume Boost",
|
||||
"ruleType": "SlabDiscount",
|
||||
"eligible": true,
|
||||
"reason": null,
|
||||
"discountAmount": 750.00,
|
||||
"freeItemId": null,
|
||||
"freeQty": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `POST /api/shop-pos/{shopPOId}/apply-scheme`
|
||||
|
||||
Apply a scheme to the ShopPO (create a redemption record and apply discount/free goods).
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{ "schemeId": "uuid" }
|
||||
```
|
||||
|
||||
**Response:** `SchemeRedemptionResponse`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"schemeId": "uuid",
|
||||
"shopPOId": "uuid",
|
||||
"discountAmount": 750.00,
|
||||
"freeItemId": null,
|
||||
"freeItemName": null,
|
||||
"freeQty": null,
|
||||
"freeItemCostValue": null,
|
||||
"redeemedAt": "2026-09-17T09:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/schemes/{id}/spend`
|
||||
|
||||
**Response:** `SchemeSpendResponse`
|
||||
|
||||
```json
|
||||
{
|
||||
"schemeId": "uuid",
|
||||
"totalDiscountSpend": 45000.00,
|
||||
"totalFreeGoodsCostSpend": 12000.00,
|
||||
"totalSpend": 57000.00,
|
||||
"redemptionCount": 38
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/schemes/{id}/roi`
|
||||
|
||||
**Response:** `SchemeROIResponse`
|
||||
|
||||
```json
|
||||
{
|
||||
"schemeId": "uuid",
|
||||
"baselineExpectedSales": 500000.00,
|
||||
"actualSales": 620000.00,
|
||||
"incrementalSales": 120000.00,
|
||||
"totalSpend": 57000.00,
|
||||
"roi": 1.105
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Config
|
||||
|
||||
Key-value store for system-wide configuration. All keys and values are strings. Only known keys have typed endpoints with validation.
|
||||
|
||||
### `GET /api/sys-config`
|
||||
|
||||
Return all config entries.
|
||||
|
||||
**Response:** `SysConfigResponse[]`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"key": "InvoiceCreationMode",
|
||||
"value": "ModeB",
|
||||
"updatedAt": "2026-09-17T00:00:00",
|
||||
"updatedBy": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### `GET /api/sys-config/invoice-creation-mode`
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{ "mode": "ModeB" }
|
||||
```
|
||||
|
||||
### `PUT /api/sys-config/invoice-creation-mode`
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{ "mode": "ModeA" }
|
||||
```
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| `ModeA` | Invoice is auto-created when a ShopPO is confirmed |
|
||||
| `ModeB` | Invoice must be created as a separate explicit step (default) |
|
||||
|
||||
**Response:** `SysConfigResponse`. `400` if value is not a valid `InvoiceCreationMode`.
|
||||
|
||||
### `GET /api/sys-config/{key}`
|
||||
|
||||
Generic getter by key string. `404` if key not set.
|
||||
|
||||
### `PUT /api/sys-config/{key}`
|
||||
|
||||
Generic raw string setter. Requires admin-level auth.
|
||||
|
||||
**Request body:**
|
||||
|
||||
```json
|
||||
{ "value": "SomeValue" }
|
||||
```
|
||||
|
||||
**Response:** `SysConfigResponse`. `400` if value fails validation for a known key.
|
||||
|
||||
---
|
||||
|
||||
## Key Relationships & Flows
|
||||
|
||||
### Core Entity Chain
|
||||
|
||||
```
|
||||
Store ──< ShopPO ──< ShopPOItem
|
||||
│
|
||||
└──► Invoice ──< Payment
|
||||
│
|
||||
└──► SaleReturn ──< SaleReturnItem
|
||||
│
|
||||
└──► SchemeRedemption
|
||||
|
||||
Ride (DeliveryRun) ──► LoadingSheet ──< LoadingSheetItem
|
||||
└──< DeliveryTask ──► ShopPO
|
||||
```
|
||||
|
||||
### ShopPO → Invoice Dependency
|
||||
|
||||
- `ShopPO.invoiceId` is `null` until an invoice is created.
|
||||
- In **ModeA**, the invoice is created automatically when the ShopPO is confirmed.
|
||||
- In **ModeB**, the invoice must be created via a dedicated endpoint (not yet exposed — planned).
|
||||
- An invoice can only exist for a confirmed ShopPO.
|
||||
|
||||
### Payment → Invoice Dependency
|
||||
|
||||
- Payments are always attached to an **Invoice**, never directly to a ShopPO.
|
||||
- `Invoice.paymentStatus` is computed:
|
||||
- `Pending` — no payments
|
||||
- `PartiallyPaid` — payments collected but total < invoice amount
|
||||
- `Paid` — fully settled
|
||||
|
||||
### Loading Sheet → Stock
|
||||
|
||||
- Confirming a loading sheet deducts each item from its **source warehouse** (`fK_WarehouseId` per line) and credits the **vehicle** stock location.
|
||||
- Each loading sheet line can optionally link to a `fK_ShopPOItemId` for traceability.
|
||||
|
||||
### Delivery Task → ShopPO Fulfillment
|
||||
|
||||
- Confirming a delivery task updates `ShopPOItem.quantityFulfilled` and transitions the ShopPO status:
|
||||
- All items fully fulfilled → `Fulfilled`
|
||||
- Some items partially fulfilled → `PartiallyFulfilled`
|
||||
|
||||
### Returns → ShopPO
|
||||
|
||||
- Returns always reference a `shopPOId`.
|
||||
- `CreditDeduction` mode: `SaleReturn.totalCreditAmount` is netted off `ShopPO.totalAmount` immediately.
|
||||
- `ItemExchange` mode: no monetary impact; items are swapped in stock.
|
||||
- Only the `reasonCode` on each return line can be edited after creation.
|
||||
|
||||
---
|
||||
|
||||
## What Is Not Yet Implemented
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| ShopPO confirm endpoint | Pending | `PUT /api/shop-pos/{id}/confirm` — triggers InvoiceCreationMode check |
|
||||
| Invoice create (ModeB) | Pending | `POST /api/invoices` with `{ shopPOId }` |
|
||||
| Invoice print | Pending | `POST /api/invoices/{id}/print` |
|
||||
| Invoice finalize | Pending | `POST /api/invoices/{id}/finalize` |
|
||||
| Delivery task auto-generation | Deferred | Route-based dispatch not yet implemented |
|
||||
| Returns create | Redesign | `POST /api/shop-pos/{id}/returns` returns `501` |
|
||||
| Permission seeding | Pending | `loading.populate`, `loading.confirm` scopes |
|
||||
Reference in New Issue
Block a user