Add DeliveryTask, Payment, and Sale features

Implemented DeliveryTask, Payment, and Sale entities with full CRUD, business logic, and API endpoints. Updated Order/OrderItem for sale linkage and fulfillment tracking. Registered new DbSets and DI services. Revised documentation for delivery, sale, and payment workflows.
This commit is contained in:
Kalana B. Thilakarathna
2026-08-24 21:34:57 +05:30
parent a833586e83
commit 6d8b6b3e59
38 changed files with 3938 additions and 563 deletions
+95 -43
View File
@@ -1,80 +1,132 @@
# 06 — Sale Ride Execution
**Status**: Not Started
**Status**: Planning
**Depends On**: 04 — Loading Event
---
## Goal
Support on-the-spot van sales with no pre-order: driver picks items/quantities, an invoice is
generated immediately, and payment is captured at point of sale. Covers `createInvoice`,
`recordPayment` from [mainPlan.md §13](./mainPlan.md#13-finance--accounting),
and reuses `applyPricingAndDiscounts` (§1) plus `evaluateSchemeEligibility`/`applySchemeDiscount`
(§11) if promotions are live by the time this ships.
Allow a sales agent on an active Sale ride to create sales in the field. Every sale creates an
order. Immediate handover deducts vehicle stock and closes the order immediately. Non-immediate
handover leaves the order pending for a future delivery ride.
---
## Background / Context
Distinct from [05_delivery_ride.md](./05_delivery_ride.md): there is no pre-existing `Order` row —
the field invoice *is* the order, created and settled in one step.
- `SaleService.CreateAsync` already exists and handles both `FulfillImmediately` paths.
- What Step 06 adds: vehicle stock validation and deduction on immediate handover, and the
Payment recording endpoint.
- `Sale.PaymentStatus` and the `Payment` model were added in pre-planning.
- Pricing is already resolved via `PriceListService` inside `SaleService`.
---
## Data Model Changes
### Field Invoice
No new entities. Everything needed was added in pre-planning:
`RideId`, `StoreId`, `CreatedByUserId`, `TotalAmount`, `PaymentStatus` (`Paid`, `PartiallyPaid`,
`OnCredit`).
| What | Where | Status |
|---|---|---|
| `Payment` entity | `Models/Payment.cs` | ✅ Done |
| `PaymentStatus` enum | `Models/Enums.cs` | ✅ Done |
| `PaymentMethod` enum | `Models/Enums.cs` | ✅ Done |
| `Sale.PaymentStatus` | `Models/Sale.cs` | ✅ Done |
| `OrderItem.QuantityFulfilled` | `Models/OrderItem.cs` | ✅ Done |
### Field Invoice Line Item
`FieldInvoiceId`, `ItemId`, `Quantity`, `UnitPrice` (subject to the discount-limit rules in
[08_sales_agent_controls.md](./08_sales_agent_controls.md)).
### Payment Record
`FieldInvoiceId`, `Amount`, `Method` (`Cash`, `Credit`), `CollectedAt`.
Migration needed for: `Payment` table, `Sale.PaymentStatus` column, `OrderItem.QuantityFulfilled` column.
---
## API Endpoints
## Changes to Existing SaleService
| Function reference | Endpoint intent |
|---|---|
| `createInvoice(orderId)` (§13) | `POST /api/rides/{rideId}/field-invoices` — orderId is the field invoice itself here |
| `recordPayment(invoiceId, amount, method)` (§13) | `POST /api/field-invoices/{id}/payments` |
| `applyPricingAndDiscounts(orderId)` (§1) | Applied at line-item entry time, within agent discount limits |
`SaleService.CreateAsync` needs two additions when `FulfillImmediately = true` and `RideId` is set:
1. **Stock check** — for each item, verify vehicle has enough stock (`StockBalance` at
`LocationType.Vehicle` with `LocationId = VehicleId`). Reject the whole sale if any item is short.
2. **Stock deduction** — debit vehicle balance per item, create one `StockMovement` per item:
- `MovementType.Sale`
- `FromLocationId = VehicleId`, `FromLocationType = Vehicle`
- `ReferenceId = OrderId`
`VehicleId` is resolved from `Sale.FK_RideId → DeliveryRun.FK_VehicleId`.
**SaleService constructor additions required:**
- `IStockRepository` — for stock balance check and debit
- `IRideRepository` — to look up the ride and resolve `FK_VehicleId`
When `FulfillImmediately = false`, no stock movement — order stays Pending for a delivery ride.
---
## New Feature — Payment
**Feature folder:** `Features/Payment/`
### API Endpoints
| Method | Route | Who | Description |
|---|---|---|---|
| POST | `/api/sales/{id}/payments` | Driver/Agent | Record a payment against a sale |
| GET | `/api/sales/{id}/payments` | Any | List all payments for a sale |
### Payment Creation Logic
On `POST /api/sales/{id}/payments`:
1. Validate sale exists and is not `Cancelled`.
2. Add `Payment` record (`FK_SaleId`, `FK_StoreId` from sale, `Amount`, `Method`, `CollectedAt`, `CollectedBy`).
3. Sum all payments for this sale. Recalculate `Sale.PaymentStatus`:
- `sum == 0` → `Pending`
- `sum < Sale.TotalAmount` → `PartiallyPaid`
- `sum >= Sale.TotalAmount` → `Paid`
4. Update `Sale.PaymentStatus` in the same transaction.
### DTOs
```csharp
public record CreatePaymentRequest(decimal Amount, PaymentMethod Method);
public record PaymentResponse(Guid Id, Guid SaleId, Guid StoreId, decimal Amount,
PaymentMethod Method, DateTime CollectedAt, Guid CollectedBy);
```
---
## Business Rules
- Driver cannot sell more than what is loaded on the vehicle — every line item is checked against
the vehicle's current stock balance ([02_stock_ledger.md](./02_stock_ledger.md)) before the
invoice is finalized.
- Each confirmed sale line creates a `Sale` stock movement against the vehicle location,
immediately (not batched at ride close).
- Discount applied per line must not exceed the agent's role-level limit (§8).
1. Driver cannot sell more than vehicle stock for immediate handover — entire sale rejected if any
item is short (no partial sale creation).
2. Every sale creates an order, even when all items are handed over immediately.
3. `FulfillImmediately = true`:
- Vehicle stock debited immediately per item.
- Order → `Fulfilled`, Sale → `Completed` at creation time (already in SaleService).
- `QuantityFulfilled = Quantity` on all order items (already in SaleService).
4. `FulfillImmediately = false`:
- No stock movement at sale time.
- Order → `Pending`, Sale → `Pending` at creation time.
5. Payment can be recorded at any time — sale does not need to be `Completed` first.
6. Overpayment is allowed (`sum > TotalAmount`) — `PaymentStatus` stays `Paid`.
7. A `Cancelled` sale cannot receive new payments.
---
## Fraud / Control Considerations
## Implementation Build Order
- Cash collected must reconcile against invoiced totals at ride close — see
[07_ride_reconciliation.md](./07_ride_reconciliation.md).
1. Migration — `Payment` table, `Sale.PaymentStatus`, `OrderItem.QuantityFulfilled`
2. Extend `SaleService.CreateAsync` — stock check + stock movement on `FulfillImmediately`
3. DTOs — `PaymentDTOs.cs` in `Features/Payment/`
4. Repository — `IPaymentRepository`, `PaymentRepository`
5. Service — `PaymentService`
6. Controller — `PaymentController`
7. DI registration in `Program.cs`
**Feature folder:** `Features/Payment/` (new), `Features/Sale/` (extended)
---
## Open Questions
## Open Questions — Resolved
- Are field invoices numbered/sequenced separately from warehouse-issued invoices, or share one
sequence?
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
- ✅ Payment timing — can be recorded at any time, not locked to sale completion
- ✅ Overpayment — allowed, status stays Paid
- ✅ Stock check scope — entire sale rejected if any item is short, no partial creation
- ✅ Non-immediate handover — no stock movement at sale time, order picked up by delivery ride