diff --git a/.claude/worktrees/agent-a203fc082069b1faf b/.claude/worktrees/agent-a203fc082069b1faf new file mode 160000 index 0000000..ade41b4 --- /dev/null +++ b/.claude/worktrees/agent-a203fc082069b1faf @@ -0,0 +1 @@ +Subproject commit ade41b4a60b1381b8f3027f457139f5b9879a869 diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs index 603d3ec..94dd9de 100644 --- a/Distribution/Data/AppDBContext.cs +++ b/Distribution/Data/AppDBContext.cs @@ -50,6 +50,12 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet GoodsReceiptItems { get; set; } public DbSet PurchaseReturns { get; set; } public DbSet PurchaseReturnItems { get; set; } + public DbSet SaleReturns { get; set; } + public DbSet SaleReturnItems { get; set; } + public DbSet CreditNotes { get; set; } + public DbSet Claims { get; set; } + public DbSet SupplierClaims { get; set; } + public DbSet SupplierClaimItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -270,6 +276,91 @@ public class AppDBContext(DbContextOptions options) : DbContext(op modelBuilder.Entity() .HasIndex(i => i.FK_PurchaseReturnId); + modelBuilder.Entity() + .HasOne(r => r.Order) + .WithMany() + .HasForeignKey(r => r.FK_OrderId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(r => r.CreatedByUser) + .WithMany() + .HasForeignKey(r => r.CreatedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(i => i.SaleReturn) + .WithMany(r => r.Items) + .HasForeignKey(i => i.FK_SaleReturnId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(i => i.Item) + .WithMany() + .HasForeignKey(i => i.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(i => i.FK_SaleReturnId); + + modelBuilder.Entity() + .HasOne(n => n.SaleReturn) + .WithMany() + .HasForeignKey(n => n.FK_SaleReturnId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.AgencyUser) + .WithMany() + .HasForeignKey(c => c.FK_AgencyUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.SettledByUser) + .WithMany() + .HasForeignKey(c => c.SettledByUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.PurchaseOrder) + .WithMany() + .HasForeignKey(c => c.FK_PurchaseOrderId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.GoodsReceipt) + .WithMany() + .HasForeignKey(c => c.FK_GoodsReceiptId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + modelBuilder.Entity() + .HasOne(c => c.RaisedByUser) + .WithMany() + .HasForeignKey(c => c.RaisedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(c => c.ResolvedByUser) + .WithMany() + .HasForeignKey(c => c.ResolvedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(i => i.SupplierClaim) + .WithMany(c => c.Items) + .HasForeignKey(i => i.FK_SupplierClaimId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(i => i.Item) + .WithMany() + .HasForeignKey(i => i.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(i => i.FK_SupplierClaimId); + modelBuilder.Entity() .HasOne(t => t.Ride) .WithMany() diff --git a/Distribution/Features/Claims/ClaimController.cs b/Distribution/Features/Claims/ClaimController.cs new file mode 100644 index 0000000..8aa16c3 --- /dev/null +++ b/Distribution/Features/Claims/ClaimController.cs @@ -0,0 +1,42 @@ +using Distribution.Infra.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.Claims; + +[ApiController] +[Authorize] +public class ClaimController(ClaimService service, ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet("api/claims/{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPost("api/claims")] + public async Task Create(CreateClaimRequest request, CancellationToken ct) + { + try + { + var result = await service.CreateAsync(request, ct); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } + + [HttpPost("api/claims/{id:guid}/settle")] + [Authorize(Policy = "claims.settle")] + public async Task Settle(Guid id, SettleClaimRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.SettleAsync(id, request, userId, ct); + return Ok(result); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } +} diff --git a/Distribution/Features/Claims/ClaimDTOs.cs b/Distribution/Features/Claims/ClaimDTOs.cs new file mode 100644 index 0000000..a459855 --- /dev/null +++ b/Distribution/Features/Claims/ClaimDTOs.cs @@ -0,0 +1,17 @@ +using Distribution.Models; + +namespace Distribution.Features.Claims; + +public record CreateClaimRequest(Guid AgencyUserId, string Details); + +public record SettleClaimRequest(decimal SettlementAmount); + +public record ClaimResponse( + Guid Id, + Guid AgencyUserId, + string Details, + ClaimStatus Status, + decimal? SettlementAmount, + DateTime? SettledAt, + bool IsActive, + DateTime CreatedAt); diff --git a/Distribution/Features/Claims/ClaimRepository.cs b/Distribution/Features/Claims/ClaimRepository.cs new file mode 100644 index 0000000..68f6d07 --- /dev/null +++ b/Distribution/Features/Claims/ClaimRepository.cs @@ -0,0 +1,22 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Claims; + +public class ClaimRepository(AppDBContext context) : IClaimRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.Claims.FirstOrDefaultAsync(c => c.Id == id); + + public async Task> GetByAgencyUserIdAsync(Guid agencyUserId) => + await context.Claims + .Where(c => c.FK_AgencyUserId == agencyUserId && c.IsActive) + .OrderByDescending(c => c.CreatedAt) + .ToListAsync(); + + public async Task AddAsync(Models.Claim entity) => + await context.Claims.AddAsync(entity); + + public void Update(Models.Claim entity) => + context.Claims.Update(entity); +} diff --git a/Distribution/Features/Claims/ClaimService.cs b/Distribution/Features/Claims/ClaimService.cs new file mode 100644 index 0000000..d882bfb --- /dev/null +++ b/Distribution/Features/Claims/ClaimService.cs @@ -0,0 +1,68 @@ +using Distribution.Infra.UoW; +using Distribution.Models; + +namespace Distribution.Features.Claims; + +public class ClaimService(IClaimRepository repo, IUnitOfWork uow) +{ + private static ClaimResponse Map(Claim c) => + new(c.Id, c.FK_AgencyUserId, c.Details, c.Status, c.SettlementAmount, c.SettledAt, c.IsActive, c.CreatedAt); + + public async Task GetByIdAsync(Guid id) + { + var c = await repo.GetByIdAsync(id); + if (c is null || !c.IsActive) return null; + return Map(c); + } + + public async Task> GetByAgencyUserIdAsync(Guid agencyUserId) => + (await repo.GetByAgencyUserIdAsync(agencyUserId)).Select(Map).ToList(); + + public async Task CreateAsync(CreateClaimRequest request, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(request.Details)) + throw new InvalidOperationException("Claim details are required."); + + var claim = new Claim + { + FK_AgencyUserId = request.AgencyUserId, + Details = request.Details + }; + + await uow.BeginAsync(ct); + try + { + await repo.AddAsync(claim); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + return Map(claim); + } + + public async Task SettleAsync(Guid id, SettleClaimRequest request, Guid approverId, CancellationToken ct = default) + { + var claim = await repo.GetByIdAsync(id); + if (claim is null || !claim.IsActive) + throw new KeyNotFoundException("Claim not found."); + if (claim.Status == ClaimStatus.Settled) + throw new InvalidOperationException("Claim is already settled."); + if (request.SettlementAmount < 0) + throw new InvalidOperationException("Settlement amount cannot be negative."); + + claim.Status = ClaimStatus.Settled; + claim.SettlementAmount = request.SettlementAmount; + claim.SettledByUserId = approverId; + claim.SettledAt = DateTime.Now; + + await uow.BeginAsync(ct); + try + { + repo.Update(claim); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + return Map(claim); + } +} diff --git a/Distribution/Features/Claims/IClaimRepository.cs b/Distribution/Features/Claims/IClaimRepository.cs new file mode 100644 index 0000000..839ee78 --- /dev/null +++ b/Distribution/Features/Claims/IClaimRepository.cs @@ -0,0 +1,9 @@ +namespace Distribution.Features.Claims; + +public interface IClaimRepository +{ + Task GetByIdAsync(Guid id); + Task> GetByAgencyUserIdAsync(Guid agencyUserId); + Task AddAsync(Models.Claim entity); + void Update(Models.Claim entity); +} diff --git a/Distribution/Features/Returns/IReturnRepository.cs b/Distribution/Features/Returns/IReturnRepository.cs new file mode 100644 index 0000000..3840cda --- /dev/null +++ b/Distribution/Features/Returns/IReturnRepository.cs @@ -0,0 +1,12 @@ +namespace Distribution.Features.Returns; + +public interface IReturnRepository +{ + Task GetByIdAsync(Guid id); + Task> GetByOrderIdAsync(Guid orderId); + Task AddAsync(Models.SaleReturn entity); + Task AddCreditNoteAsync(Models.CreditNote entity); + + /// Whether this store has ever had this item on any of its (active) orders. + Task StoreHasPriorSaleAsync(Guid storeId, Guid itemId); +} diff --git a/Distribution/Features/Returns/ReturnController.cs b/Distribution/Features/Returns/ReturnController.cs new file mode 100644 index 0000000..e5f74ef --- /dev/null +++ b/Distribution/Features/Returns/ReturnController.cs @@ -0,0 +1,33 @@ +using Distribution.Infra.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.Returns; + +[ApiController] +[Authorize] +public class ReturnController(ReturnService service, ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet("api/returns/{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("api/orders/{id:guid}/returns")] + public async Task GetByOrder(Guid id) => + Ok(await service.GetByOrderIdAsync(id)); + + [HttpPost("api/orders/{id:guid}/returns")] + public async Task Create(Guid id, CreateSaleReturnRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.CreateAsync(id, request, userId, ct); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } +} diff --git a/Distribution/Features/Returns/ReturnDTOs.cs b/Distribution/Features/Returns/ReturnDTOs.cs new file mode 100644 index 0000000..da3534d --- /dev/null +++ b/Distribution/Features/Returns/ReturnDTOs.cs @@ -0,0 +1,26 @@ +using Distribution.Models; + +namespace Distribution.Features.Returns; + +public record ReturnItemRequest(Guid ItemId, decimal Quantity, ReturnReasonCode ReasonCode); + +public record CreateSaleReturnRequest(ReturnMode Mode, List Items); + +public record SaleReturnItemResponse( + Guid Id, + Guid ItemId, + string Sku, + string ItemName, + decimal Quantity, + ReturnReasonCode ReasonCode, + decimal UnitPrice); + +public record SaleReturnResponse( + Guid Id, + Guid OrderId, + ReturnMode Mode, + decimal TotalCreditAmount, + decimal OrderTotalAmountAfter, + bool IsActive, + DateTime CreatedAt, + List Items); diff --git a/Distribution/Features/Returns/ReturnRepository.cs b/Distribution/Features/Returns/ReturnRepository.cs new file mode 100644 index 0000000..7d2cf04 --- /dev/null +++ b/Distribution/Features/Returns/ReturnRepository.cs @@ -0,0 +1,32 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Returns; + +public class ReturnRepository(AppDBContext context) : IReturnRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.SaleReturns + .Include(r => r.Order) + .Include(r => r.Items).ThenInclude(i => i.Item) + .FirstOrDefaultAsync(r => r.Id == id); + + public async Task> GetByOrderIdAsync(Guid orderId) => + await context.SaleReturns + .Include(r => r.Order) + .Include(r => r.Items).ThenInclude(i => i.Item) + .Where(r => r.FK_OrderId == orderId && r.IsActive) + .OrderByDescending(r => r.CreatedAt) + .ToListAsync(); + + public async Task AddAsync(Models.SaleReturn entity) => + await context.SaleReturns.AddAsync(entity); + + public async Task AddCreditNoteAsync(Models.CreditNote entity) => + await context.CreditNotes.AddAsync(entity); + + public async Task StoreHasPriorSaleAsync(Guid storeId, Guid itemId) => + await context.OrderItems + .Include(oi => oi.Order) + .AnyAsync(oi => oi.Order.FK_StoreId == storeId && oi.Order.IsActive && oi.FK_ItemId == itemId); +} diff --git a/Distribution/Features/Returns/ReturnService.cs b/Distribution/Features/Returns/ReturnService.cs new file mode 100644 index 0000000..cfa57b9 --- /dev/null +++ b/Distribution/Features/Returns/ReturnService.cs @@ -0,0 +1,169 @@ +using Distribution.Features.Item; +using Distribution.Features.Order; +using Distribution.Features.Ride; +using Distribution.Features.Sale; +using Distribution.Features.Stock; +using Distribution.Infra.UoW; +using Distribution.Models; + +namespace Distribution.Features.Returns; + +public class ReturnService( + IReturnRepository repo, + IOrderRepository orderRepo, + ISaleRepository saleRepo, + IRideRepository rideRepo, + IItemRepository itemRepo, + IStockRepository stockRepo, + IUnitOfWork uow) +{ + private static SaleReturnItemResponse MapItem(SaleReturnItem i) => + new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Quantity, i.ReasonCode, i.UnitPrice); + + private static SaleReturnResponse Map(SaleReturn r) => + new(r.Id, r.FK_OrderId, r.Mode, r.TotalCreditAmount, r.Order.TotalAmount, r.IsActive, r.CreatedAt, + r.Items.Where(i => i.IsActive).Select(MapItem).ToList()); + + public async Task GetByIdAsync(Guid id) + { + var r = await repo.GetByIdAsync(id); + if (r is null || !r.IsActive) return null; + return Map(r); + } + + public async Task> GetByOrderIdAsync(Guid orderId) => + (await repo.GetByOrderIdAsync(orderId)).Select(Map).ToList(); + + /// + /// Records a return recognized during the same store visit as an open order and settles it + /// immediately: stock is credited back to the rep's vehicle, and either the order total is + /// reduced (CreditDeduction) or the matching order line quantities are reduced (ItemExchange). + /// A return is only allowed for an item the store has previously ordered. + /// + public async Task CreateAsync(Guid orderId, CreateSaleReturnRequest request, Guid createdByUserId, CancellationToken ct = default) + { + var order = await orderRepo.GetByIdAsync(orderId); + if (order is null || !order.IsActive) + throw new InvalidOperationException("Order not found."); + if (order.Status is OrderStatus.Cancelled or OrderStatus.Fulfilled) + throw new InvalidOperationException("Cannot record a return against a closed order."); + + if (request.Items is null || request.Items.Count == 0) + throw new InvalidOperationException("A return must have at least one line item."); + + var sale = await saleRepo.GetByIdAsync(order.FK_SaleId) + ?? throw new InvalidOperationException("Sale not found for this order."); + if (sale.FK_RideId is null) + throw new InvalidOperationException("A return can only be recorded for an order created during an active ride."); + + var ride = await rideRepo.GetByIdAsync(sale.FK_RideId.Value) + ?? throw new InvalidOperationException("Ride not found."); + var vehicleId = ride.FK_VehicleId; + + var orderItems = await orderRepo.GetItemsByOrderIdAsync(orderId); + var orderItemsByItemId = orderItems.ToDictionary(oi => oi.FK_ItemId); + + var saleReturn = new SaleReturn + { + FK_OrderId = orderId, + Mode = request.Mode, + CreatedByUserId = createdByUserId + }; + + foreach (var line in request.Items) + { + if (line.Quantity <= 0) + throw new InvalidOperationException("Return quantity must be greater than zero."); + + var item = await itemRepo.GetByIdAsync(line.ItemId) + ?? throw new InvalidOperationException($"Item {line.ItemId} not found."); + + if (!await repo.StoreHasPriorSaleAsync(order.FK_StoreId, line.ItemId)) + throw new InvalidOperationException($"No prior sale of item '{item.Name}' found for this store."); + + var unitPrice = orderItemsByItemId.TryGetValue(line.ItemId, out var matchingOrderItem) + ? matchingOrderItem.UnitPrice + : item.SalePrice ?? 0; + + saleReturn.Items.Add(new SaleReturnItem + { + FK_ItemId = line.ItemId, + Quantity = line.Quantity, + ReasonCode = line.ReasonCode, + UnitPrice = unitPrice + }); + } + + await uow.BeginAsync(ct); + try + { + foreach (var line in saleReturn.Items) + { + var balance = await stockRepo.GetBalanceAsync(line.FK_ItemId, vehicleId); + if (balance is null) + { + await stockRepo.AddBalanceAsync(new StockBalance + { + FK_ItemId = line.FK_ItemId, + LocationId = vehicleId, + LocationType = LocationType.Vehicle, + Quantity = line.Quantity + }); + } + else + { + balance.Quantity += line.Quantity; + balance.UpdatedAt = DateTime.Now; + stockRepo.UpdateBalance(balance); + } + + await stockRepo.AddMovementAsync(new StockMovement + { + FK_ItemId = line.FK_ItemId, + ToLocationId = vehicleId, + ToLocationType = LocationType.Vehicle, + Quantity = line.Quantity, + Type = MovementType.CustomerReturn, + ReasonCode = line.ReasonCode.ToString(), + ReferenceId = saleReturn.Id, + CreatedBy = createdByUserId + }); + } + + await repo.AddAsync(saleReturn); + + if (request.Mode == ReturnMode.CreditDeduction) + { + var total = saleReturn.Items.Sum(i => i.Quantity * i.UnitPrice); + saleReturn.TotalCreditAmount = total; + order.TotalAmount = Math.Max(0, order.TotalAmount - total); + orderRepo.Update(order); + + await repo.AddCreditNoteAsync(new CreditNote + { + FK_SaleReturnId = saleReturn.Id, + Amount = total + }); + } + else // ItemExchange + { + foreach (var line in saleReturn.Items) + { + if (!orderItemsByItemId.TryGetValue(line.FK_ItemId, out var orderItem)) + continue; // no matching line on this order — nothing to offset + + var offsetQty = Math.Min(line.Quantity, orderItem.Quantity); + orderItem.Quantity -= (int)offsetQty; + order.TotalAmount = Math.Max(0, order.TotalAmount - offsetQty * orderItem.UnitPrice); + } + orderRepo.Update(order); + } + + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + var saved = await repo.GetByIdAsync(saleReturn.Id); + return Map(saved!); + } +} diff --git a/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs b/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs new file mode 100644 index 0000000..3bdcc97 --- /dev/null +++ b/Distribution/Features/SupplierClaims/ISupplierClaimRepository.cs @@ -0,0 +1,9 @@ +namespace Distribution.Features.SupplierClaims; + +public interface ISupplierClaimRepository +{ + Task GetByIdAsync(Guid id); + Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId); + Task AddAsync(Models.SupplierClaim entity); + void Update(Models.SupplierClaim entity); +} diff --git a/Distribution/Features/SupplierClaims/SupplierClaimController.cs b/Distribution/Features/SupplierClaims/SupplierClaimController.cs new file mode 100644 index 0000000..2fbbe19 --- /dev/null +++ b/Distribution/Features/SupplierClaims/SupplierClaimController.cs @@ -0,0 +1,61 @@ +using Distribution.Infra.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.SupplierClaims; + +[ApiController] +[Authorize] +public class SupplierClaimController(SupplierClaimService service, ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet("api/supplier-claims/{id:guid}")] + public async Task GetById(Guid id) + { + var result = await service.GetByIdAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpGet("api/purchase-orders/{id:guid}/claims")] + public async Task GetByPurchaseOrder(Guid id) => + Ok(await service.GetByPurchaseOrderIdAsync(id)); + + [HttpPost("api/purchase-orders/{id:guid}/claims")] + public async Task Create(Guid id, CreateSupplierClaimRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.CreateAsync(id, request, userId, ct); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } + + [HttpPost("api/supplier-claims/{id:guid}/approve")] + [Authorize(Policy = "suppliers.claims.approve")] + public async Task Approve(Guid id, ApproveSupplierClaimRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.ApproveAsync(id, request, userId, ct); + return Ok(result); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } + + [HttpPost("api/supplier-claims/{id:guid}/reject")] + [Authorize(Policy = "suppliers.claims.approve")] + public async Task Reject(Guid id, RejectSupplierClaimRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + try + { + var result = await service.RejectAsync(id, request, userId, ct); + return Ok(result); + } + catch (KeyNotFoundException) { return NotFound(); } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } +} diff --git a/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs b/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs new file mode 100644 index 0000000..0d4efd1 --- /dev/null +++ b/Distribution/Features/SupplierClaims/SupplierClaimDTOs.cs @@ -0,0 +1,41 @@ +using Distribution.Models; + +namespace Distribution.Features.SupplierClaims; + +public record SupplierClaimItemRequest(Guid ItemId, decimal Quantity, SupplierClaimIssueType IssueType); + +public record CreateSupplierClaimRequest( + Guid? GoodsReceiptId, + string Details, + string? EvidenceUrl, + List Items); + +public record ApproveSupplierClaimRequest( + SupplierClaimResolutionType ResolutionType, + decimal? ResolutionAmount, + string? ResolutionNotes); + +public record RejectSupplierClaimRequest(string? ResolutionNotes); + +public record SupplierClaimItemResponse( + Guid Id, + Guid ItemId, + string Sku, + string ItemName, + decimal Quantity, + SupplierClaimIssueType IssueType); + +public record SupplierClaimResponse( + Guid Id, + Guid PurchaseOrderId, + Guid? GoodsReceiptId, + string Details, + string? EvidenceUrl, + SupplierClaimStatus Status, + SupplierClaimResolutionType? ResolutionType, + decimal? ResolutionAmount, + string? ResolutionNotes, + DateTime? ResolvedAt, + bool IsActive, + DateTime CreatedAt, + List Items); diff --git a/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs b/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs new file mode 100644 index 0000000..6fbacce --- /dev/null +++ b/Distribution/Features/SupplierClaims/SupplierClaimRepository.cs @@ -0,0 +1,25 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.SupplierClaims; + +public class SupplierClaimRepository(AppDBContext context) : ISupplierClaimRepository +{ + public async Task GetByIdAsync(Guid id) => + await context.SupplierClaims + .Include(c => c.Items).ThenInclude(i => i.Item) + .FirstOrDefaultAsync(c => c.Id == id); + + public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => + await context.SupplierClaims + .Include(c => c.Items).ThenInclude(i => i.Item) + .Where(c => c.FK_PurchaseOrderId == purchaseOrderId && c.IsActive) + .OrderByDescending(c => c.CreatedAt) + .ToListAsync(); + + public async Task AddAsync(Models.SupplierClaim entity) => + await context.SupplierClaims.AddAsync(entity); + + public void Update(Models.SupplierClaim entity) => + context.SupplierClaims.Update(entity); +} diff --git a/Distribution/Features/SupplierClaims/SupplierClaimService.cs b/Distribution/Features/SupplierClaims/SupplierClaimService.cs new file mode 100644 index 0000000..2f927bf --- /dev/null +++ b/Distribution/Features/SupplierClaims/SupplierClaimService.cs @@ -0,0 +1,136 @@ +using Distribution.Features.Grn; +using Distribution.Features.Po; +using Distribution.Infra.UoW; +using Distribution.Models; + +namespace Distribution.Features.SupplierClaims; + +public class SupplierClaimService( + ISupplierClaimRepository repo, + IPoRepository poRepo, + IGrnRepository grnRepo, + IUnitOfWork uow) +{ + private static SupplierClaimItemResponse MapItem(SupplierClaimItem i) => + new(i.Id, i.FK_ItemId, i.Item.Sku, i.Item.Name, i.Quantity, i.IssueType); + + private static SupplierClaimResponse Map(SupplierClaim c) => + new(c.Id, c.FK_PurchaseOrderId, c.FK_GoodsReceiptId, c.Details, c.EvidenceUrl, c.Status, + c.ResolutionType, c.ResolutionAmount, c.ResolutionNotes, c.ResolvedAt, c.IsActive, c.CreatedAt, + c.Items.Where(i => i.IsActive).Select(MapItem).ToList()); + + public async Task GetByIdAsync(Guid id) + { + var c = await repo.GetByIdAsync(id); + if (c is null || !c.IsActive) return null; + return Map(c); + } + + public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => + (await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList(); + + public async Task CreateAsync(Guid purchaseOrderId, CreateSupplierClaimRequest request, Guid raisedByUserId, CancellationToken ct = default) + { + var po = await poRepo.GetByIdAsync(purchaseOrderId); + if (po is null || !po.IsActive) + throw new InvalidOperationException("Purchase order not found."); + + if (string.IsNullOrWhiteSpace(request.Details)) + throw new InvalidOperationException("Claim details are required."); + + if (request.Items is null || request.Items.Count == 0) + throw new InvalidOperationException("A supplier claim must have at least one line item."); + + if (request.GoodsReceiptId is not null) + { + var grn = await grnRepo.GetByIdAsync(request.GoodsReceiptId.Value) + ?? throw new InvalidOperationException("Goods receipt not found."); + if (grn.FK_PurchaseOrderId != purchaseOrderId) + throw new InvalidOperationException("Goods receipt does not belong to this purchase order."); + } + + var claim = new SupplierClaim + { + FK_PurchaseOrderId = purchaseOrderId, + FK_GoodsReceiptId = request.GoodsReceiptId, + Details = request.Details, + EvidenceUrl = request.EvidenceUrl, + RaisedByUserId = raisedByUserId + }; + + foreach (var line in request.Items) + { + if (line.Quantity <= 0) + throw new InvalidOperationException("Claim quantity must be greater than zero."); + + claim.Items.Add(new SupplierClaimItem + { + FK_ItemId = line.ItemId, + Quantity = line.Quantity, + IssueType = line.IssueType + }); + } + + await uow.BeginAsync(ct); + try + { + await repo.AddAsync(claim); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + var saved = await repo.GetByIdAsync(claim.Id); + return Map(saved!); + } + + public async Task ApproveAsync(Guid id, ApproveSupplierClaimRequest request, Guid approverId, CancellationToken ct = default) + { + var claim = await repo.GetByIdAsync(id) + ?? throw new KeyNotFoundException("Supplier claim not found."); + if (claim.Status != SupplierClaimStatus.Open) + throw new InvalidOperationException("Only an open claim can be approved."); + + if (request.ResolutionType != SupplierClaimResolutionType.Replacement && request.ResolutionAmount is null) + throw new InvalidOperationException("ResolutionAmount is required for Credit, Refund, and PriceAdjustment."); + + claim.Status = SupplierClaimStatus.Approved; + claim.ResolutionType = request.ResolutionType; + claim.ResolutionAmount = request.ResolutionType == SupplierClaimResolutionType.Replacement ? null : request.ResolutionAmount; + claim.ResolutionNotes = request.ResolutionNotes; + claim.ResolvedByUserId = approverId; + claim.ResolvedAt = DateTime.Now; + + await uow.BeginAsync(ct); + try + { + repo.Update(claim); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + return Map(claim); + } + + public async Task RejectAsync(Guid id, RejectSupplierClaimRequest request, Guid approverId, CancellationToken ct = default) + { + var claim = await repo.GetByIdAsync(id) + ?? throw new KeyNotFoundException("Supplier claim not found."); + if (claim.Status != SupplierClaimStatus.Open) + throw new InvalidOperationException("Only an open claim can be rejected."); + + claim.Status = SupplierClaimStatus.Rejected; + claim.ResolutionNotes = request.ResolutionNotes; + claim.ResolvedByUserId = approverId; + claim.ResolvedAt = DateTime.Now; + + await uow.BeginAsync(ct); + try + { + repo.Update(claim); + await uow.CommitAsync(ct); + } + catch { await uow.RollbackAsync(ct); throw; } + + return Map(claim); + } +} diff --git a/Distribution/Migrations/20260825073453_AddReturnsAndClaims.Designer.cs b/Distribution/Migrations/20260825073453_AddReturnsAndClaims.Designer.cs new file mode 100644 index 0000000..7dcbde6 --- /dev/null +++ b/Distribution/Migrations/20260825073453_AddReturnsAndClaims.Designer.cs @@ -0,0 +1,2494 @@ +// +using System; +using Distribution.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Distribution.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20260825073453_AddReturnsAndClaims")] + partial class AddReturnsAndClaims + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Distribution.Models.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Brands"); + }); + + modelBuilder.Entity("Distribution.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomerReturn") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("SettledAt") + .HasColumnType("datetime(6)"); + + b.Property("SettledByUserId") + .HasColumnType("char(36)"); + + b.Property("SettlementAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("SettledByUserId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("CreditNotes"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_DriverProfileId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .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.Property("RideType") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_DriverProfileId"); + + b.HasIndex("FK_RouteId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.HasIndex("FK_VehicleId"); + + b.ToTable("DeliveryRuns"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_RideId"); + + b.ToTable("DeliveryTasks"); + }); + + modelBuilder.Entity("Distribution.Models.DriverComplianceConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EnforceLicenseExpiry") + .HasColumnType("tinyint(1)"); + + b.Property("EnforceMedicalClearance") + .HasColumnType("tinyint(1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("DriverComplianceConfigs"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LicenseExpiry") + .HasColumnType("datetime(6)"); + + b.Property("LicenseNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MedicalClearanceExpiry") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_UserId") + .IsUnique(); + + b.ToTable("DriverProfiles"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ReceivedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("GoodsReceipts"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcceptedQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RejectedQuantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderItemId"); + + b.ToTable("GoodsReceiptItems"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CostPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalErpItemId") + .HasColumnType("int"); + + b.Property("FK_BaseUomId") + .HasColumnType("char(36)"); + + b.Property("FK_BrandId") + .HasColumnType("char(36)"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("FK_SubCategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceiptConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReceiptConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.ToTable("LoadingSheets"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityReceived") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityResolved") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.ToTable("LoadingSheetItems"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfilledAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId") + .IsUnique(); + + 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("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityFulfilled") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("FK_OrderId", "FK_ItemId"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CollectedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollectedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("PriceLists"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PriceListId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MinPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PriceListId", "FK_ItemId") + .IsUnique(); + + b.ToTable("PriceListItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SupplierId"); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsCancelled") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("ReceivedQuantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseOrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseReturns"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseReturnId"); + + b.ToTable("PurchaseReturnItems"); + }); + + 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.RideCancelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RequestedAt") + .HasColumnType("datetime(6)"); + + b.Property("RequestedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TypeCode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("RideCancelRequests"); + }); + + modelBuilder.Entity("Distribution.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + + b.HasData( + new + { + Id = new Guid("10000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 10, + Name = "SalesRep" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 20, + Name = "Supervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 30, + Name = "AreaManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 40, + Name = "AreaSupervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 50, + Name = "RegionalManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 60, + Name = "RegionalAgency" + }); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FK_PermissionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_RoleId", "FK_PermissionId"); + + b.HasIndex("FK_PermissionId"); + + b.ToTable("RolePermissions"); + }); + + 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("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Length") + .HasColumnType("decimal(65,30)"); + + b.Property("RouteName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfillImmediately") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("SaleDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("TotalCreditAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_OrderId"); + + b.ToTable("SaleReturns"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("SaleReturnItems"); + }); + + 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.Scheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgentLimitExempt") + .HasColumnType("tinyint(1)"); + + b.Property("BaselineExpectedSales") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeFrom") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeTo") + .HasColumnType("datetime(6)"); + + b.Property("EligibleSegment") + .HasColumnType("longtext"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("FK_TriggerItemId") + .HasColumnType("char(36)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegionId") + .HasColumnType("char(36)"); + + b.Property("RuleConfig") + .HasColumnType("longtext"); + + b.Property("RuleType") + .HasColumnType("int"); + + b.Property("TriggerQty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_TriggerItemId"); + + b.HasIndex("RegionId"); + + b.ToTable("Schemes"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SchemeId", "FK_ItemId") + .IsUnique(); + + b.ToTable("SchemeBundleComponents"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FieldInvoiceId") + .HasColumnType("char(36)"); + + b.Property("FreeItemCostValue") + .HasColumnType("decimal(65,30)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("RedeemedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_SchemeId"); + + b.ToTable("SchemeRedemptions"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("LocationId") + .HasColumnType("char(36)"); + + b.Property("LocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "LocationId") + .IsUnique(); + + b.ToTable("StockBalances"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FromLocationId") + .HasColumnType("char(36)"); + + b.Property("FromLocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("longtext"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.Property("ToLocationId") + .HasColumnType("char(36)"); + + b.Property("ToLocationType") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("KycStatus") + .HasColumnType("int"); + + b.Property("Latitude") + .HasColumnType("decimal(65,30)"); + + b.Property("Longitude") + .HasColumnType("decimal(65,30)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("OutstandingBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Segment") + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FileReference") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("StoreKycDocuments"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_CategoryId"); + + b.ToTable("SubCategories"); + }); + + modelBuilder.Entity("Distribution.Models.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactInfo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PerformanceScore") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("Distribution.Models.Uom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Uoms"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_FromUomId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ToUomId") + .HasColumnType("char(36)"); + + b.Property("Factor") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FromUomId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ToUomId"); + + b.ToTable("UomConversions"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ReportsToUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_ReportsToUserId"); + + b.HasIndex("FK_RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RegionId"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("UserRegionScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RouteId"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("UserRouteScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_StoreId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("UserStoreScopes"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DefaultWarehouseId") + .HasColumnType("char(36)"); + + 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.HasIndex("DefaultWarehouseId"); + + 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("Type") + .HasColumnType("int"); + + b.Property("VehicleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("VehicleId"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "SettledByUser") + .WithMany() + .HasForeignKey("SettledByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AgencyUser"); + + b.Navigation("SettledByUser"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany() + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.HasOne("Distribution.Models.DriverProfile", "DriverProfile") + .WithMany() + .HasForeignKey("FK_DriverProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("FK_VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DriverProfile"); + + b.Navigation("Route"); + + b.Navigation("SourceWarehouse"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany("Items") + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrderItem", "PurchaseOrderItem") + .WithMany() + .HasForeignKey("FK_PurchaseOrderItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrderItem"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.HasOne("Distribution.Models.Uom", "BaseUom") + .WithMany() + .HasForeignKey("FK_BaseUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Brand", "Brand") + .WithMany() + .HasForeignKey("FK_BrandId"); + + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.SubCategory", "SubCategory") + .WithMany() + .HasForeignKey("FK_SubCategoryId"); + + b.Navigation("BaseUom"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + + b.Navigation("SourceWarehouse"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.LoadingSheet", "LoadingSheet") + .WithMany("Items") + .HasForeignKey("FK_LoadingSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("LoadingSheet"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + 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("OrderItems") + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.PriceList", "PriceList") + .WithMany("Items") + .HasForeignKey("FK_PriceListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PriceList"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseReturn", "PurchaseReturn") + .WithMany("Items") + .HasForeignKey("FK_PurchaseReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseReturn"); + }); + + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.HasOne("Distribution.Models.Permission", "Permission") + .WithMany() + .HasForeignKey("FK_PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Ride"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany("Items") + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "TriggerItem") + .WithMany() + .HasForeignKey("FK_TriggerItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("RegionId"); + + b.Navigation("FreeItem"); + + b.Navigation("Region"); + + b.Navigation("TriggerItem"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("BundleComponents") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FreeItem"); + + b.Navigation("Order"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.HasOne("Distribution.Models.Uom", "FromUom") + .WithMany() + .HasForeignKey("FK_FromUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany("Conversions") + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Uom", "ToUom") + .WithMany() + .HasForeignKey("FK_ToUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromUom"); + + b.Navigation("Item"); + + b.Navigation("ToUom"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.HasOne("Distribution.Models.User", "ReportsTo") + .WithMany() + .HasForeignKey("FK_ReportsToUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReportsTo"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.HasOne("Distribution.Models.Warehouse", null) + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("VehicleId"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Navigation("BundleComponents"); + + b.Navigation("Redemptions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20260825073453_AddReturnsAndClaims.cs b/Distribution/Migrations/20260825073453_AddReturnsAndClaims.cs new file mode 100644 index 0000000..acddfa3 --- /dev/null +++ b/Distribution/Migrations/20260825073453_AddReturnsAndClaims.cs @@ -0,0 +1,183 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddReturnsAndClaims : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Claims", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_AgencyUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Details = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + SettlementAmount = table.Column(type: "decimal(65,30)", nullable: true), + SettledByUserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + SettledAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Claims", x => x.Id); + table.ForeignKey( + name: "FK_Claims_Users_FK_AgencyUserId", + column: x => x.FK_AgencyUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Claims_Users_SettledByUserId", + column: x => x.SettledByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "SaleReturns", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_OrderId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Mode = table.Column(type: "int", nullable: false), + TotalCreditAmount = table.Column(type: "decimal(65,30)", nullable: false), + CreatedByUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaleReturns", x => x.Id); + table.ForeignKey( + name: "FK_SaleReturns_Orders_FK_OrderId", + column: x => x.FK_OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SaleReturns_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "CreditNotes", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_SaleReturnId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Amount = table.Column(type: "decimal(65,30)", nullable: false), + IssuedAt = table.Column(type: "datetime(6)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CreditNotes", x => x.Id); + table.ForeignKey( + name: "FK_CreditNotes_SaleReturns_FK_SaleReturnId", + column: x => x.FK_SaleReturnId, + principalTable: "SaleReturns", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "SaleReturnItems", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_SaleReturnId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Quantity = table.Column(type: "decimal(65,30)", nullable: false), + ReasonCode = table.Column(type: "int", nullable: false), + UnitPrice = table.Column(type: "decimal(65,30)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SaleReturnItems", x => x.Id); + table.ForeignKey( + name: "FK_SaleReturnItems_Items_FK_ItemId", + column: x => x.FK_ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SaleReturnItems_SaleReturns_FK_SaleReturnId", + column: x => x.FK_SaleReturnId, + principalTable: "SaleReturns", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_Claims_FK_AgencyUserId", + table: "Claims", + column: "FK_AgencyUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Claims_SettledByUserId", + table: "Claims", + column: "SettledByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_CreditNotes_FK_SaleReturnId", + table: "CreditNotes", + column: "FK_SaleReturnId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleReturnItems_FK_ItemId", + table: "SaleReturnItems", + column: "FK_ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleReturnItems_FK_SaleReturnId", + table: "SaleReturnItems", + column: "FK_SaleReturnId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleReturns_CreatedByUserId", + table: "SaleReturns", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_SaleReturns_FK_OrderId", + table: "SaleReturns", + column: "FK_OrderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Claims"); + + migrationBuilder.DropTable( + name: "CreditNotes"); + + migrationBuilder.DropTable( + name: "SaleReturnItems"); + + migrationBuilder.DropTable( + name: "SaleReturns"); + } + } +} diff --git a/Distribution/Migrations/20260825084218_AddSupplierClaims.Designer.cs b/Distribution/Migrations/20260825084218_AddSupplierClaims.Designer.cs new file mode 100644 index 0000000..1c2d475 --- /dev/null +++ b/Distribution/Migrations/20260825084218_AddSupplierClaims.Designer.cs @@ -0,0 +1,2643 @@ +// +using System; +using Distribution.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Distribution.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20260825084218_AddSupplierClaims")] + partial class AddSupplierClaims + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Distribution.Models.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Brands"); + }); + + modelBuilder.Entity("Distribution.Models.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomerReturn") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("SettledAt") + .HasColumnType("datetime(6)"); + + b.Property("SettledByUserId") + .HasColumnType("char(36)"); + + b.Property("SettlementAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("SettledByUserId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("CreditNotes"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_DriverProfileId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .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.Property("RideType") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_DriverProfileId"); + + b.HasIndex("FK_RouteId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.HasIndex("FK_VehicleId"); + + b.ToTable("DeliveryRuns"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("Sequence") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_RideId"); + + b.ToTable("DeliveryTasks"); + }); + + modelBuilder.Entity("Distribution.Models.DriverComplianceConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EnforceLicenseExpiry") + .HasColumnType("tinyint(1)"); + + b.Property("EnforceMedicalClearance") + .HasColumnType("tinyint(1)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("DriverComplianceConfigs"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LicenseExpiry") + .HasColumnType("datetime(6)"); + + b.Property("LicenseNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MedicalClearanceExpiry") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_UserId") + .IsUnique(); + + b.ToTable("DriverProfiles"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("FK_ReceivedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("GoodsReceipts"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AcceptedQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RejectedQuantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderItemId"); + + b.ToTable("GoodsReceiptItems"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CostPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalErpItemId") + .HasColumnType("int"); + + b.Property("FK_BaseUomId") + .HasColumnType("char(36)"); + + b.Property("FK_BrandId") + .HasColumnType("char(36)"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("FK_SubCategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("DispatchConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_SourceWarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceiptConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReceiptConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_SourceWarehouseId"); + + b.ToTable("LoadingSheets"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityReceived") + .HasColumnType("decimal(65,30)"); + + b.Property("QuantityResolved") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.ToTable("LoadingSheetItems"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfilledAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId") + .IsUnique(); + + 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("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityFulfilled") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("FK_OrderId", "FK_ItemId"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CollectedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollectedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("PriceLists"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PriceListId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MinPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PriceListId", "FK_ItemId") + .IsUnique(); + + b.ToTable("PriceListItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_SupplierId"); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelledQuantity") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsCancelled") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("ReceivedQuantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseOrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.ToTable("PurchaseReturns"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PurchaseReturnId"); + + b.ToTable("PurchaseReturnItems"); + }); + + 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.RideCancelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RequestedAt") + .HasColumnType("datetime(6)"); + + b.Property("RequestedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TypeCode") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("RideCancelRequests"); + }); + + modelBuilder.Entity("Distribution.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + + b.HasData( + new + { + Id = new Guid("10000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 10, + Name = "SalesRep" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 20, + Name = "Supervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 30, + Name = "AreaManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 40, + Name = "AreaSupervisor" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 50, + Name = "RegionalManager" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 60, + Name = "RegionalAgency" + }); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FK_PermissionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_RoleId", "FK_PermissionId"); + + b.HasIndex("FK_PermissionId"); + + b.ToTable("RolePermissions"); + }); + + 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("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Length") + .HasColumnType("decimal(65,30)"); + + b.Property("RouteName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartPoint") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfillImmediately") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("SaleDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_RideId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("TotalCreditAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_OrderId"); + + b.ToTable("SaleReturns"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("SaleReturnItems"); + }); + + 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.Scheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgentLimitExempt") + .HasColumnType("tinyint(1)"); + + b.Property("BaselineExpectedSales") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeFrom") + .HasColumnType("datetime(6)"); + + b.Property("DateRangeTo") + .HasColumnType("datetime(6)"); + + b.Property("EligibleSegment") + .HasColumnType("longtext"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("FK_TriggerItemId") + .HasColumnType("char(36)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RegionId") + .HasColumnType("char(36)"); + + b.Property("RuleConfig") + .HasColumnType("longtext"); + + b.Property("RuleType") + .HasColumnType("int"); + + b.Property("TriggerQty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_TriggerItemId"); + + b.HasIndex("RegionId"); + + b.ToTable("Schemes"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("Qty") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SchemeId", "FK_ItemId") + .IsUnique(); + + b.ToTable("SchemeBundleComponents"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DiscountAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_FreeItemId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FieldInvoiceId") + .HasColumnType("char(36)"); + + b.Property("FreeItemCostValue") + .HasColumnType("decimal(65,30)"); + + b.Property("FreeQty") + .HasColumnType("decimal(65,30)"); + + b.Property("RedeemedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FreeItemId"); + + b.HasIndex("FK_OrderId"); + + b.HasIndex("FK_SchemeId"); + + b.ToTable("SchemeRedemptions"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("LocationId") + .HasColumnType("char(36)"); + + b.Property("LocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "LocationId") + .IsUnique(); + + b.ToTable("StockBalances"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FromLocationId") + .HasColumnType("char(36)"); + + b.Property("FromLocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("longtext"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.Property("ToLocationId") + .HasColumnType("char(36)"); + + b.Property("ToLocationType") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.ToTable("StockMovements"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("KycStatus") + .HasColumnType("int"); + + b.Property("Latitude") + .HasColumnType("decimal(65,30)"); + + b.Property("Longitude") + .HasColumnType("decimal(65,30)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("OutstandingBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("OwnerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Segment") + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("Stores"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocumentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FileReference") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("StoreKycDocuments"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_CategoryId"); + + b.ToTable("SubCategories"); + }); + + modelBuilder.Entity("Distribution.Models.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactInfo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PerformanceScore") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EvidenceUrl") + .HasColumnType("longtext"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResolutionAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("ResolutionNotes") + .HasColumnType("longtext"); + + b.Property("ResolutionType") + .HasColumnType("int"); + + b.Property("ResolvedAt") + .HasColumnType("datetime(6)"); + + b.Property("ResolvedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("RaisedByUserId"); + + b.HasIndex("ResolvedByUserId"); + + b.ToTable("SupplierClaims"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierClaimId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssueType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SupplierClaimId"); + + b.ToTable("SupplierClaimItems"); + }); + + modelBuilder.Entity("Distribution.Models.Uom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Uoms"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_FromUomId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ToUomId") + .HasColumnType("char(36)"); + + b.Property("Factor") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_FromUomId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ToUomId"); + + b.ToTable("UomConversions"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ReportsToUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RoleId") + .HasColumnType("char(36)"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_ReportsToUserId"); + + b.HasIndex("FK_RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RegionId"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("UserRegionScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_RouteId"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("UserRouteScopes"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("FK_UserId", "FK_StoreId"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("UserStoreScopes"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DefaultWarehouseId") + .HasColumnType("char(36)"); + + 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.HasIndex("DefaultWarehouseId"); + + 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("Type") + .HasColumnType("int"); + + b.Property("VehicleId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("VehicleId"); + + b.ToTable("Warehouses"); + }); + + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "SettledByUser") + .WithMany() + .HasForeignKey("SettledByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AgencyUser"); + + b.Navigation("SettledByUser"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany() + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.HasOne("Distribution.Models.DriverProfile", "DriverProfile") + .WithMany() + .HasForeignKey("FK_DriverProfileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("FK_VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DriverProfile"); + + b.Navigation("Route"); + + b.Navigation("SourceWarehouse"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.DriverProfile", b => + { + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany("Items") + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrderItem", "PurchaseOrderItem") + .WithMany() + .HasForeignKey("FK_PurchaseOrderItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrderItem"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.HasOne("Distribution.Models.Uom", "BaseUom") + .WithMany() + .HasForeignKey("FK_BaseUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Brand", "Brand") + .WithMany() + .HasForeignKey("FK_BrandId"); + + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.SubCategory", "SubCategory") + .WithMany() + .HasForeignKey("FK_SubCategoryId"); + + b.Navigation("BaseUom"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + + b.Navigation("SubCategory"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Warehouse", "SourceWarehouse") + .WithMany() + .HasForeignKey("FK_SourceWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + + b.Navigation("SourceWarehouse"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.LoadingSheet", "LoadingSheet") + .WithMany("Items") + .HasForeignKey("FK_LoadingSheetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("LoadingSheet"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + 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("OrderItems") + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.Sale", "Sale") + .WithMany() + .HasForeignKey("FK_SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sale"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.PriceList", "PriceList") + .WithMany("Items") + .HasForeignKey("FK_PriceListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PriceList"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrderItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.PurchaseReturn", "PurchaseReturn") + .WithMany("Items") + .HasForeignKey("FK_PurchaseReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseReturn"); + }); + + modelBuilder.Entity("Distribution.Models.RideCancelRequest", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ride"); + }); + + modelBuilder.Entity("Distribution.Models.RolePermission", b => + { + b.HasOne("Distribution.Models.Permission", "Permission") + .WithMany() + .HasForeignKey("FK_PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.Route", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.Sale", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Ride"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany("Items") + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SaleReturn"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "TriggerItem") + .WithMany() + .HasForeignKey("FK_TriggerItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("RegionId"); + + b.Navigation("FreeItem"); + + b.Navigation("Region"); + + b.Navigation("TriggerItem"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeBundleComponent", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("BundleComponents") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.SchemeRedemption", b => + { + b.HasOne("Distribution.Models.Item", "FreeItem") + .WithMany() + .HasForeignKey("FK_FreeItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("FreeItem"); + + b.Navigation("Order"); + + b.Navigation("Scheme"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("Distribution.Models.StoreKycDocument", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany() + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "RaisedByUser") + .WithMany() + .HasForeignKey("RaisedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("RaisedByUser"); + + b.Navigation("ResolvedByUser"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SupplierClaim", "SupplierClaim") + .WithMany("Items") + .HasForeignKey("FK_SupplierClaimId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SupplierClaim"); + }); + + modelBuilder.Entity("Distribution.Models.UomConversion", b => + { + b.HasOne("Distribution.Models.Uom", "FromUom") + .WithMany() + .HasForeignKey("FK_FromUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany("Conversions") + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.Uom", "ToUom") + .WithMany() + .HasForeignKey("FK_ToUomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromUom"); + + b.Navigation("Item"); + + b.Navigation("ToUom"); + }); + + modelBuilder.Entity("Distribution.Models.User", b => + { + b.HasOne("Distribution.Models.User", "ReportsTo") + .WithMany() + .HasForeignKey("FK_ReportsToUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Role", "Role") + .WithMany() + .HasForeignKey("FK_RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReportsTo"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Distribution.Models.UserRegionScope", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserRouteScope", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Route"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.UserStoreScope", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Store"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.Vehicle", b => + { + b.HasOne("Distribution.Models.Warehouse", null) + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("Distribution.Models.Warehouse", b => + { + b.HasOne("Distribution.Models.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("VehicleId"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceipt", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Order", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("Distribution.Models.PriceList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.PurchaseReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.Scheme", b => + { + b.Navigation("BundleComponents"); + + b.Navigation("Redemptions"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20260825084218_AddSupplierClaims.cs b/Distribution/Migrations/20260825084218_AddSupplierClaims.cs new file mode 100644 index 0000000..8ebe7d6 --- /dev/null +++ b/Distribution/Migrations/20260825084218_AddSupplierClaims.cs @@ -0,0 +1,137 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddSupplierClaims : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SupplierClaims", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_PurchaseOrderId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_GoodsReceiptId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + Details = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EvidenceUrl = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + ResolutionType = table.Column(type: "int", nullable: true), + ResolutionAmount = table.Column(type: "decimal(65,30)", nullable: true), + ResolutionNotes = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + RaisedByUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + ResolvedByUserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + ResolvedAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SupplierClaims", x => x.Id); + table.ForeignKey( + name: "FK_SupplierClaims_GoodsReceipts_FK_GoodsReceiptId", + column: x => x.FK_GoodsReceiptId, + principalTable: "GoodsReceipts", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SupplierClaims_PurchaseOrders_FK_PurchaseOrderId", + column: x => x.FK_PurchaseOrderId, + principalTable: "PurchaseOrders", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SupplierClaims_Users_RaisedByUserId", + column: x => x.RaisedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SupplierClaims_Users_ResolvedByUserId", + column: x => x.ResolvedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "SupplierClaimItems", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_SupplierClaimId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Quantity = table.Column(type: "decimal(65,30)", nullable: false), + IssueType = 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_SupplierClaimItems", x => x.Id); + table.ForeignKey( + name: "FK_SupplierClaimItems_Items_FK_ItemId", + column: x => x.FK_ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_SupplierClaimItems_SupplierClaims_FK_SupplierClaimId", + column: x => x.FK_SupplierClaimId, + principalTable: "SupplierClaims", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaimItems_FK_ItemId", + table: "SupplierClaimItems", + column: "FK_ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaimItems_FK_SupplierClaimId", + table: "SupplierClaimItems", + column: "FK_SupplierClaimId"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaims_FK_GoodsReceiptId", + table: "SupplierClaims", + column: "FK_GoodsReceiptId"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaims_FK_PurchaseOrderId", + table: "SupplierClaims", + column: "FK_PurchaseOrderId"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaims_RaisedByUserId", + table: "SupplierClaims", + column: "RaisedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_SupplierClaims_ResolvedByUserId", + table: "SupplierClaims", + column: "ResolvedByUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SupplierClaimItems"); + + migrationBuilder.DropTable( + name: "SupplierClaims"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index 9a18c9b..ea26e45 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -73,6 +73,74 @@ namespace Distribution.Migrations b.ToTable("Categories"); }); + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("SettledAt") + .HasColumnType("datetime(6)"); + + b.Property("SettledByUserId") + .HasColumnType("char(36)"); + + b.Property("SettlementAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("SettledByUserId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssuedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("CreditNotes"); + }); + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => { b.Property("Id") @@ -1006,6 +1074,75 @@ namespace Distribution.Migrations b.ToTable("Sales"); }); + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("FK_OrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Mode") + .HasColumnType("int"); + + b.Property("TotalCreditAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("FK_OrderId"); + + b.ToTable("SaleReturns"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SaleReturnId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SaleReturnId"); + + b.ToTable("SaleReturnItems"); + }); + modelBuilder.Entity("Distribution.Models.SaleRoute", b => { b.Property("Id") @@ -1377,6 +1514,98 @@ namespace Distribution.Migrations b.ToTable("Suppliers"); }); + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EvidenceUrl") + .HasColumnType("longtext"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResolutionAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("ResolutionNotes") + .HasColumnType("longtext"); + + b.Property("ResolutionType") + .HasColumnType("int"); + + b.Property("ResolvedAt") + .HasColumnType("datetime(6)"); + + b.Property("ResolvedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("RaisedByUserId"); + + b.HasIndex("ResolvedByUserId"); + + b.ToTable("SupplierClaims"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierClaimId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssueType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SupplierClaimId"); + + b.ToTable("SupplierClaimItems"); + }); + modelBuilder.Entity("Distribution.Models.Uom", b => { b.Property("Id") @@ -1610,6 +1839,35 @@ namespace Distribution.Migrations b.ToTable("Warehouses"); }); + modelBuilder.Entity("Distribution.Models.Claim", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "SettledByUser") + .WithMany() + .HasForeignKey("SettledByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AgencyUser"); + + b.Navigation("SettledByUser"); + }); + + modelBuilder.Entity("Distribution.Models.CreditNote", b => + { + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany() + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SaleReturn"); + }); + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => { b.HasOne("Distribution.Models.DriverProfile", "DriverProfile") @@ -1994,6 +2252,44 @@ namespace Distribution.Migrations b.Navigation("Store"); }); + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Order", "Order") + .WithMany() + .HasForeignKey("FK_OrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturnItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SaleReturn", "SaleReturn") + .WithMany("Items") + .HasForeignKey("FK_SaleReturnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SaleReturn"); + }); + modelBuilder.Entity("Distribution.Models.Scheme", b => { b.HasOne("Distribution.Models.Item", "FreeItem") @@ -2116,6 +2412,58 @@ namespace Distribution.Migrations b.Navigation("Category"); }); + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany() + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "RaisedByUser") + .WithMany() + .HasForeignKey("RaisedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("RaisedByUser"); + + b.Navigation("ResolvedByUser"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SupplierClaim", "SupplierClaim") + .WithMany("Items") + .HasForeignKey("FK_SupplierClaimId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SupplierClaim"); + }); + modelBuilder.Entity("Distribution.Models.UomConversion", b => { b.HasOne("Distribution.Models.Uom", "FromUom") @@ -2270,12 +2618,22 @@ namespace Distribution.Migrations b.Navigation("Items"); }); + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.Navigation("Items"); + }); + modelBuilder.Entity("Distribution.Models.Scheme", b => { b.Navigation("BundleComponents"); b.Navigation("Redemptions"); }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Navigation("Items"); + }); #pragma warning restore 612, 618 } } diff --git a/Distribution/Models/Claim.cs b/Distribution/Models/Claim.cs new file mode 100644 index 0000000..a0cc7be --- /dev/null +++ b/Distribution/Models/Claim.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class Claim : Template + { + /// User holding the RegionalAgency role that this claim is raised against. + public Guid FK_AgencyUserId { get; set; } + [ForeignKey("FK_AgencyUserId")] + public User AgencyUser { get; set; } + + public string Details { get; set; } + public ClaimStatus Status { get; set; } = ClaimStatus.Open; + public decimal? SettlementAmount { get; set; } + + public Guid? SettledByUserId { get; set; } + [ForeignKey("SettledByUserId")] + public User? SettledByUser { get; set; } + public DateTime? SettledAt { get; set; } + } +} diff --git a/Distribution/Models/CreditNote.cs b/Distribution/Models/CreditNote.cs new file mode 100644 index 0000000..aec96a0 --- /dev/null +++ b/Distribution/Models/CreditNote.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// Audit record of the credit amount netted off an order by a CreditDeduction-mode SaleReturn. + public class CreditNote : Template + { + public Guid FK_SaleReturnId { get; set; } + [ForeignKey("FK_SaleReturnId")] + public SaleReturn SaleReturn { get; set; } + + public decimal Amount { get; set; } + public DateTime IssuedAt { get; set; } = DateTime.Now; + } +} diff --git a/Distribution/Models/Enums.cs b/Distribution/Models/Enums.cs index 90f0801..588af42 100644 --- a/Distribution/Models/Enums.cs +++ b/Distribution/Models/Enums.cs @@ -1,37 +1,13 @@ namespace Distribution.Models { public enum LocationType - { - Warehouse, - Vehicle - } + { Warehouse, Vehicle } - public enum KycStatus - { - Pending, - Verified, - Rejected } + public enum KycStatus { Pending,Verified, Rejected } - public enum MovementType - { - Sale, - CustomerReturn, - Transfer, - Damage, - Adjustment, - Load, - PurchaseReceipt, - PurchaseReturn - } + public enum MovementType { Sale, CustomerReturn, Transfer, Damage, Adjustment, Load, PurchaseReceipt, PurchaseReturn } - public enum LoadingSheetStatus - { - Pending, - DispatchConfirmed, - ReceiptConfirmed, - DiscrepancyFlagged, - Resolved - } + public enum LoadingSheetStatus { Pending, DispatchConfirmed, ReceiptConfirmed, DiscrepancyFlagged, Resolved } public enum RideType { Delivery, Sale } @@ -52,10 +28,17 @@ namespace Distribution.Models public enum PaymentMethod { Cash, Credit } public enum DeliveryTaskStatus - { - Pending, - Delivered, - PartiallyDelivered, - Failed - } + { Pending, Delivered, PartiallyDelivered, Failed} + + public enum ReturnMode { CreditDeduction, ItemExchange } + + public enum ReturnReasonCode { Expired, Damaged, OrderError, Other } + + public enum ClaimStatus { Open, Settled } + + public enum SupplierClaimStatus { Open, Approved, Rejected } + + public enum SupplierClaimIssueType { Damaged, Short, Expired, WrongItem, Other } + + public enum SupplierClaimResolutionType { Replacement, Credit, Refund, PriceAdjustment } } diff --git a/Distribution/Models/SaleReturn.cs b/Distribution/Models/SaleReturn.cs new file mode 100644 index 0000000..48375b6 --- /dev/null +++ b/Distribution/Models/SaleReturn.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// + /// A return recorded by a sales rep against an open Order during the same store visit. + /// Settled immediately on creation — no supervisor approval step. + /// + public class SaleReturn : Template + { + public Guid FK_OrderId { get; set; } + [ForeignKey("FK_OrderId")] + public Order Order { get; set; } + + public ReturnMode Mode { get; set; } + + /// Amount netted off the order's TotalAmount. Zero for ItemExchange mode. + public decimal TotalCreditAmount { get; set; } + + public Guid CreatedByUserId { get; set; } + [ForeignKey("CreatedByUserId")] + public User CreatedByUser { get; set; } + + public ICollection Items { get; set; } = []; + } +} diff --git a/Distribution/Models/SaleReturnItem.cs b/Distribution/Models/SaleReturnItem.cs new file mode 100644 index 0000000..d4c8e84 --- /dev/null +++ b/Distribution/Models/SaleReturnItem.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class SaleReturnItem : Template + { + public Guid FK_SaleReturnId { get; set; } + [ForeignKey("FK_SaleReturnId")] + public SaleReturn SaleReturn { get; set; } + + public Guid FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item Item { get; set; } + + public decimal Quantity { get; set; } + public ReturnReasonCode ReasonCode { get; set; } + + /// Snapshot of the item's unit price at return time, used to compute credit amount. + public decimal UnitPrice { get; set; } + } +} diff --git a/Distribution/Models/SupplierClaim.cs b/Distribution/Models/SupplierClaim.cs new file mode 100644 index 0000000..ec019a4 --- /dev/null +++ b/Distribution/Models/SupplierClaim.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class SupplierClaim : Template + { + public Guid FK_PurchaseOrderId { get; set; } + [ForeignKey("FK_PurchaseOrderId")] + public PurchaseOrder PurchaseOrder { get; set; } + + public Guid? FK_GoodsReceiptId { get; set; } + [ForeignKey("FK_GoodsReceiptId")] + public GoodsReceipt? GoodsReceipt { get; set; } + + public string Details { get; set; } + public string? EvidenceUrl { get; set; } + + public SupplierClaimStatus Status { get; set; } = SupplierClaimStatus.Open; + + public SupplierClaimResolutionType? ResolutionType { get; set; } + public decimal? ResolutionAmount { get; set; } + public string? ResolutionNotes { get; set; } + + public Guid RaisedByUserId { get; set; } + [ForeignKey("RaisedByUserId")] + public User RaisedByUser { get; set; } + + public Guid? ResolvedByUserId { get; set; } + [ForeignKey("ResolvedByUserId")] + public User? ResolvedByUser { get; set; } + public DateTime? ResolvedAt { get; set; } + + public ICollection Items { get; set; } = []; + } +} diff --git a/Distribution/Models/SupplierClaimItem.cs b/Distribution/Models/SupplierClaimItem.cs new file mode 100644 index 0000000..082e109 --- /dev/null +++ b/Distribution/Models/SupplierClaimItem.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + public class SupplierClaimItem : Template + { + public Guid FK_SupplierClaimId { get; set; } + [ForeignKey("FK_SupplierClaimId")] + public SupplierClaim SupplierClaim { get; set; } + + public Guid FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item Item { get; set; } + + public decimal Quantity { get; set; } + public SupplierClaimIssueType IssueType { get; set; } + } +} diff --git a/Distribution/Program.cs b/Distribution/Program.cs index 32b4c37..a2f48a2 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -13,6 +13,9 @@ using Distribution.Features.Order; using Distribution.Features.PriceList; using Distribution.Features.Po; using Distribution.Features.PurchaseReturn; +using Distribution.Features.Returns; +using Distribution.Features.Claims; +using Distribution.Features.SupplierClaims; using Distribution.Features.Region; using Distribution.Features.LoadingEvent; using Distribution.Features.Ride; @@ -73,6 +76,9 @@ 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(); @@ -103,6 +109,9 @@ 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(); diff --git a/Distribution/docs/00_existing_gaps.md b/Distribution/docs/00_existing_gaps.md index 73cf60e..3c486e9 100644 --- a/Distribution/docs/00_existing_gaps.md +++ b/Distribution/docs/00_existing_gaps.md @@ -1,6 +1,6 @@ # 00 — Existing Gap Fixes -**Status**: Not Started +**Status**: Done **Depends On**: — --- diff --git a/Distribution/docs/01_item_catalog.md b/Distribution/docs/01_item_catalog.md index 4ba379e..22313ab 100644 --- a/Distribution/docs/01_item_catalog.md +++ b/Distribution/docs/01_item_catalog.md @@ -1,6 +1,6 @@ # 01 — Item Catalog -**Status**: Ready +**Status**: Done **Depends On**: 00 — Existing Gap Fixes (gaps 1 & 2 done) --- diff --git a/Distribution/docs/04_loading_event.md b/Distribution/docs/04_loading_event.md index d936cdc..e4d2cb0 100644 --- a/Distribution/docs/04_loading_event.md +++ b/Distribution/docs/04_loading_event.md @@ -1,6 +1,6 @@ # 04 - Loading Event -**Status**: Planning +**Status**: Done **Depends On**: 03 - Ride Lifecycle --- diff --git a/Distribution/docs/15_procurement.md b/Distribution/docs/15_procurement.md index aa2f165..274d8b9 100644 --- a/Distribution/docs/15_procurement.md +++ b/Distribution/docs/15_procurement.md @@ -122,6 +122,15 @@ GoodsReceiptItem ``` ### PurchaseReturn + +> **Not the same as `SaleReturn`** ([18_returns_claims.md](./18_returns_claims.md)), despite the +> similar name. `PurchaseReturn` moves goods **Distributor → Supplier** (debits the receiving +> warehouse); `SaleReturn` moves goods **Shop → Distributor** (credits a delivery vehicle). Also +> distinct from `SupplierClaim` ([18_returns_claims.md](./18_returns_claims.md)), which is for +> supplier disputes that don't involve physically returning goods (short-shipment, price dispute) +> and — unlike this entity — requires approval. See that doc's "Four distinct entities" table for +> the full comparison. + ``` PurchaseReturn ├── Id (Guid) — PK, from Template diff --git a/Distribution/docs/18_returns_claims.md b/Distribution/docs/18_returns_claims.md index 653d46e..ef2dce5 100644 --- a/Distribution/docs/18_returns_claims.md +++ b/Distribution/docs/18_returns_claims.md @@ -1,74 +1,469 @@ # 18 — Returns & Claims Management -**Status**: Not Started -**Depends On**: 05 — Delivery Ride Execution, 06 — Sale Ride Execution +**Status**: Done — migrations applied to the database. Inspection/classification for customer +returns is deferred (see Open Questions). +**Depends On**: 05 — Delivery Ride Execution, 06 — Sale Ride Execution, 15 — Procurement (for +`SupplierClaim`) --- ## Goal -Covers [mainPlan.md §12](./mainPlan.md#12-returns--claims-management) in -full. [05_delivery_ride.md](./05_delivery_ride.md) and [09_fraud_controls.md](./09_fraud_controls.md) -both reference this step's credit note and approval shapes — this is where those are actually -defined. +Covers [mainPlan.md §12](./mainPlan.md#12-returns--claims-management) — a customer-side flow where +a sales rep records a return during a store visit and a claim workflow against a Regional Agency, +plus a supplier-side flow for claims against a `PurchaseOrder`/`GoodsReceipt` that don't involve +physically returning goods. + +### Four distinct entities — don't conflate them + +`SaleReturn` and `PurchaseReturn` are both named "...Return" but move goods in opposite +directions, and neither is the same as the two `Claim` entities, which never move stock at all: + +| Entity | Direction | Where it lives | Approval? | Stock effect | +|---|---|---|---|---| +| `SaleReturn` | Shop → Distributor | This step (`Features/Returns`) | No — settles immediately | Credits the ride's **vehicle** | +| `Claim` | vs. Regional Agency | This step (`Features/Claims`) | Settle only (`claims.settle`) | None | +| `PurchaseReturn` | Distributor → Supplier | [15_procurement.md](./15_procurement.md) (`Features/PurchaseReturn`) | No — settles immediately | Debits the receiving **warehouse** | +| `SupplierClaim` | vs. Supplier | This step (`Features/SupplierClaims`) | Yes (`suppliers.claims.approve`) | None — financial/administrative record only | + +`PurchaseReturn` is for physically shipping goods back to a supplier; `SupplierClaim` is for +disputes that don't (or can't yet) involve moving goods back — a short-shipment, damage, or a +price dispute. --- ## Background / Context -No return, credit note, or claim entity exists today. `createBackorder` (§1, -[05_delivery_ride.md](./05_delivery_ride.md)) needs a credit note target once this step lands. +No return, credit note, or claim entity existed before this step. `createBackorder` (§1, +[05_delivery_ride.md](./05_delivery_ride.md)) needed a credit note target once this step landed. + +The original spec (mainPlan §12) framed customer returns as an async, supervisor-approved +workflow — `createReturnRequest` → `approveReturn` → stock credited only on approval. **That model +was dropped during planning.** In practice a sales rep records a return on the spot, during the +same store visit as a new order, and it must settle immediately (stock back in the vehicle, the +new order's amount adjusted) — there is no point in the field workflow where a return sits +"pending" a remote supervisor's sign-off. See Business Rules for what this changes. + +The existing `PurchaseReturn` ([15_procurement.md](./15_procurement.md)) only covers physically +returning received goods to a supplier and debiting stock — it has no place for a claim that +doesn't (or can't yet) involve moving goods back: a short-shipment (500 ordered, 480 received), +goods damaged in transit, or a price dispute. `SupplierClaim` was added alongside `PurchaseReturn` +for this, not as a rework of it. + +### Connections to existing modules + +- **Order Management**: a `SaleReturn` is always filed against a specific open `Order` + (`Features/Order`) and immediately nets its effect onto that order's `TotalAmount` / + `OrderItem.Quantity` — no `Invoice` entity exists yet (Finance, step 19, not built), so there is + nothing else for a credit note to net against. +- **Stock Ledger** ([02_stock_ledger.md](./02_stock_ledger.md)): `SaleReturn` credits + `StockBalance` at the active ride's vehicle (`MovementType.CustomerReturn`), the same enum value + `StockService.RecordCustomerReturnAsync` already defined but that no sales-side flow was wiring + up until this step. `SupplierClaim` writes no stock movement at all (see Business Rules). +- **Ride Execution** ([05_delivery_ride.md](./05_delivery_ride.md)): a `SaleReturn` can only be + recorded against an order created during an active ride (`Sale.FK_RideId` must be set), since + stock is credited back to that ride's vehicle (`DeliveryRun.FK_VehicleId`). +- **Region Management** ([10_region_management.md](./10_region_management.md)): there is no + separate Agency entity in the codebase — "Regional Agency" is a `Role` + (`Data/Seed/RoleAndPermissionSeed.cs`), so `Claim` references the `User` holding that role. +- **Procurement** ([15_procurement.md](./15_procurement.md)): `SupplierClaim.FK_PurchaseOrderId` + is required, `FK_GoodsReceiptId` optional; a short-shipment is already excluded from stock at + receipt time (`GoodsReceiptItem.RejectedQuantity`), so `SupplierClaim` never needs to touch + `StockBalance` itself. --- ## Data Model Changes -### Return Request +Six new tables. -`OrderId`, `Items[]` (`ItemId`, `Quantity`, `ReasonCode`), `Status` (`Pending`, `Approved`, -`Rejected`), `RequestedByUserId`. +### SaleReturn +``` +SaleReturn +├── Id (Guid) — PK, from Template +├── FK_OrderId (Guid) — required, the order this return is filed against +├── Mode (enum) — CreditDeduction, ItemExchange — chosen by the rep per return +├── TotalCreditAmount (decimal)— sum(Quantity * UnitPrice) for CreditDeduction mode; 0 for ItemExchange +├── CreatedByUserId (Guid) — required, the rep who recorded it +├── IsActive, CreatedAt — from Template +``` -### Credit Note +### SaleReturnItem +``` +SaleReturnItem +├── Id (Guid) — PK, from Template +├── FK_SaleReturnId (Guid) — required +├── FK_ItemId (Guid) — required +├── Quantity (decimal) — returned quantity, credited back to the ride's vehicle +├── ReasonCode (enum) — Expired, Damaged, OrderError, Other +├── UnitPrice (decimal) — snapshot at return time (from the matching OrderItem if one +│ exists on this order, else Item.SalePrice) +``` -`ReturnRequestId`, `InvoiceId`, `Amount`, `IssuedAt`. +### CreditNote +``` +CreditNote +├── Id (Guid) — PK, from Template +├── FK_SaleReturnId (Guid) — required +├── Amount (decimal) — amount netted off the order's TotalAmount +├── IssuedAt (DateTime) +├── IsActive, CreatedAt — from Template +``` +Audit-trail only — written for `CreditDeduction`-mode returns, not itself an approval gate or an +invoice-linked document (no `Invoice` entity exists yet; see Background/Context). ### Claim +``` +Claim +├── Id (Guid) — PK, from Template +├── FK_AgencyUserId (Guid) — required, the User holding the RegionalAgency role this claim is against +├── Details (string) — required +├── Status (enum) — Open, Settled +├── SettlementAmount (decimal?)— set once settled +├── SettledByUserId (Guid?), SettledAt (DateTime?) +├── IsActive, CreatedAt — from Template +``` +Independent of `SaleReturn`/stock — its own open/settle workflow. -`AgencyId`, `Details`, `Status` (`Open`, `Settled`), `SettlementAmount` (nullable until settled). +### SupplierClaim +``` +SupplierClaim +├── Id (Guid) — PK, from Template +├── FK_PurchaseOrderId (Guid) — required +├── FK_GoodsReceiptId (Guid?) — optional; set when the claim stems from a specific receipt +│ (e.g. short/damaged at receipt) +├── Details (string) — description of the issue +├── EvidenceUrl (string?) — link/path to a photo or document; no file-upload storage +│ exists yet, so this is a plain string field for now +├── Status (enum) — Open, Approved, Rejected +├── ResolutionType (enum?) — Replacement, Credit, Refund, PriceAdjustment — set on approve +├── ResolutionAmount (decimal?) — for Credit / Refund / PriceAdjustment +├── ResolutionNotes (string?) +├── RaisedByUserId (Guid) +├── ResolvedByUserId (Guid?), ResolvedAt (DateTime?) +├── IsActive, CreatedAt — from Template +``` + +### SupplierClaimItem +``` +SupplierClaimItem +├── Id (Guid) — PK, from Template +├── FK_SupplierClaimId (Guid) — required +├── FK_ItemId (Guid) — required +├── Quantity (decimal) +├── IssueType (enum) — Damaged, Short, Expired, WrongItem, Other +``` + +--- + +## Migrations + +Two migrations, applied: +1. **AddReturnsAndClaims** (`20260825073453_AddReturnsAndClaims`) — created `SaleReturns`, + `SaleReturnItems`, `CreditNotes`, `Claims`, with FKs to `Orders`/`Items`/`Users` and indexes on + every FK column. +2. **AddSupplierClaims** (`20260825084218_AddSupplierClaims`) — created `SupplierClaims`, + `SupplierClaimItems`, with FKs to `PurchaseOrders`/`GoodsReceipts`/`Items`/`Users` and indexes on + every FK column. + +Both applied via `dotnet ef database update`. --- ## API Endpoints -| Function reference | Endpoint intent | +> Return routes use bare `[Authorize]` — matches the Supplier/PriceList/Procurement convention (no +> per-action policy). `claims.settle` and `suppliers.claims.approve` are the two enforced policies, +> since those are the two steps with real approval weight. See Permissions to Add to Seed. + +### Returns (`Features/Returns`) +| Function reference | Endpoint | |---|---| -| `createReturnRequest(orderId, items[], reasonCode)` | `POST /api/returns` | -| `approveReturn(returnId, approverId)` | `POST /api/returns/{id}/approve` | -| `generateCreditNote(returnId)` | `POST /api/returns/{id}/credit-note` — auto-triggered on approval | -| `createClaim(agencyId, claimDetails)` | `POST /api/claims` | -| `settleClaim(claimId, settlementAmount)` | `POST /api/claims/{id}/settle` | +| `createSaleReturn(orderId, mode, items[])` | `POST /api/orders/{id}/returns` — creates and immediately settles | +| `getSaleReturn(returnId)` | `GET /api/returns/{id}` | +| `listSaleReturns(orderId)` | `GET /api/orders/{id}/returns` | + +### Claims (`Features/Claims`) +| Function reference | Endpoint | +|---|---| +| `createClaim(agencyUserId, details)` | `POST /api/claims` | +| `getClaim(claimId)` | `GET /api/claims/{id}` | +| `settleClaim(claimId, settlementAmount)` | `POST /api/claims/{id}/settle` — `[Authorize(Policy = "claims.settle")]` | + +### Supplier Claims (`Features/SupplierClaims`) +| Function reference | Endpoint | +|---|---| +| `createSupplierClaim(poId, grnId?, items[], details, evidenceUrl?)` | `POST /api/purchase-orders/{id}/claims` | +| `getSupplierClaim(claimId)` | `GET /api/supplier-claims/{id}` | +| `listSupplierClaims(poId)` | `GET /api/purchase-orders/{id}/claims` | +| `approveSupplierClaim(claimId, resolutionType, resolutionAmount?, notes?)` | `POST /api/supplier-claims/{id}/approve` — `[Authorize(Policy = "suppliers.claims.approve")]` | +| `rejectSupplierClaim(claimId, notes?)` | `POST /api/supplier-claims/{id}/reject` — `[Authorize(Policy = "suppliers.claims.approve")]` | + +`approve`/`reject` combine the "inspect the claim" and "decide the outcome" steps into one call — +unlike the customer-side flow there's no separate physical-inspection step to wait on here, since +the evidence (photos/notes, and `GoodsReceiptItem.RejectedQuantity` if applicable) is already +attached at claim-creation time. + +--- + +## Request / Response Shapes + +### POST `/api/orders/{id}/returns` +```json +{ + "mode": "CreditDeduction", + "items": [{ "itemId": "...", "quantity": 5, "reasonCode": "Expired" }] +} +``` +`mode` is `CreditDeduction` or `ItemExchange`, `reasonCode` is `Expired` | `Damaged` | +`OrderError` | `Other`. + +### POST `/api/claims` +```json +{ "agencyUserId": "...", "details": "Short-shipped 20 units on last consignment" } +``` + +### POST `/api/claims/{id}/settle` +```json +{ "settlementAmount": 1500.00 } +``` + +### POST `/api/purchase-orders/{id}/claims` +```json +{ + "goodsReceiptId": "...", + "details": "20 units arrived damaged, 480 of 500 received", + "evidenceUrl": "https://...", + "items": [{ "itemId": "...", "quantity": 20, "issueType": "Damaged" }] +} +``` +`goodsReceiptId` and `evidenceUrl` are optional. `issueType` is `Damaged` | `Short` | `Expired` | +`WrongItem` | `Other`. + +### POST `/api/supplier-claims/{id}/approve` +```json +{ "resolutionType": "Credit", "resolutionAmount": 850.00, "resolutionNotes": "Agreed with supplier" } +``` +`resolutionType` is `Replacement` | `Credit` | `Refund` | `PriceAdjustment`. `resolutionAmount` is +required unless `resolutionType` is `Replacement`. + +### POST `/api/supplier-claims/{id}/reject` +```json +{ "resolutionNotes": "Discrepancy within accepted tolerance" } +``` --- ## Business Rules -- `approveReturn` requires supervisor sign-off before stock is credited back — matches the rule - already stated in [09_fraud_controls.md](./09_fraud_controls.md). -- A `Return Request` must reference a real prior sale/delivery line — the absence of one is exactly - the `ReturnWithoutSale` anomaly flagged automatically in - [09_fraud_controls.md](./09_fraud_controls.md). -- Approving a return creates a `Return` stock movement crediting the relevant location, per - [02_stock_ledger.md](./02_stock_ledger.md). +1. `createSaleReturn` is the entry point — no prior approval step, settlement is immediate. This + deliberately supersedes the original spec's `Pending → Approved/Rejected` flow (see + Background/Context) since a rep records the return on the spot during a live store visit. +2. A return can only be filed against an order that is still open (`OrderStatus.Pending` or + `PartiallyFulfilled`) — not `Fulfilled`/`Cancelled` — and whose `Sale.FK_RideId` is set, since + stock is credited back to that ride's vehicle (`DeliveryRun.FK_VehicleId`, + `MovementType.CustomerReturn`). +3. `createSaleReturn` rejects (409 Conflict) any line item the store has never had on **any** of + its past orders — checked across the store's full order history, not just the order the return + is filed against. This lets an expired or damaged item bought weeks ago still be returned; it + only blocks an item that store never bought at all — the real fraud case originally scoped as + `ReturnWithoutSale` in [09_fraud_controls.md](./09_fraud_controls.md), which is not otherwise + built. +4. `Mode = CreditDeduction`: the returned lines' value (`Quantity * UnitPrice`) is subtracted from + `Order.TotalAmount` (floored at 0), and a `CreditNote` audit row is written. +5. `Mode = ItemExchange`: the returned quantity is subtracted directly from the matching + `OrderItem.Quantity` on the same order (a physical swap of the same item), `TotalAmount` + adjusted by the offset amount. No `CreditNote` is written. A returned item with no matching line + on the order is credited to stock but does not offset anything on the order. +6. Stock credit, the `SaleReturn`/`SaleReturnItem` insert, and the order-total/credit-note update + all run inside one `IUnitOfWork` transaction — a failure anywhere rolls back the whole return. +7. `settleClaim` can only run once per claim — rejects (409) if already `Settled`. Claims never + touch `StockBalance` or any `Order`. +8. `createSupplierClaim` requires the referenced `PurchaseOrder` to exist and be active; + `FK_GoodsReceiptId`, if given, must belong to that PO (rejects with 409 otherwise). +9. Only `Open` supplier claims can be approved or rejected; both are terminal (no re-opening) — + enforced by `ApproveAsync`/`RejectAsync` throwing if `Status != Open`. +10. `approveSupplierClaim` requires `ResolutionType`; `ResolutionAmount` is required for `Credit`, + `Refund`, and `PriceAdjustment` (409 if missing), and is discarded (`null`) for `Replacement` + even if supplied. +11. Supplier claim approval/rejection is a real sign-off step (unlike the customer-side + `SaleReturn`, which settles immediately) — gated behind + `[Authorize(Policy = "suppliers.claims.approve")]`, following the Ride cancel-request pattern + (`RideController.cs:120-146`). +12. No automatic `StockBalance`/`StockMovement` write from any `SupplierClaim` action. + `ResolutionType = Replacement` records that new stock is expected; the actual stock credit + happens through an ordinary follow-up `GoodsReceipt` once it arrives (see Open Questions). + `Credit`/`Refund`/`PriceAdjustment` have no `Invoice`/Finance ledger to post against yet + (step 19, not built) — `ResolutionAmount` is captured as an audit field only, the same + limitation `CreditNote` has on the customer side. + +--- + +## Permissions to Add to Seed + +**Status: `claims.settle` and `suppliers.claims.approve` enforced; return-creation routes +intentionally left as bare `[Authorize]`**, matching the Supplier/PriceList/Procurement convention +(steps 14/15) rather than introducing enforcement piecemeal. Both enforced policies use the same +dynamic policy-provider pattern as `rides.cancel.approve` (`PermissionPolicyProvider`) — no +explicit policy registration needed, just a seeded `Permission` row and a `RolePermission` grant. +No role assignments seeded yet; `suppliers.claims.approve` likely AreaManager (30) and above, +matching the Supplier/Procurement tier. --- ## Open Questions +- Should return-creation routes eventually get a real permission policy (`returns.create`) once + the Supplier/PriceList/Procurement group does, or do they stay bare `[Authorize]` indefinitely + since a rep needs to record a return without waiting on a scoped grant? +- `ItemExchange` mode clamps the offset to `min(returnedQty, orderItem.Quantity)` — if a rep + returns more of an item than is on the current order, the excess is credited to stock but not + reflected anywhere on the order. Worth a `CreditNote`-style fallback for the excess once this + edge case actually comes up. +- No `Invoice` entity exists yet (Finance, step 19) — `CreditNote` and `SupplierClaim`'s + `ResolutionAmount` are standalone audit fields for now, not linked to any invoice/ledger + document. Revisit once Finance & Accounting is built. - Are `Claim`s only against Regional Agencies (as the reference doc frames them), or can a claim - also be raised against an internal warehouse/supplier discrepancy? + also be raised against an internal warehouse/supplier discrepancy? Currently only + `FK_AgencyUserId` (a User with the RegionalAgency role) is supported. +- `SupplierClaim.ResolutionType = Replacement` has no link back to the eventual replacement + `GoodsReceipt` — a nullable `FK_SupplierClaimId` on `GoodsReceipt` would close that loop but + isn't built yet. +- **Inspection & Classification (deferred)**: the fuller process — inspect returned goods, + classify each line as Sellable / Damaged / Expired / Quarantine / Disposal, route sellable stock + back to available inventory while damaged/expired stock moves to a separate stock category, and + only then decide refund vs. replacement vs. credit vs. reject — is explicitly out of scope for + now. The current `SaleReturn` flow settles immediately with no inspection step (see + Background/Context for why). When it's picked up, it will need: an `InspectionResult` per + `SaleReturnItem` recorded after the item is physically back at a warehouse (not at the point of + return); a stock-location/category concept for quarantined or condemned stock (today + `StockBalance` only distinguishes `LocationType.Warehouse`/`Vehicle`); a real cash-refund concept + once Finance/Payments (step 19) exists to reverse a `Payment`; and a decision on whether + inspection reopens the immediate-settlement model into a provisional-then-confirmed one — a + bigger behavioral change than adding a field, needing its own planning pass first. --- ## Implementation Notes -> Fill this section in during planning discussion before writing any code. +### File structure (as built) +``` +Features/ + Returns/ + ReturnController.cs — /api/orders/{id}/returns (POST/GET), /api/returns/{id} (GET) + ReturnDTOs.cs — CreateSaleReturnRequest, ReturnItemRequest, SaleReturnResponse(+Item) + ReturnService.cs — CreateAsync (validate → credit stock → net order → commit), + GetByIdAsync, GetByOrderIdAsync + IReturnRepository.cs / ReturnRepository.cs — includes StoreHasPriorSaleAsync + Claims/ + ClaimController.cs — /api/claims (POST/GET), /api/claims/{id}/settle (POST) + ClaimDTOs.cs — CreateClaimRequest, SettleClaimRequest, ClaimResponse + ClaimService.cs — CreateAsync, SettleAsync, GetByIdAsync, GetByAgencyUserIdAsync + IClaimRepository.cs / ClaimRepository.cs + SupplierClaims/ + SupplierClaimController.cs — /api/purchase-orders/{id}/claims (POST/GET), + /api/supplier-claims/{id} (GET), + /api/supplier-claims/{id}/approve|reject (POST) + SupplierClaimDTOs.cs — CreateSupplierClaimRequest, ApproveSupplierClaimRequest, + RejectSupplierClaimRequest, SupplierClaimResponse(+Item) + SupplierClaimService.cs — CreateAsync, ApproveAsync, RejectAsync, GetByIdAsync, + GetByPurchaseOrderIdAsync + ISupplierClaimRepository.cs / SupplierClaimRepository.cs + +Models/ + SaleReturn.cs, SaleReturnItem.cs, CreditNote.cs, Claim.cs (new) + SupplierClaim.cs, SupplierClaimItem.cs (new) + Enums.cs (extended — ReturnMode, ReturnReasonCode, ClaimStatus, SupplierClaimStatus, + SupplierClaimIssueType, SupplierClaimResolutionType added) + +Data/AppDBContext.cs + DbSets added for all six entities; FK relationships via Fluent API, DeleteBehavior.Restrict + (SupplierClaim.FK_GoodsReceiptId, ResolvedByUserId configured as optional) + +Migrations/ + 20260825073453_AddReturnsAndClaims (applied) + 20260825084218_AddSupplierClaims (applied) + +Program.cs + IReturnRepository/ReturnRepository, IClaimRepository/ClaimRepository, + ISupplierClaimRepository/SupplierClaimRepository registered; + ReturnService, ClaimService, SupplierClaimService registered +``` + +### How the models connect +``` +Order (existing) ◄── FK_OrderId ── SaleReturn ── Mode: CreditDeduction | ItemExchange + │ + ├─ SaleReturnItem (FK_ItemId, Quantity, ReasonCode, UnitPrice) + │ └─► StockBalance/StockMovement (credited: vehicle of + │ the order's ride, MovementType.CustomerReturn) + │ + └─ CreditNote (only when Mode = CreditDeduction; + Amount = sum(Quantity * UnitPrice)) + +Claim ── FK_AgencyUserId ──► User (holding the RegionalAgency role, seeded in + Data/Seed/RoleAndPermissionSeed.cs) — no separate Agency entity + +PurchaseOrder (existing, step 15) ◄── FK_PurchaseOrderId ── SupplierClaim + │ +GoodsReceipt (existing, step 15) ◄── FK_GoodsReceiptId (optional) ┘ + │ + ├─ SupplierClaimItem (FK_ItemId, + │ Quantity, IssueType) + └─ ResolutionType: Replacement | + Credit | Refund | PriceAdjustment + (no stock/ledger write — see + Business Rules) +``` + +### Return flow (as implemented) +``` +ReturnService.CreateAsync(orderId, request, createdByUserId): + order = load Order, must be Pending/PartiallyFulfilled else throw + sale = load order's Sale, must have FK_RideId set else throw + ride = load Sale's Ride → vehicleId = ride.FK_VehicleId + orderItems = load order's OrderItems (for price snapshot + ItemExchange offset) + for each (itemId, qty, reasonCode) in request.Items: + guard StoreHasPriorSaleAsync(order.FK_StoreId, itemId) else throw 409 + unitPrice = matching OrderItem.UnitPrice if present, else Item.SalePrice + begin transaction: + for each line: credit StockBalance(itemId, vehicleId, Vehicle) += qty + add StockMovement(Type = CustomerReturn, ToLocation = vehicle, ReferenceId = return.Id) + insert SaleReturn + SaleReturnItems + if Mode == CreditDeduction: + total = sum(qty * unitPrice); SaleReturn.TotalCreditAmount = total + Order.TotalAmount = max(0, Order.TotalAmount - total) + insert CreditNote(Amount = total) + else (ItemExchange): + for each line with a matching OrderItem: + offset = min(qty, orderItem.Quantity); orderItem.Quantity -= offset + Order.TotalAmount = max(0, Order.TotalAmount - offset * orderItem.UnitPrice) + commit +``` + +### Claim flow (as implemented) +``` +ClaimService.CreateAsync(request): insert Claim(Status = Open) +ClaimService.SettleAsync(claimId, settlementAmount, approverId): + guard claim exists and Status != Settled else throw + Claim.Status = Settled; SettlementAmount = settlementAmount + Claim.SettledByUserId = approverId; SettledAt = now +``` + +### Supplier claim flow (as implemented) +``` +SupplierClaimService.CreateAsync(purchaseOrderId, request, raisedByUserId): + po = load PurchaseOrder, must exist and be active else throw + guard Details non-empty, Items non-empty + if GoodsReceiptId given: guard it exists and belongs to this PO else throw + insert SupplierClaim(Status = Open) + SupplierClaimItems + +SupplierClaimService.ApproveAsync(id, request, approverId): + guard claim exists and Status == Open else throw + guard ResolutionAmount present unless ResolutionType == Replacement else throw + Status = Approved; ResolutionType/Amount/Notes set; ResolvedByUserId = approverId; ResolvedAt = now + +SupplierClaimService.RejectAsync(id, request, approverId): + guard claim exists and Status == Open else throw + Status = Rejected; ResolutionNotes set; ResolvedByUserId = approverId; ResolvedAt = now +``` diff --git a/Distribution/docs/mainPlan.md b/Distribution/docs/mainPlan.md index e5eec00..3c63180 100644 --- a/Distribution/docs/mainPlan.md +++ b/Distribution/docs/mainPlan.md @@ -11,26 +11,32 @@ | Region / Route / Vehicle CRUD | Done | Full API surface with permission checks | | Store / Warehouse CRUD | Done | Full API surface with permission checks | | Store Management (Beyond CRUD) | Done | Credit limit, KYC, segment, dashboard, scheme eligibility; see [12_store_management.md](./12_store_management.md) | -| Order CRUD | Done | Scope-aware (user only sees their assigned stores) | +| Order CRUD | Done | Scope-aware (user only sees their assigned stores); an `Order` is now created only as a side effect of `Sale` creation, not standalone — see §1 Order Management notes below | +| Sale & Payment | Done | `Features/Sale` creates a `Sale`+linked `Order` together (pricing, KYC threshold, immediate-handover stock check); `Features/Payment` records collections against a `Sale` and drives its `PaymentStatus` | | Unit of Work + Repositories | Done | Transaction support throughout | | Soft Deletes | Done | IsActive pattern on all entities | | Price Lists / Suppliers | Done | Region-scoped price lists + supplier master; see [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md) | | Promotions & Trade Schemes | Done | BuyXGetY/FreeBundle/SlabDiscount/FlatPercent schemes, free-goods fulfillment, spend/ROI tracking; see [17_promotions_schemes.md](./17_promotions_schemes.md) | | Procurement / Purchasing | Done | Purchase Order → Goods Receipt → Purchase Return, supplier evaluation; see [15_procurement.md](./15_procurement.md) | +| Returns & Claims Management | Done | Sale returns, claims, supplier claims; see [18_returns_claims.md](./18_returns_claims.md) | -## Known Gaps in Existing System (Pre-Work) +## Known Gaps in Existing System (Pre-Work) — Resolved -These are not new features — they are fixable holes in what's already built. -They must be resolved before new feature steps begin because later steps depend on correct foundations. +Step 00 closed these. Kept here for history; all are fixed in the current codebase. + +| Gap | Impact | Resolution | +|---|---|---| +| Route DTOs missing RegionId | Can't link routes to regions via API | Fixed — `CreateRouteRequest`/`UpdateRouteRequest`/`RouteResponse` all carry `RegionId` (`Features/Route/RouteDTOs.cs`). | +| Store DTOs missing RouteId | Can't link stores to routes via API | Fixed — all Store DTOs carry `RouteId` (`Features/Store/StoreDTOs.cs`). | +| Role endpoints have no [Authorize] | Anyone can read roles without authentication | Fixed — `[Authorize]` on `RoleController` (`Features/Role/RoleController.cs`). | +| Item model is empty (no properties) | OrderItems reference items that have no data | Fixed — `Item` has `Sku`/`Name`/`Description`/category/brand/UOM/pricing fields (`Models/Item.cs`). | + +### Still open (not a step-00 blocker, tracked separately) | Gap | Impact | |---|---| -| Route DTOs missing RegionId | Can't link routes to regions via API | -| Store DTOs missing RouteId | Can't link stores to routes via API | -| Role endpoints have no [Authorize] | Anyone can read roles without authentication | -| JWT key & DB credentials in appsettings.json | Security risk in any shared/production environment | -| No input validation on any DTO | Bad data can enter the system silently | -| Item model is empty (no properties) | OrderItems reference items that have no data | +| JWT key & DB credentials in appsettings.json | Security risk in any shared/production environment — `appsettings.json` still has a literal DB password and placeholder JWT key. | +| No input validation on any DTO | Bad data can enter the system silently. | --- @@ -56,7 +62,7 @@ They must be resolved before new feature steps begin because later steps depend | 15 | Procurement / Purchasing | Done | 01, 14 | [15_procurement.md](./15_procurement.md) | | 16 | Sales Force Automation / CRM | Not Started | 12 | [16_sfa_crm.md](./16_sfa_crm.md) | | 17 | Promotions & Trade Scheme Management | Done | 01, 14 | [17_promotions_schemes.md](./17_promotions_schemes.md) | -| 18 | Returns & Claims Management | Not Started | 05, 06 | [18_returns_claims.md](./18_returns_claims.md) | +| 18 | Returns & Claims Management | Done | 05, 06 | [18_returns_claims.md](./18_returns_claims.md) | | 19 | Finance & Accounting | Not Started | 05, 06, 18 | [19_finance_accounting.md](./19_finance_accounting.md) | | 20 | Reporting, Analytics & BI | Not Started | all prior | [20_reporting_analytics.md](./20_reporting_analytics.md) | | 21 | Notifications & Alerts | Not Started | 02, 12, 08 | [21_notifications_alerts.md](./21_notifications_alerts.md) | @@ -81,18 +87,27 @@ here — see `SRS_User_Management_Module.pdf`. ### 1. Order Management +> **Design change since this section was first written**: `Order` is no longer created directly +> from `storeId/items/salesRepId`. Order creation was folded into `Features/Sale` — every `Order` +> is now a 1:1 side effect of `SaleService.CreateAsync` (see §6 Sale Ride Execution). `POST +> /api/orders` (`OrderService.CreateAsync`) is now just a lookup — it throws +> `"Orders are created automatically from sales"` if called for a sale that doesn't already have +> one, rather than creating a standalone order. All the validation described below (stock check, +> KYC threshold, pricing) now happens in `SaleService.CreateAsync` +> (`Features/Sale/SaleService.cs`), not `OrderService`. + | Function | What it does | Status | |---|---|---| -| `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Partially implemented - `OrderService.CreateAsync` exists, enforces store scope, blocks inactive stores, applies the unverified-KYC order threshold from config, and now checks stock availability (see `checkStockAvailability` below). It still has no credit-limit validation, approval flow, or order state machine. | -| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented - `Store` now has `CreditLimit` and `OutstandingBalance`, but there is no dedicated validator or order-time credit-limit enforcement yet. | -| `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Implemented — `OrderService.CreateAsync` rejects an order if any line's requested quantity exceeds total `StockBalance` for that item summed across every location (added alongside [15_procurement.md](./15_procurement.md)). Not location-specific yet (no fulfilling-warehouse concept on `Store`/`Order`), and does not reserve or decrement stock — that stays deferred to loading/delivery (§05-06). | -| `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Partially implemented — `OrderService.CreateAsync` resolves the price-list half via `PriceListService.ResolveUnitPriceAsync` (region-scoped → global → `Item.SalePrice`, or a caller-supplied `UnitPrice`). The scheme-discount half now exists as a separate call — `SchemeService.EvaluateEligibilityAsync`/`ApplySchemeAsync` (§11) — but is not yet auto-invoked from `OrderService.CreateAsync`; the caller applies a scheme explicitly via `POST /api/orders/{id}/apply-scheme` after order creation. | +| `createOrder(storeId, items[], salesRepId)` | Validates stock/credit, creates a new order in `pending` state. | Superseded — see design-change note above. The equivalent entry point is `SaleService.CreateAsync` (`createSale`, §6), which creates the `Sale` and its linked `Order` together. `OrderService.CreateAsync` itself only fetches the existing order for a given `SaleId`. | +| `validateCreditLimit(storeId, orderAmount)` | Checks store's outstanding balance + new order value against its credit limit. | Not implemented - `Store` has `CreditLimit` and `OutstandingBalance`, but there is no dedicated validator or sale/order-time credit-limit enforcement yet. | +| `checkStockAvailability(items[])` | Confirms requested SKUs/quantities are available at the fulfilling warehouse. | Partially implemented — `SaleService.CreateAsync` checks `StockBalance` at the ride's vehicle, but **only** when `FulfillImmediately = true` and a `RideId` is supplied (immediate van-sale handover); a deferred/pending sale does no stock check at create time at all. Not warehouse-wide, not reserved — a failed check simply returns `null` (400) rather than a typed error. | +| `applyPricingAndDiscounts(orderId)` | Runs the active price list and eligible scheme rules against order lines. | Partially implemented — `SaleService.CreateAsync` resolves the price-list half via `ResolveUnitPriceAsync` → `PriceListService.ResolvePriceAsync` (region-scoped via the store's route → `Item.SalePrice` fallback, or a caller-supplied `UnitPrice` validated against `[MinPrice, Price]`). The scheme-discount half is a separate call — `SchemeService.EvaluateEligibilityAsync`/`ApplySchemeAsync` (§11) — not auto-invoked from `createSale`; the caller applies a scheme explicitly via `POST /api/orders/{id}/apply-scheme` after the order exists. | | `submitOrderForApproval(orderId)` | Routes the order to the correct approver based on its value/discount threshold. | Not implemented — depends on [08_sales_agent_controls.md](./08_sales_agent_controls.md). | | `approveOrder(orderId, approverId)` / `rejectOrder(orderId, reason)` | Moves order to `approved` or `rejected`, logging the actor. | Not implemented. | -| `updateOrderStatus(orderId, status)` | Transitions order through `pending → approved → picked → dispatched → delivered → invoiced`. | Not implemented — `Order` has no status field or state machine today. | +| `updateOrderStatus(orderId, status)` | Transitions order through `pending → approved → picked → dispatched → delivered → invoiced`. | Implemented, with a smaller state machine than described — `Models/Enums.cs` defines `OrderStatus { Pending, PartiallyFulfilled, Fulfilled, Cancelled }`; `OrderService.UpdateAsync` (`PUT /api/orders/{id}`) sets `Order.Status` directly (any status accepted, no transition guard) and mirrors it onto the linked `Sale.Status`/`CompletedAt`. No `approved/picked/dispatched/invoiced` states exist. | | `convertOrderToInvoice(orderId)` | Generates the invoice once the order is dispatched/delivered. | Not implemented — depends on §13 Finance & Accounting. | | `createBackorder(orderId, unfulfilledItems[])` | Splits unfulfillable quantity into a linked backorder. | Not implemented. | -| `cancelOrder(orderId, reason)` | Cancels an order pre-dispatch and releases any reserved stock. | Not implemented — `OrderController` supports soft delete only, no reason code or stock release. | +| `cancelOrder(orderId, reason)` | Cancels an order pre-dispatch and releases any reserved stock. | Partially implemented — `OrderService.DeleteAsync` (`DELETE /api/orders/{id}`) soft-deletes the order and cascades `Sale.Status = Cancelled`, but takes no reason code and releases no reserved stock (nothing is reserved at create time to release). | | `getOrdersByStore(storeId)` / `getOrdersBySalesRep(salesRepId)` | Scoped order listings. | `getOrdersByStore` implemented (`GET /api/orders/store/{storeId}`). `getOrdersBySalesRep` not implemented. | | `getOrderAnalytics(filters)` | Returns fill rate, average order value, rejection rate over a period. | Not implemented. | @@ -129,8 +144,11 @@ here — see `SRS_User_Management_Module.pdf`. | Function | What it does | Status | |---|---|---| | `createStore(storeData, kycDocs)` | Onboards a new outlet with required KYC. | Partially implemented - store create exists, and KYC documents can now be uploaded/listed via separate `/api/stores/{id}/kyc-documents` endpoints rather than inline on create. | +| `uploadStoreKycDocument(storeId, documentType, file)` | Appends a KYC document to a store. | Implemented — `POST /api/stores/{id}/kyc-documents`. | +| `listStoreKycDocuments(storeId)` | Lists KYC documents for a store. | Implemented — `GET /api/stores/{id}/kyc-documents`. | | `verifyStoreKyc(storeId)` | Marks KYC as verified/rejected by an approver. | Implemented - `PUT /api/stores/{id}/kyc/verify` updates `Store.KycStatus`. | | `updateStoreCreditLimit(storeId, newLimit, approverId)` | Changes a store's credit limit, subject to approval authority. | Implemented - `PUT /api/stores/{id}/credit-limit` updates `Store.CreditLimit` and requires the `stores.credit.update` permission plus role level >= 20. | +| `updateStoreOutstandingBalance(storeId, amount, reason?)` | Manually adjusts the cached outstanding balance. | Implemented — `PUT /api/stores/{id}/outstanding-balance`. | | `getStoreOutstandingBalance(storeId)` | Returns current receivables against the store. | Partially implemented - `GET /api/stores/{id}/outstanding-balance` returns the cached `OutstandingBalance`, `CreditLimit`, and available credit. It is not yet driven by Finance events or invoice/payment records. | | `classifyStore(storeId, segment)` | Sets/updates the store's segment (key account, general trade, etc.). | Implemented - `PUT /api/stores/{id}/segment` updates the free-text `Store.Segment` field. | | `deactivateStore(storeId, reason)` | Soft-deletes a store, blocking new orders against it. | Partially implemented — `DELETE /api/stores/{id}` soft-deletes via `IsActive`, no reason code captured. | @@ -177,9 +195,18 @@ here — see `SRS_User_Management_Module.pdf`. |---|---|---| | `createProduct(skuData)` | Creates a SKU with category, UOM, barcode, pack hierarchy. | Not implemented — `Item` model exists but has no properties; see [01_item_catalog.md](./01_item_catalog.md). | | `deactivateSku(sku, reason)` | Retires a SKU from active ordering. | Not implemented. | +| `listPriceLists(regionId?)` | Lists price lists, optionally filtered by region. | Implemented — `GET /api/price-lists`. | +| `getPriceList(priceListId)` | Fetches a price list and its items. | Implemented — `GET /api/price-lists/{id}`. | | `createPriceList(name, region, effectiveDates)` | Defines a new price list scoped to a region/date range. | Implemented — `POST /api/price-lists`, see [14_pricing_and_suppliers.md](./14_pricing_and_suppliers.md). | -| `updateProductPricing(sku, priceListId, price)` | Sets/updates the price of a SKU within a price list. | Implemented — `PUT /api/price-lists/{id}/items/{sku}`. | +| `updatePriceList(priceListId, name, region, effectiveDates)` | Updates price-list metadata. | Implemented — `PUT /api/price-lists/{id}`. | +| `deletePriceList(priceListId)` | Soft-deletes a price list. | Implemented — `DELETE /api/price-lists/{id}`. | +| `updateProductPricing(sku, priceListId, price, minPrice?)` | Sets/updates the price of a SKU within a price list. | Implemented — `PUT /api/price-lists/{id}/items/{sku}`. | +| `removeProductPricing(sku, priceListId)` | Removes a SKU override from a price list. | Implemented — `DELETE /api/price-lists/{id}/items/{sku}`. | +| `listSuppliers()` | Lists suppliers. | Implemented — `GET /api/suppliers`. | +| `getSupplier(supplierId)` | Fetches a supplier by ID. | Implemented — `GET /api/suppliers/{id}`. | | `createSupplierMaster(supplierData)` | Registers a new supplier/vendor. | Implemented — `POST /api/suppliers`. `PerformanceScore` stays null until [15_procurement.md](./15_procurement.md)'s `evaluateSupplier` exists. | +| `updateSupplier(supplierId, supplierData)` | Updates supplier master data. | Implemented — `PUT /api/suppliers/{id}`. `PerformanceScore` remains read-only here. | +| `deleteSupplier(supplierId)` | Soft-deletes a supplier. | Implemented — `DELETE /api/suppliers/{id}`. Blocked if an active PurchaseOrder references the supplier. | ### 8. Enterprise-wide Inventory & Stock @@ -197,9 +224,21 @@ here — see `SRS_User_Management_Module.pdf`. |---|---|---| | `createPurchaseRequisition(items[], requestedBy)` | Raises an internal request to purchase stock. | Not implemented — no procurement entities exist. | | `createPurchaseOrder(supplierId, items[])` | Issues a PO to a supplier. | Implemented — `POST /api/purchase-orders`, see [15_procurement.md](./15_procurement.md). Also has `updatePurchaseOrder`, `closePurchaseOrder`, `cancelPurchaseOrder`, `cancelPurchaseOrderLine`. | +| `getPurchaseOrder(poId)` | Fetches a purchase order. | Implemented — `GET /api/purchase-orders/{id}`. | +| `listPurchaseOrders(filters)` | Lists purchase orders. | Implemented — `GET /api/purchase-orders`. | +| `updatePurchaseOrder(poId, items[])` | Updates an open purchase order. | Implemented — `PUT /api/purchase-orders/{id}`. | +| `closePurchaseOrder(poId)` | Closes a received purchase order. | Implemented — `POST /api/purchase-orders/{id}/close`. | +| `cancelPurchaseOrder(poId, reason)` | Cancels a purchase order before receipts exist. | Implemented — `POST /api/purchase-orders/{id}/cancel`. | +| `cancelPurchaseOrderLine(poId, poItemId, reason)` | Cancels a single PO line. | Implemented — `POST /api/purchase-orders/{id}/items/{itemId}/cancel`. | | `matchInvoiceToPO(invoiceId, poId, grnId)` | Performs the 3-way match (PO–GRN–Invoice) before payment. | Partially implemented — `POST /api/purchase-orders/{id}/match-invoice` reconciles PurchaseOrderItem vs GoodsReceiptItem quantities/cost only; full 3-way match activates once Finance's `Invoice` entity (§13, not built) exists. `resolveInvoiceVariance` records the mismatch and its resolution. | -| `evaluateSupplier(supplierId, criteria)` | Scores a supplier on delivery/quality/price performance. | Implemented — `POST /api/suppliers/{id}/evaluate`, the only writer of `Supplier.PerformanceScore`. Scored from accepted-vs-rejected quantity ratio and cancelled-vs-fulfilled line ratio across that supplier's PurchaseOrders/GoodsReceipts. | +| `resolveInvoiceVariance(poId, variance, resolution)` | Records how a mismatch should be handled. | Implemented — `POST /api/purchase-orders/{id}/resolve-variance`. | +| `getGoodsReceipt(grnId)` | Fetches a goods receipt. | Implemented — `GET /api/goods-receipts/{id}`. | +| `listGoodsReceipts(poId)` | Lists goods receipts for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/goods-receipts`. | +| `createGoodsReceipt(poId, warehouseId, items[])` | Records inbound stock against a purchase order. | Implemented — `POST /api/purchase-orders/{id}/goods-receipts`. | +| `getPurchaseReturn(returnId)` | Fetches a purchase return. | Implemented — `GET /api/purchase-returns/{id}`. | +| `listPurchaseReturns(poId)` | Lists purchase returns for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/returns`. | | `createPurchaseReturn(poId, items[], reason)` | Initiates a return of received goods to the supplier. | Implemented — `POST /api/purchase-orders/{id}/returns`, see [15_procurement.md](./15_procurement.md). Debits `StockBalance`/`StockMovement` at the PO's receiving warehouse. | +| `evaluateSupplier(supplierId, criteria)` | Scores a supplier on delivery/quality/price performance. | Implemented — `POST /api/suppliers/{id}/evaluate`, the only writer of `Supplier.PerformanceScore`. Scored from accepted-vs-rejected quantity ratio and cancelled-vs-fulfilled line ratio across that supplier's PurchaseOrders/GoodsReceipts. | ### 10. Sales Force Automation (SFA) / CRM @@ -225,19 +264,25 @@ here — see `SRS_User_Management_Module.pdf`. | Function | What it does | Status | |---|---|---| -| `createReturnRequest(orderId, items[], reasonCode)` | Initiates a return against a delivered order. | Not implemented — see [09_fraud_controls.md](./09_fraud_controls.md) for the fraud-control angle on returns. | -| `approveReturn(returnId, approverId)` | Approves/rejects the return request. | Not implemented. | -| `generateCreditNote(returnId)` | Issues a credit note against the original invoice. | Not implemented. | -| `createClaim(agencyId, claimDetails)` | Raises a claim against a Regional Agency (e.g., damaged stock). | Not implemented. | -| `settleClaim(claimId, settlementAmount)` | Closes out a claim with a final settlement amount. | Not implemented. | +| `createSaleReturn(orderId, mode, items[])` | Records a customer return against a delivered order. | Implemented — `POST /api/orders/{id}/returns`, settles immediately and credits the ride's vehicle. No approval step. | +| `getSaleReturn(returnId)` | Fetches a sale return by ID. | Implemented — `GET /api/returns/{id}`. | +| `listSaleReturns(orderId)` | Lists sale returns for an order. | Implemented — `GET /api/orders/{id}/returns`. | +| `createClaim(agencyUserId, details)` | Raises a claim against a Regional Agency user. | Implemented — `POST /api/claims`. | +| `getClaim(claimId)` | Fetches a claim by ID. | Implemented — `GET /api/claims/{id}`. | +| `settleClaim(claimId, settlementAmount)` | Closes out a claim with a final settlement amount. | Implemented — `POST /api/claims/{id}/settle`, requires `claims.settle`. | +| `createSupplierClaim(poId, grnId?, items[], details, evidenceUrl?)` | Raises a supplier claim against a purchase order. | Implemented — `POST /api/purchase-orders/{id}/claims`. | +| `getSupplierClaim(claimId)` | Fetches a supplier claim by ID. | Implemented — `GET /api/supplier-claims/{id}`. | +| `listSupplierClaims(poId)` | Lists supplier claims for a purchase order. | Implemented — `GET /api/purchase-orders/{id}/claims`. | +| `approveSupplierClaim(claimId, resolutionType, resolutionAmount?, notes?)` | Approves a supplier claim. | Implemented — `POST /api/supplier-claims/{id}/approve`, requires `suppliers.claims.approve`. | +| `rejectSupplierClaim(claimId, notes?)` | Rejects a supplier claim. | Implemented — `POST /api/supplier-claims/{id}/reject`, requires `suppliers.claims.approve`. | ### 13. Finance & Accounting | Function | What it does | Status | |---|---|---| -| `createInvoice(orderId)` | Generates a formal invoice from a fulfilled order. | Not implemented — no invoice entity exists. | -| `recordPayment(invoiceId, amount, method)` | Logs a payment/collection against an invoice. | Not implemented. | -| `postToGeneralLedger(transactionId)` | Posts a financial transaction to the GL. | Not implemented. | +| `createInvoice(orderId)` | Generates a formal invoice from a fulfilled order. | Not implemented — no `Invoice` entity exists; `Sale`/`Order` remain the billable record. | +| `recordPayment(invoiceId, amount, method)` | Logs a payment/collection against an invoice. | Implemented against `Sale` rather than an invoice — `POST /api/sales/{saleId}/payments` (`Features/Payment`). Each payment sums against `Sale.TotalAmount` and updates `Sale.PaymentStatus` (`Pending`/`PartiallyPaid`/`Paid`); rejects (400) if the sale is `Cancelled`. `Method` is `Cash` or `Credit` (`Models/Enums.cs`). | +| `postToGeneralLedger(transactionId)` | Posts a financial transaction to the GL. | Not implemented — no GL/ledger entity exists. | | `generateTaxInvoice(invoiceId, jurisdiction)` | Produces a tax-compliant invoice (GST/VAT e-invoicing). | Not implemented. | | `getFinancialReport(entityId, period, reportType)` | Returns P&L/cash-flow reports scoped to a region/area. | Not implemented. | @@ -359,8 +404,9 @@ and approval chains for exceptions. Supervisors can grant one-time overrides log A dedicated audit log records every sensitive event (who, what, when, from where). Automated checks flag: returns without a matching prior sale, sales outside the geofenced route, agents with consistently high variance rates, and cash collected below invoiced amounts. -Return approval requires supervisor sign-off before stock is credited. Anomaly reports are -surfaced to area managers on a scheduled basis. +Return-related processing is handled in §12 as an immediate-settlement flow; this step focuses on +audit logging and anomaly detection around those events. Anomaly reports are surfaced to area +managers on a scheduled basis. **Gate**: Every sensitive event is logged immutably. Automated flags surface to the right level of management. ---