From ac3f34e4f39d72744e04c7b5216570aeadc10cd5 Mon Sep 17 00:00:00 2001 From: "Kalana B. Thilakarathna" Date: Tue, 18 Aug 2026 08:25:18 +0530 Subject: [PATCH] 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. --- Distribution/.gitignore | 70 ++++ Distribution/AGENTS.md | 67 ++++ Distribution/Data/AppDBContext.cs | 24 ++ Distribution/Distribution.csproj | 19 + Distribution/Distribution.http | 6 + Distribution/Distribution.slnx | 3 + .../Features/Order/IOrderRepository.cs | 13 + .../Features/Order/OrderController.cs | 44 +++ Distribution/Features/Order/OrderDTOs.cs | 18 + .../Features/Order/OrderRepository.cs | 34 ++ Distribution/Features/Order/OrderService.cs | 112 ++++++ .../Features/Region/IRegionRepository.cs | 10 + .../Features/Region/RegionController.cs | 40 ++ Distribution/Features/Region/RegionDTOs.cs | 7 + .../Features/Region/RegionRepository.cs | 25 ++ Distribution/Features/Region/RegionService.cs | 73 ++++ .../Features/Route/IRouteRepository.cs | 10 + .../Features/Route/RouteController.cs | 40 ++ Distribution/Features/Route/RouteDTOs.cs | 7 + .../Features/Route/RouteRepository.cs | 25 ++ Distribution/Features/Route/RouteService.cs | 82 ++++ .../Features/Store/IStoreRepository.cs | 10 + .../Features/Store/StoreController.cs | 40 ++ Distribution/Features/Store/StoreDTOs.cs | 31 ++ .../Features/Store/StoreRepository.cs | 25 ++ Distribution/Features/Store/StoreService.cs | 91 +++++ .../Features/Vehicle/IVehicleRepository.cs | 10 + .../Features/Vehicle/VehicleController.cs | 40 ++ Distribution/Features/Vehicle/VehicleDTOs.cs | 9 + .../Features/Vehicle/VehicleRepository.cs | 25 ++ .../Features/Vehicle/VehicleService.cs | 80 ++++ .../Warehouse/IWarehouseRepository.cs | 10 + .../Features/Warehouse/WarehouseController.cs | 40 ++ .../Features/Warehouse/WarehouseDTOs.cs | 9 + .../Features/Warehouse/WarehouseRepository.cs | 25 ++ .../Features/Warehouse/WarehouseService.cs | 85 +++++ Distribution/Infra/UoW/EFUnitOfWork.cs | 50 +++ Distribution/Infra/UoW/IUnitOfWork.cs | 10 + .../20260812090735_init.Designer.cs | 353 ++++++++++++++++++ .../Migrations/20260812090735_init.cs | 278 ++++++++++++++ .../Migrations/AppDBContextModelSnapshot.cs | 350 +++++++++++++++++ Distribution/Models/DeliveryRun.cs | 19 + Distribution/Models/Item.cs | 7 + Distribution/Models/Order.cs | 13 + Distribution/Models/OrderItem.cs | 15 + Distribution/Models/Region.cs | 7 + Distribution/Models/Route.cs | 10 + Distribution/Models/SaleRoute.cs | 6 + Distribution/Models/Store.cs | 13 + Distribution/Models/Template.cs | 9 + Distribution/Models/Vehicle.cs | 19 + Distribution/Models/Warehouse.cs | 26 ++ Distribution/PLAN.md | 267 +++++++++++++ Distribution/Program.cs | 50 +++ Distribution/Properties/launchSettings.json | 23 ++ Distribution/appsettings.json | 12 + 56 files changed, 2796 insertions(+) create mode 100644 Distribution/.gitignore create mode 100644 Distribution/AGENTS.md create mode 100644 Distribution/Data/AppDBContext.cs create mode 100644 Distribution/Distribution.csproj create mode 100644 Distribution/Distribution.http create mode 100644 Distribution/Distribution.slnx create mode 100644 Distribution/Features/Order/IOrderRepository.cs create mode 100644 Distribution/Features/Order/OrderController.cs create mode 100644 Distribution/Features/Order/OrderDTOs.cs create mode 100644 Distribution/Features/Order/OrderRepository.cs create mode 100644 Distribution/Features/Order/OrderService.cs create mode 100644 Distribution/Features/Region/IRegionRepository.cs create mode 100644 Distribution/Features/Region/RegionController.cs create mode 100644 Distribution/Features/Region/RegionDTOs.cs create mode 100644 Distribution/Features/Region/RegionRepository.cs create mode 100644 Distribution/Features/Region/RegionService.cs create mode 100644 Distribution/Features/Route/IRouteRepository.cs create mode 100644 Distribution/Features/Route/RouteController.cs create mode 100644 Distribution/Features/Route/RouteDTOs.cs create mode 100644 Distribution/Features/Route/RouteRepository.cs create mode 100644 Distribution/Features/Route/RouteService.cs create mode 100644 Distribution/Features/Store/IStoreRepository.cs create mode 100644 Distribution/Features/Store/StoreController.cs create mode 100644 Distribution/Features/Store/StoreDTOs.cs create mode 100644 Distribution/Features/Store/StoreRepository.cs create mode 100644 Distribution/Features/Store/StoreService.cs create mode 100644 Distribution/Features/Vehicle/IVehicleRepository.cs create mode 100644 Distribution/Features/Vehicle/VehicleController.cs create mode 100644 Distribution/Features/Vehicle/VehicleDTOs.cs create mode 100644 Distribution/Features/Vehicle/VehicleRepository.cs create mode 100644 Distribution/Features/Vehicle/VehicleService.cs create mode 100644 Distribution/Features/Warehouse/IWarehouseRepository.cs create mode 100644 Distribution/Features/Warehouse/WarehouseController.cs create mode 100644 Distribution/Features/Warehouse/WarehouseDTOs.cs create mode 100644 Distribution/Features/Warehouse/WarehouseRepository.cs create mode 100644 Distribution/Features/Warehouse/WarehouseService.cs create mode 100644 Distribution/Infra/UoW/EFUnitOfWork.cs create mode 100644 Distribution/Infra/UoW/IUnitOfWork.cs create mode 100644 Distribution/Migrations/20260812090735_init.Designer.cs create mode 100644 Distribution/Migrations/20260812090735_init.cs create mode 100644 Distribution/Migrations/AppDBContextModelSnapshot.cs create mode 100644 Distribution/Models/DeliveryRun.cs create mode 100644 Distribution/Models/Item.cs create mode 100644 Distribution/Models/Order.cs create mode 100644 Distribution/Models/OrderItem.cs create mode 100644 Distribution/Models/Region.cs create mode 100644 Distribution/Models/Route.cs create mode 100644 Distribution/Models/SaleRoute.cs create mode 100644 Distribution/Models/Store.cs create mode 100644 Distribution/Models/Template.cs create mode 100644 Distribution/Models/Vehicle.cs create mode 100644 Distribution/Models/Warehouse.cs create mode 100644 Distribution/PLAN.md create mode 100644 Distribution/Program.cs create mode 100644 Distribution/Properties/launchSettings.json create mode 100644 Distribution/appsettings.json diff --git a/Distribution/.gitignore b/Distribution/.gitignore new file mode 100644 index 0000000..c910b44 --- /dev/null +++ b/Distribution/.gitignore @@ -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 diff --git a/Distribution/AGENTS.md b/Distribution/AGENTS.md new file mode 100644 index 0000000..bb69b7f --- /dev/null +++ b/Distribution/AGENTS.md @@ -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, EFRepository, 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. diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs new file mode 100644 index 0000000..7fa7204 --- /dev/null +++ b/Distribution/Data/AppDBContext.cs @@ -0,0 +1,24 @@ +using Distribution.Models; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Data; + +public class AppDBContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Regions { get; set; } + public DbSet Routes { get; set; } + public DbSet Vehicles { get; set; } + public DbSet Stores { get; set; } + public DbSet Warehouses { get; set; } + public DbSet SaleRoutes { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderItems { get; set; } + public DbSet DeliveryRuns { get; set; } + public DbSet Items { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(oi => new { oi.FK_OrderId, oi.FK_ItemId }); + } +} diff --git a/Distribution/Distribution.csproj b/Distribution/Distribution.csproj new file mode 100644 index 0000000..dcc0bb9 --- /dev/null +++ b/Distribution/Distribution.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/Distribution/Distribution.http b/Distribution/Distribution.http new file mode 100644 index 0000000..cfa533d --- /dev/null +++ b/Distribution/Distribution.http @@ -0,0 +1,6 @@ +@Distribution_HostAddress = http://localhost:5089 + +GET {{Distribution_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Distribution/Distribution.slnx b/Distribution/Distribution.slnx new file mode 100644 index 0000000..ad6fe35 --- /dev/null +++ b/Distribution/Distribution.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Distribution/Features/Order/IOrderRepository.cs b/Distribution/Features/Order/IOrderRepository.cs new file mode 100644 index 0000000..6bdcde3 --- /dev/null +++ b/Distribution/Features/Order/IOrderRepository.cs @@ -0,0 +1,13 @@ +namespace Distribution.Features.Order; + +public interface IOrderRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Order entity); + void Update(Models.Order entity); + void Remove(Models.Order entity); + Task> GetItemsByOrderIdAsync(Guid orderId); + Task> GetByStoreIdAsync(Guid storeId); + Task AddItemsAsync(IEnumerable items); +} diff --git a/Distribution/Features/Order/OrderController.cs b/Distribution/Features/Order/OrderController.cs new file mode 100644 index 0000000..4bfe01c --- /dev/null +++ b/Distribution/Features/Order/OrderController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("store/{storeId:guid}")] + public async Task GetByStore(Guid storeId) => + Ok(await service.GetByStoreIdAsync(storeId)); + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Order/OrderDTOs.cs b/Distribution/Features/Order/OrderDTOs.cs new file mode 100644 index 0000000..dfb7b9a --- /dev/null +++ b/Distribution/Features/Order/OrderDTOs.cs @@ -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 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 Items, + bool IsActive, + DateTime CreatedAt); diff --git a/Distribution/Features/Order/OrderRepository.cs b/Distribution/Features/Order/OrderRepository.cs new file mode 100644 index 0000000..eba8ed6 --- /dev/null +++ b/Distribution/Features/Order/OrderRepository.cs @@ -0,0 +1,34 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Order; + +public class OrderRepository(AppDBContext context) : IOrderRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Orders.FindAsync(id); + + public async Task> 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> GetItemsByOrderIdAsync(Guid orderId) => + await context.OrderItems.Where(oi => oi.FK_OrderId == orderId).ToListAsync(); + + public async Task> GetByStoreIdAsync(Guid storeId) => + await context.Orders.Where(o => o.FK_StoreId == storeId && o.IsActive).ToListAsync(); + + public async Task AddItemsAsync(IEnumerable items) => + await context.OrderItems.AddRangeAsync(items); +} diff --git a/Distribution/Features/Order/OrderService.cs b/Distribution/Features/Order/OrderService.cs new file mode 100644 index 0000000..993c2ce --- /dev/null +++ b/Distribution/Features/Order/OrderService.cs @@ -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 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> GetAllAsync() + { + var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList(); + var result = new List(); + foreach (var order in orders) + { + var items = await repo.GetItemsByOrderIdAsync(order.Id); + result.Add(ToResponse(order, items)); + } + return result; + } + + public async Task 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> GetByStoreIdAsync(Guid storeId) + { + var orders = await repo.GetByStoreIdAsync(storeId); + var result = new List(); + foreach (var order in orders) + { + var items = await repo.GetItemsByOrderIdAsync(order.Id); + result.Add(ToResponse(order, items)); + } + return result; + } + + public async Task 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 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 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; + } +} diff --git a/Distribution/Features/Region/IRegionRepository.cs b/Distribution/Features/Region/IRegionRepository.cs new file mode 100644 index 0000000..8eccc6f --- /dev/null +++ b/Distribution/Features/Region/IRegionRepository.cs @@ -0,0 +1,10 @@ +namespace Distribution.Features.Region; + +public interface IRegionRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Region entity); + void Update(Models.Region entity); + void Remove(Models.Region entity); +} diff --git a/Distribution/Features/Region/RegionController.cs b/Distribution/Features/Region/RegionController.cs new file mode 100644 index 0000000..42ab2bc --- /dev/null +++ b/Distribution/Features/Region/RegionController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Region/RegionDTOs.cs b/Distribution/Features/Region/RegionDTOs.cs new file mode 100644 index 0000000..828e5a3 --- /dev/null +++ b/Distribution/Features/Region/RegionDTOs.cs @@ -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); diff --git a/Distribution/Features/Region/RegionRepository.cs b/Distribution/Features/Region/RegionRepository.cs new file mode 100644 index 0000000..f6f6efb --- /dev/null +++ b/Distribution/Features/Region/RegionRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Region; + +public class RegionRepository(AppDBContext context) : IRegionRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Regions.FindAsync(id); + + public async Task> 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); + } +} diff --git a/Distribution/Features/Region/RegionService.cs b/Distribution/Features/Region/RegionService.cs new file mode 100644 index 0000000..532b763 --- /dev/null +++ b/Distribution/Features/Region/RegionService.cs @@ -0,0 +1,73 @@ +using Distribution.Infra.UoW; + +namespace Distribution.Features.Region; + +public class RegionService(IRegionRepository repo, IUnitOfWork uow) +{ + public async Task> GetAllAsync() => + (await repo.GetAllAsync()) + .Where(r => r.IsActive) + .Select(r => new RegionResponse(r.Id, r.Name, r.IsActive, r.CreatedAt)) + .ToList(); + + public async Task 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 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 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 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; + } +} diff --git a/Distribution/Features/Route/IRouteRepository.cs b/Distribution/Features/Route/IRouteRepository.cs new file mode 100644 index 0000000..abf0bb3 --- /dev/null +++ b/Distribution/Features/Route/IRouteRepository.cs @@ -0,0 +1,10 @@ +namespace Distribution.Features.Route; + +public interface IRouteRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Route entity); + void Update(Models.Route entity); + void Remove(Models.Route entity); +} diff --git a/Distribution/Features/Route/RouteController.cs b/Distribution/Features/Route/RouteController.cs new file mode 100644 index 0000000..afa13a6 --- /dev/null +++ b/Distribution/Features/Route/RouteController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Route/RouteDTOs.cs b/Distribution/Features/Route/RouteDTOs.cs new file mode 100644 index 0000000..15254a6 --- /dev/null +++ b/Distribution/Features/Route/RouteDTOs.cs @@ -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); diff --git a/Distribution/Features/Route/RouteRepository.cs b/Distribution/Features/Route/RouteRepository.cs new file mode 100644 index 0000000..9b99afc --- /dev/null +++ b/Distribution/Features/Route/RouteRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Route; + +public class RouteRepository(AppDBContext context) : IRouteRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Routes.FindAsync(id); + + public async Task> 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); + } +} diff --git a/Distribution/Features/Route/RouteService.cs b/Distribution/Features/Route/RouteService.cs new file mode 100644 index 0000000..708b985 --- /dev/null +++ b/Distribution/Features/Route/RouteService.cs @@ -0,0 +1,82 @@ +using Distribution.Infra.UoW; + +namespace Distribution.Features.Route; + +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)) + .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); + } + + public async Task 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 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 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; + } +} diff --git a/Distribution/Features/Store/IStoreRepository.cs b/Distribution/Features/Store/IStoreRepository.cs new file mode 100644 index 0000000..a9667d9 --- /dev/null +++ b/Distribution/Features/Store/IStoreRepository.cs @@ -0,0 +1,10 @@ +namespace Distribution.Features.Store; + +public interface IStoreRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Store entity); + void Update(Models.Store entity); + void Remove(Models.Store entity); +} diff --git a/Distribution/Features/Store/StoreController.cs b/Distribution/Features/Store/StoreController.cs new file mode 100644 index 0000000..c628405 --- /dev/null +++ b/Distribution/Features/Store/StoreController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Store/StoreDTOs.cs b/Distribution/Features/Store/StoreDTOs.cs new file mode 100644 index 0000000..6f78bae --- /dev/null +++ b/Distribution/Features/Store/StoreDTOs.cs @@ -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); diff --git a/Distribution/Features/Store/StoreRepository.cs b/Distribution/Features/Store/StoreRepository.cs new file mode 100644 index 0000000..9db5742 --- /dev/null +++ b/Distribution/Features/Store/StoreRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Store; + +public class StoreRepository(AppDBContext context) : IStoreRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Stores.FindAsync(id); + + public async Task> 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); + } +} diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs new file mode 100644 index 0000000..c4c4d4b --- /dev/null +++ b/Distribution/Features/Store/StoreService.cs @@ -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> GetAllAsync() => + (await repo.GetAllAsync()) + .Where(s => s.IsActive) + .Select(ToResponse) + .ToList(); + + public async Task GetByIdAsync(Guid id) + { + var store = await repo.GetByIdAsync(id); + if (store is null || !store.IsActive) return null; + return ToResponse(store); + } + + public async Task 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 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 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; + } +} diff --git a/Distribution/Features/Vehicle/IVehicleRepository.cs b/Distribution/Features/Vehicle/IVehicleRepository.cs new file mode 100644 index 0000000..35dd12e --- /dev/null +++ b/Distribution/Features/Vehicle/IVehicleRepository.cs @@ -0,0 +1,10 @@ +namespace Distribution.Features.Vehicle; + +public interface IVehicleRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Vehicle entity); + void Update(Models.Vehicle entity); + void Remove(Models.Vehicle entity); +} diff --git a/Distribution/Features/Vehicle/VehicleController.cs b/Distribution/Features/Vehicle/VehicleController.cs new file mode 100644 index 0000000..2d80ecd --- /dev/null +++ b/Distribution/Features/Vehicle/VehicleController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Vehicle/VehicleDTOs.cs b/Distribution/Features/Vehicle/VehicleDTOs.cs new file mode 100644 index 0000000..c7c0be5 --- /dev/null +++ b/Distribution/Features/Vehicle/VehicleDTOs.cs @@ -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); diff --git a/Distribution/Features/Vehicle/VehicleRepository.cs b/Distribution/Features/Vehicle/VehicleRepository.cs new file mode 100644 index 0000000..d0806f6 --- /dev/null +++ b/Distribution/Features/Vehicle/VehicleRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Vehicle; + +public class VehicleRepository(AppDBContext context) : IVehicleRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Vehicles.FindAsync(id); + + public async Task> 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); + } +} diff --git a/Distribution/Features/Vehicle/VehicleService.cs b/Distribution/Features/Vehicle/VehicleService.cs new file mode 100644 index 0000000..75033e6 --- /dev/null +++ b/Distribution/Features/Vehicle/VehicleService.cs @@ -0,0 +1,80 @@ +using Distribution.Infra.UoW; + +namespace Distribution.Features.Vehicle; + +public class VehicleService(IVehicleRepository repo, IUnitOfWork uow) +{ + public async Task> 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 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 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 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 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; + } +} diff --git a/Distribution/Features/Warehouse/IWarehouseRepository.cs b/Distribution/Features/Warehouse/IWarehouseRepository.cs new file mode 100644 index 0000000..a91d28e --- /dev/null +++ b/Distribution/Features/Warehouse/IWarehouseRepository.cs @@ -0,0 +1,10 @@ +namespace Distribution.Features.Warehouse; + +public interface IWarehouseRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(Models.Warehouse entity); + void Update(Models.Warehouse entity); + void Remove(Models.Warehouse entity); +} diff --git a/Distribution/Features/Warehouse/WarehouseController.cs b/Distribution/Features/Warehouse/WarehouseController.cs new file mode 100644 index 0000000..44d8cdd --- /dev/null +++ b/Distribution/Features/Warehouse/WarehouseController.cs @@ -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 GetAll() => + Ok(await service.GetAllAsync()); + + [HttpGet("{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost] + public async Task 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 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 Delete(Guid id, CancellationToken ct) + { + var deleted = await service.DeleteAsync(id, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/Distribution/Features/Warehouse/WarehouseDTOs.cs b/Distribution/Features/Warehouse/WarehouseDTOs.cs new file mode 100644 index 0000000..3d2e558 --- /dev/null +++ b/Distribution/Features/Warehouse/WarehouseDTOs.cs @@ -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); diff --git a/Distribution/Features/Warehouse/WarehouseRepository.cs b/Distribution/Features/Warehouse/WarehouseRepository.cs new file mode 100644 index 0000000..a6411aa --- /dev/null +++ b/Distribution/Features/Warehouse/WarehouseRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Warehouse; + +public class WarehouseRepository(AppDBContext context) : IWarehouseRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Warehouses.FindAsync(id); + + public async Task> 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); + } +} diff --git a/Distribution/Features/Warehouse/WarehouseService.cs b/Distribution/Features/Warehouse/WarehouseService.cs new file mode 100644 index 0000000..f4333c6 --- /dev/null +++ b/Distribution/Features/Warehouse/WarehouseService.cs @@ -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> GetAllAsync() => + (await repo.GetAllAsync()) + .Where(w => w.IsActive) + .Select(ToResponse) + .ToList(); + + public async Task GetByIdAsync(Guid id) + { + var warehouse = await repo.GetByIdAsync(id); + if (warehouse is null || !warehouse.IsActive) return null; + return ToResponse(warehouse); + } + + public async Task 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 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 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; + } +} diff --git a/Distribution/Infra/UoW/EFUnitOfWork.cs b/Distribution/Infra/UoW/EFUnitOfWork.cs new file mode 100644 index 0000000..ddf24a2 --- /dev/null +++ b/Distribution/Infra/UoW/EFUnitOfWork.cs @@ -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; + } + } + + } +} diff --git a/Distribution/Infra/UoW/IUnitOfWork.cs b/Distribution/Infra/UoW/IUnitOfWork.cs new file mode 100644 index 0000000..eea8693 --- /dev/null +++ b/Distribution/Infra/UoW/IUnitOfWork.cs @@ -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); + } +} diff --git a/Distribution/Migrations/20260812090735_init.Designer.cs b/Distribution/Migrations/20260812090735_init.Designer.cs new file mode 100644 index 0000000..85decef --- /dev/null +++ b/Distribution/Migrations/20260812090735_init.Designer.cs @@ -0,0 +1,353 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_VehicleId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Meterend") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Distribution.Models.OrderItem", b => + { + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("FK_OrderId", "FK_ItemId"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Length") + .HasColumnType("decimal(18,2)"); + + b.Property("RouteName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Distribution.Models.SaleRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("SaleRoutes"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Latitude") + .HasColumnType("decimal(18,2)"); + + b.Property("Longitude") + .HasColumnType("decimal(18,2)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("NumberPlate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Location") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VehicleId") + .HasColumnType("char(36)"); + + b.Property("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 + } + } +} diff --git a/Distribution/Migrations/20260812090735_init.cs b/Distribution/Migrations/20260812090735_init.cs new file mode 100644 index 0000000..8cf807d --- /dev/null +++ b/Distribution/Migrations/20260812090735_init.cs @@ -0,0 +1,278 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySQL:Charset", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "Items", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + Name = table.Column(type: "longtext", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + RouteName = table.Column(type: "longtext", nullable: false), + StartPoint = table.Column(type: "longtext", nullable: false), + EndPoint = table.Column(type: "longtext", nullable: false), + Length = table.Column(type: "decimal(18,2)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + OwnerName = table.Column(type: "longtext", nullable: false), + StoreName = table.Column(type: "longtext", nullable: false), + StoreAddress = table.Column(type: "longtext", nullable: false), + Latitude = table.Column(type: "decimal(18,2)", nullable: false), + Longitude = table.Column(type: "decimal(18,2)", nullable: false), + OneTimeAmount = table.Column(type: "decimal(18,2)", nullable: false), + CreditAmount = table.Column(type: "decimal(18,2)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + RegNumber = table.Column(type: "longtext", nullable: false), + NumberPlate = table.Column(type: "longtext", nullable: false), + Type = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + OrderDate = table.Column(type: "datetime(6)", nullable: false), + FK_StoreId = table.Column(type: "char(36)", nullable: false), + TotalAmount = table.Column(type: "decimal(18,2)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + FK_VehicleId = table.Column(type: "char(36)", nullable: false), + Meterstart = table.Column(type: "int", nullable: false), + Meterend = table.Column(type: "int", nullable: false), + FK_RouteId = table.Column(type: "char(36)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + Name = table.Column(type: "longtext", nullable: false), + Location = table.Column(type: "longtext", nullable: false), + type = table.Column(type: "int", nullable: false), + VehicleId = table.Column(type: "char(36)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(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(type: "char(36)", nullable: false), + FK_ItemId = table.Column(type: "char(36)", nullable: false), + Quantity = table.Column(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"); + } + + /// + 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"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs new file mode 100644 index 0000000..d1e6ccc --- /dev/null +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -0,0 +1,350 @@ +// +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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_VehicleId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Meterend") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("TotalAmount") + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Distribution.Models.OrderItem", b => + { + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("FK_OrderId", "FK_ItemId"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Regions"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Length") + .HasColumnType("decimal(18,2)"); + + b.Property("RouteName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Distribution.Models.SaleRoute", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("SaleRoutes"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Latitude") + .HasColumnType("decimal(18,2)"); + + b.Property("Longitude") + .HasColumnType("decimal(18,2)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("NumberPlate") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Vehicles"); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Location") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VehicleId") + .HasColumnType("char(36)"); + + b.Property("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 + } + } +} diff --git a/Distribution/Models/DeliveryRun.cs b/Distribution/Models/DeliveryRun.cs new file mode 100644 index 0000000..68e7bb0 --- /dev/null +++ b/Distribution/Models/DeliveryRun.cs @@ -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; } + } +} diff --git a/Distribution/Models/Item.cs b/Distribution/Models/Item.cs new file mode 100644 index 0000000..7cf29dc --- /dev/null +++ b/Distribution/Models/Item.cs @@ -0,0 +1,7 @@ +namespace Distribution.Models +{ + public class Item : Template + { + + } +} diff --git a/Distribution/Models/Order.cs b/Distribution/Models/Order.cs new file mode 100644 index 0000000..f71dbe1 --- /dev/null +++ b/Distribution/Models/Order.cs @@ -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; } + } +} diff --git a/Distribution/Models/OrderItem.cs b/Distribution/Models/OrderItem.cs new file mode 100644 index 0000000..ee95c81 --- /dev/null +++ b/Distribution/Models/OrderItem.cs @@ -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; } + } +} diff --git a/Distribution/Models/Region.cs b/Distribution/Models/Region.cs new file mode 100644 index 0000000..877ee14 --- /dev/null +++ b/Distribution/Models/Region.cs @@ -0,0 +1,7 @@ +namespace Distribution.Models +{ + public class Region : Template + { + public string Name { get; set; } + } +} diff --git a/Distribution/Models/Route.cs b/Distribution/Models/Route.cs new file mode 100644 index 0000000..c3edbef --- /dev/null +++ b/Distribution/Models/Route.cs @@ -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; + } +} diff --git a/Distribution/Models/SaleRoute.cs b/Distribution/Models/SaleRoute.cs new file mode 100644 index 0000000..a7e4e51 --- /dev/null +++ b/Distribution/Models/SaleRoute.cs @@ -0,0 +1,6 @@ +namespace Distribution.Models +{ + public class SaleRoute : Template + { + } +} diff --git a/Distribution/Models/Store.cs b/Distribution/Models/Store.cs new file mode 100644 index 0000000..9c6cf6e --- /dev/null +++ b/Distribution/Models/Store.cs @@ -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; + } +} diff --git a/Distribution/Models/Template.cs b/Distribution/Models/Template.cs new file mode 100644 index 0000000..b7641ec --- /dev/null +++ b/Distribution/Models/Template.cs @@ -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; + } +} diff --git a/Distribution/Models/Vehicle.cs b/Distribution/Models/Vehicle.cs new file mode 100644 index 0000000..255c1e7 --- /dev/null +++ b/Distribution/Models/Vehicle.cs @@ -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 + } +} + + diff --git a/Distribution/Models/Warehouse.cs b/Distribution/Models/Warehouse.cs new file mode 100644 index 0000000..b029264 --- /dev/null +++ b/Distribution/Models/Warehouse.cs @@ -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 + } +} + + diff --git a/Distribution/PLAN.md b/Distribution/PLAN.md new file mode 100644 index 0000000..8383a01 --- /dev/null +++ b/Distribution/PLAN.md @@ -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 Regions { get; set; } +public DbSet Routes { get; set; } +public DbSet Vehicles { get; set; } +public DbSet Stores { get; set; } +public DbSet Warehouses { get; set; } +public DbSet SaleRoutes { get; set; } +public DbSet Orders { get; set; } +public DbSet OrderItems { get; set; } +public DbSet DeliveryRuns { get; set; } +public DbSet Items { get; set; } + +// OnModelCreating +modelBuilder.Entity() + .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`** +``` +GetByIdAsync(Guid id) +GetAllAsync() +AddAsync(T entity) +Update(T entity) +Remove(T entity) +``` + +**`GenericRepo`** — 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`. Add feature-specific queries (e.g., `GetByStoreId`). + +#### Repository implementation (`{Feature}Repository.cs`) +Inherits `GenericRepo`. 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(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Feature services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +--- + +## 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 | diff --git a/Distribution/Program.cs b/Distribution/Program.cs new file mode 100644 index 0000000..9348904 --- /dev/null +++ b/Distribution/Program.cs @@ -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(options => + options.UseMySQL(connectionString)); + +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/Distribution/Properties/launchSettings.json b/Distribution/Properties/launchSettings.json new file mode 100644 index 0000000..61ce476 --- /dev/null +++ b/Distribution/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/Distribution/appsettings.json b/Distribution/appsettings.json new file mode 100644 index 0000000..d48934f --- /dev/null +++ b/Distribution/appsettings.json @@ -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": "*" +}