md file and gap fix

This commit is contained in:
Kalana B. Thilakarathna
2026-08-19 10:50:28 +05:30
parent 29f4fe923b
commit 56cd93c341
16 changed files with 769 additions and 15 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
namespace Distribution.Features.Route; namespace Distribution.Features.Route;
public record CreateRouteRequest(string RouteName, string StartPoint, string EndPoint, decimal Length); public record CreateRouteRequest(Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length);
public record UpdateRouteRequest(string RouteName, string StartPoint, string EndPoint, decimal Length); public record UpdateRouteRequest(Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length);
public record RouteResponse(Guid Id, string RouteName, string StartPoint, string EndPoint, decimal Length, bool IsActive, DateTime CreatedAt); public record RouteResponse(Guid Id, Guid RegionId, string RouteName, string StartPoint, string EndPoint, decimal Length, bool IsActive, DateTime CreatedAt);
+6 -4
View File
@@ -7,20 +7,21 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow)
public async Task<List<RouteResponse>> GetAllAsync() => public async Task<List<RouteResponse>> GetAllAsync() =>
(await repo.GetAllAsync()) (await repo.GetAllAsync())
.Where(r => r.IsActive) .Where(r => r.IsActive)
.Select(r => new RouteResponse(r.Id, r.RouteName, r.StartPoint, r.EndPoint, r.Length, r.IsActive, r.CreatedAt)) .Select(r => new RouteResponse(r.Id, r.FK_RegionId, r.RouteName, r.StartPoint, r.EndPoint, r.Length, r.IsActive, r.CreatedAt))
.ToList(); .ToList();
public async Task<RouteResponse?> GetByIdAsync(Guid id) public async Task<RouteResponse?> GetByIdAsync(Guid id)
{ {
var route = await repo.GetByIdAsync(id); var route = await repo.GetByIdAsync(id);
if (route is null || !route.IsActive) return null; if (route is null || !route.IsActive) return null;
return new RouteResponse(route.Id, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt); return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
} }
public async Task<RouteResponse> CreateAsync(CreateRouteRequest request, CancellationToken ct = default) public async Task<RouteResponse> CreateAsync(CreateRouteRequest request, CancellationToken ct = default)
{ {
var route = new Models.Route var route = new Models.Route
{ {
FK_RegionId = request.RegionId,
RouteName = request.RouteName, RouteName = request.RouteName,
StartPoint = request.StartPoint, StartPoint = request.StartPoint,
EndPoint = request.EndPoint, EndPoint = request.EndPoint,
@@ -37,13 +38,14 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow)
await uow.RollbackAsync(ct); await uow.RollbackAsync(ct);
throw; throw;
} }
return new RouteResponse(route.Id, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt); return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
} }
public async Task<RouteResponse?> UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default) public async Task<RouteResponse?> UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default)
{ {
var route = await repo.GetByIdAsync(id); var route = await repo.GetByIdAsync(id);
if (route is null || !route.IsActive) return null; if (route is null || !route.IsActive) return null;
route.FK_RegionId = request.RegionId;
route.RouteName = request.RouteName; route.RouteName = request.RouteName;
route.StartPoint = request.StartPoint; route.StartPoint = request.StartPoint;
route.EndPoint = request.EndPoint; route.EndPoint = request.EndPoint;
@@ -59,7 +61,7 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow)
await uow.RollbackAsync(ct); await uow.RollbackAsync(ct);
throw; throw;
} }
return new RouteResponse(route.Id, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt); return new RouteResponse(route.Id, route.FK_RegionId, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
} }
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default) public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
+3
View File
@@ -1,6 +1,7 @@
namespace Distribution.Features.Store; namespace Distribution.Features.Store;
public record CreateStoreRequest( public record CreateStoreRequest(
Guid RouteId,
string OwnerName, string OwnerName,
string StoreName, string StoreName,
string StoreAddress, string StoreAddress,
@@ -10,6 +11,7 @@ public record CreateStoreRequest(
decimal CreditAmount); decimal CreditAmount);
public record UpdateStoreRequest( public record UpdateStoreRequest(
Guid RouteId,
string OwnerName, string OwnerName,
string StoreName, string StoreName,
string StoreAddress, string StoreAddress,
@@ -20,6 +22,7 @@ public record UpdateStoreRequest(
public record StoreResponse( public record StoreResponse(
Guid Id, Guid Id,
Guid RouteId,
string OwnerName, string OwnerName,
string StoreName, string StoreName,
string StoreAddress, string StoreAddress,
+6 -7
View File
@@ -7,9 +7,6 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
{ {
private const int GlobalScopeRoleLevel = 50; private const int GlobalScopeRoleLevel = 50;
private static StoreResponse ToResponse(Models.Store s) =>
new(s.Id, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount, s.IsActive, s.CreatedAt);
private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel; private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
private async Task<bool> HasStoreScopeAsync(Guid storeId) private async Task<bool> HasStoreScopeAsync(Guid storeId)
@@ -26,7 +23,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet();
stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList(); stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList();
} }
return stores.Select(ToResponse).ToList(); return stores.Select(s => new StoreResponse(s.Id, s.FK_RouteId, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount, s.IsActive, s.CreatedAt)).ToList();
} }
public async Task<StoreResponse?> GetByIdAsync(Guid id) public async Task<StoreResponse?> GetByIdAsync(Guid id)
@@ -34,13 +31,14 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null; if (store is null || !store.IsActive) return null;
if (!await HasStoreScopeAsync(id)) return null; if (!await HasStoreScopeAsync(id)) return null;
return ToResponse(store); return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
} }
public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default) public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default)
{ {
var store = new Models.Store var store = new Models.Store
{ {
FK_RouteId = request.RouteId,
OwnerName = request.OwnerName, OwnerName = request.OwnerName,
StoreName = request.StoreName, StoreName = request.StoreName,
StoreAddress = request.StoreAddress, StoreAddress = request.StoreAddress,
@@ -60,7 +58,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
await uow.RollbackAsync(ct); await uow.RollbackAsync(ct);
throw; throw;
} }
return ToResponse(store); return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
} }
public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default) public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default)
@@ -68,6 +66,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null; if (store is null || !store.IsActive) return null;
if (!await HasStoreScopeAsync(id)) return null; if (!await HasStoreScopeAsync(id)) return null;
store.FK_RouteId = request.RouteId;
store.OwnerName = request.OwnerName; store.OwnerName = request.OwnerName;
store.StoreName = request.StoreName; store.StoreName = request.StoreName;
store.StoreAddress = request.StoreAddress; store.StoreAddress = request.StoreAddress;
@@ -86,7 +85,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe
await uow.RollbackAsync(ct); await uow.RollbackAsync(ct);
throw; throw;
} }
return ToResponse(store); return new StoreResponse(store.Id, store.FK_RouteId, store.OwnerName, store.StoreName, store.StoreAddress, store.Latitude, store.Longitude, store.OneTimeAmount, store.CreditAmount, store.IsActive, store.CreatedAt);
} }
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default) public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=localhost;Port=3306;Database=distribution_db;User=root;Password=root" "DefaultConnection": "Server=187.127.102.190;Port=3306;Database=distribution_db;User=kalana;Password=kalana@hexdive"
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
+62
View File
@@ -0,0 +1,62 @@
# 00 — Existing Gap Fixes
**Status**: Not Started
**Depends On**: —
---
## Goal
---
## Gaps to Fix
### 1. Route DTO — Missing RegionId
### 2. Store DTO — Missing RouteId
### 3. Role Controller — Missing Authorization
### 4. Secrets & Configuration Management
### 5. Input Validation
### 6. Item Model — Missing Properties
---
## Scope (What This Step Does NOT Include)
---
## Business Rules
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+52
View File
@@ -0,0 +1,52 @@
# 01 — Item Catalog
**Status**: Not Started
**Depends On**: 00 — Existing Gap Fixes
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Item
### Related Changes to Existing Models
---
## API Endpoints
---
## Business Rules
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+62
View File
@@ -0,0 +1,62 @@
# 02 — Stock Ledger
**Status**: Not Started
**Depends On**: 01 — Item Catalog
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Stock Balance (per location)
### Stock Movement Transaction
### Location Abstraction (Warehouse vs Vehicle)
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+62
View File
@@ -0,0 +1,62 @@
# 03 — Ride Lifecycle
**Status**: Not Started
**Depends On**: 02 — Stock Ledger
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Ride (extending DeliveryRun)
### Ride State Machine
### Ride Type (Delivery vs Sale)
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+62
View File
@@ -0,0 +1,62 @@
# 04 — Loading Event
**Status**: Not Started
**Depends On**: 03 — Ride Lifecycle
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Loading Sheet
### Loading Line Item
### Two-Party Confirmation
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+62
View File
@@ -0,0 +1,62 @@
# 05 — Delivery Ride Execution
**Status**: Not Started
**Depends On**: 04 — Loading Event
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Delivery Task (per order on ride)
### Delivery Outcome
### Customer Confirmation
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+62
View File
@@ -0,0 +1,62 @@
# 06 — Sale Ride Execution
**Status**: Not Started
**Depends On**: 04 — Loading Event
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Field Invoice
### Field Invoice Line Item
### Payment Record
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,68 @@
# 07 — Ride Close & Reconciliation
**Status**: Not Started
**Depends On**: 05 — Delivery Ride Execution, 06 — Sale Ride Execution
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Ride Close Summary
### Variance Record
### Supervisor Approval
---
## The Reconciliation Equation
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
@@ -0,0 +1,62 @@
# 08 — Sales Agent Controls
**Status**: Not Started
**Depends On**: 07 — Ride Close & Reconciliation
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Agent Limit Configuration (per role level)
### Override Log
### Customer Credit Profile
---
## API Endpoints
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+68
View File
@@ -0,0 +1,68 @@
# 09 — Fraud Controls & Audit Trail
**Status**: Not Started
**Depends On**: 08 — Sales Agent Controls
---
## Goal
---
## Background / Context
---
## Data Model Changes
### Audit Log Entry
### Anomaly Flag
### Return Approval Record
---
## API Endpoints
---
## Automated Checks & Flags
---
## Business Rules
---
## Fraud / Control Considerations
---
## Open Questions
---
## Implementation Notes
> Fill this section in during planning discussion before writing any code.
+128
View File
@@ -0,0 +1,128 @@
# ERP Distribution — Master Plan
## What Already Exists
| Area | Status | Notes |
|---|---|---|
| JWT Authentication | Done | Token generation, claims, 120-min expiry |
| Permission-based Authorization | Done | Dynamic policy provider, role → permission mapping |
| User Management | Done | CRUD, role assignment, scope assignment (region/route/store) |
| Role Management | Done (read-only) | 6 roles seeded, no create/update via API |
| Region / Route / Vehicle CRUD | Done | Full API surface with permission checks |
| Store / Warehouse CRUD | Done | Full API surface with permission checks |
| Order CRUD | Done | Scope-aware (user only sees their assigned stores) |
| Unit of Work + Repositories | Done | Transaction support throughout |
| Soft Deletes | Done | IsActive pattern on all entities |
## Known Gaps in Existing System (Pre-Work)
These are not new features — they are fixable holes in what's already built.
They must be resolved before new feature steps begin because later steps depend on correct foundations.
| Gap | Impact |
|---|---|
| Route DTOs missing RegionId | Can't link routes to regions via API |
| Store DTOs missing RouteId | Can't link stores to routes via API |
| Role endpoints have no [Authorize] | Anyone can read roles without authentication |
| JWT key & DB credentials in appsettings.json | Security risk in any shared/production environment |
| No input validation on any DTO | Bad data can enter the system silently |
| Item model is empty (no properties) | OrderItems reference items that have no data |
---
## Feature Steps
| # | Feature | Status | Depends On | Detail File |
|---|---|---|---|---|
| 00 | Existing Gap Fixes | Not Started | — | [00_existing_gaps.md](./00_existing_gaps.md) |
| 01 | Item Catalog | Not Started | 00 | [01_item_catalog.md](./01_item_catalog.md) |
| 02 | Stock Ledger | Not Started | 01 | [02_stock_ledger.md](./02_stock_ledger.md) |
| 03 | Ride Lifecycle | Not Started | 02 | [03_ride_lifecycle.md](./03_ride_lifecycle.md) |
| 04 | Loading Event | Not Started | 03 | [04_loading_event.md](./04_loading_event.md) |
| 05 | Delivery Ride Execution | Not Started | 04 | [05_delivery_ride.md](./05_delivery_ride.md) |
| 06 | Sale Ride Execution | Not Started | 04 | [06_sale_ride.md](./06_sale_ride.md) |
| 07 | Ride Close & Reconciliation | Not Started | 05, 06 | [07_ride_reconciliation.md](./07_ride_reconciliation.md) |
| 08 | Sales Agent Controls | Not Started | 07 | [08_sales_agent_controls.md](./08_sales_agent_controls.md) |
| 09 | Fraud Controls & Audit Trail | Not Started | 08 | [09_fraud_controls.md](./09_fraud_controls.md) |
---
## Feature Summaries
### 00 — Existing Gap Fixes
Fix all known holes in the current codebase before new features are layered on top.
Includes DTO field gaps, missing authorization on role endpoints, secrets management,
and adding input validation across all existing endpoints.
**Gate**: All existing API endpoints behave correctly and securely before step 01 begins.
### 01 — Item Catalog
Define the Item entity with real properties (name, SKU, unit of measure, category, price tiers).
Items are the unit of stock — nothing in the stock ledger makes sense without them.
This step also covers the Item CRUD API and how items relate to orders.
**Gate**: An item can be created, updated, and referenced in an order with quantity and price.
### 02 — Stock Ledger
Introduce the concept of stock per location. Every warehouse (fixed or vehicle) holds a quantity
of each item. Stock is never edited directly — all changes happen through movement transactions
(load, sale, return, damage, adjustment). The current balance is always the running sum.
This is the single most important architectural decision for the whole system.
**Gate**: Stock balance for any item at any location can be queried. Every change is a traceable transaction.
### 03 — Ride Lifecycle
A ride is a session that turns a vehicle into a temporary warehouse. It has a state machine:
Draft → Open → Active → Closed. A vehicle can only be on one active ride at a time.
Two ride types exist: Delivery (pre-orders) and Sale (van sales). The ride tracks who is driving,
which route, which vehicle, and when it started and ended.
**Gate**: A ride can be created, opened, and closed. A vehicle's stock ledger is created when a ride opens.
### 04 — Loading Event
When a ride opens, stock must be physically loaded from a source warehouse onto the vehicle.
This is a two-party event: warehouse manager confirms what was dispatched, driver confirms what was received.
Discrepancies between the two are flagged immediately and require resolution before the ride goes active.
The loading event creates stock movement transactions that debit the warehouse and credit the vehicle.
**Gate**: Stock moves from warehouse to vehicle via a confirmed loading event. Any discrepancy is logged.
### 05 — Delivery Ride Execution
On a delivery ride, the driver fulfills pre-confirmed orders. Each order becomes a delivery task.
The driver marks each as delivered, partially delivered, or failed. Partially delivered orders
generate a credit note for the undelivered quantity. Customer confirmation (signature or OTP)
is required to mark a delivery complete. Stock movements are created per delivery event.
**Gate**: A delivery ride can execute all orders on its route and produce a delivery report.
### 06 — Sale Ride Execution
On a sale ride, the driver visits customers and creates invoices on the spot. No pre-orders exist.
The driver selects items, quantities, and prices (within approved discount limits), and the invoice
is generated immediately. Payment is collected (cash or credit) at the point of sale.
The vehicle's stock ledger decreases with each sale. The driver cannot sell more than what is loaded.
**Gate**: A sale ride can generate invoices in the field. Stock on vehicle decreases with each sale.
### 07 — Ride Close & Reconciliation
When a ride ends, the system performs a full reconciliation. The equation must balance:
Opening Stock − Sales − Damages + Returns = Closing Stock (physical count)
Cash Collected + Credit Extended = Total Sales − Returns
Any variance must be approved by a supervisor with a reason code before the ride can close.
Unresolved variances are logged as discrepancies against the agent's record.
**Gate**: A ride cannot close with an unexplained variance. All discrepancies are permanently recorded.
### 08 — Sales Agent Controls
Agents operate within boundaries set by their role level. This step wires role levels to business rules:
discount limits per agent level, credit limits per customer, maximum single-transaction value,
and approval chains for exceptions. Supervisors can grant one-time overrides logged against their account.
**Gate**: An agent cannot exceed their limits without a logged, authorized override.
### 09 — Fraud Controls & Audit Trail
A dedicated audit log records every sensitive event (who, what, when, from where).
Automated checks flag: returns without a matching prior sale, sales outside the geofenced route,
agents with consistently high variance rates, and cash collected below invoiced amounts.
Return approval requires supervisor sign-off before stock is credited. Anomaly reports are
surfaced to area managers on a scheduled basis.
**Gate**: Every sensitive event is logged immutably. Automated flags surface to the right level of management.
---
## Rules
1. A feature file must be filled in and reviewed before implementation begins on that step.
2. Status must be updated in this file whenever a step transitions.
3. If implementation reveals a design change, the feature file is updated and this plan is revised.
4. Steps must be done in dependency order — no skipping.