Add refactor order_invoice_lifecycle.md file
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
# [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 its open
|
||||||
|
questions, and this document's design does not depend on it landing first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the Delivery-vs-Sale ride-type split as the organizing concept for how a store's order gets
|
||||||
|
turned into money. Today `05_delivery_ride.md` (rep takes an order now, delivers days later) and
|
||||||
|
`06_sale_ride.md` (rep+driver visit together and sell on the spot) are two separate flows. Underneath,
|
||||||
|
both are the same shape: a rep collects a **PO** from the shop — whether fulfilled immediately or
|
||||||
|
days later — and once the PO exists, an **Invoice** is generated from it. That invoice always moves
|
||||||
|
through the same three statuses regardless of which scenario produced it:
|
||||||
|
|
||||||
|
1. **Pending** — invoice just created from the PO; the PO's line items are freely editable.
|
||||||
|
2. **Draft** — the customer signs once the order is confirmed, moving it out of Pending. The PO's
|
||||||
|
line items **remain editable** in this state too — Draft is a working document, not a lock.
|
||||||
|
3. **Finalize** — the reconciliation step: item add/delete and amount deductions (returns, damage,
|
||||||
|
short-delivery, discounts) get applied here, producing the final, locked invoice amount. This is
|
||||||
|
the only state where free-form editing stops.
|
||||||
|
|
||||||
|
The two ride scenarios differ only in *timing* through this one pipeline, not in structure — no
|
||||||
|
ride-type-specific branching is needed in the PO/Invoice model itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
├── 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 for v1 (see Open Questions).
|
||||||
|
├── Status (InvoiceStatus) — Pending, Draft, Finalized
|
||||||
|
├── SignedAt (DateTime?) — set on Pending → Draft
|
||||||
|
├── FinalizedAt (DateTime?) — set on Draft → Finalized
|
||||||
|
├── FinalizedBy (Guid?) — UserId of whoever finalized it
|
||||||
|
├── TotalAmount (decimal) — mirrors the PO total while Pending/Draft; becomes the
|
||||||
|
│ authoritative, locked amount once Finalized
|
||||||
|
├── IsActive, CreatedAt — from Template
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Enum — `InvoiceStatus`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum InvoiceStatus { Pending, Draft, Finalized }
|
||||||
|
```
|
||||||
|
|
||||||
|
An `Invoice` is created automatically the moment a PO (`Order`) is created, starting at `Pending` —
|
||||||
|
there is no separate "create invoice" action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
Pending → Draft → Finalized
|
||||||
|
```
|
||||||
|
|
||||||
|
No branch/cancel path is designed in this pass (unlike the Ride state machine) — cancellation of a
|
||||||
|
PO/Invoice is not addressed here; flagged as an open question below.
|
||||||
|
|
||||||
|
| Transition | Trigger | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| (implicit) → Pending | PO (`Order`) created | Invoice is created automatically, `Status = Pending`. `OrderItem` lines fully editable. |
|
||||||
|
| Pending → Draft | `POST /api/invoices/{id}/draft` | Captures customer sign-off. Sets `SignedAt`. **`OrderItem` lines remain editable** — Draft is not a lock. Signature/OTP mechanism itself is an open question (see below), matching `05_delivery_ride.md`'s own prior deferral on customer confirmation. |
|
||||||
|
| Draft → Finalized | `POST /api/invoices/{id}/finalize` | Accepts an item-adjustment/deduction payload (add/delete lines, amount deductions for returns/damage/short-delivery/discounts), recomputes and locks `TotalAmount`. This is a distinct, audited action — not the same add/edit/remove path used in Pending/Draft. Once Finalized, the Invoice and its PO's items are fully locked. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Business Rules
|
||||||
|
|
||||||
|
1. `OrderItem` add/edit/remove is allowed while the linked `Invoice.Status` is `Pending` **or**
|
||||||
|
`Draft` — both are working-document states.
|
||||||
|
2. An `Invoice` cannot be `Finalized` without first passing through `Draft` — `Pending → Finalized`
|
||||||
|
directly is not a valid transition.
|
||||||
|
3. `Order.TotalAmount`/`Invoice.TotalAmount` is only authoritative once `Finalized`; while
|
||||||
|
Pending/Draft it reflects the current (still-changeable) line items.
|
||||||
|
4. `Order.Status` (fulfillment tracking: Pending/PartiallyFulfilled/Fulfilled/Cancelled) is
|
||||||
|
untouched and operates on a separate axis from `Invoice.Status` (billing/document lifecycle) —
|
||||||
|
whether these two should eventually merge is an open question, not resolved here.
|
||||||
|
5. `Order.FK_SaleId` is now optional — a PO/Invoice can exist and progress through Pending/Draft
|
||||||
|
before any `Sale` is recorded, covering the take-order-now/deliver-later scenario. See Impact
|
||||||
|
section for what this touches.
|
||||||
|
6. Both ride scenarios (take-order-now-deliver-later vs. on-the-spot sale) create a PO the same way
|
||||||
|
and pass through the same three Invoice statuses — the only difference is how quickly they move
|
||||||
|
through Pending → Draft → Finalized. No `RideType`-specific branching applies to the PO/Invoice
|
||||||
|
model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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 mechanism for the Pending → Draft transition** — same deferral as
|
||||||
|
`05_delivery_ride.md`'s original "signature (image) or OTP (SMS code) — one, or both, in v1?"
|
||||||
|
question. Not resolved here.
|
||||||
|
2. **One Invoice per PO, or multiple?** — this document assumes a 1:1 `Order`→`Invoice` relationship
|
||||||
|
for v1. Whether a PO could ever need multiple invoices (e.g. split billing) is not designed here.
|
||||||
|
3. **Should `Order.Status` and `Invoice.Status` eventually merge into one status axis?** — kept
|
||||||
|
deliberately separate in this pass (fulfillment progress vs. document/billing lifecycle); not
|
||||||
|
decided whether that separation should be permanent.
|
||||||
|
4. **How does `Payment` attach once `Invoice` exists?** — see Impact item 2. Not designed here.
|
||||||
|
5. **Is there a cancellation/void path for a PO or Invoice?** — the state machine in this document
|
||||||
|
has no cancel branch (unlike the Ride state machine's `CancelRequested`/`Cancelled`). Not
|
||||||
|
addressed here.
|
||||||
|
6. **Does `DeliveryTask.CustomerConfirmed` merge with the Draft transition?** — see Impact item 4.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` and its EF/Fluent configuration; audit all
|
||||||
|
code paths that currently assume it's non-null.
|
||||||
|
- Add `Models/Invoice.cs` (`FK_OrderId`, `Status`, `SignedAt`, `FinalizedAt`, `FinalizedBy`,
|
||||||
|
`TotalAmount`, plus `Template` base fields) and the `InvoiceStatus` enum.
|
||||||
|
- New `Features/Invoice/` folder (`InvoiceController`, `InvoiceService`, `IInvoiceRepository`,
|
||||||
|
`InvoiceRepository`, `InvoiceDTOs`) — do not place this under `Features/PurchaseOrder/` or reuse
|
||||||
|
any of its DTOs/names, to avoid the supplier-PO naming collision flagged above.
|
||||||
|
- `POST /api/invoices/{id}/draft` and `POST /api/invoices/{id}/finalize` endpoints; the finalize
|
||||||
|
endpoint's adjustment/deduction payload should reuse the `CreditNote` shape from
|
||||||
|
[18_returns_claims.md](../18_returns_claims.md) where applicable rather than inventing a parallel
|
||||||
|
mechanism.
|
||||||
|
- A new PO-creation path independent of `SaleService.CreateAsync`, to support creating an
|
||||||
|
`Order`/`Invoice` before any `Sale` exists (Impact item 1) — needs its own design, not specified
|
||||||
|
here.
|
||||||
|
- Resolve Open Questions 1–2 before implementing the Draft-transition endpoint and the
|
||||||
|
one-Invoice-per-PO assumption respectively.
|
||||||
Reference in New Issue
Block a user