refactor invoice and po life cycle
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,195 @@
|
|||||||
|
# [Refactor] Dispatch Pipeline — Shop PO to Invoice Finalization
|
||||||
|
|
||||||
|
**Status**: Proposed (design only — no code changes yet)
|
||||||
|
**Depends On**: [order_invoice_lifecycle.md](./order_invoice_lifecycle.md), [loading_event.md](./loading_event.md), [ride_lifecycle.md](./ride_lifecycle.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Define the end-to-end order processing pipeline from a shop's initial PO through to invoice
|
||||||
|
completion. This replaces the separate Delivery ride and Sale ride flows
|
||||||
|
([05_delivery_ride.md](../05_delivery_ride.md), [06_sale_ride.md](../06_sale_ride.md)) with a
|
||||||
|
single unified pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pipeline Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
[Stage 1] Shop PO created (editable)
|
||||||
|
↓
|
||||||
|
[Stage 2] PO Confirmed + Invoice Created → Draft (Mode A: combined / Mode B: separate)
|
||||||
|
↓
|
||||||
|
[Stage 3] Invoice Printed → agent takes to field
|
||||||
|
↓
|
||||||
|
[Stage 4] Loading Sheet Population (dispatcher-triggered, separate flow)
|
||||||
|
↓
|
||||||
|
[Stage 5] Delivery (TBD)
|
||||||
|
↓
|
||||||
|
[Stage 6] Agent returns printed invoice → office worker enters edits → Invoice Finalized
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 1 — Shop PO
|
||||||
|
|
||||||
|
**Business term**: Shop PO
|
||||||
|
**System state**: `Order` (no Invoice yet)
|
||||||
|
|
||||||
|
The entry point to the pipeline. A sales agent creates a PO for a shop capturing line items and
|
||||||
|
quantities.
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
- Items editable — every edit soft-deletes current `OrderItem` rows (`IsActive = false`) and writes
|
||||||
|
a fresh set. `IsActive = true` = latest version; `IsActive = false` = history.
|
||||||
|
- Editing allowed only while Shop PO (before confirmation).
|
||||||
|
- No Invoice exists yet at this stage.
|
||||||
|
|
||||||
|
**API:**
|
||||||
|
| Method | Route | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| POST | `/api/orders` | Create Shop PO for a store. |
|
||||||
|
| PUT | `/api/orders/{id}/items` | Rewrite line items (versioned). |
|
||||||
|
| GET | `/api/orders/{id}` | Get PO with current active items. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 2 — Confirmed PO + Invoice Created
|
||||||
|
|
||||||
|
**Business term**: Confirmed PO
|
||||||
|
**System state**: `Order` (items locked) + `Invoice` at `Draft`
|
||||||
|
|
||||||
|
PO items are locked and an Invoice is created at `Draft`. How this happens depends on the active
|
||||||
|
mode — controlled by the system-level `InvoiceCreationMode` config flag.
|
||||||
|
|
||||||
|
**Mode A — combined action (one button):**
|
||||||
|
- `POST /api/orders/{id}/confirm-and-create-invoice`
|
||||||
|
- Locks PO items and creates Invoice at `Draft` atomically.
|
||||||
|
|
||||||
|
**Mode B — separate actions:**
|
||||||
|
- `POST /api/orders/{id}/confirm` — locks PO items only.
|
||||||
|
- Office user then browses Confirmed POs (filtered by `agentId`, `routeId`, `shopId`) and creates
|
||||||
|
the invoice: `POST /api/invoices { orderId }`.
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
- Confirmation requires no signature or OTP.
|
||||||
|
- `OrderItem` lines are permanently locked after confirmation.
|
||||||
|
- `Order.Status` (fulfillment tracking) is untouched — separate axis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — Invoice Printed
|
||||||
|
|
||||||
|
**Trigger**: `POST /api/invoices/{id}/print`
|
||||||
|
|
||||||
|
Office user prints the invoice. The agent takes the physical copy to the field.
|
||||||
|
|
||||||
|
**Key rules:**
|
||||||
|
- Agent may make handwritten edits on the printed copy during the ride (quantity adjustments,
|
||||||
|
returns, damage, discounts).
|
||||||
|
- Invoice is locked from system edits between `Printed` and `Finalized`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4 — Loading Sheet Population
|
||||||
|
|
||||||
|
A dispatcher manually triggers population via `POST /api/loading-sheets/populate` with one or more
|
||||||
|
filters. The system finds all matching Confirmed POs and aggregates them into **one loading sheet**.
|
||||||
|
|
||||||
|
**Filters (at least one required):**
|
||||||
|
- `routeId` — all Confirmed POs for shops on this route
|
||||||
|
- `agentId` — all Confirmed POs created by this agent
|
||||||
|
- `shopId` — all Confirmed POs for this specific shop
|
||||||
|
|
||||||
|
**What happens:**
|
||||||
|
1. System finds all `Order` records matching the filter where items are locked (Confirmed PO)
|
||||||
|
2. One `LoadingSheet` created at `Planned` status
|
||||||
|
3. `LoadingSheetItem` rows populated from the active `OrderItem` lines of those POs
|
||||||
|
4. Each source PO recorded in `LoadingSheetOrder` linking table
|
||||||
|
5. Each item carries `FK_OrderItemId` pointing back to its source `OrderItem`
|
||||||
|
|
||||||
|
**After population — editing:**
|
||||||
|
- Dispatcher can edit item quantities while the sheet is `Planned` or `Assigned`
|
||||||
|
- Every edit soft-deletes current `LoadingSheetItem` rows (`IsActive = false`) and writes a fresh
|
||||||
|
set — same versioning as Shop PO items
|
||||||
|
|
||||||
|
**Confirmation and stock movement:**
|
||||||
|
- Ride is provided at populate time — no separate assign step
|
||||||
|
- Each item has its own `FK_WarehouseId` — one sheet can load from multiple warehouses
|
||||||
|
- Dispatcher confirms the sheet (`Planned → Confirmed`) — items frozen
|
||||||
|
- Stock movements: one debit per item's warehouse, one credit to the ride's vehicle
|
||||||
|
- Timing: open question (Path A at `Confirmed` vs Path B at `ReceiptConfirmed`)
|
||||||
|
|
||||||
|
See [loading_event.md](./loading_event.md) for the full state machine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 5 — Delivery
|
||||||
|
|
||||||
|
**Status**: TBD — to be designed in a subsequent pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 6 — Invoice Finalized
|
||||||
|
|
||||||
|
**Trigger**: `POST /api/invoices/{id}/complete`
|
||||||
|
|
||||||
|
After the ride or day-end, the agent returns the printed invoice to the office. The office worker
|
||||||
|
enters any edits the agent made into the system and submits.
|
||||||
|
|
||||||
|
**What happens:**
|
||||||
|
- Office worker enters adjustments (quantity changes, returns, damage, discounts) from the paper copy
|
||||||
|
- `Invoice.TotalAmount` is recomputed and locked
|
||||||
|
- `Invoice.Status` → `Finalized`
|
||||||
|
- `FinalizedAt` / `FinalizedBy` set
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 3 — Loading Sheet Population
|
||||||
|
|
||||||
|
A dispatcher manually triggers population via `POST /api/loading-sheets/populate` with one or more
|
||||||
|
filters. The system finds all matching Confirmed POs and aggregates them into **one loading sheet**.
|
||||||
|
|
||||||
|
**Filters (at least one required):**
|
||||||
|
- `routeId` — all Confirmed POs for shops on this route
|
||||||
|
- `agentId` — all Confirmed POs created by this agent
|
||||||
|
- `shopId` — all Confirmed POs for this specific shop
|
||||||
|
|
||||||
|
**What happens:**
|
||||||
|
1. System finds all `Order` records matching the filter where `Invoice.Status = Draft`
|
||||||
|
2. One `LoadingSheet` created at `Planned` status
|
||||||
|
3. `LoadingSheetItem` rows populated from the active `OrderItem` lines of those POs
|
||||||
|
4. Each source PO recorded in `LoadingSheetOrder` linking table
|
||||||
|
5. Each item carries `FK_OrderItemId` pointing back to its source `OrderItem`
|
||||||
|
|
||||||
|
**After population — editing:**
|
||||||
|
- Dispatcher can edit item quantities while the sheet is `Planned` or `Assigned`
|
||||||
|
- Every edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes a
|
||||||
|
fresh set — same versioning mechanism as Shop PO items
|
||||||
|
- Active rows (`IsActive = true`) = latest version; soft-deleted = history
|
||||||
|
|
||||||
|
**Confirmation and stock movement:**
|
||||||
|
- Dispatcher assigns a ride + source warehouse (`Planned → Assigned`)
|
||||||
|
- Dispatcher confirms the sheet (`Assigned → Confirmed`) — items frozen
|
||||||
|
- Stock movement timing is an open question — see [loading_event.md](./loading_event.md) Open Question 1
|
||||||
|
|
||||||
|
See [loading_event.md](./loading_event.md) for the full loading sheet state machine and dispatch/receipt flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage 4+ — Delivery and Invoice Finalization
|
||||||
|
|
||||||
|
**Status**: TBD — to be designed in a subsequent pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Who can confirm a Shop PO?** — the creating sales agent, a supervisor, or either?
|
||||||
|
2. **Can a Confirmed PO be reverted to Shop PO?** — no un-confirm path designed here.
|
||||||
|
3. **Cancellation path for a Shop PO or Confirmed PO?** — not addressed here.
|
||||||
|
4. **Exact adjustment payload for `POST /complete`** — the fields the office worker can edit
|
||||||
|
(returns, damage, discounts) are assumed but not fully specified.
|
||||||
|
5. **Does Stage 4 (loading sheet) depend on Stage 3 (invoice printed), or can they happen in
|
||||||
|
parallel?** — not decided here.
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# [Refactor] Loading Event — PO-Generated, Route-Free, No Ride Activation
|
||||||
|
|
||||||
|
**Status**: Proposed (design only — no code changes yet)
|
||||||
|
**Refactors**: [../04_loading_event.md](../04_loading_event.md) (kept as-is, this is the target design)
|
||||||
|
**Depends On**: [ride_lifecycle.md](./ride_lifecycle.md), [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Bring `04_loading_event.md` in line with the other two refactors:
|
||||||
|
|
||||||
|
1. **No more `Open -> Active`.** [ride_lifecycle.md](./ride_lifecycle.md) collapses `Ride.Status` to
|
||||||
|
`Open`/`Cancelled`/`Closed` — there is no `Active` stage left to auto-trigger. Confirming a
|
||||||
|
loading event no longer changes `Ride.Status` at all; the ride simply stays `Open` through
|
||||||
|
loading, delivery, and everything else, until it's explicitly `Closed` or `Cancelled`.
|
||||||
|
`LoadingSheet.Status` tracks loading progress entirely on its own.
|
||||||
|
2. **No more Route or `RideType`-based item population.** `04_loading_event.md`'s "Delivery rides
|
||||||
|
auto-populate from the ride's route" no longer applies — `Ride` no longer carries a route
|
||||||
|
([ride_lifecycle.md](./ride_lifecycle.md)) and no longer distinguishes Delivery/Sale. Instead, per
|
||||||
|
[order_invoice_lifecycle.md](./order_invoice_lifecycle.md), a `LoadingSheet` is generated
|
||||||
|
automatically from a single PO's items the moment that PO is confirmed (`Invoice` Pending →
|
||||||
|
Draft) — before any ride exists.
|
||||||
|
3. **A `LoadingSheet` now starts ride-less.** It's created PO-scoped
|
||||||
|
(`FK_OrderId`), not ride-scoped, and gets attached to an actual `Ride` + source warehouse later,
|
||||||
|
via a new explicit action, once a vehicle/trip is planned for it. The rest of the
|
||||||
|
dispatch/receipt/discrepancy workflow is unchanged in shape — it just now runs per PO-generated
|
||||||
|
sheet instead of per ride-created sheet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background / Context
|
||||||
|
|
||||||
|
Same physical mechanics as `04_loading_event.md`: a vehicle is a location with
|
||||||
|
`LocationType.Vehicle` in the stock ledger, and confirming receipt (or resolving a discrepancy)
|
||||||
|
creates the same two stock movements per item — debit source warehouse, credit vehicle. None of that
|
||||||
|
changes here. What changes is *when a `LoadingSheet` comes into existence* and *what it takes to
|
||||||
|
start the dispatch/receipt workflow on it*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
### Updated Entity — `LoadingSheet`
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Id | Guid | PK |
|
||||||
|
| ~~`FK_OrderId`~~ | — | **Removed.** Replaced by `LoadingSheetOrder` linking table. |
|
||||||
|
| **`FK_RideId`** | Guid | **Required. Set at population time** — dispatcher provides the ride when triggering populate. |
|
||||||
|
| ~~`FK_SourceWarehouseId`~~ | — | **Removed.** Warehouse is now tracked per item, not per sheet — a single sheet can be fulfilled from multiple warehouses (see `LoadingSheetItem` below). |
|
||||||
|
| Status | LoadingSheetStatus | See state machine below. |
|
||||||
|
| **`PopulatedBy`** | Guid | **New.** UserId of dispatcher who triggered population. |
|
||||||
|
| **`PopulatedAt`** | DateTime | **New.** When population was triggered. |
|
||||||
|
| ~~`AssignedAt`~~ | — | **Removed.** No separate assign step — ride is set at populate time. |
|
||||||
|
| ~~`AssignedBy`~~ | — | **Removed.** Same reason. |
|
||||||
|
| ConfirmedAt | DateTime? | Set when loading sheet is confirmed (locked). |
|
||||||
|
| ConfirmedBy | Guid? | UserId of whoever confirmed the sheet. |
|
||||||
|
| DispatchConfirmedAt | DateTime? | Path B only — see Open Questions. |
|
||||||
|
| DispatchConfirmedBy | Guid? | Path B only. |
|
||||||
|
| ReceiptConfirmedAt | DateTime? | Path B only. |
|
||||||
|
| ReceiptConfirmedBy | Guid? | Path B only. |
|
||||||
|
| CreatedAt | DateTime | Unchanged. |
|
||||||
|
|
||||||
|
### New Entity — `LoadingSheetOrder` (linking table)
|
||||||
|
|
||||||
|
One loading sheet aggregates multiple Confirmed POs. This table records which POs were included.
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| FK_LoadingSheetId | Guid | FK → LoadingSheet |
|
||||||
|
| FK_OrderId | Guid | FK → Order (must be a Confirmed PO at population time) |
|
||||||
|
|
||||||
|
### Updated Entity — `LoadingSheetItem`
|
||||||
|
|
||||||
|
`LoadingSheetItem` gains versioning, a source reference, and a per-item warehouse:
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Id | Guid | PK |
|
||||||
|
| FK_LoadingSheetId | Guid | FK → LoadingSheet |
|
||||||
|
| **`FK_WarehouseId`** | Guid | **New. Required.** The warehouse this specific item is loaded from. Different items on the same sheet can come from different warehouses. Set by dispatcher when editing items. |
|
||||||
|
| **`FK_OrderItemId`** | Guid? | **New.** Traces back to the source `OrderItem` from the Confirmed PO. Null for manually added lines. |
|
||||||
|
| FK_ItemId | Guid | FK → Item (product) |
|
||||||
|
| QuantityDispatched | decimal | Quantity to load from `FK_WarehouseId` |
|
||||||
|
| QuantityReceived | decimal? | Driver's received qty (Path B only) |
|
||||||
|
| QuantityResolved | decimal? | Supervisor's resolved qty after discrepancy (Path B only) |
|
||||||
|
| **`IsActive`** | bool | **Versioning.** Every edit soft-deletes current items (`IsActive = false`) and writes a fresh set. Active items = latest version; soft-deleted = history. |
|
||||||
|
| CreatedAt | DateTime | From Template |
|
||||||
|
|
||||||
|
### Updated Enum — `LoadingSheetStatus`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum LoadingSheetStatus
|
||||||
|
{
|
||||||
|
Planned, // populated by dispatcher (ride set, items editable/versioned)
|
||||||
|
Confirmed, // items locked — stock movement path TBD (see Open Questions)
|
||||||
|
DispatchConfirmed, // Path B only: warehouse confirms dispatch
|
||||||
|
ReceiptConfirmed, // Path B only: driver confirms receipt, stock moves
|
||||||
|
DiscrepancyFlagged, // Path B only: received qty mismatch
|
||||||
|
Resolved // Path B only: supervisor resolves discrepancy, stock moves
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Pending` from `04_loading_event.md` is replaced by `Planned` and `Confirmed`. `Assigned` is
|
||||||
|
removed — ride is now set at populate time, so there is no separate assign step.
|
||||||
|
`DispatchConfirmed`, `ReceiptConfirmed`, `DiscrepancyFlagged`, and `Resolved` are retained for
|
||||||
|
Path B — see Open Questions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
```text
|
||||||
|
Planned -> Confirmed -> [Path A: stock moves here]
|
||||||
|
\-> [Path B: DispatchConfirmed -> ReceiptConfirmed]
|
||||||
|
\-> DiscrepancyFlagged -> Resolved
|
||||||
|
```
|
||||||
|
|
||||||
|
| Transition | Trigger | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| (create) -> Planned | `POST /api/loading-sheets/populate` | Dispatcher provides filters (route/agent/shop) + `rideId`. System finds all matching Confirmed POs, creates one `LoadingSheet` with `FK_RideId` set, populates `LoadingSheetItem` rows from their `OrderItem` lines, records PO links in `LoadingSheetOrder`. `PopulatedBy`/`PopulatedAt` set. Items editable (versioned via `IsActive`). Dispatcher also sets `FK_WarehouseId` per item (which warehouse each item loads from). |
|
||||||
|
| Planned -> Confirmed | `POST /{id}/confirm` | Locks the sheet — `LoadingSheetItem` rows frozen (no further edits). Sets `ConfirmedAt`/`ConfirmedBy`. Stock movement path TBD (see Open Questions). |
|
||||||
|
| Confirmed -> DispatchConfirmed | `POST /{id}/confirm-dispatch` | **Path B only.** Warehouse manager confirms dispatch quantities. |
|
||||||
|
| DispatchConfirmed -> ReceiptConfirmed | `POST /{id}/confirm-receipt` | **Path B only.** Driver's received quantities all match. Stock movements created — one debit per `LoadingSheetItem.FK_WarehouseId`, one credit to the ride's vehicle. No ride status transition. |
|
||||||
|
| DispatchConfirmed -> DiscrepancyFlagged | `POST /{id}/confirm-receipt` | **Path B only.** Any line mismatch. No stock movements yet. |
|
||||||
|
| DiscrepancyFlagged -> Resolved | `POST /{id}/resolve` | **Path B only.** Supervisor enters `QuantityResolved` per line. Stock movements created using resolved quantities. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Item Population
|
||||||
|
|
||||||
|
Replaces `04_loading_event.md`'s "Item Population by Ride Type" section entirely.
|
||||||
|
|
||||||
|
A dispatcher manually triggers population via `POST /api/loading-sheets/populate` providing:
|
||||||
|
|
||||||
|
| Input | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `rideId` | **Required.** The ride this loading sheet is for. Must be an `Open` ride. |
|
||||||
|
| `routeId` | Filter — all Confirmed POs for shops on this route |
|
||||||
|
| `agentId` | Filter — all Confirmed POs created by this agent |
|
||||||
|
| `shopId` | Filter — all Confirmed POs for this specific shop |
|
||||||
|
|
||||||
|
At least one filter must be provided alongside `rideId`. The system finds all matching `Order`
|
||||||
|
records where items are locked (Confirmed PO), aggregates their active `OrderItem` lines into one
|
||||||
|
`LoadingSheet`, and records each PO in `LoadingSheetOrder`.
|
||||||
|
|
||||||
|
`LoadingSheetItem.FK_OrderItemId` traces each line back to its source `OrderItem` for audit.
|
||||||
|
|
||||||
|
After population, the dispatcher sets `FK_WarehouseId` on each item — specifying which warehouse
|
||||||
|
that item is loaded from. Different items can come from different warehouses. Items are editable
|
||||||
|
while `Planned` — every edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`)
|
||||||
|
and writes a fresh set. Latest version = `IsActive = true` rows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stock Movements
|
||||||
|
|
||||||
|
One stock movement pair per `LoadingSheetItem` (active rows only). Created exactly once.
|
||||||
|
|
||||||
|
- **Debit**: `LoadingSheetItem.FK_WarehouseId` (the warehouse that item loads from)
|
||||||
|
- **Credit**: the vehicle on the assigned ride (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`)
|
||||||
|
- **Type**: `Load`
|
||||||
|
|
||||||
|
Since different items can have different `FK_WarehouseId` values, one loading sheet can produce
|
||||||
|
stock movements from multiple warehouses into one vehicle.
|
||||||
|
|
||||||
|
**When** movements are created depends on the path chosen (see Open Questions):
|
||||||
|
- **Path A** — stock moves at `Confirmed`
|
||||||
|
- **Path B** — stock moves at `ReceiptConfirmed` or `Resolved`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
Base route: `/api/loading-sheets`
|
||||||
|
|
||||||
|
| Method | Route | Who | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/populate` | Dispatcher | Trigger population: `{ rideId, routeId?, agentId?, shopId? }` — `rideId` required, at least one filter required. Finds all matching Confirmed POs, creates one `LoadingSheet` in `Planned` status with `FK_RideId` set, populates items. |
|
||||||
|
| GET | `/` | Any | List sheets. Filter by `routeId`, `agentId`, `shopId`, `rideId`, `status`. Paginated. |
|
||||||
|
| GET | `/{id}` | Any | Get sheet detail. Active items only by default; `?history=true` includes soft-deleted versions. |
|
||||||
|
| PUT | `/{id}/items` | Dispatcher | Rewrite items while `Planned`. Each item must include `FK_WarehouseId`. Soft-deletes current rows (`IsActive = false`) and writes a fresh set. |
|
||||||
|
| POST | `/{id}/confirm` | Dispatcher | Lock the sheet. `Planned -> Confirmed`. Items frozen. |
|
||||||
|
| POST | `/{id}/confirm-dispatch` | Warehouse mgr | **Path B only.** `Confirmed -> DispatchConfirmed`. |
|
||||||
|
| PUT | `/{id}/receipt-items` | Driver | **Path B only.** Set `QuantityReceived` per line. |
|
||||||
|
| POST | `/{id}/confirm-receipt` | Driver | **Path B only.** `-> ReceiptConfirmed` or `DiscrepancyFlagged`. |
|
||||||
|
| POST | `/{id}/resolve` | Supervisor | **Path B only.** Enter `QuantityResolved` per line `-> Resolved`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Rules
|
||||||
|
|
||||||
|
1. A `LoadingSheet` is created manually by a dispatcher via `POST /api/loading-sheets/populate` —
|
||||||
|
not automatically. `rideId` and at least one PO filter (routeId, agentId, or shopId) are required.
|
||||||
|
2. The ride (`FK_RideId`) is set at populate time — there is no separate assign-ride step.
|
||||||
|
3. Only Confirmed POs (locked items) are included in population — Shop POs are excluded.
|
||||||
|
4. One `LoadingSheet` aggregates all matching Confirmed POs — not one sheet per PO.
|
||||||
|
5. A ride may have more than one `LoadingSheet` — each progresses independently.
|
||||||
|
6. The target ride must be `Open` at populate time.
|
||||||
|
7. Each `LoadingSheetItem` carries its own `FK_WarehouseId` — items on the same sheet can come
|
||||||
|
from different warehouses. There is no sheet-level source warehouse.
|
||||||
|
8. Items are editable (versioned via `IsActive`) while `Planned` — frozen at `Confirmed`.
|
||||||
|
9. Every item edit soft-deletes all current `LoadingSheetItem` rows (`IsActive = false`) and writes
|
||||||
|
a fresh set. Active rows = latest version; soft-deleted = history.
|
||||||
|
10. Stock movements are created exactly once — one debit per item warehouse, one credit to the
|
||||||
|
vehicle. Timing depends on path chosen (see Open Questions).
|
||||||
|
11. No `Ride.Status` transition is triggered at any point in this flow.
|
||||||
|
12. All list endpoints return paginated responses.
|
||||||
|
13. Vehicle capacity check is not enforced in this step.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impact on Dependent Features
|
||||||
|
|
||||||
|
1. **`ride_lifecycle.md`'s Impact item 2** (loading sheet auto-population without a route) and
|
||||||
|
**`order_invoice_lifecycle.md`'s Impact item 6** are addressed by this document: population now
|
||||||
|
comes from the PO directly, and the ride-attachment gap (`order_invoice_lifecycle.md`'s
|
||||||
|
[Open Question 7](../refactor_docs/order_invoice_lifecycle.md#open-questions)) is resolved by the
|
||||||
|
new `Planned -> Assigned` step and `POST /{id}/assign-ride` endpoint.
|
||||||
|
2. **Dispatcher role/permission is new** — `04_loading_event.md`'s permission table has no
|
||||||
|
"dispatcher" concept; `assign-ride` needs a new permission (see below), and who is authorized to
|
||||||
|
perform it (a route/trip planner role?) is not fully specified here.
|
||||||
|
3. **Multiple sheets per ride** changes how a driver/warehouse experiences dispatch — instead of one
|
||||||
|
confirm-dispatch per ride, there may now be several, one per assigned PO. UI/UX and any
|
||||||
|
"ride loading summary" view built against the old one-sheet-per-ride assumption needs rework —
|
||||||
|
not designed here.
|
||||||
|
4. **`order_invoice_lifecycle.md`'s Open Question 8** (does editing PO items post-confirm re-sync the
|
||||||
|
`LoadingSheet`?) is still open here too — this document doesn't resolve what happens to an
|
||||||
|
`Assigned` or later-stage sheet if its source PO's items change afterward.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permissions to Seed
|
||||||
|
|
||||||
|
| Code | Who |
|
||||||
|
|---|---|
|
||||||
|
| `loading.view` | All roles |
|
||||||
|
| **`loading.populate`** | **New.** Dispatcher — trigger population, edit items, confirm sheet |
|
||||||
|
| `loading.dispatch` | Warehouse manager — Path B dispatch confirmation |
|
||||||
|
| `loading.receive` | Driver — Path B receipt confirmation |
|
||||||
|
| `loading.resolve` | Supervisor — Path B discrepancy resolution |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fraud / Control Considerations
|
||||||
|
|
||||||
|
Unchanged from `04_loading_event.md`:
|
||||||
|
- Discrepancies are immediately visible and block further progress.
|
||||||
|
- `QuantityResolved` is permanently stored per line for audit purposes.
|
||||||
|
|
||||||
|
Additionally: `AssignedAt`/`AssignedBy` gives an audit trail for who committed a planned PO load to
|
||||||
|
a specific ride/vehicle, which didn't exist as a separate step before.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Path A vs Path B — when do stock movements happen?** Stock can move at `Confirmed` (Path A:
|
||||||
|
warehouse confirms and dispatches in one step) or at `ReceiptConfirmed`/`Resolved` (Path B:
|
||||||
|
driver receipt confirmation). Both paths are retained until this is decided.
|
||||||
|
2. **Who confirms the loading sheet?** — the dispatcher who populated it, the warehouse manager, or
|
||||||
|
either? Not specified here.
|
||||||
|
3. **What happens to a `Planned` sheet if it is never assigned to a ride?** — no cancellation or
|
||||||
|
timeout path is designed here.
|
||||||
|
4. Vehicle capacity check — still deferred.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Notes (for when this is implemented)
|
||||||
|
|
||||||
|
Not part of this docs-only pass, but captured for the eventual implementation step:
|
||||||
|
|
||||||
|
- Add `FK_OrderId` (required), `AssignedAt`, `AssignedBy` to `Models/LoadingSheet.cs`; make
|
||||||
|
`FK_RideId` and `FK_SourceWarehouseId` nullable.
|
||||||
|
- Replace `LoadingSheetStatus.Pending` with `Planned` and `Assigned`; update all status checks in
|
||||||
|
`LoadingSheetService`/`LoadingSheetRepository` accordingly.
|
||||||
|
- Remove `POST /api/loading-sheets` (manual create); wire automatic `LoadingSheet` creation into the
|
||||||
|
PO-confirm action from [order_invoice_lifecycle.md](./order_invoice_lifecycle.md)'s migration
|
||||||
|
notes.
|
||||||
|
- Add `POST /{id}/assign-ride` endpoint/service method, validating the target ride's `Status == Open`
|
||||||
|
(per [ride_lifecycle.md](./ride_lifecycle.md)'s narrowed `RideStatus`).
|
||||||
|
- Remove the `confirm-receipt`/`resolve` code paths' calls into ride-activation logic — there is no
|
||||||
|
`Ride.Status` transition to trigger anymore.
|
||||||
|
- Remove the ride-route-based item population path entirely (`Order.Store.FK_RouteId` aggregation);
|
||||||
|
replace with direct `FK_OrderId`-based population at sheet-creation time.
|
||||||
|
- Seed the new `loading.assign` permission.
|
||||||
|
- Resolve Open Questions 1 and 4 before finalizing the `assign-ride` endpoint's authorization rules
|
||||||
|
and the one-sheet-per-PO assumption, respectively.
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# [Refactor] Order/Invoice Lifecycle — Unified PO → Invoice Pipeline
|
||||||
|
|
||||||
|
**Status**: Proposed (design only — no code changes yet)
|
||||||
|
**Refactors**: [../05_delivery_ride.md](../05_delivery_ride.md) and
|
||||||
|
[../06_sale_ride.md](../06_sale_ride.md) (both kept as-is, this is the target design)
|
||||||
|
**Related but independent**: [ride_lifecycle.md](./ride_lifecycle.md) — that refactor removes
|
||||||
|
`RideType`/`Route`/`Status` from `Ride`; this document does not attempt to resolve most of its open
|
||||||
|
questions, and this document's design does not depend on it landing first. One exception: this
|
||||||
|
document's automatic `LoadingSheet` creation on PO confirm (see below) partially addresses
|
||||||
|
`ride_lifecycle.md`'s "loading sheet auto-population" gap — the `LoadingSheet` is now generated from
|
||||||
|
the PO/Invoice directly, not from a Route. How that `LoadingSheet` later attaches to an actual Ride
|
||||||
|
(vehicle) is resolved in [loading_event.md](./loading_event.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the Delivery-vs-Sale ride-type split with a single unified pipeline. The PO and Invoice are
|
||||||
|
two separate but linked concerns — the PO tracks what the shop ordered; the Invoice tracks the
|
||||||
|
billing document that goes with the delivery.
|
||||||
|
|
||||||
|
### PO lifecycle (on `Order`)
|
||||||
|
|
||||||
|
1. **Shop PO** — PO created by sales agent, line items freely editable (versioned via `IsActive`).
|
||||||
|
2. **Confirmed PO** — office user confirms the PO, locking its items. Invoice creation depends on
|
||||||
|
the system's active mode (see [Invoice Creation Modes](#invoice-creation-modes) below).
|
||||||
|
|
||||||
|
### Invoice lifecycle (on `Invoice`)
|
||||||
|
|
||||||
|
An Invoice is **not** auto-created when a PO is created. It is created by an office user action —
|
||||||
|
either combined with PO confirmation (Mode A) or as a separate step after confirmation (Mode B).
|
||||||
|
Once created, the Invoice always moves through these states:
|
||||||
|
|
||||||
|
1. **Draft** — Invoice created, ready to be printed.
|
||||||
|
2. **Printed** — Invoice physically printed and handed to the agent for the field. The agent makes
|
||||||
|
handwritten edits on the paper copy during the ride.
|
||||||
|
3. **Finalized** — after the ride or day-end, the agent returns the printed invoice. The office
|
||||||
|
worker enters any edits the agent made into the system and submits — locking the invoice as the
|
||||||
|
final billing record.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model Changes
|
||||||
|
|
||||||
|
### Reshaped Entity — `Order` (business term: "PO")
|
||||||
|
|
||||||
|
The existing `Order` entity ([Order.cs](../../Models)) is reshaped into this role, keeping its class
|
||||||
|
name as `Order` in code.
|
||||||
|
|
||||||
|
> **Naming note**: `Distribution/Models/PurchaseOrder.cs` and `Distribution/Features/PurchaseOrder/`
|
||||||
|
> already exist in the codebase — that's the *supplier-facing* procurement flow (the distributor
|
||||||
|
> buying from its own suppliers), completely unrelated to this store-facing PO. To avoid confusion,
|
||||||
|
> this document deliberately keeps the class name `Order`, using "PO" only as the business-facing
|
||||||
|
> term for what the store ordered. Do **not** name the reshaped entity/class `PurchaseOrder`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Order ("PO")
|
||||||
|
├── Id (Guid) — PK, from Template
|
||||||
|
├── FK_StoreId (Guid) — required, unchanged
|
||||||
|
├── FK_SaleId (Guid?) — now OPTIONAL (was required). See Impact section — a PO can now
|
||||||
|
│ exist before any Sale/payment event completes.
|
||||||
|
├── InvoiceId (Guid?) — NOT a FK constraint. Set when the Invoice is created for this PO
|
||||||
|
│ (Mode A or B). Null until then. Stored for lookup convenience —
|
||||||
|
│ no referential enforcement at DB level.
|
||||||
|
├── TotalAmount (decimal) — unchanged, though only authoritative once the linked Invoice is
|
||||||
|
│ Finalized (see below)
|
||||||
|
├── Status (OrderStatus) — unchanged: Pending, PartiallyFulfilled, Fulfilled, Cancelled.
|
||||||
|
│ This tracks fulfillment progress, a separate axis from the new
|
||||||
|
│ InvoiceStatus below — not merged in this pass.
|
||||||
|
├── FulfilledAt (DateTime?) — unchanged
|
||||||
|
├── IsActive, CreatedAt — from Template
|
||||||
|
```
|
||||||
|
|
||||||
|
`OrderItem` (the PO's line items) is unchanged in shape (`FK_ItemId`, `Quantity`,
|
||||||
|
`QuantityFulfilled`, `UnitPrice`) — editability is now gated by the linked `Invoice`'s status (see
|
||||||
|
below), not by `Order.Status` directly.
|
||||||
|
|
||||||
|
### New Entity — `Invoice`
|
||||||
|
|
||||||
|
```
|
||||||
|
Invoice
|
||||||
|
├── Id (Guid) — PK, from Template
|
||||||
|
├── FK_OrderId (Guid) — required. One PO → one Invoice (1:1).
|
||||||
|
├── Status (InvoiceStatus) — Draft, Printed, Finalized
|
||||||
|
├── CreatedBy (Guid) — UserId of office user who created the invoice
|
||||||
|
├── PrintedAt (DateTime?) — set on Draft → Printed
|
||||||
|
├── PrintedBy (Guid?) — UserId of whoever triggered the print
|
||||||
|
├── FinalizedAt (DateTime?) — set on Printed → Finalized
|
||||||
|
├── FinalizedBy (Guid?) — UserId of office worker who entered agent edits and submitted
|
||||||
|
├── TotalAmount (decimal) — reflects current line items through Draft/Printed; becomes the
|
||||||
|
│ authoritative locked amount once Finalized
|
||||||
|
├── IsActive, CreatedAt — from Template
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Enum — `InvoiceStatus`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum InvoiceStatus { Draft, Printed, Finalized } // Finalized is the terminal state
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoice Creation Modes
|
||||||
|
|
||||||
|
In a given system instance, **exactly one** of the following modes is active. The active mode is
|
||||||
|
controlled by a system-level configuration flag (`InvoiceCreationMode: "A" | "B"`). Both modes
|
||||||
|
cannot be active at the same time.
|
||||||
|
|
||||||
|
**Mode A — Confirm PO + Create Invoice (combined action)**
|
||||||
|
- Office user presses "Confirm PO & Create Invoice"
|
||||||
|
- Single endpoint: `POST /api/orders/{id}/confirm-and-create-invoice`
|
||||||
|
- Confirms the PO (locks `OrderItem` lines) and creates the Invoice at `Draft` in one action.
|
||||||
|
|
||||||
|
**Mode B — Create Invoice from Confirmed POs (separate action)**
|
||||||
|
- PO confirmation is a separate step: `POST /api/orders/{id}/confirm`
|
||||||
|
- Office user then navigates to invoice creation, filters confirmed POs by agent ID, route ID, or
|
||||||
|
shop ID to find the right PO, and creates the invoice: `POST /api/invoices { orderId }`
|
||||||
|
- Invoice created at `Draft`.
|
||||||
|
|
||||||
|
### `LoadingSheet` — populated from Confirmed POs, separate flow
|
||||||
|
|
||||||
|
Loading sheets are populated from Confirmed POs as a separate dispatcher-triggered flow — not
|
||||||
|
auto-triggered at PO confirm or invoice create time. One loading sheet aggregates multiple Confirmed
|
||||||
|
POs based on route/agent/shop filters. See [loading_event.md](./loading_event.md) and
|
||||||
|
[dispatch_pipeline.md](./dispatch_pipeline.md) for the full loading sheet lifecycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
### PO (`Order`) transitions
|
||||||
|
|
||||||
|
```
|
||||||
|
(create) → Shop PO → Confirmed PO
|
||||||
|
```
|
||||||
|
|
||||||
|
| Transition | Trigger | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| (create) → Shop PO | `POST /api/orders` | PO created. `OrderItem` lines editable — every edit soft-deletes current rows (`IsActive = false`) and writes a fresh set. Active rows = latest version. |
|
||||||
|
| Shop PO → Confirmed PO | **Mode A:** `POST /api/orders/{id}/confirm-and-create-invoice` | Confirms PO and creates Invoice at `Draft` in one atomic action. `OrderItem` lines locked. `Order.InvoiceId` set to the new Invoice's Id. |
|
||||||
|
| Shop PO → Confirmed PO | **Mode B:** `POST /api/orders/{id}/confirm` | Confirms PO only. `OrderItem` lines locked. `Order.InvoiceId` remains null until invoice is created separately. |
|
||||||
|
|
||||||
|
### Invoice transitions
|
||||||
|
|
||||||
|
```
|
||||||
|
Draft → Printed → Finalized
|
||||||
|
```
|
||||||
|
|
||||||
|
| Transition | Trigger | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| (create) → Draft | **Mode A:** combined with PO confirm above | Invoice created at `Draft`. |
|
||||||
|
| (create) → Draft | **Mode B:** `POST /api/invoices` `{ orderId }` | Office user selects a Confirmed PO (filtered by agentId / routeId / shopId) and creates the Invoice at `Draft`. `Order.InvoiceId` set to the new Invoice's Id. |
|
||||||
|
| Draft → Printed | `POST /api/invoices/{id}/print` | Invoice is physically printed. `PrintedAt`/`PrintedBy` set. Agent takes the printed copy to field and makes handwritten edits during the ride. |
|
||||||
|
| Printed → Finalized | `POST /api/invoices/{id}/complete` | After ride/day-end, agent returns printed invoice. Office worker enters any edits the agent made (quantity adjustments, returns, damage, discounts) and submits. `TotalAmount` locked. `FinalizedAt`/`FinalizedBy` set. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Rules
|
||||||
|
|
||||||
|
1. `OrderItem` editing is allowed **only** while the PO is a Shop PO (before confirmation). Every
|
||||||
|
edit soft-deletes all current `OrderItem` rows (`IsActive = false`) and creates a fresh set —
|
||||||
|
active rows are the latest version, soft-deleted rows are history. Once confirmed, items are
|
||||||
|
locked permanently.
|
||||||
|
2. Invoice is **not** auto-created at PO creation — it is always a deliberate office user action.
|
||||||
|
3. Exactly one Invoice Creation Mode (A or B) is active per system instance — controlled by a
|
||||||
|
system-level configuration flag (`InvoiceCreationMode`). Both modes cannot be active at the
|
||||||
|
same time.
|
||||||
|
4. Mode A only: a PO cannot be confirmed without simultaneously creating its invoice — the two
|
||||||
|
actions are atomic.
|
||||||
|
5. Mode B only: a PO can be confirmed without an invoice existing; the invoice is created later as
|
||||||
|
a separate action by the office user.
|
||||||
|
6. An Invoice cannot skip `Printed` — `Draft → Finalized` directly is not a valid transition.
|
||||||
|
7. `Invoice.TotalAmount` is only authoritative once `Finalized`; through `Draft`/`Printed` it
|
||||||
|
reflects the current line items.
|
||||||
|
8. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is a
|
||||||
|
separate axis from `Invoice.Status` — not merged in this pass.
|
||||||
|
9. `Order.FK_SaleId` is optional — a PO can exist and be confirmed before any `Sale` is recorded.
|
||||||
|
10. Loading sheet population is a separate dispatcher-triggered flow — not triggered by PO
|
||||||
|
confirmation or invoice creation. See [dispatch_pipeline.md](./dispatch_pipeline.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impact on Dependent Features
|
||||||
|
|
||||||
|
Reshaping `Order` and introducing `Invoice` touches several existing behaviors. Each is flagged here
|
||||||
|
as **known-affected, deferred follow-up work**, not solved in this pass:
|
||||||
|
|
||||||
|
1. **`SaleService.CreateAsync`** — currently the only path that creates an `Order`, and it always
|
||||||
|
sets a non-null `FK_SaleId`. With `FK_SaleId` now optional, a new PO-creation path (not
|
||||||
|
necessarily originating from a `Sale`) needs to exist for the take-order-now/deliver-later
|
||||||
|
scenario. Any other code that assumes `Order.Sale` is always non-null also needs auditing —
|
||||||
|
not enumerated exhaustively here.
|
||||||
|
2. **`Payment` attachment** — per `06_sale_ride.md`, `Payment` currently attaches to `Sale`. Once
|
||||||
|
`Invoice` exists as the real billing document, `Payment` more naturally attaches to the `Invoice`
|
||||||
|
instead. Flagged as a design question for [19_finance_accounting.md](../19_finance_accounting.md)
|
||||||
|
to reconcile — not decided here.
|
||||||
|
3. **`19_finance_accounting.md`'s proposed Invoice status** (`Issued, PartiallyPaid, Paid`) needs
|
||||||
|
reconciling with this document's `Pending/Draft/Finalized`. These are likely two different axes
|
||||||
|
on the same `Invoice` — document lifecycle status (this doc) vs. payment-collection status (the
|
||||||
|
finance doc) — but that reconciliation is not attempted here.
|
||||||
|
4. **`DeliveryTask.CustomerConfirmed`** (boolean, from `05_delivery_ride.md`) overlaps conceptually
|
||||||
|
with the new Pending → Draft transition (both represent "the customer confirmed this"). Whether
|
||||||
|
they merge into one confirmation event, or stay deliberately separate (delivery confirmation vs.
|
||||||
|
invoice sign-off, which could happen at different times), is not decided here.
|
||||||
|
5. **`05_delivery_ride.md` and `06_sale_ride.md`** both go stale the moment this design ships and
|
||||||
|
need their own rewrite/retirement — tracked as follow-up, not attempted in this pass.
|
||||||
|
6. **`LoadingSheet` generation moves from route-scoped to PO-scoped.** `LoadingSheetRepository`'s
|
||||||
|
existing `GetAggregatedOrderItemsByRouteIdAsync` (aggregating by `Order.Store.FK_RouteId`) is
|
||||||
|
superseded by generating one `LoadingSheet` per PO at confirm time. This directly answers part of
|
||||||
|
`ride_lifecycle.md`'s Impact item 2 (loading sheet auto-population without a route) — but raises a
|
||||||
|
new gap: nothing in this design says how/when a PO-scoped `LoadingSheet` gets matched to an actual
|
||||||
|
Ride/vehicle for physical loading, since `ride_lifecycle.md` also removed `FK_SourceWarehouseId`
|
||||||
|
and any route/store linkage from `Ride`. Flagged as [Open Question 7](#open-questions), not solved
|
||||||
|
here.
|
||||||
|
|
||||||
|
None of the above are implemented or redesigned by this document. They're called out so the gap is
|
||||||
|
visible and intentional rather than discovered later as a silent regression.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. ~~**Signature/OTP on PO confirmation**~~ — **Resolved:** dropped. Plain action, no OTP.
|
||||||
|
2. ~~**One Invoice per PO or multiple?**~~ — **Resolved:** 1:1 for v1.
|
||||||
|
3. **Should `Order.Status` and `Invoice.Status` eventually merge?** — kept separate in this pass;
|
||||||
|
not decided.
|
||||||
|
4. **How does `Payment` attach to `Invoice`?** — see Impact item 2. Not designed here.
|
||||||
|
5. **Cancellation/void path for a PO or Invoice?** — not addressed here.
|
||||||
|
6. **What edits can the agent make on the printed invoice?** — quantity adjustments, returns,
|
||||||
|
damage, and discounts are assumed but the exact adjustment payload for `POST /complete` is not
|
||||||
|
specified here.
|
||||||
|
7. ~~**How does a LoadingSheet get matched to a Ride?**~~ — **Resolved in
|
||||||
|
[loading_event.md](./loading_event.md):** `POST /{id}/assign-ride`.
|
||||||
|
8. ~~**Does editing PO items after confirm re-sync the LoadingSheet?**~~ — **Resolved:** items lock
|
||||||
|
at confirm; no re-sync needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Notes (for when this is implemented)
|
||||||
|
|
||||||
|
Not part of this docs-only pass, but captured for the eventual implementation step:
|
||||||
|
|
||||||
|
- Make `Order.FK_SaleId` nullable in `Models/Order.cs`; audit all code paths that assume it's
|
||||||
|
non-null.
|
||||||
|
- Add `Order.InvoiceId` (Guid?, no FK constraint) to `Models/Order.cs` — configure explicitly in
|
||||||
|
Fluent API to ensure no referential constraint is generated. Set it in the same transaction as
|
||||||
|
Invoice creation (both Mode A and Mode B).
|
||||||
|
- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `CreatedBy`, `PrintedAt`, `PrintedBy`,
|
||||||
|
`FinalizedAt`, `FinalizedBy`, `TotalAmount`, plus `Template` base fields) and the `InvoiceStatus`
|
||||||
|
enum (`Draft`, `Printed`, `Finalized`).
|
||||||
|
- New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`,
|
||||||
|
`InvoiceRepository`, `InvoiceDTOs`) — do not place under `Features/PurchaseOrder/`.
|
||||||
|
- Add a system configuration flag `InvoiceCreationMode` (`"A"` or `"B"`) — read at startup; gate
|
||||||
|
the Mode A and Mode B endpoints accordingly so only the active mode's endpoints are reachable.
|
||||||
|
- **Mode A endpoint:** `POST /api/orders/{id}/confirm-and-create-invoice` — atomic: locks
|
||||||
|
`OrderItem` rows and creates `Invoice` at `Draft` in one transaction.
|
||||||
|
- **Mode B endpoints:** `POST /api/orders/{id}/confirm` (locks items only) + `POST /api/invoices`
|
||||||
|
`{ orderId }` (creates Invoice at `Draft` from a Confirmed PO). Add `GET /api/orders` filter
|
||||||
|
params `agentId`, `routeId`, `shopId` to support the office user's PO selection UI.
|
||||||
|
- `POST /api/invoices/{id}/print` — sets `PrintedAt`/`PrintedBy`, transitions to `Printed`.
|
||||||
|
- `POST /api/invoices/{id}/complete` — accepts adjustment payload (quantities, returns, damage,
|
||||||
|
discounts); locks `TotalAmount`; sets `FinalizedAt`/`FinalizedBy`. Resolve Open Question 6
|
||||||
|
before specifying the exact payload shape.
|
||||||
|
- `LoadingSheet` population is a separate flow — see [dispatch_pipeline.md](./dispatch_pipeline.md).
|
||||||
|
- A new PO-creation path independent of `SaleService.CreateAsync` is needed for the
|
||||||
|
take-order-now/deliver-later scenario (Impact item 1) — not specified here.
|
||||||
Reference in New Issue
Block a user