diff --git a/Distribution/Features/Route/RouteDTOs.cs b/Distribution/Features/Route/RouteDTOs.cs index 15254a6..9ec9018 100644 --- a/Distribution/Features/Route/RouteDTOs.cs +++ b/Distribution/Features/Route/RouteDTOs.cs @@ -1,7 +1,7 @@ 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); diff --git a/Distribution/Features/Route/RouteService.cs b/Distribution/Features/Route/RouteService.cs index 708b985..0d41774 100644 --- a/Distribution/Features/Route/RouteService.cs +++ b/Distribution/Features/Route/RouteService.cs @@ -7,20 +7,21 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow) public async Task> GetAllAsync() => (await repo.GetAllAsync()) .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(); public async Task GetByIdAsync(Guid id) { var route = await repo.GetByIdAsync(id); 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 CreateAsync(CreateRouteRequest request, CancellationToken ct = default) { var route = new Models.Route { + FK_RegionId = request.RegionId, RouteName = request.RouteName, StartPoint = request.StartPoint, EndPoint = request.EndPoint, @@ -37,13 +38,14 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow) await uow.RollbackAsync(ct); 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 UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default) { var route = await repo.GetByIdAsync(id); if (route is null || !route.IsActive) return null; + route.FK_RegionId = request.RegionId; route.RouteName = request.RouteName; route.StartPoint = request.StartPoint; route.EndPoint = request.EndPoint; @@ -59,7 +61,7 @@ public class RouteService(IRouteRepository repo, IUnitOfWork uow) await uow.RollbackAsync(ct); 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 DeleteAsync(Guid id, CancellationToken ct = default) diff --git a/Distribution/Features/Store/StoreDTOs.cs b/Distribution/Features/Store/StoreDTOs.cs index 6f78bae..d502aad 100644 --- a/Distribution/Features/Store/StoreDTOs.cs +++ b/Distribution/Features/Store/StoreDTOs.cs @@ -1,6 +1,7 @@ namespace Distribution.Features.Store; public record CreateStoreRequest( + Guid RouteId, string OwnerName, string StoreName, string StoreAddress, @@ -10,6 +11,7 @@ public record CreateStoreRequest( decimal CreditAmount); public record UpdateStoreRequest( + Guid RouteId, string OwnerName, string StoreName, string StoreAddress, @@ -20,6 +22,7 @@ public record UpdateStoreRequest( public record StoreResponse( Guid Id, + Guid RouteId, string OwnerName, string StoreName, string StoreAddress, diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs index 6f6fb32..d847aa5 100644 --- a/Distribution/Features/Store/StoreService.cs +++ b/Distribution/Features/Store/StoreService.cs @@ -7,9 +7,6 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe { 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 async Task HasStoreScopeAsync(Guid storeId) @@ -26,7 +23,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); 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 GetByIdAsync(Guid id) @@ -34,13 +31,14 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe var store = await repo.GetByIdAsync(id); if (store is null || !store.IsActive) 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 CreateAsync(CreateStoreRequest request, CancellationToken ct = default) { var store = new Models.Store { + FK_RouteId = request.RouteId, OwnerName = request.OwnerName, StoreName = request.StoreName, StoreAddress = request.StoreAddress, @@ -60,7 +58,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe await uow.RollbackAsync(ct); 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 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); if (store is null || !store.IsActive) return null; if (!await HasStoreScopeAsync(id)) return null; + store.FK_RouteId = request.RouteId; store.OwnerName = request.OwnerName; store.StoreName = request.StoreName; store.StoreAddress = request.StoreAddress; @@ -86,7 +85,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe await uow.RollbackAsync(ct); 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 DeleteAsync(Guid id, CancellationToken ct = default) diff --git a/Distribution/appsettings.json b/Distribution/appsettings.json index 6b24767..3b03741 100644 --- a/Distribution/appsettings.json +++ b/Distribution/appsettings.json @@ -1,6 +1,6 @@ { "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": { "LogLevel": { diff --git a/Distribution/docs/00_existing_gaps.md b/Distribution/docs/00_existing_gaps.md new file mode 100644 index 0000000..97779fd --- /dev/null +++ b/Distribution/docs/00_existing_gaps.md @@ -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. diff --git a/Distribution/docs/01_item_catalog.md b/Distribution/docs/01_item_catalog.md new file mode 100644 index 0000000..deb0a7f --- /dev/null +++ b/Distribution/docs/01_item_catalog.md @@ -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. diff --git a/Distribution/docs/02_stock_ledger.md b/Distribution/docs/02_stock_ledger.md new file mode 100644 index 0000000..579237a --- /dev/null +++ b/Distribution/docs/02_stock_ledger.md @@ -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. diff --git a/Distribution/docs/03_ride_lifecycle.md b/Distribution/docs/03_ride_lifecycle.md new file mode 100644 index 0000000..c661bc2 --- /dev/null +++ b/Distribution/docs/03_ride_lifecycle.md @@ -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. diff --git a/Distribution/docs/04_loading_event.md b/Distribution/docs/04_loading_event.md new file mode 100644 index 0000000..5a8f747 --- /dev/null +++ b/Distribution/docs/04_loading_event.md @@ -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. diff --git a/Distribution/docs/05_delivery_ride.md b/Distribution/docs/05_delivery_ride.md new file mode 100644 index 0000000..f18bbc0 --- /dev/null +++ b/Distribution/docs/05_delivery_ride.md @@ -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. diff --git a/Distribution/docs/06_sale_ride.md b/Distribution/docs/06_sale_ride.md new file mode 100644 index 0000000..a497294 --- /dev/null +++ b/Distribution/docs/06_sale_ride.md @@ -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. diff --git a/Distribution/docs/07_ride_reconciliation.md b/Distribution/docs/07_ride_reconciliation.md new file mode 100644 index 0000000..0e53706 --- /dev/null +++ b/Distribution/docs/07_ride_reconciliation.md @@ -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. diff --git a/Distribution/docs/08_sales_agent_controls.md b/Distribution/docs/08_sales_agent_controls.md new file mode 100644 index 0000000..1bafeac --- /dev/null +++ b/Distribution/docs/08_sales_agent_controls.md @@ -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. diff --git a/Distribution/docs/09_fraud_controls.md b/Distribution/docs/09_fraud_controls.md new file mode 100644 index 0000000..7e8534d --- /dev/null +++ b/Distribution/docs/09_fraud_controls.md @@ -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. diff --git a/Distribution/docs/mainPlan.md b/Distribution/docs/mainPlan.md new file mode 100644 index 0000000..981e2b2 --- /dev/null +++ b/Distribution/docs/mainPlan.md @@ -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.