Refactor sales pipeline: ShopPO, Invoice, SysConfig models
Major refactor replacing Sale/Order with ShopPO → Invoice → LoadingSheet flow. Removed legacy Sale/Order models and features; introduced ShopPO, ShopPOItem, Invoice, LoadingSheetOrder, and SysConfig models. Updated related entities, enums, repositories, and controllers. Added new migrations for schema changes and system config. Overhauled business logic and DI registrations. Updated documentation with migration plan.
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
# Refactor Implementation Plan
|
||||
|
||||
**Status**: Ready to implement
|
||||
**Design docs**: [order_invoice_lifecycle.md](./order_invoice_lifecycle.md), [loading_event.md](./loading_event.md), [dispatch_pipeline.md](./dispatch_pipeline.md), [ride_lifecycle.md](./ride_lifecycle.md)
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Replace the Sale/Order/LoadingSheet pipeline with a unified ShopPO → Invoice → LoadingSheet flow:
|
||||
|
||||
- `Order` entity renamed to `ShopPO` throughout
|
||||
- `Sale` entity and feature removed entirely
|
||||
- `Invoice` entity introduced as the billing document
|
||||
- `Payment` moved from Sale to Invoice
|
||||
- `LoadingSheet` overhauled: dispatcher-triggered, multi-PO, per-item warehouse
|
||||
- `DeliveryRun` (Ride) simplified: no route, no type, no Active state
|
||||
- `SaleReturn` FK renamed only — full redesign is a separate pass
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
```
|
||||
Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 (migration)
|
||||
→ Phase 6, 7, 8, 9, 10 (independent after migration)
|
||||
→ Phase 11 → Phase 12
|
||||
```
|
||||
|
||||
Phases 6–10 have no dependencies on each other and can be worked in parallel.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Enums
|
||||
|
||||
**File**: `Models/Enums.cs`
|
||||
|
||||
| Enum | Action | New Values |
|
||||
|---|---|---|
|
||||
| `LoadingSheetStatus` | Replace all values | `Planned`, `Confirmed` |
|
||||
| `RideStatus` | Remove `Draft`, `Active`, `CancelRequested` | Keep `Open`, `Cancelled`, `Closed` |
|
||||
| `RideType` | **Delete entire enum** | — |
|
||||
| `InvoiceStatus` | **Add new** | `Draft`, `Printed`, `Finalized` |
|
||||
| `OrderStatus` | Rename to `ShopPOStatus` | Keep values: `Pending`, `PartiallyFulfilled`, `Fulfilled`, `Cancelled` |
|
||||
| `SaleStatus` | **Delete entire enum** | — |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Remove Sale
|
||||
|
||||
Sale has no downstream model dependencies except `Order.FK_SaleId` and `Payment.FK_SaleId` — both handled in Phase 3.
|
||||
|
||||
### Delete files
|
||||
- `Models/Sale.cs`
|
||||
- `Features/Sale/SaleController.cs`
|
||||
- `Features/Sale/SaleService.cs`
|
||||
- `Features/Sale/ISaleRepository.cs`
|
||||
- `Features/Sale/SaleRepository.cs`
|
||||
- `Features/Sale/SaleDTOs.cs`
|
||||
|
||||
### Update `Data/AppDBContext.cs`
|
||||
- Remove `DbSet<Sale> Sales`
|
||||
- Remove all Fluent API config blocks referencing `Sale`
|
||||
|
||||
### Update `Program.cs`
|
||||
- Remove `ISaleRepository` / `SaleRepository` registration
|
||||
- Remove `SaleService` registration
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Model Changes
|
||||
|
||||
### 3.1 Rename `Order` → `ShopPO`
|
||||
|
||||
Create `Models/ShopPO.cs`. Delete `Models/Order.cs`.
|
||||
|
||||
```csharp
|
||||
public class ShopPO : Template
|
||||
{
|
||||
public Guid FK_StoreId { get; set; }
|
||||
public Store Store { get; set; }
|
||||
|
||||
public Guid RaisedById { get; set; } // agent who created the PO
|
||||
|
||||
public Guid? InvoiceId { get; set; } // set when Invoice is created — NO FK constraint
|
||||
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
public ShopPOStatus Status { get; set; } = ShopPOStatus.Pending;
|
||||
|
||||
public DateTime? ConfirmedAt { get; set; } // null = Shop PO, set = Confirmed PO
|
||||
public Guid? ConfirmedBy { get; set; }
|
||||
|
||||
public DateTime? FulfilledAt { get; set; }
|
||||
|
||||
public ICollection<ShopPOItem> Items { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Removed from current `Order.cs`: `FK_SaleId`, `OrderDate` (use `CreatedAt` from Template).
|
||||
|
||||
### 3.2 Rename `OrderItem` → `ShopPOItem`
|
||||
|
||||
Create `Models/ShopPOItem.cs`. Delete `Models/OrderItem.cs`.
|
||||
|
||||
```csharp
|
||||
public class ShopPOItem : Template // Template gives Id, IsActive, CreatedAt
|
||||
{
|
||||
public Guid FK_ShopPOId { get; set; }
|
||||
public ShopPO ShopPO { get; set; }
|
||||
|
||||
public Guid FK_ItemId { get; set; }
|
||||
public Item Item { get; set; }
|
||||
|
||||
public int Quantity { get; set; }
|
||||
public int QuantityFulfilled { get; set; } = 0;
|
||||
public decimal UnitPrice { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
No property changes beyond the FK rename. The existing `IsActive` (from Template) already supports versioning.
|
||||
|
||||
### 3.3 Update `Models/LoadingSheet.cs`
|
||||
|
||||
Remove:
|
||||
- `FK_SourceWarehouseId` and its navigation property
|
||||
- `DispatchConfirmedAt`, `DispatchConfirmedBy`
|
||||
- `ReceiptConfirmedAt`, `ReceiptConfirmedBy`
|
||||
|
||||
Add:
|
||||
- `PopulatedBy` (Guid)
|
||||
- `PopulatedAt` (DateTime)
|
||||
- `ConfirmedAt` (DateTime?)
|
||||
- `ConfirmedBy` (Guid?)
|
||||
|
||||
`FK_RideId` already exists — no change needed.
|
||||
|
||||
### 3.4 Update `Models/LoadingSheetItem.cs`
|
||||
|
||||
Make it inherit `Template` (adds `Id` PK, `IsActive`, `CreatedAt`).
|
||||
|
||||
Remove:
|
||||
- `QuantityReceived`
|
||||
- `QuantityResolved`
|
||||
|
||||
Add:
|
||||
- `FK_WarehouseId` (Guid, required) — warehouse this item is loaded from
|
||||
- `FK_ShopPOItemId` (Guid?, nullable) — traces back to source `ShopPOItem`
|
||||
|
||||
### 3.5 New `Models/LoadingSheetOrder.cs`
|
||||
|
||||
```csharp
|
||||
public class LoadingSheetOrder
|
||||
{
|
||||
public Guid FK_LoadingSheetId { get; set; }
|
||||
public LoadingSheet LoadingSheet { get; set; }
|
||||
|
||||
public Guid FK_ShopPOId { get; set; }
|
||||
public ShopPO ShopPO { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Composite PK on `(FK_LoadingSheetId, FK_ShopPOId)` — configured in DbContext.
|
||||
|
||||
### 3.6 Update `Models/DeliveryRun.cs`
|
||||
|
||||
Remove:
|
||||
- `RideType` (enum deleted in Phase 1)
|
||||
- `FK_RouteId` and its navigation property
|
||||
- `FK_SourceWarehouseId` and its navigation property
|
||||
- `ActivatedAt`
|
||||
|
||||
Keep: `FK_VehicleId`, `FK_DriverProfileId`, `Status`, `Meterstart`, `Meterend`, `StartedAt`, `ClosedAt`.
|
||||
|
||||
### 3.7 Update `Models/Payment.cs`
|
||||
|
||||
Remove:
|
||||
- `FK_SaleId`
|
||||
- `FK_StoreId` (derivable via Invoice → ShopPO → Store)
|
||||
|
||||
Add:
|
||||
- `FK_InvoiceId` (Guid, required)
|
||||
|
||||
### 3.8 New `Models/Invoice.cs`
|
||||
|
||||
```csharp
|
||||
public class Invoice : Template
|
||||
{
|
||||
public Guid FK_ShopPOId { get; set; }
|
||||
public ShopPO ShopPO { get; set; }
|
||||
|
||||
public InvoiceStatus Status { get; set; } = InvoiceStatus.Draft;
|
||||
public PaymentStatus PaymentStatus { get; set; } = PaymentStatus.Pending;
|
||||
|
||||
public Guid CreatedBy { get; set; }
|
||||
|
||||
public DateTime? PrintedAt { get; set; }
|
||||
public Guid? PrintedBy { get; set; }
|
||||
|
||||
public DateTime? FinalizedAt { get; set; }
|
||||
public Guid? FinalizedBy { get; set; }
|
||||
|
||||
public decimal TotalAmount { get; set; }
|
||||
|
||||
public ICollection<Payment> Payments { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.9 FK Renames on Other Models
|
||||
|
||||
These models reference `Order` only — no Sale reference. Rename FK and navigation property only:
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `Models/DeliveryTask.cs` | `FK_OrderId` → `FK_ShopPOId`, navigation `Order` → `ShopPO` |
|
||||
| `Models/SchemeRedemption.cs` | `FK_OrderId` → `FK_ShopPOId`, navigation `Order` → `ShopPO` |
|
||||
| `Models/SaleReturn.cs` | `FK_OrderId` → `FK_ShopPOId`, navigation `Order` → `ShopPO` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — DbContext (`Data/AppDBContext.cs`)
|
||||
|
||||
### DbSet changes
|
||||
```csharp
|
||||
// Remove
|
||||
DbSet<Sale> Sales
|
||||
DbSet<Order> Orders
|
||||
DbSet<OrderItem> OrderItems
|
||||
|
||||
// Add
|
||||
DbSet<ShopPO> ShopPOs
|
||||
DbSet<ShopPOItem> ShopPOItems
|
||||
DbSet<Invoice> Invoices
|
||||
DbSet<LoadingSheetOrder> LoadingSheetOrders
|
||||
```
|
||||
|
||||
### Fluent API changes
|
||||
|
||||
**`ShopPO`**
|
||||
- `ShopPO.InvoiceId`: configure as plain column with no FK constraint — `builder.Property(x => x.InvoiceId).HasColumnName("InvoiceId")` (no `HasForeignKey`)
|
||||
- `ShopPO` → `Store`: Restrict delete
|
||||
- Remove previous unique index on `FK_SaleId`
|
||||
|
||||
**`ShopPOItem`**
|
||||
- Remove unique composite index on `(FK_OrderId, FK_ItemId)` — versioning requires multiple rows with same combo
|
||||
- `ShopPOItem` → `ShopPO`: Restrict delete
|
||||
- `ShopPOItem` → `Item`: Restrict delete
|
||||
|
||||
**`Invoice`**
|
||||
- `Invoice` → `ShopPO`: Restrict delete
|
||||
- One-to-one: one ShopPO has at most one Invoice
|
||||
|
||||
**`LoadingSheetOrder`**
|
||||
- Composite PK: `(FK_LoadingSheetId, FK_ShopPOId)`
|
||||
- `LoadingSheetOrder` → `LoadingSheet`: Restrict delete
|
||||
- `LoadingSheetOrder` → `ShopPO`: Restrict delete
|
||||
|
||||
**`LoadingSheet`**
|
||||
- Remove: Warehouse relation for `FK_SourceWarehouseId`
|
||||
|
||||
**`LoadingSheetItem`**
|
||||
- Add: `FK_WarehouseId` → Warehouse (Restrict delete)
|
||||
- Add: `FK_ShopPOItemId` → ShopPOItem (SetNull — nullable FK)
|
||||
|
||||
**`Payment`**
|
||||
- Remove: Sale relation for `FK_SaleId`, Store relation for `FK_StoreId`
|
||||
- Add: `FK_InvoiceId` → Invoice (Restrict delete)
|
||||
|
||||
**`DeliveryRun`**
|
||||
- Remove: Route relation for `FK_RouteId`, Warehouse relation for `FK_SourceWarehouseId`, RideType property
|
||||
|
||||
**`DeliveryTask`**, **`SchemeRedemption`**, **`SaleReturn`**
|
||||
- Update FK navigation references from `Order` → `ShopPO`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Migration
|
||||
|
||||
One EF Core migration: `RefactorSalesToShopPOAndInvoice`
|
||||
|
||||
Key DDL generated:
|
||||
```sql
|
||||
-- ShopPO (was Orders table)
|
||||
RENAME TABLE Orders TO ShopPOs;
|
||||
ALTER TABLE ShopPOs DROP COLUMN FK_SaleId, DROP COLUMN OrderDate;
|
||||
ALTER TABLE ShopPOs ADD RaisedById CHAR(36) NOT NULL,
|
||||
InvoiceId CHAR(36) NULL,
|
||||
ConfirmedAt DATETIME NULL,
|
||||
ConfirmedBy CHAR(36) NULL;
|
||||
ALTER TABLE ShopPOs RENAME COLUMN Status TO Status; -- enum value update handled by app
|
||||
DROP INDEX IX_Orders_FK_SaleId ON ShopPOs;
|
||||
|
||||
-- ShopPOItems (was OrderItems table)
|
||||
RENAME TABLE OrderItems TO ShopPOItems;
|
||||
ALTER TABLE ShopPOItems RENAME COLUMN FK_OrderId TO FK_ShopPOId;
|
||||
DROP INDEX IX_OrderItems_FK_OrderId_FK_ItemId ON ShopPOItems; -- remove unique constraint
|
||||
|
||||
-- LoadingSheets
|
||||
ALTER TABLE LoadingSheets DROP COLUMN FK_SourceWarehouseId,
|
||||
DROP COLUMN DispatchConfirmedAt, DROP COLUMN DispatchConfirmedBy,
|
||||
DROP COLUMN ReceiptConfirmedAt, DROP COLUMN ReceiptConfirmedBy;
|
||||
ALTER TABLE LoadingSheets ADD PopulatedBy CHAR(36) NOT NULL,
|
||||
PopulatedAt DATETIME NOT NULL,
|
||||
ConfirmedAt DATETIME NULL,
|
||||
ConfirmedBy CHAR(36) NULL;
|
||||
|
||||
-- LoadingSheetItems
|
||||
ALTER TABLE LoadingSheetItems DROP COLUMN QuantityReceived, DROP COLUMN QuantityResolved;
|
||||
ALTER TABLE LoadingSheetItems ADD FK_WarehouseId CHAR(36) NOT NULL,
|
||||
FK_ShopPOItemId CHAR(36) NULL,
|
||||
IsActive TINYINT(1) NOT NULL DEFAULT 1,
|
||||
CreatedAt DATETIME NOT NULL;
|
||||
|
||||
-- DeliveryRuns
|
||||
ALTER TABLE DeliveryRuns DROP COLUMN RideType, DROP COLUMN FK_RouteId,
|
||||
DROP COLUMN FK_SourceWarehouseId, DROP COLUMN ActivatedAt;
|
||||
|
||||
-- Payments
|
||||
ALTER TABLE Payments DROP COLUMN FK_SaleId, DROP COLUMN FK_StoreId;
|
||||
ALTER TABLE Payments ADD FK_InvoiceId CHAR(36) NOT NULL;
|
||||
|
||||
-- DeliveryTasks, SchemeRedemptions, SaleReturns
|
||||
ALTER TABLE DeliveryTasks RENAME COLUMN FK_OrderId TO FK_ShopPOId;
|
||||
ALTER TABLE SchemeRedemptions RENAME COLUMN FK_OrderId TO FK_ShopPOId;
|
||||
ALTER TABLE SaleReturns RENAME COLUMN FK_OrderId TO FK_ShopPOId;
|
||||
|
||||
-- New tables
|
||||
CREATE TABLE Invoices (...);
|
||||
CREATE TABLE LoadingSheetOrders (FK_LoadingSheetId, FK_ShopPOId, PRIMARY KEY (FK_LoadingSheetId, FK_ShopPOId));
|
||||
|
||||
-- Drop Sale table
|
||||
DROP TABLE Sales;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — New Invoice Feature
|
||||
|
||||
**New folder**: `Features/Invoice/`
|
||||
|
||||
### `InvoiceDTOs.cs`
|
||||
|
||||
```csharp
|
||||
// Mode B create
|
||||
record CreateInvoiceRequest(Guid ShopPOId);
|
||||
|
||||
// Mode A — called internally from ShopPOService, no separate request needed
|
||||
|
||||
record InvoiceResponse(Guid Id, Guid ShopPOId, InvoiceStatus Status,
|
||||
PaymentStatus PaymentStatus, Guid CreatedBy,
|
||||
DateTime? PrintedAt, Guid? PrintedBy,
|
||||
DateTime? FinalizedAt, Guid? FinalizedBy,
|
||||
decimal TotalAmount, DateTime CreatedAt);
|
||||
|
||||
// Finalize payload — exact adjustment fields TBD (free issues handling open)
|
||||
record FinalizeInvoiceRequest(List<InvoiceItemAdjustment> Adjustments);
|
||||
record InvoiceItemAdjustment(Guid ShopPOItemId, int AdjustedQuantity, string? Note);
|
||||
```
|
||||
|
||||
### `IInvoiceRepository.cs`
|
||||
- `GetByIdAsync(Guid id)`
|
||||
- `GetByShopPOIdAsync(Guid shopPOId)`
|
||||
- `GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, int page, int pageSize)`
|
||||
- `AddAsync(Invoice invoice)`
|
||||
- `Update(Invoice invoice)`
|
||||
|
||||
### `InvoiceRepository.cs`
|
||||
EF implementation of above.
|
||||
|
||||
### `InvoiceService.cs`
|
||||
- `CreateAsync(Guid shopPOId, Guid createdBy)` — Mode B; validates ShopPO is Confirmed, no Invoice exists yet; sets `ShopPO.InvoiceId` in same transaction
|
||||
- `PrintAsync(Guid id)` — `Draft → Printed`; sets `PrintedAt`/`PrintedBy`
|
||||
- `FinalizeAsync(Guid id, FinalizeInvoiceRequest request)` — `Printed → Finalized`; applies adjustments, locks `TotalAmount`, sets `FinalizedAt`/`FinalizedBy`
|
||||
|
||||
### `InvoiceController.cs`
|
||||
|
||||
| Method | Route | Mode gate | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/invoices` | Mode B only | Create Invoice for a Confirmed ShopPO |
|
||||
| GET | `/api/invoices` | Any | List with filters |
|
||||
| GET | `/api/invoices/{id}` | Any | Get detail |
|
||||
| POST | `/api/invoices/{id}/print` | Any | `Draft → Printed` |
|
||||
| POST | `/api/invoices/{id}/finalize` | Any | `Printed → Finalized` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — ShopPO Feature (replaces Order)
|
||||
|
||||
**Delete**: `Features/Order/` (all files)
|
||||
**New folder**: `Features/ShopPO/`
|
||||
|
||||
### `ShopPODTOs.cs`
|
||||
|
||||
```csharp
|
||||
record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items);
|
||||
record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null);
|
||||
|
||||
record UpdateShopPOItemsRequest(List<ShopPOItemRequest> Items); // versioned rewrite
|
||||
|
||||
record ShopPOResponse(Guid Id, Guid StoreId, Guid RaisedById,
|
||||
Guid? InvoiceId, decimal TotalAmount, ShopPOStatus Status,
|
||||
DateTime? ConfirmedAt, Guid? ConfirmedBy, DateTime? FulfilledAt,
|
||||
List<ShopPOItemResponse> Items, bool IsActive, DateTime CreatedAt);
|
||||
|
||||
record ShopPOItemResponse(Guid Id, Guid ItemId, int Quantity,
|
||||
int QuantityFulfilled, decimal UnitPrice, bool IsActive);
|
||||
```
|
||||
|
||||
### `IShopPORepository.cs`
|
||||
- `GetByIdAsync(Guid id)` — active items only by default
|
||||
- `GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, bool? confirmed, int page, int pageSize)`
|
||||
- `GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)` — used by Mode B invoice creation and LoadingSheet population
|
||||
- `AddAsync(ShopPO po)`
|
||||
- `ReplaceItemsAsync(Guid shopPOId, List<ShopPOItem> newItems)` — soft-delete current IsActive rows, insert fresh set
|
||||
- `Update(ShopPO po)`
|
||||
|
||||
### `ShopPOService.cs`
|
||||
- `CreateAsync(CreateShopPORequest request)` — creates ShopPO with `ConfirmedAt = null`; resolves item prices via `PriceListService`
|
||||
- `UpdateItemsAsync(Guid id, UpdateShopPOItemsRequest request)` — only while `ConfirmedAt == null`; calls versioned replace
|
||||
- `ConfirmAsync(Guid id)` — Mode B: sets `ConfirmedAt`/`ConfirmedBy`; locks items
|
||||
- `ConfirmAndCreateInvoiceAsync(Guid id)` — Mode A: confirm + create Invoice atomically in one transaction; sets `ShopPO.InvoiceId`
|
||||
- `CancelAsync(Guid id)` — sets `Status = Cancelled`; throws if linked Invoice exists (cascade rules not yet defined — see Open Items)
|
||||
- `GetAllAsync(...)`, `GetByIdAsync(...)`
|
||||
|
||||
### `ShopPOController.cs`
|
||||
|
||||
| Method | Route | Who | Description |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/shop-pos` | Agent | Create Shop PO |
|
||||
| GET | `/api/shop-pos` | Any | List (filter: agentId, routeId, shopId, confirmed) |
|
||||
| GET | `/api/shop-pos/{id}` | Any | Get detail |
|
||||
| PUT | `/api/shop-pos/{id}/items` | Agent | Versioned item rewrite (Shop PO only) |
|
||||
| POST | `/api/shop-pos/{id}/confirm` | Office | Mode B — confirm PO only |
|
||||
| POST | `/api/shop-pos/{id}/confirm-and-create-invoice` | Office | Mode A — atomic confirm + invoice |
|
||||
| POST | `/api/shop-pos/{id}/cancel` | Agent/Office | Cancel Shop PO or Confirmed PO |
|
||||
|
||||
Mode A endpoint returns `405 Method Not Allowed` when `InvoiceCreationMode = "B"` and vice versa.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Update Payment Feature
|
||||
|
||||
### `Models/Payment.cs`
|
||||
Already updated in Phase 3 (`FK_InvoiceId`, removed `FK_SaleId` and `FK_StoreId`).
|
||||
|
||||
### `PaymentDTOs.cs`
|
||||
```csharp
|
||||
// Before: CreatePaymentRequest(Amount, Method) against saleId in route
|
||||
// After: CreatePaymentRequest(Amount, Method) against invoiceId in route
|
||||
record CreatePaymentRequest(decimal Amount, PaymentMethod Method);
|
||||
record PaymentResponse(Guid Id, Guid InvoiceId, decimal Amount,
|
||||
PaymentMethod Method, DateTime CollectedAt, Guid CollectedBy);
|
||||
```
|
||||
|
||||
### `IPaymentRepository.cs` / `PaymentRepository.cs`
|
||||
- Replace `GetBySaleIdAsync` → `GetByInvoiceIdAsync(Guid invoiceId)`
|
||||
- `AddAsync`, `Update` unchanged
|
||||
|
||||
### `PaymentService.cs`
|
||||
On `RecordPaymentAsync(Guid invoiceId, CreatePaymentRequest request)`:
|
||||
1. Validate Invoice exists and is not `Cancelled`/voided
|
||||
2. Add `Payment` record with `FK_InvoiceId`
|
||||
3. Sum all payments for this Invoice
|
||||
4. Recalculate `Invoice.PaymentStatus`:
|
||||
- `sum == 0` → `Pending`
|
||||
- `sum < Invoice.TotalAmount` → `PartiallyPaid`
|
||||
- `sum >= Invoice.TotalAmount` → `Paid`
|
||||
5. Update `Invoice.PaymentStatus` in same transaction
|
||||
|
||||
### `PaymentController.cs`
|
||||
Change route from `/api/sales/{saleId}/payments` → `/api/invoices/{invoiceId}/payments`
|
||||
|
||||
| Method | Route | Description |
|
||||
|---|---|---|
|
||||
| POST | `/api/invoices/{invoiceId}/payments` | Record payment against invoice |
|
||||
| GET | `/api/invoices/{invoiceId}/payments` | List payments for invoice |
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Overhaul LoadingEvent Feature
|
||||
|
||||
Near-complete rewrite of service and controller. See `loading_event.md` for full state machine.
|
||||
|
||||
### `LoadingSheetDTOs.cs` — replace with
|
||||
|
||||
```csharp
|
||||
record PopulateRequest(Guid RideId, Guid? RouteId, Guid? AgentId, Guid? ShopId);
|
||||
|
||||
record UpdateLoadingSheetItemsRequest(List<LoadingSheetItemInput> Items);
|
||||
record LoadingSheetItemInput(Guid ItemId, Guid WarehouseId, decimal Quantity, Guid? ShopPOItemId = null);
|
||||
|
||||
record LoadingSheetResponse(Guid Id, Guid RideId, LoadingSheetStatus Status,
|
||||
Guid PopulatedBy, DateTime PopulatedAt,
|
||||
DateTime? ConfirmedAt, Guid? ConfirmedBy,
|
||||
List<Guid> SourceShopPOIds,
|
||||
List<LoadingSheetItemResponse> Items,
|
||||
DateTime CreatedAt);
|
||||
|
||||
record LoadingSheetItemResponse(Guid Id, Guid ItemId, Guid WarehouseId,
|
||||
decimal QuantityDispatched, Guid? ShopPOItemId, bool IsActive);
|
||||
```
|
||||
|
||||
### `ILoadingSheetRepository.cs` / `LoadingSheetRepository.cs` — replace with
|
||||
|
||||
- `PopulateAsync(PopulateRequest request, Guid populatedBy)` — finds all Confirmed ShopPOs matching filters, creates one `LoadingSheet` at `Planned`, populates `LoadingSheetItem` rows from active `ShopPOItem` lines, inserts `LoadingSheetOrder` rows
|
||||
- `GetAllAsync(Guid? routeId, Guid? agentId, Guid? shopId, Guid? rideId, LoadingSheetStatus? status, int page, int pageSize)` — `routeId` filter joins via `Store.FK_RouteId → ShopPO.FK_StoreId → LoadingSheetOrder.FK_ShopPOId`
|
||||
- `GetByIdAsync(Guid id, bool includeHistory = false)` — `includeHistory = true` includes `IsActive = false` items
|
||||
- `ReplaceItemsAsync(Guid sheetId, List<LoadingSheetItemInput> items)` — soft-delete all current `IsActive = true` rows, insert fresh set
|
||||
- `ConfirmAsync(Guid id, Guid confirmedBy)` — `Planned → Confirmed`; creates stock movements in transaction
|
||||
|
||||
### `LoadingSheetService.cs` — replace with
|
||||
|
||||
- `PopulateAsync(PopulateRequest request)` — validates ride is `Open`; at least one filter provided; all matching ShopPOs are Confirmed; delegates to repository
|
||||
- `UpdateItemsAsync(Guid id, UpdateLoadingSheetItemsRequest request)` — validates sheet is `Planned`; calls versioned replace
|
||||
- `ConfirmAsync(Guid id)` — validates sheet is `Planned`; creates stock movements: one `Load` debit per `LoadingSheetItem.FK_WarehouseId`, one credit to ride's vehicle (`LoadingSheet.FK_RideId → DeliveryRun.FK_VehicleId`); all in one transaction; transitions to `Confirmed`
|
||||
|
||||
### `LoadingSheetController.cs` — replace endpoints
|
||||
|
||||
| Remove | Add |
|
||||
|---|---|
|
||||
| `POST /` | `POST /populate` |
|
||||
| `POST /{id}/confirm-dispatch` | `POST /{id}/confirm` |
|
||||
| `PUT /{id}/receipt-items` | *(removed)* |
|
||||
| `POST /{id}/confirm-receipt` | *(removed)* |
|
||||
| `POST /{id}/resolve` | *(removed)* |
|
||||
| `PATCH /{id}/dispatch` | *(removed)* |
|
||||
| `PATCH /{id}/receipt` | *(removed)* |
|
||||
| `PATCH /{id}/resolve` | *(removed)* |
|
||||
| `GET /` | `GET /` (add routeId, agentId, shopId filters) |
|
||||
| `GET /{id}` | `GET /{id}` (add `?history=true`) |
|
||||
| `PUT /{id}/items` | `PUT /{id}/items` (versioned, Planned only) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Overhaul Ride Feature
|
||||
|
||||
See `ride_lifecycle.md` for full state machine. Ride now starts `Open` immediately on creation.
|
||||
|
||||
### `RideDTOs.cs`
|
||||
```csharp
|
||||
// Remove: RouteId, SourceWarehouseId, RideType from request and response
|
||||
record CreateRideRequest(Guid VehicleId, Guid DriverProfileId, int Meterstart);
|
||||
record RideResponse(Guid Id, Guid VehicleId, Guid DriverProfileId,
|
||||
RideStatus Status, int Meterstart, int? Meterend,
|
||||
DateTime? StartedAt, DateTime? ClosedAt, DateTime CreatedAt);
|
||||
```
|
||||
|
||||
### `RideService.cs` — replace state machine
|
||||
|
||||
| Remove | Replace with |
|
||||
|---|---|
|
||||
| `CreateAsync` (creates Draft) | `CreateAsync` (creates at `Open` directly) |
|
||||
| `OpenAsync` (Draft → Open) | *(merged into create)* |
|
||||
| `ActivateAsync` (Open → Active) | *(removed)* |
|
||||
| `RaiseCancelRequestAsync` | *(removed — no approval flow)* |
|
||||
| `ApproveCancelAsync` | *(removed)* |
|
||||
| `RejectCancelAsync` | *(removed)* |
|
||||
| `GetCancelRequestsAsync` | *(removed)* |
|
||||
| `GetCancelRequestAsync` | *(removed)* |
|
||||
| `GetBreakdownAssistAsync` | *(removed)* |
|
||||
| `CloseAsync` (Active → Closed) | `CloseAsync` (Open → Closed) |
|
||||
| *(no cancel)* | `CancelAsync` (Open → Cancelled, direct) |
|
||||
|
||||
`AssignDriverAsync` — keep, remove Draft-status guard.
|
||||
|
||||
### `RideController.cs`
|
||||
Remove endpoints: open, activate, cancel-request, approve-cancel, reject-cancel, breakdown-assist.
|
||||
Add endpoint: `POST /{id}/cancel`.
|
||||
|
||||
### FK renames in other features
|
||||
- `Features/DeliveryRide/DeliveryTaskService.cs` — rename `FK_OrderId` → `FK_ShopPOId` references; update Active status guard to `Open` (**note**: full delivery redesign is deferred — minimum change only)
|
||||
- `Features/Returns/ReturnService.cs` — rename `FK_OrderId` → `FK_ShopPOId` references only (full redesign deferred)
|
||||
|
||||
---
|
||||
|
||||
## Phase 11 — Program.cs + Config
|
||||
|
||||
### `Program.cs`
|
||||
```csharp
|
||||
// Remove
|
||||
builder.Services.AddScoped<ISaleRepository, SaleRepository>();
|
||||
builder.Services.AddScoped<SaleService>();
|
||||
builder.Services.AddScoped<IRideCancelRequestRepository, RideCancelRequestRepository>(); // approval flow removed
|
||||
|
||||
// Add
|
||||
builder.Services.AddScoped<IInvoiceRepository, InvoiceRepository>();
|
||||
builder.Services.AddScoped<IInvoiceService, InvoiceService>();
|
||||
builder.Services.AddScoped<IShopPORepository, ShopPORepository>();
|
||||
builder.Services.AddScoped<IShopPOService, ShopPOService>();
|
||||
|
||||
// Update (rename existing registrations)
|
||||
// IOrderRepository → IShopPORepository (above replaces it)
|
||||
```
|
||||
|
||||
### `appsettings.json` / `appsettings.Development.json`
|
||||
```json
|
||||
{
|
||||
"InvoiceCreationMode": "B"
|
||||
}
|
||||
```
|
||||
|
||||
Read in `ShopPOController` via `IConfiguration` to gate Mode A vs Mode B endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — Permission Seeding
|
||||
|
||||
Add two permission records (seed migration or data seeder):
|
||||
|
||||
| Code | Description | Role |
|
||||
|---|---|---|
|
||||
| `loading.populate` | Trigger loading sheet population; edit items | Dispatcher |
|
||||
| `loading.confirm` | Confirm loading sheet; commits stock movements | TBD (see Open Items) |
|
||||
|
||||
---
|
||||
|
||||
## File Change Summary
|
||||
|
||||
### New files
|
||||
```
|
||||
Models/ShopPO.cs
|
||||
Models/ShopPOItem.cs
|
||||
Models/Invoice.cs
|
||||
Models/LoadingSheetOrder.cs
|
||||
Features/ShopPO/ShopPOController.cs
|
||||
Features/ShopPO/ShopPOService.cs
|
||||
Features/ShopPO/IShopPORepository.cs
|
||||
Features/ShopPO/ShopPORepository.cs
|
||||
Features/ShopPO/ShopPODTOs.cs
|
||||
Features/Invoice/InvoiceController.cs
|
||||
Features/Invoice/InvoiceService.cs
|
||||
Features/Invoice/IInvoiceRepository.cs
|
||||
Features/Invoice/InvoiceRepository.cs
|
||||
Features/Invoice/InvoiceDTOs.cs
|
||||
```
|
||||
|
||||
### Deleted files
|
||||
```
|
||||
Models/Sale.cs
|
||||
Models/Order.cs
|
||||
Models/OrderItem.cs
|
||||
Features/Sale/SaleController.cs
|
||||
Features/Sale/SaleService.cs
|
||||
Features/Sale/ISaleRepository.cs
|
||||
Features/Sale/SaleRepository.cs
|
||||
Features/Sale/SaleDTOs.cs
|
||||
Features/Order/OrderController.cs
|
||||
Features/Order/OrderService.cs
|
||||
Features/Order/IOrderRepository.cs
|
||||
Features/Order/OrderRepository.cs
|
||||
Features/Order/OrderDTOs.cs
|
||||
```
|
||||
|
||||
### Modified files
|
||||
```
|
||||
Models/Enums.cs
|
||||
Models/LoadingSheet.cs
|
||||
Models/LoadingSheetItem.cs
|
||||
Models/DeliveryRun.cs
|
||||
Models/Payment.cs
|
||||
Models/DeliveryTask.cs (FK rename only)
|
||||
Models/SchemeRedemption.cs (FK rename only)
|
||||
Models/SaleReturn.cs (FK rename only)
|
||||
Data/AppDBContext.cs
|
||||
Features/LoadingEvent/LoadingSheetService.cs
|
||||
Features/LoadingEvent/LoadingSheetController.cs
|
||||
Features/LoadingEvent/ILoadingSheetRepository.cs
|
||||
Features/LoadingEvent/LoadingSheetRepository.cs
|
||||
Features/LoadingEvent/LoadingSheetDTOs.cs
|
||||
Features/Payment/PaymentService.cs
|
||||
Features/Payment/PaymentController.cs
|
||||
Features/Payment/IPaymentRepository.cs
|
||||
Features/Payment/PaymentRepository.cs
|
||||
Features/Payment/PaymentDTOs.cs
|
||||
Features/Ride/RideService.cs
|
||||
Features/Ride/RideController.cs
|
||||
Features/Ride/RideDTOs.cs
|
||||
Features/DeliveryRide/DeliveryTaskService.cs (FK rename + Active→Open guard)
|
||||
Features/Returns/ReturnService.cs (FK rename only)
|
||||
Program.cs
|
||||
appsettings.json
|
||||
appsettings.Development.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Items (must resolve before coding the relevant phase)
|
||||
|
||||
| # | 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 |
|
||||
|
||||
---
|
||||
|
||||
## Deferred (not in this refactor)
|
||||
|
||||
- `SaleReturn` / `CreditNote` — full redesign is a separate pass
|
||||
- `DeliveryTask` full redesign (Stage 5 — Delivery) — separate pass
|
||||
- Vehicle stock reconciliation at ride Close/Cancel — separate pass
|
||||
- Order attachment mechanism for delivery tasks — separate pass
|
||||
Reference in New Issue
Block a user