Initial project setup with domain, UoW, and API structure

Added .gitignore, solution, and project files for .NET 10. Introduced AGENTS.md and PLAN.md for guidelines and planning. Implemented appsettings.json, launchSettings.json, and MySQL config. Created AppDBContext and all domain models with composite keys. Added IUnitOfWork and EFUnitOfWork. Structured feature folders for Region, Route, Vehicle, Store, Warehouse, and Order with DTOs, repositories, services, and controllers. Added initial EF Core migrations and sample HTTP request. Registered dependencies and enabled OpenAPI in development.
This commit is contained in:
Kalana B. Thilakarathna
2026-08-18 08:25:18 +05:30
commit ac3f34e4f3
56 changed files with 2796 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
## Build outputs
bin/
obj/
## Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
## NuGet
*.nupkg
*.snupkg
**/[Pp]ackages/
!**/[Pp]ackages/build/
*.nuget.props
*.nuget.targets
project.lock.json
project.fragment.lock.json
artifacts/
## ASP.NET Core
**/wwwroot/lib/
## Secrets & environment-specific config
appsettings.Development.json
appsettings.Local.json
appsettings.*.local.json
secrets.json
*.pfx
*.p12
## Entity Framework
# Keep Migrations — they are source code
# Add this only if you intentionally exclude migrations:
# **/Migrations/
## Logs
logs/
*.log
*.txt.log
## OS
Thumbs.db
ehthumbs.db
Desktop.ini
.DS_Store
*.lnk
## JetBrains Rider / ReSharper
.idea/
*.DotSettings.user
_ReSharper*/
## Publish output
publish/
**/publish/
## Test results
TestResults/
*.coverage
*.coveragexml
## Temporary files
*.tmp
*.temp
*.bak
*.orig
*.rej
+67
View File
@@ -0,0 +1,67 @@
# Repository Guidelines
## Hard Rules — Always Enforced
1. **Never call `uow.SaveChangesAsync()` in any repository or service.** Only `uow.CommitAsync()` (or `BeginAsync`/`CommitAsync`/`RollbackAsync` for transactions) may be called, and only from the service layer.
2. **Never use `DateTime.UtcNow`.** Use `DateTime.Now` (server time) everywhere.
3. **Never build a feature that was not explicitly requested.** Confirm with the user before creating any new entity, endpoint, service, or infrastructure component.
---
## Project Structure & Module Organization
The repository contains one solution file, `Distribution.slnx`, and one ASP.NET Core 10 Web API project in `Distribution/`. The intended folder layout is:
```
Distribution/
├── Controllers/ # Thin HTTP endpoints — no business logic, no UoW calls
├── Data/
│ └── AppDBContext.cs # EF Core DbContext — add DbSets here
├── Domain/
│ └── Entities/ # Plain POCO entities; one file per entity
├── Infra/
│ ├── UoW/ # IUnitOfWork + EFUnitOfWork (already implemented)
│ └── Repositories/ # IRepository<T>, EFRepository<T>, and per-aggregate repos
├── Application/
│ └── Services/ # Business logic; owns the uow.CommitAsync() call
└── DTOs/ # Request/Response DTOs per module
```
Core entrypoint: `Distribution/Program.cs`. Configuration: `appsettings.json` / `appsettings.Development.json`. Launch profiles: `Distribution/Properties/launchSettings.json`. Do not commit build output from `Distribution/bin/` or `Distribution/obj/`.
## UoW Call-Chain Rule
```
Controller → Service → Repository (EF change tracking only)
↓
uow.CommitAsync() ← only the service calls this
```
- Repositories call `AddAsync`, `Update`, `Remove` on the DbSet — nothing more.
- Services call `_uow.CommitAsync(ct)` to flush and commit.
- For multi-step operations, services call `BeginAsync` → work → `CommitAsync` (or `RollbackAsync` in catch).
- `uow.SaveChangesAsync()` exists on the interface but must never appear in repo or service code.
---
## Build, Test, and Development Commands
Run commands from the repository root unless noted otherwise.
- `dotnet restore Distribution.slnx`: restore NuGet packages.
- `dotnet build Distribution.slnx`: compile the solution.
- `dotnet run --project Distribution/Distribution.csproj`: start the API locally.
- `dotnet watch run --project Distribution/Distribution.csproj`: run with hot reload during development.
- `dotnet test`: run automated tests once a test project is added.
The default development URLs are defined in `launchSettings.json`, currently `http://localhost:5089` and `https://localhost:7025`.
## Coding Style & Naming Conventions
Use standard C# conventions: 4-space indentation, PascalCase for public types and members, camelCase for local variables and parameters, and one class per file. Name controllers with the `*Controller.cs` pattern and keep route names explicit. Nullable reference types are enabled, so handle nullability deliberately rather than suppressing warnings.
## Testing Guidelines
There is no test project in the repository yet. Add new tests in a sibling project such as `Distribution.Tests/` and include it in the solution. Prefer xUnit for .NET API coverage. Name test files after the subject under test, for example `OrdersControllerTests.cs`, and use method names like `Get_ReturnsNotFound_WhenIdIsMissing`.
## Commit & Pull Request Guidelines
The current branch has no commit history yet, so use a clean convention from the start: short imperative subjects such as `Add orders controller` or `Configure OpenAPI in development`. Keep commits focused on one change. Pull requests should include a brief summary, impacted endpoints or config files, test evidence (`dotnet build`, `dotnet test`), and sample request or response details when API behavior changes.
## Configuration Notes
Keep secrets out of `appsettings.json`; use environment variables or user secrets for local development. Restrict `appsettings.Development.json` to non-sensitive machine-local defaults.
+24
View File
@@ -0,0 +1,24 @@
using Distribution.Models;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Data;
public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(options)
{
public DbSet<Region> Regions { get; set; }
public DbSet<Models.Route> Routes { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Warehouse> Warehouses { get; set; }
public DbSet<SaleRoute> SaleRoutes { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<DeliveryRun> DeliveryRuns { get; set; }
public DbSet<Item> Items { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.FK_OrderId, oi.FK_ItemId });
}
}
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.OpenApi" Version="2.11.0" />
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.7" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@Distribution_HostAddress = http://localhost:5089
GET {{Distribution_HostAddress}}/weatherforecast/
Accept: application/json
###
+3
View File
@@ -0,0 +1,3 @@
<Solution>
<Project Path="Distribution.csproj" />
</Solution>
@@ -0,0 +1,13 @@
namespace Distribution.Features.Order;
public interface IOrderRepository
{
Task<Models.Order?> GetByIdAsync(Guid id);
Task<List<Models.Order>> GetAllAsync();
Task AddAsync(Models.Order entity);
void Update(Models.Order entity);
void Remove(Models.Order entity);
Task<List<Models.OrderItem>> GetItemsByOrderIdAsync(Guid orderId);
Task<List<Models.Order>> GetByStoreIdAsync(Guid storeId);
Task AddItemsAsync(IEnumerable<Models.OrderItem> items);
}
@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Order;
[ApiController]
[Route("api/orders")]
public class OrderController(OrderService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("store/{storeId:guid}")]
public async Task<IActionResult> GetByStore(Guid storeId) =>
Ok(await service.GetByStoreIdAsync(storeId));
[HttpPost]
public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateOrderRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace Distribution.Features.Order;
public record OrderItemRequest(Guid ItemId, int Quantity);
public record CreateOrderRequest(Guid StoreId, DateTime OrderDate, decimal TotalAmount, List<OrderItemRequest> Items);
public record UpdateOrderRequest(DateTime OrderDate, decimal TotalAmount);
public record OrderItemResponse(Guid ItemId, int Quantity);
public record OrderResponse(
Guid Id,
DateTime OrderDate,
Guid StoreId,
decimal TotalAmount,
List<OrderItemResponse> Items,
bool IsActive,
DateTime CreatedAt);
@@ -0,0 +1,34 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Order;
public class OrderRepository(AppDBContext context) : IOrderRepository
{
public async Task<Models.Order?> GetByIdAsync(Guid id) =>
await context.Orders.FindAsync(id);
public async Task<List<Models.Order>> GetAllAsync() =>
await context.Orders.ToListAsync();
public async Task AddAsync(Models.Order entity) =>
await context.Orders.AddAsync(entity);
public void Update(Models.Order entity) =>
context.Orders.Update(entity);
public void Remove(Models.Order entity)
{
entity.IsActive = false;
context.Orders.Update(entity);
}
public async Task<List<Models.OrderItem>> GetItemsByOrderIdAsync(Guid orderId) =>
await context.OrderItems.Where(oi => oi.FK_OrderId == orderId).ToListAsync();
public async Task<List<Models.Order>> GetByStoreIdAsync(Guid storeId) =>
await context.Orders.Where(o => o.FK_StoreId == storeId && o.IsActive).ToListAsync();
public async Task AddItemsAsync(IEnumerable<Models.OrderItem> items) =>
await context.OrderItems.AddRangeAsync(items);
}
+112
View File
@@ -0,0 +1,112 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Order;
public class OrderService(IOrderRepository repo, IUnitOfWork uow)
{
private static OrderResponse ToResponse(Models.Order o, List<Models.OrderItem> items) =>
new(o.Id, o.OrderDate, o.FK_StoreId, o.TotalAmount,
items.Select(i => new OrderItemResponse(i.FK_ItemId, i.Quantity)).ToList(),
o.IsActive, o.CreatedAt);
public async Task<List<OrderResponse>> GetAllAsync()
{
var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList();
var result = new List<OrderResponse>();
foreach (var order in orders)
{
var items = await repo.GetItemsByOrderIdAsync(order.Id);
result.Add(ToResponse(order, items));
}
return result;
}
public async Task<OrderResponse?> GetByIdAsync(Guid id)
{
var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return null;
var items = await repo.GetItemsByOrderIdAsync(id);
return ToResponse(order, items);
}
public async Task<List<OrderResponse>> GetByStoreIdAsync(Guid storeId)
{
var orders = await repo.GetByStoreIdAsync(storeId);
var result = new List<OrderResponse>();
foreach (var order in orders)
{
var items = await repo.GetItemsByOrderIdAsync(order.Id);
result.Add(ToResponse(order, items));
}
return result;
}
public async Task<OrderResponse> CreateAsync(CreateOrderRequest request, CancellationToken ct = default)
{
var order = new Models.Order
{
FK_StoreId = request.StoreId,
OrderDate = request.OrderDate,
TotalAmount = request.TotalAmount
};
var orderItems = request.Items.Select(i => new Models.OrderItem
{
FK_OrderId = order.Id,
FK_ItemId = i.ItemId,
Quantity = i.Quantity
}).ToList();
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(order);
await repo.AddItemsAsync(orderItems);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(order, orderItems);
}
public async Task<OrderResponse?> UpdateAsync(Guid id, UpdateOrderRequest request, CancellationToken ct = default)
{
var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return null;
order.OrderDate = request.OrderDate;
order.TotalAmount = request.TotalAmount;
await uow.BeginAsync(ct);
try
{
repo.Update(order);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
var items = await repo.GetItemsByOrderIdAsync(id);
return ToResponse(order, items);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(order);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
@@ -0,0 +1,10 @@
namespace Distribution.Features.Region;
public interface IRegionRepository
{
Task<Models.Region?> GetByIdAsync(Guid id);
Task<List<Models.Region>> GetAllAsync();
Task AddAsync(Models.Region entity);
void Update(Models.Region entity);
void Remove(Models.Region entity);
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Region;
[ApiController]
[Route("api/regions")]
public class RegionController(RegionService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateRegionRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateRegionRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
@@ -0,0 +1,7 @@
namespace Distribution.Features.Region;
public record CreateRegionRequest(string Name);
public record UpdateRegionRequest(string Name);
public record RegionResponse(Guid Id, string Name, bool IsActive, DateTime CreatedAt);
@@ -0,0 +1,25 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Region;
public class RegionRepository(AppDBContext context) : IRegionRepository
{
public async Task<Models.Region?> GetByIdAsync(Guid id) =>
await context.Regions.FindAsync(id);
public async Task<List<Models.Region>> GetAllAsync() =>
await context.Regions.ToListAsync();
public async Task AddAsync(Models.Region entity) =>
await context.Regions.AddAsync(entity);
public void Update(Models.Region entity) =>
context.Regions.Update(entity);
public void Remove(Models.Region entity)
{
entity.IsActive = false;
context.Regions.Update(entity);
}
}
@@ -0,0 +1,73 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Region;
public class RegionService(IRegionRepository repo, IUnitOfWork uow)
{
public async Task<List<RegionResponse>> GetAllAsync() =>
(await repo.GetAllAsync())
.Where(r => r.IsActive)
.Select(r => new RegionResponse(r.Id, r.Name, r.IsActive, r.CreatedAt))
.ToList();
public async Task<RegionResponse?> GetByIdAsync(Guid id)
{
var region = await repo.GetByIdAsync(id);
if (region is null || !region.IsActive) return null;
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
}
public async Task<RegionResponse> CreateAsync(CreateRegionRequest request, CancellationToken ct = default)
{
var region = new Models.Region { Name = request.Name };
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(region);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
}
public async Task<RegionResponse?> UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default)
{
var region = await repo.GetByIdAsync(id);
if (region is null || !region.IsActive) return null;
region.Name = request.Name;
await uow.BeginAsync(ct);
try
{
repo.Update(region);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var region = await repo.GetByIdAsync(id);
if (region is null || !region.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(region);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
@@ -0,0 +1,10 @@
namespace Distribution.Features.Route;
public interface IRouteRepository
{
Task<Models.Route?> GetByIdAsync(Guid id);
Task<List<Models.Route>> GetAllAsync();
Task AddAsync(Models.Route entity);
void Update(Models.Route entity);
void Remove(Models.Route entity);
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Route;
[ApiController]
[Route("api/routes")]
public class RouteController(RouteService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateRouteRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateRouteRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Distribution.Features.Route;
public record CreateRouteRequest(string RouteName, string StartPoint, string EndPoint, decimal Length);
public record UpdateRouteRequest(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);
@@ -0,0 +1,25 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Route;
public class RouteRepository(AppDBContext context) : IRouteRepository
{
public async Task<Models.Route?> GetByIdAsync(Guid id) =>
await context.Routes.FindAsync(id);
public async Task<List<Models.Route>> GetAllAsync() =>
await context.Routes.ToListAsync();
public async Task AddAsync(Models.Route entity) =>
await context.Routes.AddAsync(entity);
public void Update(Models.Route entity) =>
context.Routes.Update(entity);
public void Remove(Models.Route entity)
{
entity.IsActive = false;
context.Routes.Update(entity);
}
}
@@ -0,0 +1,82 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Route;
public class RouteService(IRouteRepository repo, IUnitOfWork uow)
{
public async Task<List<RouteResponse>> 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))
.ToList();
public async Task<RouteResponse?> 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);
}
public async Task<RouteResponse> CreateAsync(CreateRouteRequest request, CancellationToken ct = default)
{
var route = new Models.Route
{
RouteName = request.RouteName,
StartPoint = request.StartPoint,
EndPoint = request.EndPoint,
Length = request.Length
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(route);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new RouteResponse(route.Id, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
}
public async Task<RouteResponse?> UpdateAsync(Guid id, UpdateRouteRequest request, CancellationToken ct = default)
{
var route = await repo.GetByIdAsync(id);
if (route is null || !route.IsActive) return null;
route.RouteName = request.RouteName;
route.StartPoint = request.StartPoint;
route.EndPoint = request.EndPoint;
route.Length = request.Length;
await uow.BeginAsync(ct);
try
{
repo.Update(route);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new RouteResponse(route.Id, route.RouteName, route.StartPoint, route.EndPoint, route.Length, route.IsActive, route.CreatedAt);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var route = await repo.GetByIdAsync(id);
if (route is null || !route.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(route);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
@@ -0,0 +1,10 @@
namespace Distribution.Features.Store;
public interface IStoreRepository
{
Task<Models.Store?> GetByIdAsync(Guid id);
Task<List<Models.Store>> GetAllAsync();
Task AddAsync(Models.Store entity);
void Update(Models.Store entity);
void Remove(Models.Store entity);
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Store;
[ApiController]
[Route("api/stores")]
public class StoreController(StoreService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateStoreRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateStoreRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
+31
View File
@@ -0,0 +1,31 @@
namespace Distribution.Features.Store;
public record CreateStoreRequest(
string OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount);
public record UpdateStoreRequest(
string OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount);
public record StoreResponse(
Guid Id,
string OwnerName,
string StoreName,
string StoreAddress,
decimal Latitude,
decimal Longitude,
decimal OneTimeAmount,
decimal CreditAmount,
bool IsActive,
DateTime CreatedAt);
@@ -0,0 +1,25 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Store;
public class StoreRepository(AppDBContext context) : IStoreRepository
{
public async Task<Models.Store?> GetByIdAsync(Guid id) =>
await context.Stores.FindAsync(id);
public async Task<List<Models.Store>> GetAllAsync() =>
await context.Stores.ToListAsync();
public async Task AddAsync(Models.Store entity) =>
await context.Stores.AddAsync(entity);
public void Update(Models.Store entity) =>
context.Stores.Update(entity);
public void Remove(Models.Store entity)
{
entity.IsActive = false;
context.Stores.Update(entity);
}
}
@@ -0,0 +1,91 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Store;
public class StoreService(IStoreRepository repo, IUnitOfWork uow)
{
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);
public async Task<List<StoreResponse>> GetAllAsync() =>
(await repo.GetAllAsync())
.Where(s => s.IsActive)
.Select(ToResponse)
.ToList();
public async Task<StoreResponse?> GetByIdAsync(Guid id)
{
var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null;
return ToResponse(store);
}
public async Task<StoreResponse> CreateAsync(CreateStoreRequest request, CancellationToken ct = default)
{
var store = new Models.Store
{
OwnerName = request.OwnerName,
StoreName = request.StoreName,
StoreAddress = request.StoreAddress,
Latitude = request.Latitude,
Longitude = request.Longitude,
OneTimeAmount = request.OneTimeAmount,
CreditAmount = request.CreditAmount
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(store);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(store);
}
public async Task<StoreResponse?> UpdateAsync(Guid id, UpdateStoreRequest request, CancellationToken ct = default)
{
var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null;
store.OwnerName = request.OwnerName;
store.StoreName = request.StoreName;
store.StoreAddress = request.StoreAddress;
store.Latitude = request.Latitude;
store.Longitude = request.Longitude;
store.OneTimeAmount = request.OneTimeAmount;
store.CreditAmount = request.CreditAmount;
await uow.BeginAsync(ct);
try
{
repo.Update(store);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(store);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(store);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
@@ -0,0 +1,10 @@
namespace Distribution.Features.Vehicle;
public interface IVehicleRepository
{
Task<Models.Vehicle?> GetByIdAsync(Guid id);
Task<List<Models.Vehicle>> GetAllAsync();
Task AddAsync(Models.Vehicle entity);
void Update(Models.Vehicle entity);
void Remove(Models.Vehicle entity);
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Vehicle;
[ApiController]
[Route("api/vehicles")]
public class VehicleController(VehicleService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateVehicleRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateVehicleRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
@@ -0,0 +1,9 @@
using Distribution.Models;
namespace Distribution.Features.Vehicle;
public record CreateVehicleRequest(string RegNumber, string NumberPlate, VehicleType Type);
public record UpdateVehicleRequest(string RegNumber, string NumberPlate, VehicleType Type);
public record VehicleResponse(Guid Id, string RegNumber, string NumberPlate, VehicleType Type, bool IsActive, DateTime CreatedAt);
@@ -0,0 +1,25 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Vehicle;
public class VehicleRepository(AppDBContext context) : IVehicleRepository
{
public async Task<Models.Vehicle?> GetByIdAsync(Guid id) =>
await context.Vehicles.FindAsync(id);
public async Task<List<Models.Vehicle>> GetAllAsync() =>
await context.Vehicles.ToListAsync();
public async Task AddAsync(Models.Vehicle entity) =>
await context.Vehicles.AddAsync(entity);
public void Update(Models.Vehicle entity) =>
context.Vehicles.Update(entity);
public void Remove(Models.Vehicle entity)
{
entity.IsActive = false;
context.Vehicles.Update(entity);
}
}
@@ -0,0 +1,80 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Vehicle;
public class VehicleService(IVehicleRepository repo, IUnitOfWork uow)
{
public async Task<List<VehicleResponse>> GetAllAsync() =>
(await repo.GetAllAsync())
.Where(v => v.IsActive)
.Select(v => new VehicleResponse(v.Id, v.RegNumber, v.NumberPlate, v.Type, v.IsActive, v.CreatedAt))
.ToList();
public async Task<VehicleResponse?> GetByIdAsync(Guid id)
{
var vehicle = await repo.GetByIdAsync(id);
if (vehicle is null || !vehicle.IsActive) return null;
return new VehicleResponse(vehicle.Id, vehicle.RegNumber, vehicle.NumberPlate, vehicle.Type, vehicle.IsActive, vehicle.CreatedAt);
}
public async Task<VehicleResponse> CreateAsync(CreateVehicleRequest request, CancellationToken ct = default)
{
var vehicle = new Models.Vehicle
{
RegNumber = request.RegNumber,
NumberPlate = request.NumberPlate,
Type = request.Type
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(vehicle);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new VehicleResponse(vehicle.Id, vehicle.RegNumber, vehicle.NumberPlate, vehicle.Type, vehicle.IsActive, vehicle.CreatedAt);
}
public async Task<VehicleResponse?> UpdateAsync(Guid id, UpdateVehicleRequest request, CancellationToken ct = default)
{
var vehicle = await repo.GetByIdAsync(id);
if (vehicle is null || !vehicle.IsActive) return null;
vehicle.RegNumber = request.RegNumber;
vehicle.NumberPlate = request.NumberPlate;
vehicle.Type = request.Type;
await uow.BeginAsync(ct);
try
{
repo.Update(vehicle);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new VehicleResponse(vehicle.Id, vehicle.RegNumber, vehicle.NumberPlate, vehicle.Type, vehicle.IsActive, vehicle.CreatedAt);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var vehicle = await repo.GetByIdAsync(id);
if (vehicle is null || !vehicle.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(vehicle);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
@@ -0,0 +1,10 @@
namespace Distribution.Features.Warehouse;
public interface IWarehouseRepository
{
Task<Models.Warehouse?> GetByIdAsync(Guid id);
Task<List<Models.Warehouse>> GetAllAsync();
Task AddAsync(Models.Warehouse entity);
void Update(Models.Warehouse entity);
void Remove(Models.Warehouse entity);
}
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Warehouse;
[ApiController]
[Route("api/warehouses")]
public class WarehouseController(WarehouseService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
var result = await service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateWarehouseRequest request, CancellationToken ct)
{
var result = await service.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, UpdateWarehouseRequest request, CancellationToken ct)
{
var result = await service.UpdateAsync(id, request, ct);
return result is null ? NotFound() : Ok(result);
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
}
}
@@ -0,0 +1,9 @@
using Distribution.Models;
namespace Distribution.Features.Warehouse;
public record CreateWarehouseRequest(string Name, string Location, WareHouseType Type, Guid? VehicleId);
public record UpdateWarehouseRequest(string Name, string Location, WareHouseType Type, Guid? VehicleId);
public record WarehouseResponse(Guid Id, string Name, string Location, WareHouseType Type, Guid? VehicleId, bool IsActive, DateTime CreatedAt);
@@ -0,0 +1,25 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Warehouse;
public class WarehouseRepository(AppDBContext context) : IWarehouseRepository
{
public async Task<Models.Warehouse?> GetByIdAsync(Guid id) =>
await context.Warehouses.FindAsync(id);
public async Task<List<Models.Warehouse>> GetAllAsync() =>
await context.Warehouses.ToListAsync();
public async Task AddAsync(Models.Warehouse entity) =>
await context.Warehouses.AddAsync(entity);
public void Update(Models.Warehouse entity) =>
context.Warehouses.Update(entity);
public void Remove(Models.Warehouse entity)
{
entity.IsActive = false;
context.Warehouses.Update(entity);
}
}
@@ -0,0 +1,85 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.Warehouse;
public class WarehouseService(IWarehouseRepository repo, IUnitOfWork uow)
{
private static WarehouseResponse ToResponse(Models.Warehouse w) =>
new(w.Id, w.Name, w.Location, w.Type, w.VehicleId, w.IsActive, w.CreatedAt);
public async Task<List<WarehouseResponse>> GetAllAsync() =>
(await repo.GetAllAsync())
.Where(w => w.IsActive)
.Select(ToResponse)
.ToList();
public async Task<WarehouseResponse?> GetByIdAsync(Guid id)
{
var warehouse = await repo.GetByIdAsync(id);
if (warehouse is null || !warehouse.IsActive) return null;
return ToResponse(warehouse);
}
public async Task<WarehouseResponse> CreateAsync(CreateWarehouseRequest request, CancellationToken ct = default)
{
var warehouse = new Models.Warehouse
{
Name = request.Name,
Location = request.Location,
Type = request.Type,
VehicleId = request.VehicleId
};
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(warehouse);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(warehouse);
}
public async Task<WarehouseResponse?> UpdateAsync(Guid id, UpdateWarehouseRequest request, CancellationToken ct = default)
{
var warehouse = await repo.GetByIdAsync(id);
if (warehouse is null || !warehouse.IsActive) return null;
warehouse.Name = request.Name;
warehouse.Location = request.Location;
warehouse.Type = request.Type;
warehouse.VehicleId = request.VehicleId;
await uow.BeginAsync(ct);
try
{
repo.Update(warehouse);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToResponse(warehouse);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var warehouse = await repo.GetByIdAsync(id);
if (warehouse is null || !warehouse.IsActive) return false;
await uow.BeginAsync(ct);
try
{
repo.Remove(warehouse);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return true;
}
}
+50
View File
@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Storage;
using Distribution.Data;
namespace Distribution.Infra.UoW
{
public class EFUnitOfWork : IUnitOfWork
{
private readonly AppDBContext _dbContext;
private IDbContextTransaction? _transaction;
public EFUnitOfWork(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task BeginAsync(CancellationToken ct = default)
{
if (_transaction == null)
_transaction = await _dbContext.Database.BeginTransactionAsync(ct);
}
public async Task SaveChangesAsync(CancellationToken ct = default)
{
await _dbContext.SaveChangesAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
await _dbContext.SaveChangesAsync(ct);
if (_transaction != null)
{
await _transaction.CommitAsync(ct);
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_transaction != null)
{
await _transaction.RollbackAsync(ct);
await _transaction.DisposeAsync();
_transaction = null;
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Distribution.Infra.UoW
{
public interface IUnitOfWork
{
Task BeginAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
}
+353
View File
@@ -0,0 +1,353 @@
// <auto-generated />
using System;
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Distribution.Migrations
{
[DbContext(typeof(AppDBContext))]
[Migration("20260812090735_init")]
partial class init
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_RouteId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_VehicleId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<int>("Meterend")
.HasColumnType("int");
b.Property<int>("Meterstart")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("FK_RouteId");
b.HasIndex("FK_VehicleId");
b.ToTable("DeliveryRuns");
});
modelBuilder.Entity("Distribution.Models.Item", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("Items");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_StoreId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("OrderDate")
.HasColumnType("datetime(6)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("FK_StoreId");
b.ToTable("Orders");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.Property<Guid>("FK_OrderId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("FK_OrderId", "FK_ItemId");
b.HasIndex("FK_ItemId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Distribution.Models.Region", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Regions");
});
modelBuilder.Entity("Distribution.Models.Route", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("EndPoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Length")
.HasColumnType("decimal(18,2)");
b.Property<string>("RouteName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StartPoint")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Routes");
});
modelBuilder.Entity("Distribution.Models.SaleRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("SaleRoutes");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("CreditAmount")
.HasColumnType("decimal(18,2)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Latitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Longitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("OneTimeAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("OwnerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreName")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("Distribution.Models.Vehicle", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("NumberPlate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("RegNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Vehicles");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Location")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid?>("VehicleId")
.HasColumnType("char(36)");
b.Property<int>("type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("VehicleId");
b.ToTable("Warehouses");
});
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.HasOne("Distribution.Models.Route", "Route")
.WithMany()
.HasForeignKey("FK_RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("FK_VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Route");
b.Navigation("Vehicle");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.HasOne("Distribution.Models.Store", "Store")
.WithMany()
.HasForeignKey("FK_StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Store");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Order", "Order")
.WithMany()
.HasForeignKey("FK_OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
b.Navigation("Order");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("VehicleId");
b.Navigation("Vehicle");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,278 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Items",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Items", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Regions",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Regions", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Routes",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
RouteName = table.Column<string>(type: "longtext", nullable: false),
StartPoint = table.Column<string>(type: "longtext", nullable: false),
EndPoint = table.Column<string>(type: "longtext", nullable: false),
Length = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Routes", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "SaleRoutes",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SaleRoutes", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Stores",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
OwnerName = table.Column<string>(type: "longtext", nullable: false),
StoreName = table.Column<string>(type: "longtext", nullable: false),
StoreAddress = table.Column<string>(type: "longtext", nullable: false),
Latitude = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
Longitude = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
OneTimeAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreditAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Stores", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Vehicles",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
RegNumber = table.Column<string>(type: "longtext", nullable: false),
NumberPlate = table.Column<string>(type: "longtext", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Vehicles", x => x.Id);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
OrderDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
FK_StoreId = table.Column<Guid>(type: "char(36)", nullable: false),
TotalAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_Stores_FK_StoreId",
column: x => x.FK_StoreId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "DeliveryRuns",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
FK_VehicleId = table.Column<Guid>(type: "char(36)", nullable: false),
Meterstart = table.Column<int>(type: "int", nullable: false),
Meterend = table.Column<int>(type: "int", nullable: false),
FK_RouteId = table.Column<Guid>(type: "char(36)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeliveryRuns", x => x.Id);
table.ForeignKey(
name: "FK_DeliveryRuns_Routes_FK_RouteId",
column: x => x.FK_RouteId,
principalTable: "Routes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DeliveryRuns_Vehicles_FK_VehicleId",
column: x => x.FK_VehicleId,
principalTable: "Vehicles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "Warehouses",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "longtext", nullable: false),
Location = table.Column<string>(type: "longtext", nullable: false),
type = table.Column<int>(type: "int", nullable: false),
VehicleId = table.Column<Guid>(type: "char(36)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Warehouses", x => x.Id);
table.ForeignKey(
name: "FK_Warehouses_Vehicles_VehicleId",
column: x => x.VehicleId,
principalTable: "Vehicles",
principalColumn: "Id");
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "OrderItems",
columns: table => new
{
FK_OrderId = table.Column<Guid>(type: "char(36)", nullable: false),
FK_ItemId = table.Column<Guid>(type: "char(36)", nullable: false),
Quantity = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrderItems", x => new { x.FK_OrderId, x.FK_ItemId });
table.ForeignKey(
name: "FK_OrderItems_Items_FK_ItemId",
column: x => x.FK_ItemId,
principalTable: "Items",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrderItems_Orders_FK_OrderId",
column: x => x.FK_OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_DeliveryRuns_FK_RouteId",
table: "DeliveryRuns",
column: "FK_RouteId");
migrationBuilder.CreateIndex(
name: "IX_DeliveryRuns_FK_VehicleId",
table: "DeliveryRuns",
column: "FK_VehicleId");
migrationBuilder.CreateIndex(
name: "IX_OrderItems_FK_ItemId",
table: "OrderItems",
column: "FK_ItemId");
migrationBuilder.CreateIndex(
name: "IX_Orders_FK_StoreId",
table: "Orders",
column: "FK_StoreId");
migrationBuilder.CreateIndex(
name: "IX_Warehouses_VehicleId",
table: "Warehouses",
column: "VehicleId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeliveryRuns");
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "Regions");
migrationBuilder.DropTable(
name: "SaleRoutes");
migrationBuilder.DropTable(
name: "Warehouses");
migrationBuilder.DropTable(
name: "Routes");
migrationBuilder.DropTable(
name: "Items");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "Vehicles");
migrationBuilder.DropTable(
name: "Stores");
}
}
}
@@ -0,0 +1,350 @@
// <auto-generated />
using System;
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Distribution.Migrations
{
[DbContext(typeof(AppDBContext))]
partial class AppDBContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_RouteId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_VehicleId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<int>("Meterend")
.HasColumnType("int");
b.Property<int>("Meterstart")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("FK_RouteId");
b.HasIndex("FK_VehicleId");
b.ToTable("DeliveryRuns");
});
modelBuilder.Entity("Distribution.Models.Item", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("Items");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("FK_StoreId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("OrderDate")
.HasColumnType("datetime(6)");
b.Property<decimal>("TotalAmount")
.HasColumnType("decimal(18,2)");
b.HasKey("Id");
b.HasIndex("FK_StoreId");
b.ToTable("Orders");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.Property<Guid>("FK_OrderId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("FK_OrderId", "FK_ItemId");
b.HasIndex("FK_ItemId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Distribution.Models.Region", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Regions");
});
modelBuilder.Entity("Distribution.Models.Route", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("EndPoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Length")
.HasColumnType("decimal(18,2)");
b.Property<string>("RouteName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StartPoint")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Routes");
});
modelBuilder.Entity("Distribution.Models.SaleRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.HasKey("Id");
b.ToTable("SaleRoutes");
});
modelBuilder.Entity("Distribution.Models.Store", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("CreditAmount")
.HasColumnType("decimal(18,2)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<decimal>("Latitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("Longitude")
.HasColumnType("decimal(18,2)");
b.Property<decimal>("OneTimeAmount")
.HasColumnType("decimal(18,2)");
b.Property<string>("OwnerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("StoreName")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("Distribution.Models.Vehicle", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("NumberPlate")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("RegNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Vehicles");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Location")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid?>("VehicleId")
.HasColumnType("char(36)");
b.Property<int>("type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("VehicleId");
b.ToTable("Warehouses");
});
modelBuilder.Entity("Distribution.Models.DeliveryRun", b =>
{
b.HasOne("Distribution.Models.Route", "Route")
.WithMany()
.HasForeignKey("FK_RouteId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("FK_VehicleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Route");
b.Navigation("Vehicle");
});
modelBuilder.Entity("Distribution.Models.Order", b =>
{
b.HasOne("Distribution.Models.Store", "Store")
.WithMany()
.HasForeignKey("FK_StoreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Store");
});
modelBuilder.Entity("Distribution.Models.OrderItem", b =>
{
b.HasOne("Distribution.Models.Item", "Item")
.WithMany()
.HasForeignKey("FK_ItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Distribution.Models.Order", "Order")
.WithMany()
.HasForeignKey("FK_OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Item");
b.Navigation("Order");
});
modelBuilder.Entity("Distribution.Models.Warehouse", b =>
{
b.HasOne("Distribution.Models.Vehicle", "Vehicle")
.WithMany()
.HasForeignKey("VehicleId");
b.Navigation("Vehicle");
});
#pragma warning restore 612, 618
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class DeliveryRun : Template
{
public Guid FK_VehicleId { get; set; }
[ForeignKey("FK_VehicleId")]
public Vehicle Vehicle { get; set; }
//public Guid FK_DriverId { get; set; }
//[ForeignKey("FK_DriverId")]
//public Driver Driver { get; set; }
public int Meterstart { get; set; }
public int Meterend { get; set; }
public Guid FK_RouteId { get; set; }
[ForeignKey("FK_RouteId")]
public Route Route { get; set; }
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Distribution.Models
{
public class Item : Template
{
}
}
+13
View File
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class Order : Template
{
public DateTime OrderDate { get; set; }
public Guid FK_StoreId { get; set; }
[ForeignKey("FK_StoreId")]
public Store Store { get; set; }
public decimal TotalAmount { get; set; }
}
}
+15
View File
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class OrderItem : Template
{
public Guid FK_OrderId { get; set; }
[ForeignKey("FK_OrderId")]
public Order Order { get; set; }
public Guid FK_ItemId { get; set; }
[ForeignKey("FK_ItemId")]
public Item Item { get; set; }
public int Quantity { get; set; }
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Distribution.Models
{
public class Region : Template
{
public string Name { get; set; }
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Distribution.Models
{
public class Route : Template
{
public String RouteName { get; set; }
public String StartPoint { get; set; }
public String EndPoint { get; set; }
public decimal Length { get; set; } = 0;
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Distribution.Models
{
public class SaleRoute : Template
{
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Distribution.Models
{
public class Store : Template
{
public String OwnerName { get; set; }
public String StoreName { get; set; }
public String StoreAddress { get; set; }
public decimal Latitude { get; set; } = 0;
public decimal Longitude { get; set; } = 0;
public decimal OneTimeAmount { get; set; } = 0;
public decimal CreditAmount { get; set; } = 0;
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace Distribution.Models
{
public class Template
{
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime CreatedAt { get; set; } = DateTime.Now;
public bool IsActive { get; set; } = true;
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace Distribution.Models
{
public class Vehicle : Template
{
public string RegNumber { get; set; }
public string NumberPlate { get; set; }
public VehicleType Type { get; set; }
}
public enum VehicleType
{
Car,
Truck,
Van,
Motorcycle,
Bicycle
}
}
+26
View File
@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Distribution.Models
{
public class Warehouse : Template
{
public string Name { get; set; }
public string Location { get; set; }
public WareHouseType Type { get; set; }
public Guid? VehicleId { get; set; }
[ForeignKey("VehicleId")]
public Vehicle Vehicle { get; set; }
}
public enum WareHouseType
{
DistributionCenter,
FulfillmentCenter,
CrossDocking,
ColdStorage,
BondedWarehouse,
AutomatedWarehouse
}
}
+267
View File
@@ -0,0 +1,267 @@
# Phase Plan — Route & Store Management
## Scope
Build the full vertical-slice stack for: **Region, Route, Vehicle, Store, Warehouse, Order, OrderItem**.
Item model is sourced from ERP Core — excluded from this phase.
SaleRoute shape is TBD — excluded until clarified.
---
## Project Structure (target)
```
Distribution/
├── Models/ # All entity POCOs — unchanged
│ ├── Template.cs
│ ├── Region.cs
│ ├── Route.cs
│ ├── Vehicle.cs
│ ├── Store.cs
│ ├── Warehouse.cs
│ ├── SaleRoute.cs # TBD
│ ├── Order.cs
│ ├── OrderItem.cs
│ ├── DeliveryRun.cs
│ └── Item.cs # Sourced from ERP Core
├── Data/
│ └── AppDBContext.cs # DbSets + Fluent API config
├── Infra/
│ ├── UoW/ # Already implemented
│ │ ├── IUnitOfWork.cs
│ │ └── EFUnitOfWork.cs
│ └── Repositories/ # Shared generic base only
│ ├── IGenericRepo.cs
│ └── GenericRepo.cs
└── Features/
├── Region/
│ ├── RegionDTOs.cs
│ ├── IRegionRepository.cs
│ ├── RegionRepository.cs
│ ├── RegionService.cs
│ └── RegionController.cs
├── Route/
│ ├── RouteDTOs.cs
│ ├── IRouteRepository.cs
│ ├── RouteRepository.cs
│ ├── RouteService.cs
│ └── RouteController.cs
├── Vehicle/
│ ├── VehicleDTOs.cs
│ ├── IVehicleRepository.cs
│ ├── VehicleRepository.cs
│ ├── VehicleService.cs
│ └── VehicleController.cs
├── Store/
│ ├── StoreDTOs.cs
│ ├── IStoreRepository.cs
│ ├── StoreRepository.cs
│ ├── StoreService.cs
│ └── StoreController.cs
├── Warehouse/
│ ├── WarehouseDTOs.cs
│ ├── IWarehouseRepository.cs
│ ├── WarehouseRepository.cs
│ ├── WarehouseService.cs
│ └── WarehouseController.cs
└── Order/
├── OrderDTOs.cs
├── IOrderRepository.cs
├── OrderRepository.cs
├── OrderService.cs
└── OrderController.cs
```
---
## Hard Rules (from AGENTS.md)
- Only `uow.CommitAsync()` in services — never in repositories.
- Use `DateTime.Now` — never `DateTime.UtcNow`.
- Never build anything not explicitly requested.
---
## Step 2 — Complete AppDBContext
File: `Data/AppDBContext.cs`
Add DbSets and Fluent API configuration:
```csharp
public DbSet<Region> Regions { get; set; }
public DbSet<Route> Routes { get; set; }
public DbSet<Vehicle> Vehicles { get; set; }
public DbSet<Store> Stores { get; set; }
public DbSet<Warehouse> Warehouses { get; set; }
public DbSet<SaleRoute> SaleRoutes { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<DeliveryRun> DeliveryRuns { get; set; }
public DbSet<Item> Items { get; set; }
// OnModelCreating
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.FK_OrderId, oi.FK_ItemId });
```
---
## Step 3 — Initial Migration
```powershell
dotnet ef migrations add InitialCreate --project Distribution
dotnet ef database update --project Distribution
```
Each developer applies this locally. No migration files are shared.
---
## Step 4 — Generic Repository (Infra/Repositories)
**`IGenericRepo<T>`**
```
GetByIdAsync(Guid id)
GetAllAsync()
AddAsync(T entity)
Update(T entity)
Remove(T entity)
```
**`GenericRepo<T>`** — implements above using DbContext. No `SaveChanges` calls.
---
## Step 5 — Feature Implementation Order
Build each feature in this dependency order:
```
1. Region → no dependencies
2. Route → no dependencies
3. Vehicle → no dependencies
4. Store → no dependencies
5. Warehouse → depends on Vehicle (FK)
6. Order → depends on Store (FK)
7. OrderItem → depends on Order + Item (composite FK)
```
### Per-feature deliverables
#### DTOs (`{Feature}DTOs.cs`)
- `CreateRequest` — fields accepted on POST
- `UpdateRequest` — fields accepted on PUT
- `Response` — shape returned to client (never expose the entity directly)
#### Repository interface (`I{Feature}Repository.cs`)
Extends `IGenericRepo<T>`. Add feature-specific queries (e.g., `GetByStoreId`).
#### Repository implementation (`{Feature}Repository.cs`)
Inherits `GenericRepo<T>`. Implements feature-specific queries. No `CommitAsync` calls.
#### Service (`{Feature}Service.cs`)
- Contains all business logic.
- Calls `_uow.CommitAsync(ct)` after write operations.
- For multi-step writes: `BeginAsync → work → CommitAsync` (catch → `RollbackAsync`).
#### Controller (`{Feature}Controller.cs`)
Thin — maps HTTP → service call → returns result. No logic, no UoW calls.
---
## Step 6 — Endpoints
### Region — `/api/regions`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active regions |
| GET | `/{id}` | Get region by id |
| POST | `/` | Create region |
| PUT | `/{id}` | Update region |
| DELETE | `/{id}` | Soft delete (set IsActive = false) |
### Route — `/api/routes`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active routes |
| GET | `/{id}` | Get route by id |
| POST | `/` | Create route |
| PUT | `/{id}` | Update route |
| DELETE | `/{id}` | Soft delete |
### Vehicle — `/api/vehicles`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active vehicles |
| GET | `/{id}` | Get vehicle by id |
| POST | `/` | Create vehicle |
| PUT | `/{id}` | Update vehicle |
| DELETE | `/{id}` | Soft delete |
### Store — `/api/stores`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active stores |
| GET | `/{id}` | Get store by id |
| POST | `/` | Create store |
| PUT | `/{id}` | Update store |
| DELETE | `/{id}` | Soft delete |
### Warehouse — `/api/warehouses`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active warehouses |
| GET | `/{id}` | Get warehouse by id |
| POST | `/` | Create warehouse |
| PUT | `/{id}` | Update warehouse |
| DELETE | `/{id}` | Soft delete |
### Order — `/api/orders`
| Method | Route | Description |
|---|---|---|
| GET | `/` | Get all active orders |
| GET | `/{id}` | Get order with items |
| GET | `/store/{storeId}` | Get orders by store |
| POST | `/` | Create order with items |
| PUT | `/{id}` | Update order |
| DELETE | `/{id}` | Soft delete |
---
## Step 7 — DI Registration (Program.cs)
Register all feature repositories and services:
```csharp
// Generic base
builder.Services.AddScoped(typeof(IGenericRepo<>), typeof(GenericRepo<>));
// Feature repositories
builder.Services.AddScoped<IRegionRepository, RegionRepository>();
builder.Services.AddScoped<IRouteRepository, RouteRepository>();
builder.Services.AddScoped<IVehicleRepository, VehicleRepository>();
builder.Services.AddScoped<IStoreRepository, StoreRepository>();
builder.Services.AddScoped<IWarehouseRepository, WarehouseRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// Feature services
builder.Services.AddScoped<RegionService>();
builder.Services.AddScoped<RouteService>();
builder.Services.AddScoped<VehicleService>();
builder.Services.AddScoped<StoreService>();
builder.Services.AddScoped<WarehouseService>();
builder.Services.AddScoped<OrderService>();
```
---
## Deferred
| Item | Reason |
|---|---|
| `SaleRoute` | Shape undefined — implement after clarification |
| `Item` model | Sourced from ERP Core — user handles integration |
| `DeliveryRun` | Not in scope for this phase |
| Driver FK on DeliveryRun | Commented out — depends on HR/Driver module |
| Tests | No test project yet — add after endpoints stabilize |
+50
View File
@@ -0,0 +1,50 @@
using Distribution.Data;
using Distribution.Features.Order;
using Distribution.Features.Region;
using Distribution.Features.Route;
using Distribution.Features.Store;
using Distribution.Features.Vehicle;
using Distribution.Features.Warehouse;
using Distribution.Infra.UoW;
using Microsoft.EntityFrameworkCore;
using MySql.EntityFrameworkCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' was not found.");
builder.Services.AddDbContext<AppDBContext>(options =>
options.UseMySQL(connectionString));
builder.Services.AddScoped<IUnitOfWork, EFUnitOfWork>();
builder.Services.AddScoped<IRegionRepository, RegionRepository>();
builder.Services.AddScoped<IRouteRepository, RouteRepository>();
builder.Services.AddScoped<IVehicleRepository, VehicleRepository>();
builder.Services.AddScoped<IStoreRepository, StoreRepository>();
builder.Services.AddScoped<IWarehouseRepository, WarehouseRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<RegionService>();
builder.Services.AddScoped<RouteService>();
builder.Services.AddScoped<VehicleService>();
builder.Services.AddScoped<StoreService>();
builder.Services.AddScoped<WarehouseService>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddControllers();
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5089",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7025;http://localhost:5089",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Port=3306;Database=distribution_db;User=root;Password=root"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}