diff --git a/Distribution.Tests/Services/RegionServiceTests.cs b/Distribution.Tests/Services/RegionServiceTests.cs index 358cca7..cb9453d 100644 --- a/Distribution.Tests/Services/RegionServiceTests.cs +++ b/Distribution.Tests/Services/RegionServiceTests.cs @@ -18,6 +18,7 @@ public class RegionServiceTests public RegionServiceTests() { _currentUser.Setup(u => u.RoleLevel).Returns(60); + _repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync([]); _sut = new RegionService(_repo.Object, _uow.Object, _currentUser.Object, _scopeAccess.Object); } @@ -148,6 +149,123 @@ public class RegionServiceTests _uow.Verify(u => u.CommitAsync(default), Times.Once); } + // Region code + + [Fact] + public async Task CreateAsync_GeneratesFirstCode_WhenNoRegionsExist() + { + _repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync([]); + + var result = await _sut.CreateAsync(new CreateRegionRequest("North")); + + Assert.Equal("REG-0001", result.Code); + } + + [Fact] + public async Task CreateAsync_GeneratesNextCode_FromHighestExisting_IgnoringNonMatching() + { + _repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync(["REG-0002", "REG-0009", "CUSTOM", "REG-abc", ""]); + + var result = await _sut.CreateAsync(new CreateRegionRequest("North")); + + Assert.Equal("REG-0010", result.Code); + } + + [Fact] + public async Task CreateAsync_StoresProvidedCode_AsEntered() + { + var result = await _sut.CreateAsync(new CreateRegionRequest("North", " nth-1 ")); + + Assert.Equal("nth-1", result.Code); + _repo.Verify(r => r.GetAllCodesAsync(), Times.Never); + } + + [Fact] + public async Task CreateAsync_Throws_WhenProvidedCodeInUse() + { + _repo.Setup(r => r.CodeExistsAsync("NTH-1", null)).ReturnsAsync(true); + + await Assert.ThrowsAsync(() => + _sut.CreateAsync(new CreateRegionRequest("North", "NTH-1"))); + + _uow.Verify(u => u.CommitAsync(default), Times.Never); + } + + [Fact] + public async Task GetNextCodeAsync_ReturnsSuggestion() + { + _repo.Setup(r => r.GetAllCodesAsync()).ReturnsAsync(["REG-0003"]); + + var result = await _sut.GetNextCodeAsync(); + + Assert.Equal("REG-0004", result.Code); + } + + [Fact] + public async Task UpdateAsync_ChangesCode_StoredAsEntered() + { + var region = ActiveRegion(); + region.Code = "REG-0001"; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.CodeExistsAsync("north-1", region.Id)).ReturnsAsync(false); + + var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", " north-1 ")); + + Assert.Equal("north-1", result!.Code); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task UpdateAsync_KeepsCode_WhenCodeBlank() + { + var region = ActiveRegion(); + region.Code = "REG-0001"; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + + var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", " ")); + + Assert.Equal("REG-0001", result!.Code); + _repo.Verify(r => r.CodeExistsAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task UpdateAsync_Throws_WhenCodeUsedByAnotherRegion() + { + var region = ActiveRegion(); + region.Code = "REG-0001"; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.CodeExistsAsync("REG-0002", region.Id)).ReturnsAsync(true); + + await Assert.ThrowsAsync(() => + _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", "REG-0002"))); + + _uow.Verify(u => u.CommitAsync(default), Times.Never); + } + + [Fact] + public async Task UpdateAsync_AllowsResubmittingOwnCode() + { + var region = ActiveRegion(); + region.Code = "REG-0001"; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + + var result = await _sut.UpdateAsync(region.Id, new UpdateRegionRequest("North", "REG-0001")); + + Assert.Equal("REG-0001", result!.Code); + } + + [Fact] + public async Task GetByIdAsync_ReturnsCode() + { + var region = ActiveRegion(); + region.Code = "REG-0007"; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + + var result = await _sut.GetByIdAsync(region.Id); + + Assert.Equal("REG-0007", result!.Code); + } + // DeleteAsync [Fact] @@ -195,4 +313,93 @@ public class RegionServiceTests _uow.Verify(u => u.RollbackAsync(default), Times.Once); } + + // Region manager + + private static User ManagerUser(string role = "RegionalManager", bool active = true) => new() + { + Id = Guid.NewGuid(), Username = "mgr", FullName = "Mgr One", IsActive = active, + Role = new Role { Name = role, Level = 50 } + }; + + [Fact] + public async Task SetManagerAsync_ReturnsNull_WhenRegionMissing() + { + _repo.Setup(r => r.GetByIdAsync(It.IsAny())).ReturnsAsync((Region?)null); + + Assert.Null(await _sut.SetManagerAsync(Guid.NewGuid(), new SetRegionManagerRequest(Guid.NewGuid()))); + } + + [Fact] + public async Task SetManagerAsync_Throws_WhenUserNotRegionalManager() + { + var region = ActiveRegion(); + var user = ManagerUser("SalesRep"); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + + await Assert.ThrowsAsync(() => + _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id))); + _uow.Verify(u => u.CommitAsync(default), Times.Never); + } + + [Fact] + public async Task SetManagerAsync_ReplacesExistingManager_AndAddsScope() + { + var region = ActiveRegion(); + var user = ManagerUser(); + var old = new UserRegionScope { FK_UserId = Guid.NewGuid(), FK_RegionId = region.Id }; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([old]); + _repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync((UserRegionScope?)null); + + var result = await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id)); + + Assert.Equal(user.Id, result!.UserId); + _repo.Verify(r => r.RemoveRegionScopes(It.Is>(s => s.Single() == old)), Times.Once); + _repo.Verify(r => r.AddRegionScopeAsync(It.Is(s => s.FK_UserId == user.Id && s.FK_RegionId == region.Id)), Times.Once); + _uow.Verify(u => u.CommitAsync(default), Times.Once); + } + + [Fact] + public async Task SetManagerAsync_DoesNotDuplicateScope_WhenAlreadyManager() + { + var region = ActiveRegion(); + var user = ManagerUser(); + var existing = new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id }; + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetUserWithRoleAsync(user.Id)).ReturnsAsync(user); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([existing]); + _repo.Setup(r => r.GetRegionScopeAsync(user.Id, region.Id)).ReturnsAsync(existing); + + await _sut.SetManagerAsync(region.Id, new SetRegionManagerRequest(user.Id)); + + _repo.Verify(r => r.AddRegionScopeAsync(It.IsAny()), Times.Never); + _repo.Verify(r => r.RemoveRegionScopes(It.Is>(s => !s.Any())), Times.Once); + } + + [Fact] + public async Task RemoveManagerAsync_ReturnsFalse_WhenNoManager() + { + var region = ActiveRegion(); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)).ReturnsAsync([]); + + Assert.False(await _sut.RemoveManagerAsync(region.Id)); + } + + [Fact] + public async Task GetManagerAsync_ReturnsManager() + { + var region = ActiveRegion(); + var user = ManagerUser(); + _repo.Setup(r => r.GetByIdAsync(region.Id)).ReturnsAsync(region); + _repo.Setup(r => r.GetManagerScopesAsync(region.Id)) + .ReturnsAsync([new UserRegionScope { FK_UserId = user.Id, FK_RegionId = region.Id, User = user }]); + + var result = await _sut.GetManagerAsync(region.Id); + + Assert.Equal("mgr", result!.Username); + } } diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs index c65d2e3..afab016 100644 --- a/Distribution/Data/AppDBContext.cs +++ b/Distribution/Data/AppDBContext.cs @@ -16,6 +16,8 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet Uoms { get; set; } public DbSet UomConversions { get; set; } public DbSet Items { get; set; } + public DbSet ItemBatches { get; set; } + public DbSet ItemSerialNumbers { get; set; } public DbSet SaleRoutes { get; set; } public DbSet ShopPOs { get; set; } public DbSet ShopPOItems { get; set; } @@ -47,6 +49,8 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet PurchaseOrderItems { get; set; } public DbSet GoodsReceipts { get; set; } public DbSet GoodsReceiptItems { get; set; } + public DbSet GoodsReceiptItemBatches { get; set; } + public DbSet GoodsReceiptItemSerials { get; set; } public DbSet PurchaseReturns { get; set; } public DbSet PurchaseReturnItems { get; set; } public DbSet SaleReturns { get; set; } @@ -83,8 +87,15 @@ public class AppDBContext(DbContextOptions options) : DbContext(op // --- Unique indexes --- + // NOTE: MySQL treats each NULL as distinct in a unique index, so this does not by itself + // prevent duplicate rows for untracked items (both FK columns null) — StockRepository's + // get-then-update pattern is what keeps those to one row per (Item, Location), same as before. modelBuilder.Entity() - .HasIndex(sb => new { sb.FK_ItemId, sb.LocationId }) + .HasIndex(sb => new { sb.FK_ItemId, sb.LocationId, sb.FK_ItemBatchId, sb.FK_ItemSerialNumberId }) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(s => new { s.FK_ItemId, s.SerialNumber }) .IsUnique(); modelBuilder.Entity() @@ -292,6 +303,13 @@ public class AppDBContext(DbContextOptions options) : DbContext(op .HasForeignKey(gr => gr.FK_PurchaseOrderId) .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .HasOne(gr => gr.Supplier) + .WithMany() + .HasForeignKey(gr => gr.FK_SupplierId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + modelBuilder.Entity() .HasOne(gr => gr.Warehouse) .WithMany() @@ -310,12 +328,92 @@ public class AppDBContext(DbContextOptions options) : DbContext(op .HasForeignKey(i => i.FK_PurchaseOrderItemId) .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .HasOne(i => i.Item) + .WithMany() + .HasForeignKey(i => i.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + modelBuilder.Entity() .HasIndex(i => i.FK_GoodsReceiptId); modelBuilder.Entity() .HasIndex(i => i.FK_PurchaseOrderItemId); + // --- ItemBatch / ItemSerialNumber --- + + modelBuilder.Entity() + .HasOne(b => b.Item) + .WithMany() + .HasForeignKey(b => b.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(b => new { b.FK_ItemId, b.BatchNumber }); + + modelBuilder.Entity() + .HasOne(s => s.Item) + .WithMany() + .HasForeignKey(s => s.FK_ItemId) + .OnDelete(DeleteBehavior.Restrict); + + // --- StockBalance / StockMovement batch & serial links --- + + modelBuilder.Entity() + .HasOne(sb => sb.ItemBatch) + .WithMany() + .HasForeignKey(sb => sb.FK_ItemBatchId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + modelBuilder.Entity() + .HasOne(sb => sb.ItemSerialNumber) + .WithMany() + .HasForeignKey(sb => sb.FK_ItemSerialNumberId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + modelBuilder.Entity() + .HasOne(sm => sm.ItemBatch) + .WithMany() + .HasForeignKey(sm => sm.FK_ItemBatchId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + modelBuilder.Entity() + .HasOne(sm => sm.ItemSerialNumber) + .WithMany() + .HasForeignKey(sm => sm.FK_ItemSerialNumberId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + // --- GoodsReceiptItemBatch / GoodsReceiptItemSerial --- + + modelBuilder.Entity() + .HasOne(b => b.GoodsReceiptItem) + .WithMany(i => i.Batches) + .HasForeignKey(b => b.FK_GoodsReceiptItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(b => b.ItemBatch) + .WithMany() + .HasForeignKey(b => b.FK_ItemBatchId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(s => s.GoodsReceiptItem) + .WithMany(i => i.Serials) + .HasForeignKey(s => s.FK_GoodsReceiptItemId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(s => s.ItemSerialNumber) + .WithMany() + .HasForeignKey(s => s.FK_ItemSerialNumberId) + .OnDelete(DeleteBehavior.Restrict); + // --- PurchaseReturn --- modelBuilder.Entity() @@ -446,6 +544,15 @@ public class AppDBContext(DbContextOptions options) : DbContext(op // --- Regional --- + modelBuilder.Entity() + .Property(r => r.Code) + .HasMaxLength(50); + + modelBuilder.Entity() + .HasIndex(r => r.Code) + .IsUnique(); + + modelBuilder.Entity() .HasIndex(t => new { t.FK_RegionId, t.Period }) .IsUnique(); @@ -478,7 +585,13 @@ public class AppDBContext(DbContextOptions options) : DbContext(op .HasMaxLength(100); modelBuilder.Entity().HasData( - new SysConfig { Key = "InvoiceCreationMode", Value = "ModeB", UpdatedAt = new DateTime(2026, 9, 17), UpdatedBy = null } + new SysConfig { Key = "InvoiceCreationMode", Value = "ModeB", UpdatedAt = new DateTime(2026, 9, 17), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeySerialNumberTrackingEnabled, Value = "false", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyBatchTrackingEnabled, Value = "false", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyExpiryDateTrackingEnabled, Value = "false", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyWarrantyTrackingEnabled, Value = "false", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyCustomWarrantyOverrideEnabled, Value = "false", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null }, + new SysConfig { Key = Distribution.Features.SysConfig.SysConfigService.KeyDefaultWarrantyPeriodDays, Value = "0", UpdatedAt = new DateTime(2026, 10, 1), UpdatedBy = null } ); Seed.RoleAndPermissionSeed.Apply(modelBuilder); diff --git a/Distribution/Data/Seed/MasterDataSeed.cs b/Distribution/Data/Seed/MasterDataSeed.cs index 53d8a40..121ee2b 100644 --- a/Distribution/Data/Seed/MasterDataSeed.cs +++ b/Distribution/Data/Seed/MasterDataSeed.cs @@ -84,8 +84,8 @@ public static class MasterDataSeed ); modelBuilder.Entity().HasData( - new Region { Id = WesternRegionId, Name = "Western Province", CreatedAt = SeedDate, IsActive = true }, - new Region { Id = CentralRegionId, Name = "Central Province", CreatedAt = SeedDate, IsActive = true } + new Region { Id = WesternRegionId, Code = "REG-0001", Name = "Western Province", CreatedAt = SeedDate, IsActive = true }, + new Region { Id = CentralRegionId, Code = "REG-0002", Name = "Central Province", CreatedAt = SeedDate, IsActive = true } ); modelBuilder.Entity().HasData( diff --git a/Distribution/Features/Grn/GrnController.cs b/Distribution/Features/Grn/GrnController.cs index f87f56a..7f43379 100644 --- a/Distribution/Features/Grn/GrnController.cs +++ b/Distribution/Features/Grn/GrnController.cs @@ -39,6 +39,20 @@ public class GrnController(GrnService service, ICurrentUserService currentUser) catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } } + /// Goods received without a purchase order. + [HttpPost("api/goods-receipts/direct")] + public async Task CreateDirect(CreateDirectGrnRequest request, CancellationToken ct) + { + var userId = currentUser.UserId ?? request.ReceivedByUserId; + if (userId is null) return BadRequest(new { error = "ReceivedByUserId is required while auth is unavailable" }); + try + { + var result = await service.CreateDirectAsync(request, userId.Value, ct); + return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + } + catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); } + } + [HttpGet("api/purchase-orders/{id:guid}/goods-receipts")] public async Task GetByPurchaseOrder(Guid id) => Ok(await service.GetByPurchaseOrderIdAsync(id)); diff --git a/Distribution/Features/Grn/GrnDTOs.cs b/Distribution/Features/Grn/GrnDTOs.cs index b5d8a02..d4b9692 100644 --- a/Distribution/Features/Grn/GrnDTOs.cs +++ b/Distribution/Features/Grn/GrnDTOs.cs @@ -1,6 +1,19 @@ namespace Distribution.Features.Grn; -public record GrnItemRequest(Guid PurchaseOrderItemId, int AcceptedQuantity, int RejectedQuantity); +/// BatchNumber is null for an expiry-only lot (TracksExpiryDate without TracksBatches). +public record GrnBatchLineRequest(string? BatchNumber, DateTime? ExpiryDate, int Quantity); + +public record GrnItemRequest( + // Set for a PO-based receipt; null for a direct receipt (ItemId is used instead). + Guid? PurchaseOrderItemId, + int AcceptedQuantity, + int RejectedQuantity, + // Populated only when the item has TracksBatches and/or TracksExpiryDate set. + List? Batches = null, + // Populated only when the item has TracksSerialNumbers set — must contain exactly AcceptedQuantity entries. + List? SerialNumbers = null, + // Set for a direct receipt line (no purchase order). + Guid? ItemId = null); public record CreateGrnRequest( Guid WarehouseId, @@ -15,21 +28,35 @@ public record CreateFlatGrnRequest( // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. Guid? ReceivedByUserId = null); +/// A goods receipt with no purchase order. SupplierId is optional. +public record CreateDirectGrnRequest( + Guid WarehouseId, + List Items, + Guid? SupplierId = null, + // TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28. + Guid? ReceivedByUserId = null); + +public record GrnBatchLineResponse(string? BatchNumber, DateTime? ExpiryDate, int Quantity); + public record GrnItemResponse( Guid Id, - Guid PurchaseOrderItemId, + Guid? PurchaseOrderItemId, Guid ItemId, string Sku, string ItemName, int AcceptedQuantity, - int RejectedQuantity); + int RejectedQuantity, + List Batches, + List SerialNumbers); public record GrnResponse( Guid Id, - Guid PurchaseOrderId, + Guid? PurchaseOrderId, Guid ReceivedByUserId, Guid WarehouseId, string WarehouseName, + Guid? SupplierId, + string? SupplierName, DateTime ReceivedAt, DateTime CreatedAt, List Items); diff --git a/Distribution/Features/Grn/GrnRepository.cs b/Distribution/Features/Grn/GrnRepository.cs index 22cc657..01181f7 100644 --- a/Distribution/Features/Grn/GrnRepository.cs +++ b/Distribution/Features/Grn/GrnRepository.cs @@ -8,13 +8,21 @@ public class GrnRepository(AppDBContext context) : IGrnRepository public async Task GetByIdAsync(Guid id) => await context.GoodsReceipts .Include(gr => gr.Warehouse) - .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi.Item) + .Include(gr => gr.Supplier) + .Include(gr => gr.Items).ThenInclude(i => i.Item) + .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi!.Item) + .Include(gr => gr.Items).ThenInclude(i => i.Batches).ThenInclude(b => b.ItemBatch) + .Include(gr => gr.Items).ThenInclude(i => i.Serials).ThenInclude(s => s.ItemSerialNumber) .FirstOrDefaultAsync(gr => gr.Id == id); public async Task> GetAllAsync(Guid? purchaseOrderId, int page, int pageSize) => await context.GoodsReceipts .Include(gr => gr.Warehouse) - .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi.Item) + .Include(gr => gr.Supplier) + .Include(gr => gr.Items).ThenInclude(i => i.Item) + .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi!.Item) + .Include(gr => gr.Items).ThenInclude(i => i.Batches).ThenInclude(b => b.ItemBatch) + .Include(gr => gr.Items).ThenInclude(i => i.Serials).ThenInclude(s => s.ItemSerialNumber) .Where(gr => gr.IsActive && (purchaseOrderId == null || gr.FK_PurchaseOrderId == purchaseOrderId)) .OrderByDescending(gr => gr.ReceivedAt) .Skip((page - 1) * pageSize) @@ -29,7 +37,11 @@ public class GrnRepository(AppDBContext context) : IGrnRepository public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => await context.GoodsReceipts .Include(gr => gr.Warehouse) - .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi.Item) + .Include(gr => gr.Supplier) + .Include(gr => gr.Items).ThenInclude(i => i.Item) + .Include(gr => gr.Items).ThenInclude(i => i.PurchaseOrderItem).ThenInclude(poi => poi!.Item) + .Include(gr => gr.Items).ThenInclude(i => i.Batches).ThenInclude(b => b.ItemBatch) + .Include(gr => gr.Items).ThenInclude(i => i.Serials).ThenInclude(s => s.ItemSerialNumber) .Where(gr => gr.FK_PurchaseOrderId == purchaseOrderId && gr.IsActive) .OrderByDescending(gr => gr.ReceivedAt) .ToListAsync(); @@ -43,8 +55,8 @@ public class GrnRepository(AppDBContext context) : IGrnRepository public async Task> GetAcceptedTotalsByItemAsync(Guid purchaseOrderId) => await context.GoodsReceiptItems .Include(i => i.PurchaseOrderItem) - .Where(i => i.PurchaseOrderItem.FK_PurchaseOrderId == purchaseOrderId && i.IsActive) - .GroupBy(i => i.PurchaseOrderItem.FK_ItemId) + .Where(i => i.PurchaseOrderItem!.FK_PurchaseOrderId == purchaseOrderId && i.IsActive) + .GroupBy(i => i.PurchaseOrderItem!.FK_ItemId) .Select(g => new { ItemId = g.Key, Total = g.Sum(i => i.AcceptedQuantity) }) .ToDictionaryAsync(x => x.ItemId, x => x.Total); } diff --git a/Distribution/Features/Grn/GrnService.cs b/Distribution/Features/Grn/GrnService.cs index 10bae5f..dd5df45 100644 --- a/Distribution/Features/Grn/GrnService.cs +++ b/Distribution/Features/Grn/GrnService.cs @@ -9,14 +9,23 @@ public class GrnService( IGrnRepository repo, IPoRepository poRepo, IStockRepository stockRepo, - IUnitOfWork uow) + IUnitOfWork uow, + Distribution.Features.Item.IItemRepository itemRepo, + Distribution.Features.Supplier.ISupplierRepository supplierRepo) { - private static GrnItemResponse MapItem(Models.GoodsReceiptItem i) => - new(i.Id, i.FK_PurchaseOrderItemId, i.PurchaseOrderItem.FK_ItemId, i.PurchaseOrderItem.Item.Sku, - i.PurchaseOrderItem.Item.Name, i.AcceptedQuantity, i.RejectedQuantity); + private static GrnItemResponse MapItem(Models.GoodsReceiptItem i) + { + // Direct-receipt lines carry the item themselves; PO lines resolve it through the PO line. + var item = i.Item ?? i.PurchaseOrderItem!.Item; + return new(i.Id, i.FK_PurchaseOrderItemId, item.Id, item.Sku, item.Name, + i.AcceptedQuantity, i.RejectedQuantity, + i.Batches.Select(b => new GrnBatchLineResponse(b.ItemBatch.BatchNumber, b.ItemBatch.ExpiryDate, b.Quantity)).ToList(), + i.Serials.Select(s => s.ItemSerialNumber.SerialNumber).ToList()); + } private static GrnResponse Map(Models.GoodsReceipt gr) => new(gr.Id, gr.FK_PurchaseOrderId, gr.FK_ReceivedByUserId, gr.FK_WarehouseId, gr.Warehouse?.Name ?? string.Empty, + gr.FK_SupplierId, gr.Supplier?.Name, gr.ReceivedAt, gr.CreatedAt, gr.Items.Where(i => i.IsActive).Select(MapItem).ToList()); public async Task> GetAllAsync(Guid? purchaseOrderId, int page, int pageSize) @@ -36,10 +45,60 @@ public class GrnService( public async Task> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) => (await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList(); + /// + /// Validates that a GRN line's batch/serial payload matches the item's tracking flags. When an + /// item tracks both serial numbers and batches/expiry, serial tracking takes precedence for + /// stock-ledger granularity in this phase — batch/expiry metadata for that combination is not + /// yet linked into the ledger (follow-up work). + /// + private static void ValidateLineShape(Models.Item item, GrnItemRequest line) + { + if (item.TracksSerialNumbers) + { + if (line.AcceptedQuantity > 0 && (line.SerialNumbers is null || line.SerialNumbers.Count != line.AcceptedQuantity)) + throw new InvalidOperationException( + $"Item {item.Sku} tracks serial numbers — expected {line.AcceptedQuantity} serial number(s)."); + + if (line.SerialNumbers is { Count: > 0 } && line.SerialNumbers.Distinct().Count() != line.SerialNumbers.Count) + throw new InvalidOperationException($"Duplicate serial numbers supplied for item {item.Sku}."); + } + else if (line.SerialNumbers is { Count: > 0 }) + { + throw new InvalidOperationException($"Item {item.Sku} does not track serial numbers."); + } + else if (item.TracksBatches || item.TracksExpiryDate) + { + if (line.AcceptedQuantity > 0) + { + if (line.Batches is null || line.Batches.Count == 0) + throw new InvalidOperationException($"Item {item.Sku} requires batch/expiry information."); + if (line.Batches.Sum(b => b.Quantity) != line.AcceptedQuantity) + throw new InvalidOperationException($"Batch quantities for item {item.Sku} must sum to the accepted quantity."); + } + + foreach (var b in line.Batches ?? []) + { + if (b.Quantity <= 0) + throw new InvalidOperationException($"Batch quantity for item {item.Sku} must be positive."); + if (item.TracksBatches && string.IsNullOrWhiteSpace(b.BatchNumber)) + throw new InvalidOperationException($"Item {item.Sku} requires a batch number."); + if (!item.TracksBatches && !string.IsNullOrWhiteSpace(b.BatchNumber)) + throw new InvalidOperationException($"Item {item.Sku} does not track batch numbers."); + if (item.TracksExpiryDate && b.ExpiryDate is null) + throw new InvalidOperationException($"Item {item.Sku} requires an expiry date."); + } + } + else if (line.Batches is { Count: > 0 }) + { + throw new InvalidOperationException($"Item {item.Sku} does not track batches or expiry dates."); + } + } + /// /// Validates the Po status, increments PurchaseOrderItem.ReceivedQuantity, writes StockBalance/ - /// StockMovement for AcceptedQuantity only (RejectedQuantity never touches stock), and - /// transitions the Po to PartiallyReceived/Received. All in one unit of work. + /// StockMovement for AcceptedQuantity only (RejectedQuantity never touches stock), creates + /// ItemBatch/ItemSerialNumber records for tracked items, and transitions the Po to + /// PartiallyReceived/Received. All in one unit of work. /// public async Task CreateAsync(Guid purchaseOrderId, CreateGrnRequest request, Guid receivedByUserId, CancellationToken ct = default) { @@ -64,8 +123,13 @@ public class GrnService( ReceivedAt = DateTime.Now }; + var grLines = new List<(Models.GoodsReceiptItem Entity, GrnItemRequest Line, Models.Item Item)>(); + foreach (var line in request.Items) { + if (line.PurchaseOrderItemId is null) + throw new InvalidOperationException("Each line of a PO receipt needs a PurchaseOrderItemId."); + var poItem = po.Items.FirstOrDefault(i => i.Id == line.PurchaseOrderItemId && i.IsActive) ?? throw new InvalidOperationException($"Purchase order line {line.PurchaseOrderItemId} not found on this Po."); @@ -79,14 +143,27 @@ public class GrnService( throw new InvalidOperationException( $"Accepted + rejected quantity for item {poItem.FK_ItemId} would exceed the ordered quantity."); + ValidateLineShape(poItem.Item, line); + + if (poItem.Item.TracksSerialNumbers && line.SerialNumbers is { Count: > 0 }) + { + foreach (var serialNumber in line.SerialNumbers) + { + if (await stockRepo.SerialNumberExistsAsync(poItem.FK_ItemId, serialNumber)) + throw new InvalidOperationException($"Serial number '{serialNumber}' already exists for item {poItem.Item.Sku}."); + } + } + poItem.ReceivedQuantity += line.AcceptedQuantity; - goodsReceipt.Items.Add(new Models.GoodsReceiptItem + var entity = new Models.GoodsReceiptItem { FK_PurchaseOrderItemId = poItem.Id, AcceptedQuantity = line.AcceptedQuantity, RejectedQuantity = line.RejectedQuantity - }); + }; + goodsReceipt.Items.Add(entity); + grLines.Add((entity, line, poItem.Item)); } var activeLines = po.Items.Where(i => i.IsActive && !i.IsCancelled).ToList(); @@ -94,48 +171,230 @@ public class GrnService( ? PurchaseOrderStatus.Received : PurchaseOrderStatus.PartiallyReceived; + return await PostReceiptAsync(goodsReceipt, grLines, request.WarehouseId, receivedByUserId, po, ct); + } + /// + /// A receipt with no purchase order: lines name the item directly. Stock, serial and batch + /// handling is identical to a PO receipt; there is no PO quantity or status to update. + /// + public async Task CreateDirectAsync(CreateDirectGrnRequest request, Guid receivedByUserId, CancellationToken ct = default) + { + if (request.Items is null || request.Items.Count == 0) + throw new InvalidOperationException("A goods receipt must have at least one line item."); + + if (request.SupplierId is not null && await supplierRepo.GetByIdAsync(request.SupplierId.Value) is not { IsActive: true }) + throw new InvalidOperationException("Supplier not found."); + + var goodsReceipt = new Models.GoodsReceipt + { + FK_PurchaseOrderId = null, + FK_SupplierId = request.SupplierId, + FK_ReceivedByUserId = receivedByUserId, + FK_WarehouseId = request.WarehouseId, + ReceivedAt = DateTime.Now + }; + + var grLines = new List<(Models.GoodsReceiptItem Entity, GrnItemRequest Line, Models.Item Item)>(); + var serialsInRequest = new HashSet<(Guid ItemId, string Serial)>(); + + foreach (var line in request.Items) + { + if (line.ItemId is null) + throw new InvalidOperationException("Each line of a direct receipt needs an ItemId."); + + var item = await itemRepo.GetByIdAsync(line.ItemId.Value); + if (item is null || !item.IsActive) + throw new InvalidOperationException($"Item {line.ItemId} not found."); + + if (line.AcceptedQuantity < 0 || line.RejectedQuantity < 0) + throw new InvalidOperationException("Accepted/rejected quantities cannot be negative."); + + if (line.AcceptedQuantity + line.RejectedQuantity == 0) + throw new InvalidOperationException($"Line for item {item.Sku} has no quantity."); + + ValidateLineShape(item, line); + + if (item.TracksSerialNumbers && line.SerialNumbers is { Count: > 0 }) + { + foreach (var serialNumber in line.SerialNumbers) + { + if (!serialsInRequest.Add((item.Id, serialNumber))) + throw new InvalidOperationException($"Serial number '{serialNumber}' appears on more than one line for item {item.Sku}."); + if (await stockRepo.SerialNumberExistsAsync(item.Id, serialNumber)) + throw new InvalidOperationException($"Serial number '{serialNumber}' already exists for item {item.Sku}."); + } + } + + var entity = new Models.GoodsReceiptItem + { + FK_ItemId = item.Id, + AcceptedQuantity = line.AcceptedQuantity, + RejectedQuantity = line.RejectedQuantity + }; + goodsReceipt.Items.Add(entity); + grLines.Add((entity, line, item)); + } + + return await PostReceiptAsync(goodsReceipt, grLines, request.WarehouseId, receivedByUserId, null, ct); + } + + /// + /// Writes the receipt, its serials/batches, the stock balances and movements, and (for a PO + /// receipt) the PO update in one unit of work. Shared by PO-based and direct receipts. + /// + private async Task PostReceiptAsync( + Models.GoodsReceipt goodsReceipt, + List<(Models.GoodsReceiptItem Entity, GrnItemRequest Line, Models.Item Item)> grLines, + Guid warehouseId, + Guid receivedByUserId, + Models.PurchaseOrder? po, + CancellationToken ct) + { await uow.BeginAsync(ct); try { await repo.AddAsync(goodsReceipt); - foreach (var line in goodsReceipt.Items.Where(i => i.AcceptedQuantity > 0)) + foreach (var (entity, line, item) in grLines.Where(x => x.Line.AcceptedQuantity > 0)) { - var poItem = po.Items.First(i => i.Id == line.FK_PurchaseOrderItemId); - - var balance = await stockRepo.GetBalanceAsync(poItem.FK_ItemId, request.WarehouseId); - if (balance is null) + if (item.TracksSerialNumbers) { - await stockRepo.AddBalanceAsync(new StockBalance + foreach (var serialNumber in line.SerialNumbers!) { - FK_ItemId = poItem.FK_ItemId, - LocationId = request.WarehouseId, - LocationType = LocationType.Warehouse, - Quantity = line.AcceptedQuantity - }); + var serial = new ItemSerialNumber + { + FK_ItemId = item.Id, + SerialNumber = serialNumber, + Status = SerialStatus.InStock, + CurrentLocationId = warehouseId, + CurrentLocationType = LocationType.Warehouse, + ReceivedAt = goodsReceipt.ReceivedAt + }; + + // Warranty is a customer-facing promise: the clock starts when the serial is sold, + // not at receipt. Only snapshot the item's custom period here; WarrantyExpiresAt + // stays null until the sale/delivery flow sets it. + if (item.HasWarranty && item.AllowCustomWarrantyPeriod) + serial.WarrantyPeriodDaysOverride = item.WarrantyPeriodDays; + + await stockRepo.AddItemSerialNumberAsync(serial); + entity.Serials.Add(new GoodsReceiptItemSerial { FK_GoodsReceiptItemId = entity.Id, ItemSerialNumber = serial }); + + await stockRepo.AddBalanceAsync(new StockBalance + { + FK_ItemId = item.Id, + LocationId = warehouseId, + LocationType = LocationType.Warehouse, + FK_ItemSerialNumberId = serial.Id, + Quantity = 1 + }); + + await stockRepo.AddMovementAsync(new StockMovement + { + FK_ItemId = item.Id, + FromLocationId = StockService.ExternalLocationId, + FromLocationType = LocationType.External, + ToLocationId = warehouseId, + ToLocationType = LocationType.Warehouse, + FK_ItemSerialNumberId = serial.Id, + Quantity = 1, + Type = MovementType.PurchaseReceipt, + ReferenceId = goodsReceipt.Id, + CreatedBy = receivedByUserId + }); + } + } + else if (item.TracksBatches || item.TracksExpiryDate) + { + foreach (var b in line.Batches!) + { + var batch = !string.IsNullOrWhiteSpace(b.BatchNumber) + ? await stockRepo.FindBatchAsync(item.Id, b.BatchNumber) + : null; + + if (batch is null) + { + batch = new ItemBatch + { + FK_ItemId = item.Id, + BatchNumber = b.BatchNumber, + ExpiryDate = b.ExpiryDate + }; + await stockRepo.AddItemBatchAsync(batch); + } + + entity.Batches.Add(new GoodsReceiptItemBatch { FK_GoodsReceiptItemId = entity.Id, ItemBatch = batch, Quantity = b.Quantity }); + + var balance = await stockRepo.GetBalanceAsync(item.Id, warehouseId, batch.Id, null); + if (balance is null) + { + await stockRepo.AddBalanceAsync(new StockBalance + { + FK_ItemId = item.Id, + LocationId = warehouseId, + LocationType = LocationType.Warehouse, + FK_ItemBatchId = batch.Id, + Quantity = b.Quantity + }); + } + else + { + balance.Quantity += b.Quantity; + balance.UpdatedAt = DateTime.Now; + stockRepo.UpdateBalance(balance); + } + + await stockRepo.AddMovementAsync(new StockMovement + { + FK_ItemId = item.Id, + FromLocationId = StockService.ExternalLocationId, + FromLocationType = LocationType.External, + ToLocationId = warehouseId, + ToLocationType = LocationType.Warehouse, + FK_ItemBatchId = batch.Id, + Quantity = b.Quantity, + Type = MovementType.PurchaseReceipt, + ReferenceId = goodsReceipt.Id, + CreatedBy = receivedByUserId + }); + } } else { - balance.Quantity += line.AcceptedQuantity; - balance.UpdatedAt = DateTime.Now; - stockRepo.UpdateBalance(balance); - } + var balance = await stockRepo.GetBalanceAsync(item.Id, warehouseId); + if (balance is null) + { + await stockRepo.AddBalanceAsync(new StockBalance + { + FK_ItemId = item.Id, + LocationId = warehouseId, + LocationType = LocationType.Warehouse, + Quantity = line.AcceptedQuantity + }); + } + else + { + balance.Quantity += line.AcceptedQuantity; + balance.UpdatedAt = DateTime.Now; + stockRepo.UpdateBalance(balance); + } - await stockRepo.AddMovementAsync(new StockMovement - { - FK_ItemId = poItem.FK_ItemId, - FromLocationId = StockService.ExternalLocationId, - FromLocationType = LocationType.External, - ToLocationId = request.WarehouseId, - ToLocationType = LocationType.Warehouse, - Quantity = line.AcceptedQuantity, - Type = MovementType.PurchaseReceipt, - ReferenceId = goodsReceipt.Id, - CreatedBy = receivedByUserId - }); + await stockRepo.AddMovementAsync(new StockMovement + { + FK_ItemId = item.Id, + FromLocationId = StockService.ExternalLocationId, + FromLocationType = LocationType.External, + ToLocationId = warehouseId, + ToLocationType = LocationType.Warehouse, + Quantity = line.AcceptedQuantity, + Type = MovementType.PurchaseReceipt, + ReferenceId = goodsReceipt.Id, + CreatedBy = receivedByUserId + }); + } } - poRepo.Update(po); + if (po is not null) poRepo.Update(po); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } diff --git a/Distribution/Features/Item/ItemDTOs.cs b/Distribution/Features/Item/ItemDTOs.cs index 977980c..d973780 100644 --- a/Distribution/Features/Item/ItemDTOs.cs +++ b/Distribution/Features/Item/ItemDTOs.cs @@ -12,7 +12,13 @@ public record CreateItemRequest( Guid BaseUomId, decimal? SalePrice, List? Conversions, - decimal? ReorderLevel = null); + decimal? ReorderLevel = null, + bool TracksSerialNumbers = false, + bool TracksBatches = false, + bool TracksExpiryDate = false, + bool HasWarranty = false, + int? WarrantyPeriodDays = null, + bool AllowCustomWarrantyPeriod = false); public record UpdateItemRequest( string Name, @@ -23,7 +29,13 @@ public record UpdateItemRequest( Guid BaseUomId, decimal? SalePrice, bool IsActive = true, - decimal? ReorderLevel = null); + decimal? ReorderLevel = null, + bool TracksSerialNumbers = false, + bool TracksBatches = false, + bool TracksExpiryDate = false, + bool HasWarranty = false, + int? WarrantyPeriodDays = null, + bool AllowCustomWarrantyPeriod = false); public record ReplaceConversionsRequest(List Conversions); @@ -43,6 +55,12 @@ public record ItemSummaryResponse( string BaseUomName, decimal? SalePrice, decimal? ReorderLevel, + bool TracksSerialNumbers, + bool TracksBatches, + bool TracksExpiryDate, + bool HasWarranty, + int? WarrantyPeriodDays, + bool AllowCustomWarrantyPeriod, Models.SyncSource SyncSource, bool IsActive, DateTime CreatedAt); @@ -62,6 +80,12 @@ public record ItemDetailResponse( string BaseUomName, decimal? SalePrice, decimal? ReorderLevel, + bool TracksSerialNumbers, + bool TracksBatches, + bool TracksExpiryDate, + bool HasWarranty, + int? WarrantyPeriodDays, + bool AllowCustomWarrantyPeriod, Models.SyncSource SyncSource, bool IsActive, DateTime CreatedAt, diff --git a/Distribution/Features/Item/ItemService.cs b/Distribution/Features/Item/ItemService.cs index 82b76a0..a796fda 100644 --- a/Distribution/Features/Item/ItemService.cs +++ b/Distribution/Features/Item/ItemService.cs @@ -2,7 +2,7 @@ using Distribution.Infra.UoW; namespace Distribution.Features.Item; -public class ItemService(IItemRepository repo, IUnitOfWork uow) +public class ItemService(IItemRepository repo, IUnitOfWork uow, SysConfig.SysConfigService sysConfig) { private static UomConversionResponse MapConversion(Models.UomConversion c) => new(c.Id, c.FK_FromUomId, c.FromUom.Name, c.FK_ToUomId, c.ToUom.Name, c.Factor); @@ -13,7 +13,10 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) i.FK_SubCategoryId, i.SubCategory?.Name, i.FK_BrandId, i.Brand?.Name, i.FK_BaseUomId, i.BaseUom.Name, - i.SalePrice, i.ReorderLevel, i.SyncSource, i.IsActive, i.CreatedAt); + i.SalePrice, i.ReorderLevel, + i.TracksSerialNumbers, i.TracksBatches, i.TracksExpiryDate, + i.HasWarranty, i.WarrantyPeriodDays, i.AllowCustomWarrantyPeriod, + i.SyncSource, i.IsActive, i.CreatedAt); private static ItemDetailResponse MapDetail(Models.Item i) => new(i.Id, i.Sku, i.Name, i.Description, @@ -21,9 +24,44 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) i.FK_SubCategoryId, i.SubCategory?.Name, i.FK_BrandId, i.Brand?.Name, i.FK_BaseUomId, i.BaseUom.Name, - i.SalePrice, i.ReorderLevel, i.SyncSource, i.IsActive, i.CreatedAt, + i.SalePrice, i.ReorderLevel, + i.TracksSerialNumbers, i.TracksBatches, i.TracksExpiryDate, + i.HasWarranty, i.WarrantyPeriodDays, i.AllowCustomWarrantyPeriod, + i.SyncSource, i.IsActive, i.CreatedAt, i.Conversions.Select(MapConversion).ToList()); + /// + /// Rejects per-item tracking flags that are off system-wide, and warranty-period values that + /// don't make sense given HasWarranty/AllowCustomWarrantyPeriod. + /// + private async Task ValidateTrackingFlagsAsync(bool tracksSerialNumbers, bool tracksBatches, + bool tracksExpiryDate, bool hasWarranty, int? warrantyPeriodDays, bool allowCustomWarrantyPeriod) + { + if (tracksSerialNumbers && !await sysConfig.GetSerialNumberTrackingEnabledAsync()) + throw new InvalidOperationException("Serial number tracking is disabled system-wide."); + + if (tracksBatches && !await sysConfig.GetBatchTrackingEnabledAsync()) + throw new InvalidOperationException("Batch tracking is disabled system-wide."); + + if (tracksExpiryDate && !await sysConfig.GetExpiryDateTrackingEnabledAsync()) + throw new InvalidOperationException("Expiry date tracking is disabled system-wide."); + + if (hasWarranty && !await sysConfig.GetWarrantyTrackingEnabledAsync()) + throw new InvalidOperationException("Warranty tracking is disabled system-wide."); + + if (allowCustomWarrantyPeriod && !await sysConfig.GetCustomWarrantyOverrideEnabledAsync()) + throw new InvalidOperationException("Custom warranty period override is disabled system-wide."); + + if (!hasWarranty && (warrantyPeriodDays is not null || allowCustomWarrantyPeriod)) + throw new InvalidOperationException("Warranty period/override can only be set when HasWarranty is true."); + + if (warrantyPeriodDays is not null && !allowCustomWarrantyPeriod) + throw new InvalidOperationException("WarrantyPeriodDays can only be set when AllowCustomWarrantyPeriod is true."); + + if (warrantyPeriodDays is < 0) + throw new InvalidOperationException("WarrantyPeriodDays cannot be negative."); + } + public async Task> GetAllAsync(Guid? categoryId = null, Guid? subCategoryId = null, Guid? brandId = null) => (await repo.GetAllAsync(categoryId, subCategoryId, brandId)) .Select(MapSummary) @@ -41,6 +79,9 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) if (await repo.SkuExistsAsync(request.Sku)) throw new InvalidOperationException($"SKU '{request.Sku}' already exists."); + await ValidateTrackingFlagsAsync(request.TracksSerialNumbers, request.TracksBatches, + request.TracksExpiryDate, request.HasWarranty, request.WarrantyPeriodDays, request.AllowCustomWarrantyPeriod); + var item = new Models.Item { Sku = request.Sku, @@ -52,6 +93,12 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) FK_BaseUomId = request.BaseUomId, SalePrice = request.SalePrice, ReorderLevel = request.ReorderLevel, + TracksSerialNumbers = request.TracksSerialNumbers, + TracksBatches = request.TracksBatches, + TracksExpiryDate = request.TracksExpiryDate, + HasWarranty = request.HasWarranty, + WarrantyPeriodDays = request.WarrantyPeriodDays, + AllowCustomWarrantyPeriod = request.AllowCustomWarrantyPeriod, SyncSource = Models.SyncSource.Manual }; @@ -78,6 +125,9 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) if (item is null) return null; if (item.SyncSource == Models.SyncSource.Erp) return null; + await ValidateTrackingFlagsAsync(request.TracksSerialNumbers, request.TracksBatches, + request.TracksExpiryDate, request.HasWarranty, request.WarrantyPeriodDays, request.AllowCustomWarrantyPeriod); + item.Name = request.Name; item.Description = request.Description; item.FK_CategoryId = request.CategoryId; @@ -86,6 +136,12 @@ public class ItemService(IItemRepository repo, IUnitOfWork uow) item.FK_BaseUomId = request.BaseUomId; item.SalePrice = request.SalePrice; item.ReorderLevel = request.ReorderLevel; + item.TracksSerialNumbers = request.TracksSerialNumbers; + item.TracksBatches = request.TracksBatches; + item.TracksExpiryDate = request.TracksExpiryDate; + item.HasWarranty = request.HasWarranty; + item.WarrantyPeriodDays = request.WarrantyPeriodDays; + item.AllowCustomWarrantyPeriod = request.AllowCustomWarrantyPeriod; item.IsActive = request.IsActive; await uow.BeginAsync(ct); diff --git a/Distribution/Features/ProductReport/IProductReportRepository.cs b/Distribution/Features/ProductReport/IProductReportRepository.cs new file mode 100644 index 0000000..2063ca3 --- /dev/null +++ b/Distribution/Features/ProductReport/IProductReportRepository.cs @@ -0,0 +1,18 @@ +using Distribution.Models; + +namespace Distribution.Features.ProductReport; + +public record SalesAggregate(Guid ItemId, string Sku, string Name, decimal? CostPrice, + int Orders, int Stores, decimal QtyOrdered, decimal QtyFulfilled, decimal SalesValue); + +public record ReturnAggregate(Guid ItemId, string Sku, string Name, ReturnReasonCode Reason, + decimal Qty, decimal Value); + +public interface IProductReportRepository +{ + Task<(List Rows, int Total)> GetStockAsync(ProductReportFilter f, int page, int pageSize); + Task<(List Rows, int Total)> GetSalesAsync(ProductReportFilter f, int page, int pageSize); + Task> GetReturnsAsync(ProductReportFilter f); + Task> GetSoldQtyAsync(ProductReportFilter f, IEnumerable itemIds); + Task GetTotalSalesAsync(ProductReportFilter f); +} diff --git a/Distribution/Features/ProductReport/ProductReportController.cs b/Distribution/Features/ProductReport/ProductReportController.cs new file mode 100644 index 0000000..1d29f42 --- /dev/null +++ b/Distribution/Features/ProductReport/ProductReportController.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.ProductReport; + +[ApiController] +[Route("api/reports/products")] +public class ProductReportController(ProductReportService service) : ControllerBase +{ + private IActionResult? Check(ProductReportFilter f, int page, int pageSize) + { + var error = ProductReportService.Validate(f, page, pageSize); + return error is null ? null : BadRequest(new { message = error }); + } + + [HttpGet("stock")] + public async Task Stock( + [FromQuery] Guid? brandId, [FromQuery] Guid? categoryId, [FromQuery] Guid? subCategoryId, + [FromQuery] Guid? warehouseId, [FromQuery] bool activeOnly = true, [FromQuery] bool lowStockOnly = false, + [FromQuery] string? search = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var f = new ProductReportFilter(brandId, categoryId, subCategoryId, warehouseId, null, null, null, + null, null, activeOnly, lowStockOnly, search); + return Check(f, page, pageSize) ?? Ok(await service.GetStockAsync(f, page, pageSize)); + } + + [HttpGet("sales")] + public async Task Sales( + [FromQuery] Guid? brandId, [FromQuery] Guid? categoryId, [FromQuery] Guid? subCategoryId, + [FromQuery] Guid? regionId, [FromQuery] Guid? routeId, [FromQuery] Guid? storeId, + [FromQuery] DateTime? from, [FromQuery] DateTime? to, + [FromQuery] string? search = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var f = new ProductReportFilter(brandId, categoryId, subCategoryId, null, regionId, routeId, storeId, + from, to, false, false, search); + return Check(f, page, pageSize) ?? Ok(await service.GetSalesAsync(f, page, pageSize)); + } + + [HttpGet("returns")] + public async Task Returns( + [FromQuery] Guid? brandId, [FromQuery] Guid? categoryId, [FromQuery] Guid? subCategoryId, + [FromQuery] Guid? regionId, [FromQuery] Guid? routeId, [FromQuery] Guid? storeId, + [FromQuery] DateTime? from, [FromQuery] DateTime? to, + [FromQuery] string? search = null, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + var f = new ProductReportFilter(brandId, categoryId, subCategoryId, null, regionId, routeId, storeId, + from, to, false, false, search); + return Check(f, page, pageSize) ?? Ok(await service.GetReturnsAsync(f, page, pageSize)); + } + + [HttpGet("summary")] + public async Task Summary( + [FromQuery] Guid? brandId, [FromQuery] Guid? categoryId, [FromQuery] Guid? warehouseId, + [FromQuery] DateTime? from, [FromQuery] DateTime? to) + { + var f = new ProductReportFilter(brandId, categoryId, null, warehouseId, null, null, null, + from, to, true, false, null); + return Check(f, 1, 1) ?? Ok(await service.GetSummaryAsync(f)); + } +} diff --git a/Distribution/Features/ProductReport/ProductReportDTOs.cs b/Distribution/Features/ProductReport/ProductReportDTOs.cs new file mode 100644 index 0000000..d961568 --- /dev/null +++ b/Distribution/Features/ProductReport/ProductReportDTOs.cs @@ -0,0 +1,26 @@ +using Distribution.Models; + +namespace Distribution.Features.ProductReport; + +public record ProductStockRow( + Guid ItemId, string Sku, string Name, string? Brand, string Category, string? SubCategory, string Uom, + decimal? SalePrice, decimal? CostPrice, decimal? ReorderLevel, + decimal WarehouseQty, decimal VehicleQty, decimal TotalQty, decimal StockValue, + bool IsLowStock, bool IsActive); + +public record ProductSalesRow( + Guid ItemId, string Sku, string Name, + int Orders, int Stores, decimal QtyOrdered, decimal QtyFulfilled, + decimal SalesValue, decimal? CostValue, decimal? Margin, decimal? MarginPercent); + +public record ProductReturnRow( + Guid ItemId, string Sku, string Name, + decimal ReturnedQty, decimal ReturnedValue, decimal SoldQty, decimal? ReturnPercent, + Dictionary QtyByReason); + +public record ProductReportSummary(int ItemCount, decimal TotalQty, decimal TotalValue, decimal TotalSales); + +public record ProductReportFilter( + Guid? BrandId, Guid? CategoryId, Guid? SubCategoryId, Guid? WarehouseId, + Guid? RegionId, Guid? RouteId, Guid? StoreId, + DateTime? From, DateTime? To, bool ActiveOnly, bool LowStockOnly, string? Search); diff --git a/Distribution/Features/ProductReport/ProductReportRepository.cs b/Distribution/Features/ProductReport/ProductReportRepository.cs new file mode 100644 index 0000000..a1a75e6 --- /dev/null +++ b/Distribution/Features/ProductReport/ProductReportRepository.cs @@ -0,0 +1,156 @@ +using Distribution.Data; +using Distribution.Models; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.ProductReport; + +public class ProductReportRepository(AppDBContext db) : IProductReportRepository +{ + public async Task<(List Rows, int Total)> GetStockAsync(ProductReportFilter f, int page, int pageSize) + { + var balances = db.StockBalances.AsNoTracking().AsQueryable(); + if (f.WarehouseId.HasValue) + balances = balances.Where(b => b.LocationType == LocationType.Warehouse && b.LocationId == f.WarehouseId.Value); + + var items = db.Items.AsNoTracking().AsQueryable(); + if (f.ActiveOnly) items = items.Where(i => i.IsActive); + if (f.BrandId.HasValue) items = items.Where(i => i.FK_BrandId == f.BrandId.Value); + if (f.CategoryId.HasValue) items = items.Where(i => i.FK_CategoryId == f.CategoryId.Value); + if (f.SubCategoryId.HasValue) items = items.Where(i => i.FK_SubCategoryId == f.SubCategoryId.Value); + if (!string.IsNullOrWhiteSpace(f.Search)) + { + var s = f.Search.Trim(); + items = items.Where(i => i.Name.Contains(s) || i.Sku.Contains(s)); + } + + var withQty = items.Select(i => new + { + Item = i, + Wh = balances.Where(b => b.FK_ItemId == i.Id && b.LocationType == LocationType.Warehouse) + .Sum(b => b.Quantity), + Veh = balances.Where(b => b.FK_ItemId == i.Id && b.LocationType == LocationType.Vehicle) + .Sum(b => b.Quantity), + }); + + if (f.LowStockOnly) + withQty = withQty.Where(x => x.Item.ReorderLevel != null && x.Wh <= x.Item.ReorderLevel); + + var total = await withQty.CountAsync(); + var page_ = await withQty + .OrderBy(x => x.Item.Name) + .Skip((page - 1) * pageSize).Take(pageSize) + .Select(x => new + { + x.Item.Id, x.Item.Sku, x.Item.Name, + Brand = x.Item.Brand != null ? x.Item.Brand.Name : null, + Category = x.Item.Category.Name, + SubCategory = x.Item.SubCategory != null ? x.Item.SubCategory.Name : null, + Uom = x.Item.BaseUom.Name, + x.Item.SalePrice, x.Item.CostPrice, x.Item.ReorderLevel, x.Item.IsActive, + x.Wh, x.Veh, + }) + .ToListAsync(); + + var rows = page_.Select(r => new ProductStockRow( + r.Id, r.Sku, r.Name, r.Brand, r.Category, r.SubCategory, r.Uom, + r.SalePrice, r.CostPrice, r.ReorderLevel, + r.Wh, r.Veh, r.Wh + r.Veh, (r.Wh + r.Veh) * (r.CostPrice ?? 0m), + r.ReorderLevel != null && r.Wh <= r.ReorderLevel, + r.IsActive)).ToList(); + return (rows, total); + } + + // Sold = shop PO lines on non-cancelled POs whose invoice is Finalized, dated by Invoice.FinalizedAt. + private IQueryable SoldLines(ProductReportFilter f) + { + var lines = + from li in db.ShopPOItems.AsNoTracking() + join inv in db.Invoices.AsNoTracking() on li.FK_ShopPOId equals inv.FK_ShopPOId + where inv.Status == InvoiceStatus.Finalized + && li.ShopPO.Status != ShopPOStatus.Cancelled + && (!f.From.HasValue || inv.FinalizedAt >= f.From.Value) + && (!f.To.HasValue || inv.FinalizedAt < f.To.Value.Date.AddDays(1)) + select li; + + if (f.BrandId.HasValue) lines = lines.Where(l => l.Item.FK_BrandId == f.BrandId.Value); + if (f.CategoryId.HasValue) lines = lines.Where(l => l.Item.FK_CategoryId == f.CategoryId.Value); + if (f.SubCategoryId.HasValue) lines = lines.Where(l => l.Item.FK_SubCategoryId == f.SubCategoryId.Value); + if (f.StoreId.HasValue) lines = lines.Where(l => l.ShopPO.FK_StoreId == f.StoreId.Value); + if (f.RouteId.HasValue) lines = lines.Where(l => l.ShopPO.Store.FK_RouteId == f.RouteId.Value); + if (f.RegionId.HasValue) lines = lines.Where(l => l.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); + if (!string.IsNullOrWhiteSpace(f.Search)) + { + var s = f.Search.Trim(); + lines = lines.Where(l => l.Item.Name.Contains(s) || l.Item.Sku.Contains(s)); + } + return lines; + } + + public async Task<(List Rows, int Total)> GetSalesAsync(ProductReportFilter f, int page, int pageSize) + { + var query = SoldLines(f) + .GroupBy(l => new { l.FK_ItemId, l.Item.Sku, l.Item.Name, l.Item.CostPrice }) + .Select(g => new + { + g.Key.FK_ItemId, g.Key.Sku, g.Key.Name, g.Key.CostPrice, + Orders = g.Select(x => x.FK_ShopPOId).Distinct().Count(), + QtyOrdered = g.Sum(x => (decimal)x.Quantity), + QtyFulfilled = g.Sum(x => (decimal)x.QuantityFulfilled), + SalesValue = g.Sum(x => x.QuantityFulfilled * x.UnitPrice), + }); + + var total = await query.CountAsync(); + var page_ = await query.OrderByDescending(r => r.SalesValue).ThenBy(r => r.Name) + .Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(); + + // Distinct stores per item is counted separately: MySQL can't run the correlated + // "SELECT DISTINCT ... WHERE outer.col" derived table EF generates inside a GroupBy. + var ids = page_.Select(r => r.FK_ItemId).ToList(); + var storePairs = await SoldLines(f) + .Where(l => ids.Contains(l.FK_ItemId)) + .Select(l => new { l.FK_ItemId, l.ShopPO.FK_StoreId }) + .Distinct() + .ToListAsync(); + var storeCounts = storePairs.GroupBy(p => p.FK_ItemId).ToDictionary(g => g.Key, g => g.Count()); + + var rows = page_.Select(r => new SalesAggregate(r.FK_ItemId, r.Sku, r.Name, r.CostPrice, + r.Orders, storeCounts.GetValueOrDefault(r.FK_ItemId), r.QtyOrdered, r.QtyFulfilled, r.SalesValue)).ToList(); + return (rows, total); + } + + public Task GetTotalSalesAsync(ProductReportFilter f) => + SoldLines(f).SumAsync(x => x.QuantityFulfilled * x.UnitPrice); + + public async Task> GetSoldQtyAsync(ProductReportFilter f, IEnumerable itemIds) + { + var ids = itemIds.ToList(); + return await SoldLines(f with { Search = null }) + .Where(l => ids.Contains(l.FK_ItemId)) + .GroupBy(l => l.FK_ItemId) + .Select(g => new { g.Key, Qty = g.Sum(x => (decimal)x.QuantityFulfilled) }) + .ToDictionaryAsync(x => x.Key, x => x.Qty); + } + + public Task> GetReturnsAsync(ProductReportFilter f) + { + var q = db.SaleReturnItems.AsNoTracking().AsQueryable(); + if (f.From.HasValue) q = q.Where(r => r.SaleReturn.CreatedAt >= f.From.Value); + if (f.To.HasValue) q = q.Where(r => r.SaleReturn.CreatedAt < f.To.Value.Date.AddDays(1)); + if (f.BrandId.HasValue) q = q.Where(r => r.Item.FK_BrandId == f.BrandId.Value); + if (f.CategoryId.HasValue) q = q.Where(r => r.Item.FK_CategoryId == f.CategoryId.Value); + if (f.SubCategoryId.HasValue) q = q.Where(r => r.Item.FK_SubCategoryId == f.SubCategoryId.Value); + if (f.StoreId.HasValue) q = q.Where(r => r.SaleReturn.ShopPO.FK_StoreId == f.StoreId.Value); + if (f.RouteId.HasValue) q = q.Where(r => r.SaleReturn.ShopPO.Store.FK_RouteId == f.RouteId.Value); + if (f.RegionId.HasValue) q = q.Where(r => r.SaleReturn.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); + if (!string.IsNullOrWhiteSpace(f.Search)) + { + var s = f.Search.Trim(); + q = q.Where(r => r.Item.Name.Contains(s) || r.Item.Sku.Contains(s)); + } + + return q.GroupBy(r => new { r.FK_ItemId, r.Item.Sku, r.Item.Name, r.ReasonCode }) + .Select(g => new ReturnAggregate(g.Key.FK_ItemId, g.Key.Sku, g.Key.Name, g.Key.ReasonCode, + g.Sum(x => x.Quantity), g.Sum(x => x.Quantity * x.UnitPrice))) + .ToListAsync(); + } +} diff --git a/Distribution/Features/ProductReport/ProductReportService.cs b/Distribution/Features/ProductReport/ProductReportService.cs new file mode 100644 index 0000000..b82f990 --- /dev/null +++ b/Distribution/Features/ProductReport/ProductReportService.cs @@ -0,0 +1,72 @@ +using Distribution.Features.Stock; + +namespace Distribution.Features.ProductReport; + +public class ProductReportService(IProductReportRepository repo) +{ + public const int MaxRangeDays = 366; + + public static string? Validate(ProductReportFilter f, int page, int pageSize) + { + if (page < 1 || pageSize < 1 || pageSize > 200) return "page must be >= 1 and pageSize between 1 and 200."; + if (f.From.HasValue && f.To.HasValue) + { + if (f.To < f.From) return "'to' must not be before 'from'."; + if ((f.To.Value - f.From.Value).TotalDays > MaxRangeDays) return $"Date range cannot exceed {MaxRangeDays} days."; + } + return null; + } + + public async Task> GetStockAsync(ProductReportFilter f, int page, int pageSize) + { + var (rows, total) = await repo.GetStockAsync(f, page, pageSize); + return new PagedResponse(page, pageSize, total, rows); + } + + public async Task> GetSalesAsync(ProductReportFilter f, int page, int pageSize) + { + var (rows, total) = await repo.GetSalesAsync(f, page, pageSize); + var mapped = rows.Select(r => + { + decimal? cost = r.CostPrice.HasValue ? r.CostPrice.Value * r.QtyFulfilled : null; + decimal? margin = cost.HasValue ? r.SalesValue - cost.Value : null; + decimal? pct = margin.HasValue && r.SalesValue != 0 ? Math.Round(margin.Value / r.SalesValue * 100, 2) : null; + return new ProductSalesRow(r.ItemId, r.Sku, r.Name, r.Orders, r.Stores, r.QtyOrdered, r.QtyFulfilled, + r.SalesValue, cost, margin, pct); + }).ToList(); + return new PagedResponse(page, pageSize, total, mapped); + } + + public async Task> GetReturnsAsync(ProductReportFilter f, int page, int pageSize) + { + var agg = await repo.GetReturnsAsync(f); + var grouped = agg.GroupBy(a => new { a.ItemId, a.Sku, a.Name }) + .Select(g => new + { + g.Key, + Qty = g.Sum(x => x.Qty), + Value = g.Sum(x => x.Value), + ByReason = g.ToDictionary(x => x.Reason, x => x.Qty), + }) + .OrderByDescending(x => x.Value).ThenBy(x => x.Key.Name) + .ToList(); + + var pageRows = grouped.Skip((page - 1) * pageSize).Take(pageSize).ToList(); + var sold = await repo.GetSoldQtyAsync(f, pageRows.Select(r => r.Key.ItemId)); + + var rows = pageRows.Select(r => + { + var soldQty = sold.GetValueOrDefault(r.Key.ItemId); + decimal? pct = soldQty > 0 ? Math.Round(r.Qty / soldQty * 100, 2) : null; + return new ProductReturnRow(r.Key.ItemId, r.Key.Sku, r.Key.Name, r.Qty, r.Value, soldQty, pct, r.ByReason); + }).ToList(); + return new PagedResponse(page, pageSize, grouped.Count, rows); + } + + public async Task GetSummaryAsync(ProductReportFilter f) + { + var (rows, total) = await repo.GetStockAsync(f, 1, int.MaxValue); + var totalSales = await repo.GetTotalSalesAsync(f); + return new ProductReportSummary(total, rows.Sum(r => r.TotalQty), rows.Sum(r => r.StockValue), totalSales); + } +} diff --git a/Distribution/Features/Region/IRegionRepository.cs b/Distribution/Features/Region/IRegionRepository.cs index 36b8c95..1575063 100644 --- a/Distribution/Features/Region/IRegionRepository.cs +++ b/Distribution/Features/Region/IRegionRepository.cs @@ -4,6 +4,14 @@ public interface IRegionRepository { Task GetByIdAsync(Guid id); Task> GetAllAsync(); + Task> GetAllCodesAsync(); + Task CodeExistsAsync(string code, Guid? excludeId = null); + + Task GetUserWithRoleAsync(Guid userId); + Task> GetManagerScopesAsync(Guid regionId); + Task GetRegionScopeAsync(Guid userId, Guid regionId); + Task AddRegionScopeAsync(Models.UserRegionScope scope); + void RemoveRegionScopes(IEnumerable scopes); Task AddAsync(Models.Region entity); void Update(Models.Region entity); void Remove(Models.Region entity); diff --git a/Distribution/Features/Region/RegionController.cs b/Distribution/Features/Region/RegionController.cs index c21dbd9..c7a658d 100644 --- a/Distribution/Features/Region/RegionController.cs +++ b/Distribution/Features/Region/RegionController.cs @@ -10,6 +10,11 @@ public class RegionController(RegionService service) : ControllerBase public async Task GetAll() => Ok(await service.GetAllAsync()); + // Suggested code for the Add Region form; the user may override it. + [HttpGet("next-code")] + public async Task GetNextCode() => + Ok(await service.GetNextCodeAsync()); + [HttpGet("{id:guid}")] public async Task GetById(Guid id) { @@ -38,6 +43,34 @@ public class RegionController(RegionService service) : ControllerBase return deleted ? NoContent() : NotFound(); } + [HttpGet("{id:guid}/manager")] + public async Task GetManager(Guid id) + { + var result = await service.GetManagerAsync(id); + return result is null ? NotFound() : Ok(result); + } + + [HttpPut("{id:guid}/manager")] + public async Task SetManager(Guid id, SetRegionManagerRequest request, CancellationToken ct) + { + try + { + var result = await service.SetManagerAsync(id, request, ct); + return result is null ? NotFound() : Ok(result); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpDelete("{id:guid}/manager")] + public async Task RemoveManager(Guid id, CancellationToken ct) + { + var removed = await service.RemoveManagerAsync(id, ct); + return removed ? NoContent() : NotFound(); + } + //targets/budgets/agencies/tax-rule. [HttpPut("{id:guid}/manage")] public async Task Manage(Guid id, ManageRegionRequest request, CancellationToken ct) diff --git a/Distribution/Features/Region/RegionDTOs.cs b/Distribution/Features/Region/RegionDTOs.cs index 896f0f2..fe7efb5 100644 --- a/Distribution/Features/Region/RegionDTOs.cs +++ b/Distribution/Features/Region/RegionDTOs.cs @@ -1,10 +1,14 @@ namespace Distribution.Features.Region; -public record CreateRegionRequest(string Name); +// Code is optional: blank/null auto-generates the next REG-#### code; otherwise stored as entered (trimmed). +public record CreateRegionRequest(string Name, string? Code = null); -public record UpdateRegionRequest(string Name); +public record NextRegionCodeResponse(string Code); -public record RegionResponse(Guid Id, string Name, bool IsActive, DateTime CreatedAt); +// Code is auto-generated on create; on update it is optional (null/blank keeps the current code). +public record UpdateRegionRequest(string Name, string? Code = null); + +public record RegionResponse(Guid Id, string Code, string Name, bool IsActive, DateTime CreatedAt); public record RegionalTargetResponse(Guid RegionId, string Period, decimal TargetValue); @@ -61,3 +65,9 @@ public record RegionManageEntryResponse( string? RuleConfig, Guid? AgencyUserId, DateTime CreatedAt); + +// Region manager = the single active user with the RegionalManager role scoped to the region +// (UserRegionScope). Users themselves are owned by the auth service; this only links one to a region. +public record SetRegionManagerRequest(Guid UserId); + +public record RegionManagerResponse(Guid RegionId, Guid UserId, string Username, string FullName); diff --git a/Distribution/Features/Region/RegionRepository.cs b/Distribution/Features/Region/RegionRepository.cs index c760c31..73551e8 100644 --- a/Distribution/Features/Region/RegionRepository.cs +++ b/Distribution/Features/Region/RegionRepository.cs @@ -11,6 +11,32 @@ public class RegionRepository(AppDBContext context) : IRegionRepository public async Task> GetAllAsync() => await context.Regions.ToListAsync(); + // Includes soft-deleted regions: the unique index on Code doesn't know about IsActive. + public async Task> GetAllCodesAsync() => + await context.Regions.Select(r => r.Code).ToListAsync(); + + public async Task CodeExistsAsync(string code, Guid? excludeId = null) => + await context.Regions.AnyAsync(r => r.Code == code && (excludeId == null || r.Id != excludeId)); + + public async Task GetUserWithRoleAsync(Guid userId) => + await context.Users.Include(u => u.Role).FirstOrDefaultAsync(u => u.Id == userId); + + public async Task> GetManagerScopesAsync(Guid regionId) => + await context.UserRegionScopes + .Include(s => s.User).ThenInclude(u => u.Role) + .Where(s => s.FK_RegionId == regionId && s.User.IsActive && s.User.Role.Name == "RegionalManager") + .ToListAsync(); + + public async Task GetRegionScopeAsync(Guid userId, Guid regionId) => + await context.UserRegionScopes.FirstOrDefaultAsync(s => s.FK_UserId == userId && s.FK_RegionId == regionId); + + public async Task AddRegionScopeAsync(Models.UserRegionScope scope) => + await context.UserRegionScopes.AddAsync(scope); + + // Scope rows are hard-deleted (composite key user+region), same as UserRepository.ReplaceRegionScopesAsync. + public void RemoveRegionScopes(IEnumerable scopes) => + context.UserRegionScopes.RemoveRange(scopes); + public async Task AddAsync(Models.Region entity) => await context.Regions.AddAsync(entity); diff --git a/Distribution/Features/Region/RegionService.cs b/Distribution/Features/Region/RegionService.cs index ce26dc8..37b5c3c 100644 --- a/Distribution/Features/Region/RegionService.cs +++ b/Distribution/Features/Region/RegionService.cs @@ -6,6 +6,7 @@ namespace Distribution.Features.Region; public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess) { private const int RegionalAgencyRoleLevel = 60; + private const string CodePrefix = "REG-"; // TODO: re-enable the RegionalManager+scope / RegionalAgency role gate on ManageAsync once // the role model for this endpoint is finalized. Left open (allow-all) for now on purpose. @@ -14,19 +15,42 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser public async Task> GetAllAsync() => (await repo.GetAllAsync()) .Where(r => r.IsActive) - .Select(r => new RegionResponse(r.Id, r.Name, r.IsActive, r.CreatedAt)) + .Select(r => new RegionResponse(r.Id, r.Code, r.Name, r.IsActive, r.CreatedAt)) .ToList(); public async Task GetByIdAsync(Guid id) { var region = await repo.GetByIdAsync(id); if (region is null || !region.IsActive) return null; - return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt); + return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt); + } + + // Next code = highest existing REG-#### number + 1 (soft-deleted regions included so codes are never reused). + public async Task GetNextCodeAsync() => new(await GenerateCodeAsync()); + + private async Task GenerateCodeAsync() + { + var max = (await repo.GetAllCodesAsync()) + .Where(c => c.StartsWith(CodePrefix, StringComparison.OrdinalIgnoreCase) + && int.TryParse(c[CodePrefix.Length..], out _)) + .Select(c => int.Parse(c[CodePrefix.Length..])) + .DefaultIfEmpty(0) + .Max(); + return $"{CodePrefix}{max + 1:D4}"; } public async Task CreateAsync(CreateRegionRequest request, CancellationToken ct = default) { - var region = new Models.Region { Name = request.Name }; + string code; + if (string.IsNullOrWhiteSpace(request.Code)) + code = await GenerateCodeAsync(); + else + { + code = request.Code.Trim(); + if (await repo.CodeExistsAsync(code)) + throw new InvalidOperationException($"Region code '{code}' is already in use."); + } + var region = new Models.Region { Name = request.Name, Code = code }; await uow.BeginAsync(ct); try { @@ -38,7 +62,7 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser await uow.RollbackAsync(ct); throw; } - return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt); + return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt); } public async Task UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default) @@ -46,6 +70,13 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser var region = await repo.GetByIdAsync(id); if (region is null || !region.IsActive) return null; region.Name = request.Name; + if (!string.IsNullOrWhiteSpace(request.Code)) + { + var code = request.Code.Trim(); + if (code != region.Code && await repo.CodeExistsAsync(code, id)) + throw new InvalidOperationException($"Region code '{code}' is already in use."); + region.Code = code; + } await uow.BeginAsync(ct); try { @@ -57,7 +88,68 @@ public class RegionService(IRegionRepository repo, IUnitOfWork uow, ICurrentUser await uow.RollbackAsync(ct); throw; } - return new RegionResponse(region.Id, region.Name, region.IsActive, region.CreatedAt); + return new RegionResponse(region.Id, region.Code, region.Name, region.IsActive, region.CreatedAt); + } + + private const string RegionalManagerRole = "RegionalManager"; + + public async Task GetManagerAsync(Guid regionId) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + var scope = (await repo.GetManagerScopesAsync(regionId)).FirstOrDefault(); + return scope is null ? null : new RegionManagerResponse(regionId, scope.FK_UserId, scope.User.Username, scope.User.FullName); + } + + // A region has exactly one manager: assigning replaces any existing RegionalManager scoped to it. + // Returns null when the region does not exist; throws InvalidOperationException for an invalid user. + public async Task SetManagerAsync(Guid regionId, SetRegionManagerRequest request, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return null; + + var user = await repo.GetUserWithRoleAsync(request.UserId); + if (user is null || !user.IsActive) + throw new InvalidOperationException("User not found."); + if (user.Role?.Name != RegionalManagerRole) + throw new InvalidOperationException($"User must have the {RegionalManagerRole} role to manage a region."); + + await uow.BeginAsync(ct); + try + { + var existing = await repo.GetManagerScopesAsync(regionId); + repo.RemoveRegionScopes(existing.Where(s => s.FK_UserId != user.Id)); + if (await repo.GetRegionScopeAsync(user.Id, regionId) is null) + await repo.AddRegionScopeAsync(new Models.UserRegionScope { FK_UserId = user.Id, FK_RegionId = regionId }); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return new RegionManagerResponse(regionId, user.Id, user.Username, user.FullName); + } + + public async Task RemoveManagerAsync(Guid regionId, CancellationToken ct = default) + { + var region = await repo.GetByIdAsync(regionId); + if (region is null || !region.IsActive) return false; + var existing = await repo.GetManagerScopesAsync(regionId); + if (existing.Count == 0) return false; + + await uow.BeginAsync(ct); + try + { + repo.RemoveRegionScopes(existing); + await uow.CommitAsync(ct); + } + catch + { + await uow.RollbackAsync(ct); + throw; + } + return true; } public async Task DeleteAsync(Guid id, CancellationToken ct = default) diff --git a/Distribution/Features/Stock/IStockRepository.cs b/Distribution/Features/Stock/IStockRepository.cs index dbc9208..832ce86 100644 --- a/Distribution/Features/Stock/IStockRepository.cs +++ b/Distribution/Features/Stock/IStockRepository.cs @@ -6,6 +6,7 @@ public interface IStockRepository { // Balance Task GetBalanceAsync(Guid itemId, Guid locationId); + Task GetBalanceAsync(Guid itemId, Guid locationId, Guid? itemBatchId, Guid? itemSerialNumberId); Task> GetBalancesByLocationAsync(Guid locationId, LocationType locationType); Task> GetBalancesAsync(Guid? locationId, LocationType? locationType, Guid? itemId, int page, int pageSize); Task GetBalancesCountAsync(Guid? locationId, LocationType? locationType, Guid? itemId); @@ -20,4 +21,12 @@ public interface IStockRepository void UpdateMovement(StockMovement movement); Task> GetMovementsAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to, int page, int pageSize); Task GetMovementsCountAsync(Guid? locationId, Guid? itemId, MovementType? type, DateTime? from, DateTime? to); + + // Batches — a null batchNumber always creates a fresh expiry-only lot (no dedup possible on null). + Task FindBatchAsync(Guid itemId, string batchNumber); + Task AddItemBatchAsync(ItemBatch batch); + + // Serial numbers + Task SerialNumberExistsAsync(Guid itemId, string serialNumber); + Task AddItemSerialNumberAsync(ItemSerialNumber serial); } diff --git a/Distribution/Features/Stock/StockRepository.cs b/Distribution/Features/Stock/StockRepository.cs index 20ba415..087a888 100644 --- a/Distribution/Features/Stock/StockRepository.cs +++ b/Distribution/Features/Stock/StockRepository.cs @@ -9,7 +9,14 @@ public class StockRepository(AppDBContext db) : IStockRepository public Task GetBalanceAsync(Guid itemId, Guid locationId) => db.StockBalances .Include(b => b.Item) - .FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId); + .FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId + && b.FK_ItemBatchId == null && b.FK_ItemSerialNumberId == null); + + public Task GetBalanceAsync(Guid itemId, Guid locationId, Guid? itemBatchId, Guid? itemSerialNumberId) => + db.StockBalances + .Include(b => b.Item) + .FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.LocationId == locationId + && b.FK_ItemBatchId == itemBatchId && b.FK_ItemSerialNumberId == itemSerialNumberId); public Task> GetBalancesByLocationAsync(Guid locationId, LocationType locationType) => db.StockBalances @@ -90,4 +97,16 @@ public class StockRepository(AppDBContext db) : IStockRepository if (to.HasValue) query = query.Where(m => m.CreatedAt <= to.Value); return query.CountAsync(); } + + public Task FindBatchAsync(Guid itemId, string batchNumber) => + db.ItemBatches.FirstOrDefaultAsync(b => b.FK_ItemId == itemId && b.BatchNumber == batchNumber); + + public async Task AddItemBatchAsync(ItemBatch batch) => + await db.ItemBatches.AddAsync(batch); + + public Task SerialNumberExistsAsync(Guid itemId, string serialNumber) => + db.ItemSerialNumbers.AnyAsync(s => s.FK_ItemId == itemId && s.SerialNumber == serialNumber); + + public async Task AddItemSerialNumberAsync(ItemSerialNumber serial) => + await db.ItemSerialNumbers.AddAsync(serial); } diff --git a/Distribution/Features/SysConfig/SysConfigController.cs b/Distribution/Features/SysConfig/SysConfigController.cs index a2571bc..bfacd85 100644 --- a/Distribution/Features/SysConfig/SysConfigController.cs +++ b/Distribution/Features/SysConfig/SysConfigController.cs @@ -42,10 +42,11 @@ public class SysConfigController(SysConfigService service, ICurrentUserService c [HttpPut("{key}")] public async Task Set(string key, SetSysConfigRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + var userId = currentUser.UserId ?? request.UserId; + if (userId is null) return BadRequest(new { error = "UserId is required while auth is unavailable" }); try { - var result = await service.SetAsync(key, request.Value, userId, ct); + var result = await service.SetAsync(key, request.Value, userId.Value, ct); return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } diff --git a/Distribution/Features/SysConfig/SysConfigDTOs.cs b/Distribution/Features/SysConfig/SysConfigDTOs.cs index 6084673..2907521 100644 --- a/Distribution/Features/SysConfig/SysConfigDTOs.cs +++ b/Distribution/Features/SysConfig/SysConfigDTOs.cs @@ -4,6 +4,6 @@ namespace Distribution.Features.SysConfig; public record SysConfigResponse(string Key, string Value, DateTime UpdatedAt, Guid? UpdatedBy); -public record SetSysConfigRequest(string Value); +public record SetSysConfigRequest(string Value, Guid? UserId); public record SetInvoiceCreationModeRequest(InvoiceCreationMode Mode); diff --git a/Distribution/Features/SysConfig/SysConfigService.cs b/Distribution/Features/SysConfig/SysConfigService.cs index 397d53c..a47bca1 100644 --- a/Distribution/Features/SysConfig/SysConfigService.cs +++ b/Distribution/Features/SysConfig/SysConfigService.cs @@ -7,6 +7,22 @@ public class SysConfigService(ISysConfigRepository repo, IUnitOfWork uow) { public const string KeyInvoiceCreationMode = "InvoiceCreationMode"; + public const string KeySerialNumberTrackingEnabled = "Feature.SerialNumberTracking"; + public const string KeyBatchTrackingEnabled = "Feature.BatchTracking"; + public const string KeyExpiryDateTrackingEnabled = "Feature.ExpiryDateTracking"; + public const string KeyWarrantyTrackingEnabled = "Feature.WarrantyTracking"; + public const string KeyCustomWarrantyOverrideEnabled = "Feature.CustomWarrantyOverride"; + public const string KeyDefaultWarrantyPeriodDays = "Feature.DefaultWarrantyPeriodDays"; + + private static readonly string[] BooleanFeatureKeys = + [ + KeySerialNumberTrackingEnabled, + KeyBatchTrackingEnabled, + KeyExpiryDateTrackingEnabled, + KeyWarrantyTrackingEnabled, + KeyCustomWarrantyOverrideEnabled, + ]; + public async Task> GetAllAsync() { var configs = await repo.GetAllAsync(); @@ -55,11 +71,35 @@ public class SysConfigService(ISysConfigRepository repo, IUnitOfWork uow) return Enum.TryParse(config.Value, out var mode) ? mode : InvoiceCreationMode.ModeB; } + public async Task GetSerialNumberTrackingEnabledAsync() => await GetBoolAsync(KeySerialNumberTrackingEnabled); + public async Task GetBatchTrackingEnabledAsync() => await GetBoolAsync(KeyBatchTrackingEnabled); + public async Task GetExpiryDateTrackingEnabledAsync() => await GetBoolAsync(KeyExpiryDateTrackingEnabled); + public async Task GetWarrantyTrackingEnabledAsync() => await GetBoolAsync(KeyWarrantyTrackingEnabled); + public async Task GetCustomWarrantyOverrideEnabledAsync() => await GetBoolAsync(KeyCustomWarrantyOverrideEnabled); + + public async Task GetDefaultWarrantyPeriodDaysAsync() + { + var config = await repo.GetByKeyAsync(KeyDefaultWarrantyPeriodDays); + return config is not null && int.TryParse(config.Value, out var days) ? days : 0; + } + + private async Task GetBoolAsync(string key) + { + var config = await repo.GetByKeyAsync(key); + return config is not null && bool.TryParse(config.Value, out var enabled) && enabled; + } + private static void ValidateValue(string key, string value) { if (key == KeyInvoiceCreationMode && !Enum.TryParse(value, out _)) throw new InvalidOperationException( $"Invalid value '{value}' for '{key}'. Allowed values: {string.Join(", ", Enum.GetNames())}."); + + if (BooleanFeatureKeys.Contains(key) && !bool.TryParse(value, out _)) + throw new InvalidOperationException($"Invalid value '{value}' for '{key}'. Allowed values: true, false."); + + if (key == KeyDefaultWarrantyPeriodDays && (!int.TryParse(value, out var days) || days < 0)) + throw new InvalidOperationException($"Invalid value '{value}' for '{key}'. Must be a non-negative integer."); } private static SysConfigResponse ToResponse(Models.SysConfig c) => diff --git a/Distribution/Migrations/20261001080630_AddItemTrackingConfig.Designer.cs b/Distribution/Migrations/20261001080630_AddItemTrackingConfig.Designer.cs new file mode 100644 index 0000000..d6a2fdd --- /dev/null +++ b/Distribution/Migrations/20261001080630_AddItemTrackingConfig.Designer.cs @@ -0,0 +1,4347 @@ +// +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("20261001080630_AddItemTrackingConfig")] + partial class AddItemTrackingConfig + { + /// + 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"); + + b.HasData( + new + { + Id = new Guid("21000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Coca-Cola" + }, + new + { + Id = new Guid("21000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Lays" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0000-0000-000000000001"), + AllowCustomerReturn = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Beverages" + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000002"), + AllowCustomerReturn = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Snacks" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("53000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 27, 0, 0, 0, 0, DateTimeKind.Unspecified), + Details = "Distributor margin claim for Q3 promotional shortfall", + FK_AgencyUserId = new Guid("70000000-0000-0000-0000-000000000001"), + IsActive = true, + SettledAt = new DateTime(2026, 8, 28, 0, 0, 0, 0, DateTimeKind.Unspecified), + SettledByUserId = new Guid("29000000-0000-0000-0000-000000000006"), + SettlementAmount = 15000m, + Status = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("54000000-0000-0000-0000-000000000001"), + Amount = 300m, + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_SaleReturnId = new Guid("51000000-0000-0000-0000-000000000001"), + IsActive = true, + IssuedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CancelTypeCode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_DriverProfileId") + .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("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_DriverProfileId"); + + b.HasIndex("FK_VehicleId"); + + b.ToTable("DeliveryRuns"); + + b.HasData( + new + { + Id = new Guid("31000000-0000-0000-0000-000000000001"), + ClosedAt = new DateTime(2026, 8, 25, 14, 0, 0, 0, DateTimeKind.Unspecified), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_DriverProfileId = new Guid("30000000-0000-0000-0000-000000000001"), + FK_VehicleId = new Guid("28000000-0000-0000-0000-000000000001"), + IsActive = true, + Meterend = 10550, + Meterstart = 10500, + StartedAt = new DateTime(2026, 8, 25, 8, 0, 0, 0, DateTimeKind.Unspecified), + Status = 2 + }, + new + { + Id = new Guid("31000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 30, 8, 45, 0, 0, DateTimeKind.Unspecified), + FK_DriverProfileId = new Guid("30000000-0000-0000-0000-000000000002"), + FK_VehicleId = new Guid("28000000-0000-0000-0000-000000000002"), + IsActive = true, + Meterend = 0, + Meterstart = 20300, + StartedAt = new DateTime(2026, 8, 30, 9, 0, 0, 0, DateTimeKind.Unspecified), + Status = 0 + }); + }); + + 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_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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_RideId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("DeliveryTasks"); + + b.HasData( + new + { + Id = new Guid("37000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 30, 10, 5, 0, 0, DateTimeKind.Unspecified), + CustomerConfirmed = false, + FK_RideId = new Guid("31000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), + IsActive = true, + Sequence = 1, + Status = 0 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("64000000-0000-0000-0000-000000000001"), + EnforceLicenseExpiry = true, + EnforceMedicalClearance = true, + UpdatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + UpdatedBy = new Guid("29000000-0000-0000-0000-000000000001") + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("30000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000004"), + IsActive = true, + LicenseExpiry = new DateTime(2027, 6, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + LicenseNumber = "B1234567", + MedicalClearanceExpiry = new DateTime(2027, 1, 31, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("30000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000005"), + IsActive = true, + LicenseExpiry = new DateTime(2027, 9, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + LicenseNumber = "B7654321", + MedicalClearanceExpiry = new DateTime(2027, 3, 31, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("43000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + FK_ReceivedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + ReceivedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("44000000-0000-0000-0000-000000000001"), + AcceptedQuantity = 200, + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000001"), + IsActive = true, + RejectedQuantity = 0 + }, + new + { + Id = new Guid("44000000-0000-0000-0000-000000000002"), + AcceptedQuantity = 98, + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000002"), + IsActive = true, + RejectedQuantity = 2 + }); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemBatchId"); + + b.ToTable("GoodsReceiptItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("GoodsReceiptItemSerials"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.Property("FinalizedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalizedBy") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("PrintedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrintedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("Invoices"); + + b.HasData( + new + { + Id = new Guid("34500000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 8, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + FinalizedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FinalizedBy = new Guid("29000000-0000-0000-0000-000000000001"), + IsActive = true, + PaymentStatus = 2, + PrintedAt = new DateTime(2026, 8, 25, 8, 45, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000002"), + Status = 2, + TotalAmount = 3000m + }, + new + { + Id = new Guid("34500000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + PaymentStatus = 1, + PrintedAt = new DateTime(2026, 8, 31, 9, 15, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 1, + TotalAmount = 2400m + }); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomWarrantyPeriod") + .HasColumnType("tinyint(1)"); + + 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("HasWarranty") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReorderLevel") + .HasColumnType("decimal(65,30)"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.Property("TracksBatches") + .HasColumnType("tinyint(1)"); + + b.Property("TracksExpiryDate") + .HasColumnType("tinyint(1)"); + + b.Property("TracksSerialNumbers") + .HasColumnType("tinyint(1)"); + + b.Property("WarrantyPeriodDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + + b.HasData( + new + { + Id = new Guid("23000000-0000-0000-0000-000000000001"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 500ml bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 500ml", + SalePrice = 60m, + Sku = "BEV-COKE-500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000002"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 1.5L bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 1.5L", + SalePrice = 120m, + Sku = "BEV-COKE-1500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000003"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Classic Salted 52g pack", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Classic 52g", + SalePrice = 50m, + Sku = "SNK-LAYS-52", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000004"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Family Pack 150g", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Family Pack 150g", + SalePrice = 150m, + Sku = "SNK-LAYS-FAM", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }); + }); + + modelBuilder.Entity("Distribution.Models.ItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchNumber") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ManufactureDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "BatchNumber"); + + b.ToTable("ItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentLocationId") + .HasColumnType("char(36)"); + + b.Property("CurrentLocationType") + .HasColumnType("int"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("WarrantyExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("WarrantyPeriodDaysOverride") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "SerialNumber") + .IsUnique(); + + b.ToTable("ItemSerialNumbers"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PopulatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PopulatedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("LoadingSheets"); + + b.HasData( + new + { + Id = new Guid("35000000-0000-0000-0000-000000000001"), + ConfirmedAt = new DateTime(2026, 8, 25, 7, 45, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_RideId = new Guid("31000000-0000-0000-0000-000000000001"), + IsActive = true, + PopulatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + PopulatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + Status = 1 + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOItemId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.HasIndex("FK_ShopPOItemId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("LoadingSheetItems"); + + b.HasData( + new + { + Id = new Guid("36000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 50m + }, + new + { + Id = new Guid("36000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 30m + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetOrder", b => + { + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.HasKey("FK_LoadingSheetId", "FK_ShopPOId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("LoadingSheetOrders"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("ChequeDate") + .HasColumnType("datetime(6)"); + + b.Property("ChequeNumber") + .HasColumnType("longtext"); + + b.Property("CollectedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollectedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntryMode") + .HasColumnType("int"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("RecordedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceId"); + + b.ToTable("Payments"); + + b.HasData( + new + { + Id = new Guid("47000000-0000-0000-0000-000000000001"), + Amount = 3000m, + CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"), + IsActive = true, + Method = 0 + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000002"), + Amount = 1000m, + CollectedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 0 + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000003"), + Amount = 800m, + ChequeDate = new DateTime(2026, 8, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + ChequeNumber = "000456", + CollectedAt = new DateTime(2026, 8, 31, 11, 0, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 18, 0, 0, 0, DateTimeKind.Unspecified), + EntryMode = 1, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 1, + RecordedBy = new Guid("29000000-0000-0000-0000-000000000001") + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000004"), + Amount = 300m, + CollectedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 2 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("39000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + EffectiveFrom = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + EffectiveTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Name = "Western Province Price List" + }); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgencyMarginPercent") + .HasColumnType("decimal(65,30)"); + + 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.Property("RetailPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PriceListId", "FK_ItemId") + .IsUnique(); + + b.ToTable("PriceListItems"); + + b.HasData( + new + { + Id = new Guid("40000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_PriceListId = new Guid("39000000-0000-0000-0000-000000000001"), + IsActive = true, + MinPrice = 55m, + Price = 60m + }, + new + { + Id = new Guid("40000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PriceListId = new Guid("39000000-0000-0000-0000-000000000001"), + IsActive = true, + MinPrice = 45m, + Price = 50m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("41000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_SupplierId = new Guid("38000000-0000-0000-0000-000000000001"), + IsActive = true, + Status = 2 + }, + new + { + Id = new Guid("41000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_SupplierId = new Guid("38000000-0000-0000-0000-000000000002"), + IsActive = true, + Status = 2 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("42000000-0000-0000-0000-000000000001"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + IsCancelled = false, + Quantity = 200, + ReceivedQuantity = 200, + UnitCost = 40m + }, + new + { + Id = new Guid("42000000-0000-0000-0000-000000000002"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + IsCancelled = false, + Quantity = 100, + ReceivedQuantity = 98, + UnitCost = 80m + }, + new + { + Id = new Guid("42000000-0000-0000-0000-000000000003"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000002"), + IsActive = true, + IsCancelled = false, + Quantity = 150, + ReceivedQuantity = 150, + UnitCost = 35m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("45000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000002"), + IsActive = true, + Reason = "Damaged in transit" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("46000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PurchaseReturnId = new Guid("45000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 10 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("24000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Western Province" + }, + new + { + Id = new Guid("24000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Central Province" + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("FK_RegionId", "FK_AgencyUserId") + .IsUnique(); + + b.ToTable("RegionAgencyMaps"); + + b.HasData( + new + { + Id = new Guid("57000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_AgencyUserId = new Guid("70000000-0000-0000-0000-000000000001"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BudgetAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalBudgets"); + + b.HasData( + new + { + Id = new Guid("58000000-0000-0000-0000-000000000001"), + BudgetAmount = 200000m, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Period = "2026-08" + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetValue") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalTargets"); + + b.HasData( + new + { + Id = new Guid("59000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Period = "2026-08", + TargetValue = 500000m + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RuleConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId") + .IsUnique(); + + b.ToTable("RegionalTaxRules"); + + b.HasData( + new + { + Id = new Guid("60000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + RuleConfig = "{\"rate\": 0.15}" + }); + }); + + 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") + .HasColumnType("longtext"); + + b.Property("FK_OwnerUserId") + .HasColumnType("char(36)"); + + 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") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_OwnerUserId"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("Routes"); + + b.HasData( + new + { + Id = new Guid("25000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Dehiwala", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Length = 18.5m, + RouteName = "Colombo City Route", + StartPoint = "Colombo Fort" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Negombo", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Length = 32.0m, + RouteName = "Negombo Coastal Route", + StartPoint = "Wattala" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Peradeniya", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + IsActive = true, + Length = 12.0m, + RouteName = "Kandy Town Route", + StartPoint = "Kandy Lake" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Matale", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + IsActive = true, + Length = 26.0m, + RouteName = "Matale Route", + StartPoint = "Kandy" + }); + }); + + modelBuilder.Entity("Distribution.Models.RouteVisitLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckInAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInLatitude") + .HasColumnType("decimal(65,30)"); + + b.Property("CheckInLongitude") + .HasColumnType("decimal(65,30)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeviationFlagged") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.HasIndex("FK_StoreId"); + + b.HasIndex("FK_UserId"); + + b.ToTable("RouteVisitLogs"); + + b.HasData( + new + { + Id = new Guid("66000000-0000-0000-0000-000000000001"), + CheckInAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + CheckInLatitude = 6.9344m, + CheckInLongitude = 79.8428m, + CheckOutAt = new DateTime(2026, 8, 25, 9, 25, 0, 0, DateTimeKind.Unspecified), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000001"), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000002"), + IsActive = true, + IsDeviationFlagged = false + }, + new + { + Id = new Guid("66000000-0000-0000-0000-000000000002"), + CheckInAt = new DateTime(2026, 8, 25, 10, 0, 0, 0, DateTimeKind.Unspecified), + CheckInLatitude = 6.9000m, + CheckInLongitude = 79.8654m, + CreatedAt = new DateTime(2026, 8, 25, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000002"), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000002"), + IsActive = true, + IsDeviationFlagged = true + }); + }); + + 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_ShopPOId") + .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_ShopPOId"); + + b.ToTable("SaleReturns"); + + b.HasData( + new + { + Id = new Guid("51000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Mode = 0, + TotalCreditAmount = 300m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("52000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_SaleReturnId = new Guid("51000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 5m, + ReasonCode = 1, + UnitPrice = 60m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("69000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true + }, + new + { + Id = new Guid("69000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("48000000-0000-0000-0000-000000000001"), + AgentLimitExempt = false, + BaselineExpectedSales = 500000m, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeFrom = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + FK_TriggerItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FreeQty = 1m, + IsActive = true, + Name = "Buy 10 Coke Get 1 Free", + RuleType = 0, + TriggerQty = 10 + }, + new + { + Id = new Guid("48000000-0000-0000-0000-000000000002"), + AgentLimitExempt = false, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeFrom = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000004"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + FreeQty = 1m, + IsActive = true, + Name = "Snack Combo Bundle", + RuleType = 3 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("49000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_SchemeId = new Guid("48000000-0000-0000-0000-000000000002"), + Qty = 5 + }); + }); + + 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_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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_SchemeId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("SchemeRedemptions"); + + b.HasData( + new + { + Id = new Guid("50000000-0000-0000-0000-000000000001"), + DiscountAmount = 0m, + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_SchemeId = new Guid("48000000-0000-0000-0000-000000000001"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + FreeItemCostValue = 40m, + FreeQty = 1m, + RedeemedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Distribution.Models.ShopPO", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CancelledBy") + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfilledAt") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("InvoiceId"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedById") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("ShopPOs"); + + b.HasData( + new + { + Id = new Guid("33000000-0000-0000-0000-000000000001"), + ConfirmedAt = new DateTime(2026, 8, 25, 8, 30, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000001"), + FulfilledAt = new DateTime(2026, 8, 25, 13, 0, 0, 0, DateTimeKind.Unspecified), + InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000002"), + Status = 2, + TotalAmount = 3000m + }, + new + { + Id = new Guid("33000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 0, + TotalAmount = 1200m + }, + new + { + Id = new Guid("33000000-0000-0000-0000-000000000003"), + ConfirmedAt = new DateTime(2026, 8, 31, 8, 30, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + FulfilledAt = new DateTime(2026, 8, 31, 13, 0, 0, 0, DateTimeKind.Unspecified), + InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 2, + TotalAmount = 2400m + }); + }); + + modelBuilder.Entity("Distribution.Models.ShopPOItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("ShopPOItems"); + + b.HasData( + new + { + Id = new Guid("34000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 30, + QuantityFulfilled = 30, + UnitPrice = 60m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 24, + QuantityFulfilled = 24, + UnitPrice = 50m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), + IsActive = true, + Quantity = 10, + QuantityFulfilled = 0, + UnitPrice = 120m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + Quantity = 20, + QuantityFulfilled = 20, + UnitPrice = 120m + }); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .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_ItemBatchId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.HasIndex("FK_ItemId", "LocationId", "FK_ItemBatchId", "FK_ItemSerialNumberId") + .IsUnique(); + + b.ToTable("StockBalances"); + + b.HasData( + new + { + Id = new Guid("61000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 150m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 90m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000003"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("28000000-0000-0000-0000-000000000001"), + LocationType = 1, + Quantity = 20m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .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_ItemBatchId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("StockMovements"); + + b.HasData( + new + { + Id = new Guid("62000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + FromLocationType = 2, + Quantity = 200m, + ReferenceId = new Guid("43000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + ToLocationType = 0, + Type = 6 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + FromLocationType = 0, + Quantity = 50m, + ReferenceId = new Guid("35000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + ToLocationType = 1, + Type = 5 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + FromLocationType = 1, + Quantity = 30m, + ReferenceId = new Guid("32000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + ToLocationType = 2, + Type = 0 + }); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + 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("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") + .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"); + + b.HasData( + new + { + Id = new Guid("26000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 50000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.9344m, + Longitude = 79.8428m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Saman Kumara", + Segment = "Gold", + StoreAddress = "12 York Street, Colombo Fort", + StoreName = "Fort Mini Mart" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 30000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.8508m, + Longitude = 79.8654m, + OneTimeAmount = 0m, + OutstandingBalance = 5000m, + OwnerName = "Nadeesha Silva", + Segment = "Silver", + StoreAddress = "45 Galle Road, Dehiwala", + StoreName = "Dehiwala Grocery" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 40000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 6.9890m, + Longitude = 79.8912m, + OneTimeAmount = 0m, + OutstandingBalance = 1200m, + OwnerName = "Ruwan Gunasekara", + Segment = "Gold", + StoreAddress = "88 Negombo Road, Wattala", + StoreName = "Wattala Supermarket" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 25000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 7.2094m, + Longitude = 79.8380m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Chamari Peiris", + StoreAddress = "5 Beach Road, Negombo", + StoreName = "Negombo Beach Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 35000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000003"), + IsActive = true, + Latitude = 7.2906m, + Longitude = 80.6337m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Dinesh Wickramasinghe", + Segment = "Key Account", + StoreAddress = "3 Lake Road, Kandy", + StoreName = "Kandy Lake View Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 20000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000004"), + IsActive = true, + Latitude = 7.4675m, + Longitude = 80.6234m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Ishara Bandara", + StoreAddress = "20 Main Street, Matale", + StoreName = "Matale Central Store" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("38000000-0000-0000-0000-000000000001"), + ContactInfo = "+94 11 234 5678", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Ceylon Beverages Ltd", + PerformanceScore = 4.2m + }, + new + { + Id = new Guid("38000000-0000-0000-0000-000000000002"), + ContactInfo = "+94 11 876 5432", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Snack Foods (Pvt) Ltd" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("55000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + Details = "2 units of Coke 1.5L received damaged", + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + RaisedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + ResolutionAmount = 160m, + ResolutionNotes = "Credited against next invoice", + ResolutionType = 1, + ResolvedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + ResolvedByUserId = new Guid("29000000-0000-0000-0000-000000000006"), + Status = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("56000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_SupplierClaimId = new Guid("55000000-0000-0000-0000-000000000001"), + IsActive = true, + IssueType = 0, + Quantity = 2m + }); + }); + + modelBuilder.Entity("Distribution.Models.SysConfig", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Key"); + + b.ToTable("SysConfigs"); + + b.HasData( + new + { + Key = "InvoiceCreationMode", + UpdatedAt = new DateTime(2026, 9, 17, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "ModeB" + }, + new + { + Key = "Feature.SerialNumberTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.BatchTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.ExpiryDateTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.WarrantyTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.CustomWarrantyOverride", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.DefaultWarrantyPeriodDays", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "0" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("22000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Piece" + }, + new + { + Id = new Guid("67000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Case" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("67000000-0000-0000-0000-000000000002"), + FK_FromUomId = new Guid("67000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_ToUomId = new Guid("22000000-0000-0000-0000-000000000001"), + Factor = 24m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"), + FullName = "Sunil Fernando", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "sunil.fernando" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Nimal Perera", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "nimal.perera" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Kamal Silva", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "kamal.silva" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Ruwan Jayasuriya", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "ruwan.jayasuriya" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Priyantha Kumara", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "priyantha.kumara" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"), + FullName = "Priya Rajapaksa", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "priya.rajapaksa" + }, + new + { + Id = new Guid("70000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"), + FullName = "Channa De Silva", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "channa.desilva" + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000006"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000001"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000002"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000003"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("28000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + DefaultWarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + NumberPlate = "CAB-1234", + RegNumber = "WP-CAB-1234", + Type = 2 + }, + new + { + Id = new Guid("28000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + DefaultWarehouseId = new Guid("27000000-0000-0000-0000-000000000002"), + IsActive = true, + NumberPlate = "KY-5678", + RegNumber = "CP-KY-5678", + Type = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("27000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Location = "Peliyagoda, Colombo", + Name = "Colombo Distribution Center", + Type = 0 + }, + new + { + Id = new Guid("27000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Location = "Katugastota, Kandy", + Name = "Kandy Fulfillment Center", + Type = 1 + }); + }); + + 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.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("FK_VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DriverProfile"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + + b.Navigation("ShopPO"); + }); + + 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.GoodsReceiptItemBatch", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Batches") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemBatch"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Serials") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemSerialNumber"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ShopPO"); + }); + + 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.ItemBatch", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + }); + + 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.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPOItem", "ShopPOItem") + .WithMany() + .HasForeignKey("FK_ShopPOItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("LoadingSheet"); + + b.Navigation("ShopPOItem"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetOrder", b => + { + b.HasOne("Distribution.Models.LoadingSheet", "LoadingSheet") + .WithMany("Orders") + .HasForeignKey("FK_LoadingSheetId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LoadingSheet"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Invoice"); + }); + + 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.RegionAgencyMap", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AgencyUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + 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.User", "OwnerUser") + .WithMany() + .HasForeignKey("FK_OwnerUserId"); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OwnerUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RouteVisitLog", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("Route"); + + b.Navigation("Store"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("ShopPO"); + }); + + 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.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("FreeItem"); + + b.Navigation("Scheme"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.ShopPO", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.ShopPOItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany("Items") + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); + }); + + 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.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.GoodsReceiptItem", b => + { + b.Navigation("Batches"); + + b.Navigation("Serials"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Orders"); + }); + + 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.ShopPO", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20261001080630_AddItemTrackingConfig.cs b/Distribution/Migrations/20261001080630_AddItemTrackingConfig.cs new file mode 100644 index 0000000..23e476f --- /dev/null +++ b/Distribution/Migrations/20261001080630_AddItemTrackingConfig.cs @@ -0,0 +1,505 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Distribution.Migrations +{ + /// + public partial class AddItemTrackingConfig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FK_ItemBatchId", + table: "StockMovements", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FK_ItemSerialNumberId", + table: "StockMovements", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FK_ItemBatchId", + table: "StockBalances", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FK_ItemSerialNumberId", + table: "StockBalances", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "AllowCustomWarrantyPeriod", + table: "Items", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "HasWarranty", + table: "Items", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "TracksBatches", + table: "Items", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "TracksExpiryDate", + table: "Items", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "TracksSerialNumbers", + table: "Items", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "WarrantyPeriodDays", + table: "Items", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "ItemBatches", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + BatchNumber = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + ManufactureDate = table.Column(type: "datetime(6)", nullable: true), + ExpiryDate = 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_ItemBatches", x => x.Id); + table.ForeignKey( + name: "FK_ItemBatches_Items_FK_ItemId", + column: x => x.FK_ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "ItemSerialNumbers", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + SerialNumber = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Status = table.Column(type: "int", nullable: false), + CurrentLocationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + CurrentLocationType = table.Column(type: "int", nullable: true), + ReceivedAt = table.Column(type: "datetime(6)", nullable: false), + WarrantyPeriodDaysOverride = table.Column(type: "int", nullable: true), + WarrantyExpiresAt = 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_ItemSerialNumbers", x => x.Id); + table.ForeignKey( + name: "FK_ItemSerialNumbers_Items_FK_ItemId", + column: x => x.FK_ItemId, + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "GoodsReceiptItemBatches", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_GoodsReceiptItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemBatchId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Quantity = 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_GoodsReceiptItemBatches", x => x.Id); + table.ForeignKey( + name: "FK_GoodsReceiptItemBatches_GoodsReceiptItems_FK_GoodsReceiptIte~", + column: x => x.FK_GoodsReceiptItemId, + principalTable: "GoodsReceiptItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_GoodsReceiptItemBatches_ItemBatches_FK_ItemBatchId", + column: x => x.FK_ItemBatchId, + principalTable: "ItemBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "GoodsReceiptItemSerials", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_GoodsReceiptItemId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_ItemSerialNumberId = 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_GoodsReceiptItemSerials", x => x.Id); + table.ForeignKey( + name: "FK_GoodsReceiptItemSerials_GoodsReceiptItems_FK_GoodsReceiptIte~", + column: x => x.FK_GoodsReceiptItemId, + principalTable: "GoodsReceiptItems", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_GoodsReceiptItemSerials_ItemSerialNumbers_FK_ItemSerialNumbe~", + column: x => x.FK_ItemSerialNumberId, + principalTable: "ItemSerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "Items", + keyColumn: "Id", + keyValue: new Guid("23000000-0000-0000-0000-000000000001"), + columns: new[] { "AllowCustomWarrantyPeriod", "HasWarranty", "TracksBatches", "TracksExpiryDate", "TracksSerialNumbers", "WarrantyPeriodDays" }, + values: new object[] { false, false, false, false, false, null }); + + migrationBuilder.UpdateData( + table: "Items", + keyColumn: "Id", + keyValue: new Guid("23000000-0000-0000-0000-000000000002"), + columns: new[] { "AllowCustomWarrantyPeriod", "HasWarranty", "TracksBatches", "TracksExpiryDate", "TracksSerialNumbers", "WarrantyPeriodDays" }, + values: new object[] { false, false, false, false, false, null }); + + migrationBuilder.UpdateData( + table: "Items", + keyColumn: "Id", + keyValue: new Guid("23000000-0000-0000-0000-000000000003"), + columns: new[] { "AllowCustomWarrantyPeriod", "HasWarranty", "TracksBatches", "TracksExpiryDate", "TracksSerialNumbers", "WarrantyPeriodDays" }, + values: new object[] { false, false, false, false, false, null }); + + migrationBuilder.UpdateData( + table: "Items", + keyColumn: "Id", + keyValue: new Guid("23000000-0000-0000-0000-000000000004"), + columns: new[] { "AllowCustomWarrantyPeriod", "HasWarranty", "TracksBatches", "TracksExpiryDate", "TracksSerialNumbers", "WarrantyPeriodDays" }, + values: new object[] { false, false, false, false, false, null }); + + migrationBuilder.UpdateData( + table: "StockBalances", + keyColumn: "Id", + keyValue: new Guid("61000000-0000-0000-0000-000000000001"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "StockBalances", + keyColumn: "Id", + keyValue: new Guid("61000000-0000-0000-0000-000000000002"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "StockBalances", + keyColumn: "Id", + keyValue: new Guid("61000000-0000-0000-0000-000000000003"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "StockMovements", + keyColumn: "Id", + keyValue: new Guid("62000000-0000-0000-0000-000000000001"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "StockMovements", + keyColumn: "Id", + keyValue: new Guid("62000000-0000-0000-0000-000000000002"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.UpdateData( + table: "StockMovements", + keyColumn: "Id", + keyValue: new Guid("62000000-0000-0000-0000-000000000003"), + columns: new[] { "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + values: new object[] { null, null }); + + migrationBuilder.InsertData( + table: "SysConfigs", + columns: new[] { "Key", "UpdatedAt", "UpdatedBy", "Value" }, + values: new object[,] + { + { "Feature.BatchTracking", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "false" }, + { "Feature.CustomWarrantyOverride", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "false" }, + { "Feature.DefaultWarrantyPeriodDays", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "0" }, + { "Feature.ExpiryDateTracking", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "false" }, + { "Feature.SerialNumberTracking", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "false" }, + { "Feature.WarrantyTracking", new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), null, "false" } + }); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_FK_ItemBatchId", + table: "StockMovements", + column: "FK_ItemBatchId"); + + migrationBuilder.CreateIndex( + name: "IX_StockMovements_FK_ItemSerialNumberId", + table: "StockMovements", + column: "FK_ItemSerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_StockBalances_FK_ItemBatchId", + table: "StockBalances", + column: "FK_ItemBatchId"); + + migrationBuilder.CreateIndex( + name: "IX_StockBalances_FK_ItemId_LocationId_FK_ItemBatchId_FK_ItemSer~", + table: "StockBalances", + columns: new[] { "FK_ItemId", "LocationId", "FK_ItemBatchId", "FK_ItemSerialNumberId" }, + unique: true); + + // The old 2-column index must be dropped only after the new composite index exists, + // since MySQL won't drop an index that's the only one backing a FK (FK_ItemId here). + migrationBuilder.DropIndex( + name: "IX_StockBalances_FK_ItemId_LocationId", + table: "StockBalances"); + + migrationBuilder.CreateIndex( + name: "IX_StockBalances_FK_ItemSerialNumberId", + table: "StockBalances", + column: "FK_ItemSerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceiptItemBatches_FK_GoodsReceiptItemId", + table: "GoodsReceiptItemBatches", + column: "FK_GoodsReceiptItemId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceiptItemBatches_FK_ItemBatchId", + table: "GoodsReceiptItemBatches", + column: "FK_ItemBatchId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceiptItemSerials_FK_GoodsReceiptItemId", + table: "GoodsReceiptItemSerials", + column: "FK_GoodsReceiptItemId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceiptItemSerials_FK_ItemSerialNumberId", + table: "GoodsReceiptItemSerials", + column: "FK_ItemSerialNumberId"); + + migrationBuilder.CreateIndex( + name: "IX_ItemBatches_FK_ItemId_BatchNumber", + table: "ItemBatches", + columns: new[] { "FK_ItemId", "BatchNumber" }); + + migrationBuilder.CreateIndex( + name: "IX_ItemSerialNumbers_FK_ItemId_SerialNumber", + table: "ItemSerialNumbers", + columns: new[] { "FK_ItemId", "SerialNumber" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_StockBalances_ItemBatches_FK_ItemBatchId", + table: "StockBalances", + column: "FK_ItemBatchId", + principalTable: "ItemBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_StockBalances_ItemSerialNumbers_FK_ItemSerialNumberId", + table: "StockBalances", + column: "FK_ItemSerialNumberId", + principalTable: "ItemSerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_StockMovements_ItemBatches_FK_ItemBatchId", + table: "StockMovements", + column: "FK_ItemBatchId", + principalTable: "ItemBatches", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_StockMovements_ItemSerialNumbers_FK_ItemSerialNumberId", + table: "StockMovements", + column: "FK_ItemSerialNumberId", + principalTable: "ItemSerialNumbers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_StockBalances_ItemBatches_FK_ItemBatchId", + table: "StockBalances"); + + migrationBuilder.DropForeignKey( + name: "FK_StockBalances_ItemSerialNumbers_FK_ItemSerialNumberId", + table: "StockBalances"); + + migrationBuilder.DropForeignKey( + name: "FK_StockMovements_ItemBatches_FK_ItemBatchId", + table: "StockMovements"); + + migrationBuilder.DropForeignKey( + name: "FK_StockMovements_ItemSerialNumbers_FK_ItemSerialNumberId", + table: "StockMovements"); + + migrationBuilder.DropTable( + name: "GoodsReceiptItemBatches"); + + migrationBuilder.DropTable( + name: "GoodsReceiptItemSerials"); + + migrationBuilder.DropTable( + name: "ItemBatches"); + + migrationBuilder.DropTable( + name: "ItemSerialNumbers"); + + migrationBuilder.DropIndex( + name: "IX_StockMovements_FK_ItemBatchId", + table: "StockMovements"); + + migrationBuilder.DropIndex( + name: "IX_StockMovements_FK_ItemSerialNumberId", + table: "StockMovements"); + + migrationBuilder.DropIndex( + name: "IX_StockBalances_FK_ItemBatchId", + table: "StockBalances"); + + // Recreate the old 2-column index before dropping the composite one, since MySQL + // won't drop an index that's the only one backing the FK_ItemId FK constraint. + migrationBuilder.CreateIndex( + name: "IX_StockBalances_FK_ItemId_LocationId", + table: "StockBalances", + columns: new[] { "FK_ItemId", "LocationId" }, + unique: true); + + migrationBuilder.DropIndex( + name: "IX_StockBalances_FK_ItemId_LocationId_FK_ItemBatchId_FK_ItemSer~", + table: "StockBalances"); + + migrationBuilder.DropIndex( + name: "IX_StockBalances_FK_ItemSerialNumberId", + table: "StockBalances"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.BatchTracking"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.CustomWarrantyOverride"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.DefaultWarrantyPeriodDays"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.ExpiryDateTracking"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.SerialNumberTracking"); + + migrationBuilder.DeleteData( + table: "SysConfigs", + keyColumn: "Key", + keyValue: "Feature.WarrantyTracking"); + + migrationBuilder.DropColumn( + name: "FK_ItemBatchId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "FK_ItemSerialNumberId", + table: "StockMovements"); + + migrationBuilder.DropColumn( + name: "FK_ItemBatchId", + table: "StockBalances"); + + migrationBuilder.DropColumn( + name: "FK_ItemSerialNumberId", + table: "StockBalances"); + + migrationBuilder.DropColumn( + name: "AllowCustomWarrantyPeriod", + table: "Items"); + + migrationBuilder.DropColumn( + name: "HasWarranty", + table: "Items"); + + migrationBuilder.DropColumn( + name: "TracksBatches", + table: "Items"); + + migrationBuilder.DropColumn( + name: "TracksExpiryDate", + table: "Items"); + + migrationBuilder.DropColumn( + name: "TracksSerialNumbers", + table: "Items"); + + migrationBuilder.DropColumn( + name: "WarrantyPeriodDays", + table: "Items"); + } + } +} diff --git a/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.Designer.cs b/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.Designer.cs new file mode 100644 index 0000000..8832d51 --- /dev/null +++ b/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.Designer.cs @@ -0,0 +1,4369 @@ +// +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("20261006070042_AddDirectGoodsReceipt")] + partial class AddDirectGoodsReceipt + { + /// + 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"); + + b.HasData( + new + { + Id = new Guid("21000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Coca-Cola" + }, + new + { + Id = new Guid("21000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Lays" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0000-0000-000000000001"), + AllowCustomerReturn = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Beverages" + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000002"), + AllowCustomerReturn = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Snacks" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("53000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 27, 0, 0, 0, 0, DateTimeKind.Unspecified), + Details = "Distributor margin claim for Q3 promotional shortfall", + FK_AgencyUserId = new Guid("70000000-0000-0000-0000-000000000001"), + IsActive = true, + SettledAt = new DateTime(2026, 8, 28, 0, 0, 0, 0, DateTimeKind.Unspecified), + SettledByUserId = new Guid("29000000-0000-0000-0000-000000000006"), + SettlementAmount = 15000m, + Status = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("54000000-0000-0000-0000-000000000001"), + Amount = 300m, + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_SaleReturnId = new Guid("51000000-0000-0000-0000-000000000001"), + IsActive = true, + IssuedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CancelTypeCode") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_DriverProfileId") + .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("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_DriverProfileId"); + + b.HasIndex("FK_VehicleId"); + + b.ToTable("DeliveryRuns"); + + b.HasData( + new + { + Id = new Guid("31000000-0000-0000-0000-000000000001"), + ClosedAt = new DateTime(2026, 8, 25, 14, 0, 0, 0, DateTimeKind.Unspecified), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_DriverProfileId = new Guid("30000000-0000-0000-0000-000000000001"), + FK_VehicleId = new Guid("28000000-0000-0000-0000-000000000001"), + IsActive = true, + Meterend = 10550, + Meterstart = 10500, + StartedAt = new DateTime(2026, 8, 25, 8, 0, 0, 0, DateTimeKind.Unspecified), + Status = 2 + }, + new + { + Id = new Guid("31000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 30, 8, 45, 0, 0, DateTimeKind.Unspecified), + FK_DriverProfileId = new Guid("30000000-0000-0000-0000-000000000002"), + FK_VehicleId = new Guid("28000000-0000-0000-0000-000000000002"), + IsActive = true, + Meterend = 0, + Meterstart = 20300, + StartedAt = new DateTime(2026, 8, 30, 9, 0, 0, 0, DateTimeKind.Unspecified), + Status = 0 + }); + }); + + 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_RideId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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_RideId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("DeliveryTasks"); + + b.HasData( + new + { + Id = new Guid("37000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 30, 10, 5, 0, 0, DateTimeKind.Unspecified), + CustomerConfirmed = false, + FK_RideId = new Guid("31000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), + IsActive = true, + Sequence = 1, + Status = 0 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("64000000-0000-0000-0000-000000000001"), + EnforceLicenseExpiry = true, + EnforceMedicalClearance = true, + UpdatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + UpdatedBy = new Guid("29000000-0000-0000-0000-000000000001") + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("30000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000004"), + IsActive = true, + LicenseExpiry = new DateTime(2027, 6, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + LicenseNumber = "B1234567", + MedicalClearanceExpiry = new DateTime(2027, 1, 31, 0, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("30000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000005"), + IsActive = true, + LicenseExpiry = new DateTime(2027, 9, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), + LicenseNumber = "B7654321", + MedicalClearanceExpiry = new DateTime(2027, 3, 31, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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_SupplierId") + .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_SupplierId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("GoodsReceipts"); + + b.HasData( + new + { + Id = new Guid("43000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + FK_ReceivedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + ReceivedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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_ItemId") + .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_ItemId"); + + b.HasIndex("FK_PurchaseOrderItemId"); + + b.ToTable("GoodsReceiptItems"); + + b.HasData( + new + { + Id = new Guid("44000000-0000-0000-0000-000000000001"), + AcceptedQuantity = 200, + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000001"), + IsActive = true, + RejectedQuantity = 0 + }, + new + { + Id = new Guid("44000000-0000-0000-0000-000000000002"), + AcceptedQuantity = 98, + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000002"), + IsActive = true, + RejectedQuantity = 2 + }); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemBatchId"); + + b.ToTable("GoodsReceiptItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("GoodsReceiptItemSerials"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.Property("FinalizedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalizedBy") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("PrintedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrintedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("Invoices"); + + b.HasData( + new + { + Id = new Guid("34500000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 8, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + FinalizedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FinalizedBy = new Guid("29000000-0000-0000-0000-000000000001"), + IsActive = true, + PaymentStatus = 2, + PrintedAt = new DateTime(2026, 8, 25, 8, 45, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000002"), + Status = 2, + TotalAmount = 3000m + }, + new + { + Id = new Guid("34500000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + PaymentStatus = 1, + PrintedAt = new DateTime(2026, 8, 31, 9, 15, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 1, + TotalAmount = 2400m + }); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomWarrantyPeriod") + .HasColumnType("tinyint(1)"); + + 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("HasWarranty") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReorderLevel") + .HasColumnType("decimal(65,30)"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.Property("TracksBatches") + .HasColumnType("tinyint(1)"); + + b.Property("TracksExpiryDate") + .HasColumnType("tinyint(1)"); + + b.Property("TracksSerialNumbers") + .HasColumnType("tinyint(1)"); + + b.Property("WarrantyPeriodDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + + b.HasData( + new + { + Id = new Guid("23000000-0000-0000-0000-000000000001"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 500ml bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 500ml", + SalePrice = 60m, + Sku = "BEV-COKE-500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000002"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 1.5L bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 1.5L", + SalePrice = 120m, + Sku = "BEV-COKE-1500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000003"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Classic Salted 52g pack", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Classic 52g", + SalePrice = 50m, + Sku = "SNK-LAYS-52", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000004"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Family Pack 150g", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Family Pack 150g", + SalePrice = 150m, + Sku = "SNK-LAYS-FAM", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }); + }); + + modelBuilder.Entity("Distribution.Models.ItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchNumber") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ManufactureDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "BatchNumber"); + + b.ToTable("ItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentLocationId") + .HasColumnType("char(36)"); + + b.Property("CurrentLocationType") + .HasColumnType("int"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("WarrantyExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("WarrantyPeriodDaysOverride") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "SerialNumber") + .IsUnique(); + + b.ToTable("ItemSerialNumbers"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PopulatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PopulatedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("LoadingSheets"); + + b.HasData( + new + { + Id = new Guid("35000000-0000-0000-0000-000000000001"), + ConfirmedAt = new DateTime(2026, 8, 25, 7, 45, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_RideId = new Guid("31000000-0000-0000-0000-000000000001"), + IsActive = true, + PopulatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + PopulatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + Status = 1 + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOItemId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.HasIndex("FK_ShopPOItemId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("LoadingSheetItems"); + + b.HasData( + new + { + Id = new Guid("36000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 50m + }, + new + { + Id = new Guid("36000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 30m + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetOrder", b => + { + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.HasKey("FK_LoadingSheetId", "FK_ShopPOId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("LoadingSheetOrders"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("ChequeDate") + .HasColumnType("datetime(6)"); + + b.Property("ChequeNumber") + .HasColumnType("longtext"); + + b.Property("CollectedAt") + .HasColumnType("datetime(6)"); + + b.Property("CollectedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntryMode") + .HasColumnType("int"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("RecordedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceId"); + + b.ToTable("Payments"); + + b.HasData( + new + { + Id = new Guid("47000000-0000-0000-0000-000000000001"), + Amount = 3000m, + CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"), + IsActive = true, + Method = 0 + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000002"), + Amount = 1000m, + CollectedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 0 + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000003"), + Amount = 800m, + ChequeDate = new DateTime(2026, 8, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + ChequeNumber = "000456", + CollectedAt = new DateTime(2026, 8, 31, 11, 0, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 18, 0, 0, 0, DateTimeKind.Unspecified), + EntryMode = 1, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 1, + RecordedBy = new Guid("29000000-0000-0000-0000-000000000001") + }, + new + { + Id = new Guid("47000000-0000-0000-0000-000000000004"), + Amount = 300m, + CollectedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), + CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), + EntryMode = 0, + FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + Method = 2 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("39000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + EffectiveFrom = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + EffectiveTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Name = "Western Province Price List" + }); + }); + + modelBuilder.Entity("Distribution.Models.PriceListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AgencyMarginPercent") + .HasColumnType("decimal(65,30)"); + + 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.Property("RetailPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_PriceListId", "FK_ItemId") + .IsUnique(); + + b.ToTable("PriceListItems"); + + b.HasData( + new + { + Id = new Guid("40000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_PriceListId = new Guid("39000000-0000-0000-0000-000000000001"), + IsActive = true, + MinPrice = 55m, + Price = 60m + }, + new + { + Id = new Guid("40000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PriceListId = new Guid("39000000-0000-0000-0000-000000000001"), + IsActive = true, + MinPrice = 45m, + Price = 50m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("41000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_SupplierId = new Guid("38000000-0000-0000-0000-000000000001"), + IsActive = true, + Status = 2 + }, + new + { + Id = new Guid("41000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_SupplierId = new Guid("38000000-0000-0000-0000-000000000002"), + IsActive = true, + Status = 2 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("42000000-0000-0000-0000-000000000001"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + IsCancelled = false, + Quantity = 200, + ReceivedQuantity = 200, + UnitCost = 40m + }, + new + { + Id = new Guid("42000000-0000-0000-0000-000000000002"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + IsCancelled = false, + Quantity = 100, + ReceivedQuantity = 98, + UnitCost = 80m + }, + new + { + Id = new Guid("42000000-0000-0000-0000-000000000003"), + CancelledQuantity = 0, + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000002"), + IsActive = true, + IsCancelled = false, + Quantity = 150, + ReceivedQuantity = 150, + UnitCost = 35m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("45000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000002"), + IsActive = true, + Reason = "Damaged in transit" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("46000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_PurchaseReturnId = new Guid("45000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 10 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("24000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Western Province" + }, + new + { + Id = new Guid("24000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Central Province" + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionAgencyMap", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_AgencyUserId") + .HasColumnType("char(36)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_AgencyUserId"); + + b.HasIndex("FK_RegionId", "FK_AgencyUserId") + .IsUnique(); + + b.ToTable("RegionAgencyMaps"); + + b.HasData( + new + { + Id = new Guid("57000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_AgencyUserId = new Guid("70000000-0000-0000-0000-000000000001"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BudgetAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalBudgets"); + + b.HasData( + new + { + Id = new Guid("58000000-0000-0000-0000-000000000001"), + BudgetAmount = 200000m, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Period = "2026-08" + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Period") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TargetValue") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId", "Period") + .IsUnique(); + + b.ToTable("RegionalTargets"); + + b.HasData( + new + { + Id = new Guid("59000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Period = "2026-08", + TargetValue = 500000m + }); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RegionId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RuleConfig") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RegionId") + .IsUnique(); + + b.ToTable("RegionalTaxRules"); + + b.HasData( + new + { + Id = new Guid("60000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + RuleConfig = "{\"rate\": 0.15}" + }); + }); + + 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") + .HasColumnType("longtext"); + + b.Property("FK_OwnerUserId") + .HasColumnType("char(36)"); + + 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") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_OwnerUserId"); + + b.HasIndex("FK_RegionId"); + + b.ToTable("Routes"); + + b.HasData( + new + { + Id = new Guid("25000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Dehiwala", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Length = 18.5m, + RouteName = "Colombo City Route", + StartPoint = "Colombo Fort" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Negombo", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + IsActive = true, + Length = 32.0m, + RouteName = "Negombo Coastal Route", + StartPoint = "Wattala" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Peradeniya", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + IsActive = true, + Length = 12.0m, + RouteName = "Kandy Town Route", + StartPoint = "Kandy Lake" + }, + new + { + Id = new Guid("25000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + EndPoint = "Matale", + FK_OwnerUserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + IsActive = true, + Length = 26.0m, + RouteName = "Matale Route", + StartPoint = "Kandy" + }); + }); + + modelBuilder.Entity("Distribution.Models.RouteVisitLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckInAt") + .HasColumnType("datetime(6)"); + + b.Property("CheckInLatitude") + .HasColumnType("decimal(65,30)"); + + b.Property("CheckInLongitude") + .HasColumnType("decimal(65,30)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDeviationFlagged") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.HasIndex("FK_StoreId"); + + b.HasIndex("FK_UserId"); + + b.ToTable("RouteVisitLogs"); + + b.HasData( + new + { + Id = new Guid("66000000-0000-0000-0000-000000000001"), + CheckInAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + CheckInLatitude = 6.9344m, + CheckInLongitude = 79.8428m, + CheckOutAt = new DateTime(2026, 8, 25, 9, 25, 0, 0, DateTimeKind.Unspecified), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000001"), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000002"), + IsActive = true, + IsDeviationFlagged = false + }, + new + { + Id = new Guid("66000000-0000-0000-0000-000000000002"), + CheckInAt = new DateTime(2026, 8, 25, 10, 0, 0, 0, DateTimeKind.Unspecified), + CheckInLatitude = 6.9000m, + CheckInLongitude = 79.8654m, + CreatedAt = new DateTime(2026, 8, 25, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000002"), + FK_UserId = new Guid("29000000-0000-0000-0000-000000000002"), + IsActive = true, + IsDeviationFlagged = true + }); + }); + + 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_ShopPOId") + .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_ShopPOId"); + + b.ToTable("SaleReturns"); + + b.HasData( + new + { + Id = new Guid("51000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreatedByUserId = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Mode = 0, + TotalCreditAmount = 300m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("52000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 26, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_SaleReturnId = new Guid("51000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 5m, + ReasonCode = 1, + UnitPrice = 60m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("69000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true + }, + new + { + Id = new Guid("69000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("48000000-0000-0000-0000-000000000001"), + AgentLimitExempt = false, + BaselineExpectedSales = 500000m, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeFrom = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + FK_TriggerItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FreeQty = 1m, + IsActive = true, + Name = "Buy 10 Coke Get 1 Free", + RuleType = 0, + TriggerQty = 10 + }, + new + { + Id = new Guid("48000000-0000-0000-0000-000000000002"), + AgentLimitExempt = false, + CreatedAt = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeFrom = new DateTime(2026, 8, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + DateRangeTo = new DateTime(2026, 12, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000004"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000002"), + FreeQty = 1m, + IsActive = true, + Name = "Snack Combo Bundle", + RuleType = 3 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("49000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_SchemeId = new Guid("48000000-0000-0000-0000-000000000002"), + Qty = 5 + }); + }); + + 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_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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_SchemeId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("SchemeRedemptions"); + + b.HasData( + new + { + Id = new Guid("50000000-0000-0000-0000-000000000001"), + DiscountAmount = 0m, + FK_FreeItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_SchemeId = new Guid("48000000-0000-0000-0000-000000000001"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + FreeItemCostValue = 40m, + FreeQty = 1m, + RedeemedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Distribution.Models.ShopPO", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CancelledBy") + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("FulfilledAt") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("InvoiceId"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedById") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_StoreId"); + + b.ToTable("ShopPOs"); + + b.HasData( + new + { + Id = new Guid("33000000-0000-0000-0000-000000000001"), + ConfirmedAt = new DateTime(2026, 8, 25, 8, 30, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000001"), + FulfilledAt = new DateTime(2026, 8, 25, 13, 0, 0, 0, DateTimeKind.Unspecified), + InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000002"), + Status = 2, + TotalAmount = 3000m + }, + new + { + Id = new Guid("33000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 0, + TotalAmount = 1200m + }, + new + { + Id = new Guid("33000000-0000-0000-0000-000000000003"), + ConfirmedAt = new DateTime(2026, 8, 31, 8, 30, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + FulfilledAt = new DateTime(2026, 8, 31, 13, 0, 0, 0, DateTimeKind.Unspecified), + InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"), + IsActive = true, + RaisedById = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 2, + TotalAmount = 2400m + }); + }); + + modelBuilder.Entity("Distribution.Models.ShopPOItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .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("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("ShopPOItems"); + + b.HasData( + new + { + Id = new Guid("34000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 30, + QuantityFulfilled = 30, + UnitPrice = 60m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 24, + QuantityFulfilled = 24, + UnitPrice = 50m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), + IsActive = true, + Quantity = 10, + QuantityFulfilled = 0, + UnitPrice = 120m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + Quantity = 20, + QuantityFulfilled = 20, + UnitPrice = 120m + }); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .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_ItemBatchId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.HasIndex("FK_ItemId", "LocationId", "FK_ItemBatchId", "FK_ItemSerialNumberId") + .IsUnique(); + + b.ToTable("StockBalances"); + + b.HasData( + new + { + Id = new Guid("61000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 150m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 90m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000003"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("28000000-0000-0000-0000-000000000001"), + LocationType = 1, + Quantity = 20m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + 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_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .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_ItemBatchId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("StockMovements"); + + b.HasData( + new + { + Id = new Guid("62000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + FromLocationType = 2, + Quantity = 200m, + ReferenceId = new Guid("43000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + ToLocationType = 0, + Type = 6 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + FromLocationType = 0, + Quantity = 50m, + ReferenceId = new Guid("35000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + ToLocationType = 1, + Type = 5 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + FromLocationType = 1, + Quantity = 30m, + ReferenceId = new Guid("32000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + ToLocationType = 2, + Type = 0 + }); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + 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("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") + .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"); + + b.HasData( + new + { + Id = new Guid("26000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 50000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.9344m, + Longitude = 79.8428m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Saman Kumara", + Segment = "Gold", + StoreAddress = "12 York Street, Colombo Fort", + StoreName = "Fort Mini Mart" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 30000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.8508m, + Longitude = 79.8654m, + OneTimeAmount = 0m, + OutstandingBalance = 5000m, + OwnerName = "Nadeesha Silva", + Segment = "Silver", + StoreAddress = "45 Galle Road, Dehiwala", + StoreName = "Dehiwala Grocery" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 40000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 6.9890m, + Longitude = 79.8912m, + OneTimeAmount = 0m, + OutstandingBalance = 1200m, + OwnerName = "Ruwan Gunasekara", + Segment = "Gold", + StoreAddress = "88 Negombo Road, Wattala", + StoreName = "Wattala Supermarket" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 25000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 7.2094m, + Longitude = 79.8380m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Chamari Peiris", + StoreAddress = "5 Beach Road, Negombo", + StoreName = "Negombo Beach Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 35000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000003"), + IsActive = true, + Latitude = 7.2906m, + Longitude = 80.6337m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Dinesh Wickramasinghe", + Segment = "Key Account", + StoreAddress = "3 Lake Road, Kandy", + StoreName = "Kandy Lake View Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 20000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000004"), + IsActive = true, + Latitude = 7.4675m, + Longitude = 80.6234m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Ishara Bandara", + StoreAddress = "20 Main Street, Matale", + StoreName = "Matale Central Store" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("38000000-0000-0000-0000-000000000001"), + ContactInfo = "+94 11 234 5678", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Ceylon Beverages Ltd", + PerformanceScore = 4.2m + }, + new + { + Id = new Guid("38000000-0000-0000-0000-000000000002"), + ContactInfo = "+94 11 876 5432", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Snack Foods (Pvt) Ltd" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("55000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + Details = "2 units of Coke 1.5L received damaged", + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + RaisedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + ResolutionAmount = 160m, + ResolutionNotes = "Credited against next invoice", + ResolutionType = 1, + ResolvedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + ResolvedByUserId = new Guid("29000000-0000-0000-0000-000000000006"), + Status = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("56000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_SupplierClaimId = new Guid("55000000-0000-0000-0000-000000000001"), + IsActive = true, + IssueType = 0, + Quantity = 2m + }); + }); + + modelBuilder.Entity("Distribution.Models.SysConfig", b => + { + b.Property("Key") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Key"); + + b.ToTable("SysConfigs"); + + b.HasData( + new + { + Key = "InvoiceCreationMode", + UpdatedAt = new DateTime(2026, 9, 17, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "ModeB" + }, + new + { + Key = "Feature.SerialNumberTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.BatchTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.ExpiryDateTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.WarrantyTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.CustomWarrantyOverride", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.DefaultWarrantyPeriodDays", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "0" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("22000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Piece" + }, + new + { + Id = new Guid("67000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Case" + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("67000000-0000-0000-0000-000000000002"), + FK_FromUomId = new Guid("67000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_ToUomId = new Guid("22000000-0000-0000-0000-000000000001"), + Factor = 24m + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000002"), + FullName = "Sunil Fernando", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "sunil.fernando" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Nimal Perera", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "nimal.perera" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Kamal Silva", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "kamal.silva" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Ruwan Jayasuriya", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "ruwan.jayasuriya" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ReportsToUserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000001"), + FullName = "Priyantha Kumara", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "priyantha.kumara" + }, + new + { + Id = new Guid("29000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000005"), + FullName = "Priya Rajapaksa", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "priya.rajapaksa" + }, + new + { + Id = new Guid("70000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000006"), + FullName = "Channa De Silva", + IsActive = true, + PasswordHash = "AQAAAAIAAYagAAAAEKGnq/U4j6uM9g5mFkxbUE8WU/AhnSNiwrKCiPAqGnzNcaMgZAF5Zal/rJUUYPZPpg==", + Username = "channa.desilva" + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000006"), + FK_RegionId = new Guid("24000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000001"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000001"), + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000002"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + FK_UserId = new Guid("29000000-0000-0000-0000-000000000003"), + FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("68000000-0000-0000-0000-000000000003"), + IsActive = true + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("28000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + DefaultWarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + NumberPlate = "CAB-1234", + RegNumber = "WP-CAB-1234", + Type = 2 + }, + new + { + Id = new Guid("28000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + DefaultWarehouseId = new Guid("27000000-0000-0000-0000-000000000002"), + IsActive = true, + NumberPlate = "KY-5678", + RegNumber = "CP-KY-5678", + Type = 1 + }); + }); + + 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"); + + b.HasData( + new + { + Id = new Guid("27000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Location = "Peliyagoda, Colombo", + Name = "Colombo Distribution Center", + Type = 0 + }, + new + { + Id = new Guid("27000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Location = "Katugastota, Kandy", + Name = "Kandy Fulfillment Center", + Type = 1 + }); + }); + + 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.Vehicle", "Vehicle") + .WithMany() + .HasForeignKey("FK_VehicleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DriverProfile"); + + b.Navigation("Vehicle"); + }); + + modelBuilder.Entity("Distribution.Models.DeliveryTask", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + + b.Navigation("ShopPO"); + }); + + 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); + + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Supplier"); + + 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.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.PurchaseOrderItem", "PurchaseOrderItem") + .WithMany() + .HasForeignKey("FK_PurchaseOrderItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GoodsReceipt"); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrderItem"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemBatch", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Batches") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemBatch"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Serials") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemSerialNumber"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ShopPO"); + }); + + 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.ItemBatch", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.HasOne("Distribution.Models.DeliveryRun", "Ride") + .WithMany() + .HasForeignKey("FK_RideId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ride"); + }); + + 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.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPOItem", "ShopPOItem") + .WithMany() + .HasForeignKey("FK_ShopPOItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Distribution.Models.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("FK_WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("LoadingSheet"); + + b.Navigation("ShopPOItem"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetOrder", b => + { + b.HasOne("Distribution.Models.LoadingSheet", "LoadingSheet") + .WithMany("Orders") + .HasForeignKey("FK_LoadingSheetId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LoadingSheet"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Invoice"); + }); + + 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.RegionAgencyMap", b => + { + b.HasOne("Distribution.Models.User", "AgencyUser") + .WithMany() + .HasForeignKey("FK_AgencyUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AgencyUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalBudget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTarget", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RegionalTaxRule", b => + { + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Region"); + }); + + 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.User", "OwnerUser") + .WithMany() + .HasForeignKey("FK_OwnerUserId"); + + b.HasOne("Distribution.Models.Region", "Region") + .WithMany() + .HasForeignKey("FK_RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OwnerUser"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Distribution.Models.RouteVisitLog", b => + { + b.HasOne("Distribution.Models.Route", "Route") + .WithMany() + .HasForeignKey("FK_RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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("Route"); + + b.Navigation("Store"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.SaleReturn", b => + { + b.HasOne("Distribution.Models.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("ShopPO"); + }); + + 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.Scheme", "Scheme") + .WithMany("Redemptions") + .HasForeignKey("FK_SchemeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany() + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("FreeItem"); + + b.Navigation("Scheme"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.ShopPO", b => + { + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.ShopPOItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPO", "ShopPO") + .WithMany("Items") + .HasForeignKey("FK_ShopPOId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ShopPO"); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); + }); + + 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.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.GoodsReceiptItem", b => + { + b.Navigation("Batches"); + + b.Navigation("Serials"); + }); + + modelBuilder.Entity("Distribution.Models.Invoice", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Orders"); + }); + + 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.ShopPO", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.cs b/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.cs new file mode 100644 index 0000000..1e79e81 --- /dev/null +++ b/Distribution/Migrations/20261006070042_AddDirectGoodsReceipt.cs @@ -0,0 +1,148 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddDirectGoodsReceipt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "FK_PurchaseOrderId", + table: "GoodsReceipts", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FK_SupplierId", + table: "GoodsReceipts", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "FK_PurchaseOrderItemId", + table: "GoodsReceiptItems", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "FK_ItemId", + table: "GoodsReceiptItems", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.UpdateData( + table: "GoodsReceiptItems", + keyColumn: "Id", + keyValue: new Guid("44000000-0000-0000-0000-000000000001"), + column: "FK_ItemId", + value: null); + + migrationBuilder.UpdateData( + table: "GoodsReceiptItems", + keyColumn: "Id", + keyValue: new Guid("44000000-0000-0000-0000-000000000002"), + column: "FK_ItemId", + value: null); + + migrationBuilder.UpdateData( + table: "GoodsReceipts", + keyColumn: "Id", + keyValue: new Guid("43000000-0000-0000-0000-000000000001"), + column: "FK_SupplierId", + value: null); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceipts_FK_SupplierId", + table: "GoodsReceipts", + column: "FK_SupplierId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodsReceiptItems_FK_ItemId", + table: "GoodsReceiptItems", + column: "FK_ItemId"); + + migrationBuilder.AddForeignKey( + name: "FK_GoodsReceiptItems_Items_FK_ItemId", + table: "GoodsReceiptItems", + column: "FK_ItemId", + principalTable: "Items", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_GoodsReceipts_Suppliers_FK_SupplierId", + table: "GoodsReceipts", + column: "FK_SupplierId", + principalTable: "Suppliers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_GoodsReceiptItems_Items_FK_ItemId", + table: "GoodsReceiptItems"); + + migrationBuilder.DropForeignKey( + name: "FK_GoodsReceipts_Suppliers_FK_SupplierId", + table: "GoodsReceipts"); + + migrationBuilder.DropIndex( + name: "IX_GoodsReceipts_FK_SupplierId", + table: "GoodsReceipts"); + + migrationBuilder.DropIndex( + name: "IX_GoodsReceiptItems_FK_ItemId", + table: "GoodsReceiptItems"); + + migrationBuilder.DropColumn( + name: "FK_SupplierId", + table: "GoodsReceipts"); + + migrationBuilder.DropColumn( + name: "FK_ItemId", + table: "GoodsReceiptItems"); + + migrationBuilder.AlterColumn( + name: "FK_PurchaseOrderId", + table: "GoodsReceipts", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "FK_PurchaseOrderItemId", + table: "GoodsReceiptItems", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + } + } +} diff --git a/Distribution/Migrations/20261006100000_AddRegionCode.cs b/Distribution/Migrations/20261006100000_AddRegionCode.cs new file mode 100644 index 0000000..376f7d6 --- /dev/null +++ b/Distribution/Migrations/20261006100000_AddRegionCode.cs @@ -0,0 +1,54 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + [DbContext(typeof(AppDBContext))] + [Migration("20261006100000_AddRegionCode")] + public partial class AddRegionCode : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Code", + table: "Regions", + type: "varchar(50)", + maxLength: 50, + nullable: false, + defaultValue: ""); + + // Backfill existing rows (seeded regions first) with REG-0001, REG-0002, ... before the unique index. + migrationBuilder.Sql(@" + UPDATE `Regions` r + JOIN ( + SELECT `Id`, ROW_NUMBER() OVER ( + ORDER BY CASE WHEN `Id` LIKE '24000000-%' THEN 0 ELSE 1 END, `CreatedAt`, `Id`) AS rn + FROM `Regions` + ) x ON r.`Id` = x.`Id` + SET r.`Code` = CONCAT('REG-', LPAD(x.rn, 4, '0'));"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_Code", + table: "Regions", + column: "Code", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Regions_Code", + table: "Regions"); + + migrationBuilder.DropColumn( + name: "Code", + table: "Regions"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index 6393e1d..33c22c4 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -427,12 +427,15 @@ namespace Distribution.Migrations b.Property("CreatedAt") .HasColumnType("datetime(6)"); - b.Property("FK_PurchaseOrderId") + b.Property("FK_PurchaseOrderId") .HasColumnType("char(36)"); b.Property("FK_ReceivedByUserId") .HasColumnType("char(36)"); + b.Property("FK_SupplierId") + .HasColumnType("char(36)"); + b.Property("FK_WarehouseId") .HasColumnType("char(36)"); @@ -446,6 +449,8 @@ namespace Distribution.Migrations b.HasIndex("FK_PurchaseOrderId"); + b.HasIndex("FK_SupplierId"); + b.HasIndex("FK_WarehouseId"); b.ToTable("GoodsReceipts"); @@ -478,7 +483,10 @@ namespace Distribution.Migrations b.Property("FK_GoodsReceiptId") .HasColumnType("char(36)"); - b.Property("FK_PurchaseOrderItemId") + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderItemId") .HasColumnType("char(36)"); b.Property("IsActive") @@ -491,6 +499,8 @@ namespace Distribution.Migrations b.HasIndex("FK_GoodsReceiptId"); + b.HasIndex("FK_ItemId"); + b.HasIndex("FK_PurchaseOrderItemId"); b.ToTable("GoodsReceiptItems"); @@ -518,6 +528,63 @@ namespace Distribution.Migrations }); }); + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemBatchId"); + + b.ToTable("GoodsReceiptItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("GoodsReceiptItemSerials"); + }); + modelBuilder.Entity("Distribution.Models.Invoice", b => { b.Property("Id") @@ -600,6 +667,9 @@ namespace Distribution.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("AllowCustomWarrantyPeriod") + .HasColumnType("tinyint(1)"); + b.Property("CostPrice") .HasColumnType("decimal(65,30)"); @@ -624,6 +694,9 @@ namespace Distribution.Migrations b.Property("FK_SubCategoryId") .HasColumnType("char(36)"); + b.Property("HasWarranty") + .HasColumnType("tinyint(1)"); + b.Property("IsActive") .HasColumnType("tinyint(1)"); @@ -634,7 +707,6 @@ namespace Distribution.Migrations .IsRequired() .HasColumnType("longtext"); - b.Property("ReorderLevel") .HasColumnType("decimal(65,30)"); @@ -648,6 +720,18 @@ namespace Distribution.Migrations b.Property("SyncSource") .HasColumnType("int"); + b.Property("TracksBatches") + .HasColumnType("tinyint(1)"); + + b.Property("TracksExpiryDate") + .HasColumnType("tinyint(1)"); + + b.Property("TracksSerialNumbers") + .HasColumnType("tinyint(1)"); + + b.Property("WarrantyPeriodDays") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("FK_BaseUomId"); @@ -664,61 +748,157 @@ namespace Distribution.Migrations new { Id = new Guid("23000000-0000-0000-0000-000000000001"), + AllowCustomWarrantyPeriod = false, CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), Description = "Coca-Cola 500ml bottle", FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, IsActive = true, Name = "Coca-Cola 500ml", SalePrice = 60m, Sku = "BEV-COKE-500", - SyncSource = 0 + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false }, new { Id = new Guid("23000000-0000-0000-0000-000000000002"), + AllowCustomWarrantyPeriod = false, CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), Description = "Coca-Cola 1.5L bottle", FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, IsActive = true, Name = "Coca-Cola 1.5L", SalePrice = 120m, Sku = "BEV-COKE-1500", - SyncSource = 0 + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false }, new { Id = new Guid("23000000-0000-0000-0000-000000000003"), + AllowCustomWarrantyPeriod = false, CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), Description = "Lays Classic Salted 52g pack", FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, IsActive = true, Name = "Lays Classic 52g", SalePrice = 50m, Sku = "SNK-LAYS-52", - SyncSource = 0 + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false }, new { Id = new Guid("23000000-0000-0000-0000-000000000004"), + AllowCustomWarrantyPeriod = false, CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), Description = "Lays Family Pack 150g", FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, IsActive = true, Name = "Lays Family Pack 150g", SalePrice = 150m, Sku = "SNK-LAYS-FAM", - SyncSource = 0 + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false }); }); + modelBuilder.Entity("Distribution.Models.ItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchNumber") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ManufactureDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "BatchNumber"); + + b.ToTable("ItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentLocationId") + .HasColumnType("char(36)"); + + b.Property("CurrentLocationType") + .HasColumnType("int"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("WarrantyExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("WarrantyPeriodDaysOverride") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "SerialNumber") + .IsUnique(); + + b.ToTable("ItemSerialNumbers"); + }); + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => { b.Property("Id") @@ -1294,6 +1474,11 @@ namespace Distribution.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + b.Property("CreatedAt") .HasColumnType("datetime(6)"); @@ -1306,6 +1491,9 @@ namespace Distribution.Migrations b.HasKey("Id"); + b.HasIndex("Code") + .IsUnique(); + b.ToTable("Regions"); b.HasData( @@ -1314,6 +1502,7 @@ namespace Distribution.Migrations Id = new Guid("24000000-0000-0000-0000-000000000001"), CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), IsActive = true, + Code = "REG-0001", Name = "Western Province" }, new @@ -1321,6 +1510,7 @@ namespace Distribution.Migrations Id = new Guid("24000000-0000-0000-0000-000000000002"), CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), IsActive = true, + Code = "REG-0002", Name = "Central Province" }); }); @@ -2239,9 +2429,15 @@ namespace Distribution.Migrations .ValueGeneratedOnAdd() .HasColumnType("char(36)"); + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + b.Property("FK_ItemId") .HasColumnType("char(36)"); + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + b.Property("LocationId") .HasColumnType("char(36)"); @@ -2256,7 +2452,11 @@ namespace Distribution.Migrations b.HasKey("Id"); - b.HasIndex("FK_ItemId", "LocationId") + b.HasIndex("FK_ItemBatchId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.HasIndex("FK_ItemId", "LocationId", "FK_ItemBatchId", "FK_ItemSerialNumberId") .IsUnique(); b.ToTable("StockBalances"); @@ -2303,9 +2503,15 @@ namespace Distribution.Migrations b.Property("CreatedBy") .HasColumnType("char(36)"); + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + b.Property("FK_ItemId") .HasColumnType("char(36)"); + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + b.Property("FromLocationId") .HasColumnType("char(36)"); @@ -2332,8 +2538,12 @@ namespace Distribution.Migrations b.HasKey("Id"); + b.HasIndex("FK_ItemBatchId"); + b.HasIndex("FK_ItemId"); + b.HasIndex("FK_ItemSerialNumberId"); + b.ToTable("StockMovements"); b.HasData( @@ -2763,6 +2973,42 @@ namespace Distribution.Migrations Key = "InvoiceCreationMode", UpdatedAt = new DateTime(2026, 9, 17, 0, 0, 0, 0, DateTimeKind.Unspecified), Value = "ModeB" + }, + new + { + Key = "Feature.SerialNumberTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.BatchTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.ExpiryDateTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.WarrantyTracking", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.CustomWarrantyOverride", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "false" + }, + new + { + Key = "Feature.DefaultWarrantyPeriodDays", + UpdatedAt = new DateTime(2026, 10, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + Value = "0" }); }); @@ -3256,8 +3502,12 @@ namespace Distribution.Migrations b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") .WithMany() .HasForeignKey("FK_PurchaseOrderId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict); b.HasOne("Distribution.Models.Warehouse", "Warehouse") .WithMany() @@ -3267,6 +3517,8 @@ namespace Distribution.Migrations b.Navigation("PurchaseOrder"); + b.Navigation("Supplier"); + b.Navigation("Warehouse"); }); @@ -3278,17 +3530,61 @@ namespace Distribution.Migrations .OnDelete(DeleteBehavior.Restrict) .IsRequired(); + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Distribution.Models.PurchaseOrderItem", "PurchaseOrderItem") .WithMany() .HasForeignKey("FK_PurchaseOrderItemId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.Restrict); b.Navigation("GoodsReceipt"); + b.Navigation("Item"); + b.Navigation("PurchaseOrderItem"); }); + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemBatch", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Batches") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemBatch"); + }); + + modelBuilder.Entity("Distribution.Models.GoodsReceiptItemSerial", b => + { + b.HasOne("Distribution.Models.GoodsReceiptItem", "GoodsReceiptItem") + .WithMany("Serials") + .HasForeignKey("FK_GoodsReceiptItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("GoodsReceiptItem"); + + b.Navigation("ItemSerialNumber"); + }); + modelBuilder.Entity("Distribution.Models.Invoice", b => { b.HasOne("Distribution.Models.ShopPO", "ShopPO") @@ -3331,6 +3627,28 @@ namespace Distribution.Migrations b.Navigation("SubCategory"); }); + modelBuilder.Entity("Distribution.Models.ItemBatch", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => { b.HasOne("Distribution.Models.DeliveryRun", "Ride") @@ -3746,24 +4064,52 @@ namespace Distribution.Migrations modelBuilder.Entity("Distribution.Models.StockBalance", b => { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Distribution.Models.Item", "Item") .WithMany() .HasForeignKey("FK_ItemId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); }); modelBuilder.Entity("Distribution.Models.StockMovement", b => { + b.HasOne("Distribution.Models.ItemBatch", "ItemBatch") + .WithMany() + .HasForeignKey("FK_ItemBatchId") + .OnDelete(DeleteBehavior.Restrict); + b.HasOne("Distribution.Models.Item", "Item") .WithMany() .HasForeignKey("FK_ItemId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Distribution.Models.ItemSerialNumber", "ItemSerialNumber") + .WithMany() + .HasForeignKey("FK_ItemSerialNumberId") + .OnDelete(DeleteBehavior.Restrict); + b.Navigation("Item"); + + b.Navigation("ItemBatch"); + + b.Navigation("ItemSerialNumber"); }); modelBuilder.Entity("Distribution.Models.Store", b => @@ -3964,6 +4310,13 @@ namespace Distribution.Migrations b.Navigation("Items"); }); + modelBuilder.Entity("Distribution.Models.GoodsReceiptItem", b => + { + b.Navigation("Batches"); + + b.Navigation("Serials"); + }); + modelBuilder.Entity("Distribution.Models.Invoice", b => { b.Navigation("Payments"); diff --git a/Distribution/Models/Enums.cs b/Distribution/Models/Enums.cs index b99ea98..26a62c1 100644 --- a/Distribution/Models/Enums.cs +++ b/Distribution/Models/Enums.cs @@ -44,4 +44,6 @@ namespace Distribution.Models /// ModeB: Invoice created as a separate explicit step. /// public enum InvoiceCreationMode { ModeA, ModeB } + + public enum SerialStatus { InStock, Sold, Returned, Damaged } } diff --git a/Distribution/Models/GoodsReceipt.cs b/Distribution/Models/GoodsReceipt.cs index 4ce1368..cee1549 100644 --- a/Distribution/Models/GoodsReceipt.cs +++ b/Distribution/Models/GoodsReceipt.cs @@ -4,9 +4,15 @@ namespace Distribution.Models { public class GoodsReceipt : Template { - public Guid FK_PurchaseOrderId { get; set; } + /// Null for a direct receipt (goods received without a purchase order). + public Guid? FK_PurchaseOrderId { get; set; } [ForeignKey("FK_PurchaseOrderId")] - public PurchaseOrder PurchaseOrder { get; set; } + public PurchaseOrder? PurchaseOrder { get; set; } + + /// Optional supplier for a direct receipt; for PO receipts the supplier comes from the PO. + public Guid? FK_SupplierId { get; set; } + [ForeignKey("FK_SupplierId")] + public Supplier? Supplier { get; set; } public Guid FK_ReceivedByUserId { get; set; } diff --git a/Distribution/Models/GoodsReceiptItem.cs b/Distribution/Models/GoodsReceiptItem.cs index 7a04ebb..b905f69 100644 --- a/Distribution/Models/GoodsReceiptItem.cs +++ b/Distribution/Models/GoodsReceiptItem.cs @@ -8,14 +8,26 @@ namespace Distribution.Models [ForeignKey("FK_GoodsReceiptId")] public GoodsReceipt GoodsReceipt { get; set; } - public Guid FK_PurchaseOrderItemId { get; set; } + /// Null for a direct receipt line (no purchase order); FK_ItemId is set instead. + public Guid? FK_PurchaseOrderItemId { get; set; } [ForeignKey("FK_PurchaseOrderItemId")] - public PurchaseOrderItem PurchaseOrderItem { get; set; } + public PurchaseOrderItem? PurchaseOrderItem { get; set; } + + /// Set for direct receipt lines. PO-based lines resolve the item through PurchaseOrderItem. + public Guid? FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item? Item { get; set; } /// Added to warehouse stock. public int AcceptedQuantity { get; set; } /// Damaged/short at receipt, not added to stock. public int RejectedQuantity { get; set; } = 0; + + /// Populated when the received item tracks batches and/or expiry dates. + public ICollection Batches { get; set; } = []; + + /// Populated when the received item tracks serial numbers. + public ICollection Serials { get; set; } = []; } } diff --git a/Distribution/Models/GoodsReceiptItemBatch.cs b/Distribution/Models/GoodsReceiptItemBatch.cs new file mode 100644 index 0000000..2d0ae9d --- /dev/null +++ b/Distribution/Models/GoodsReceiptItemBatch.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// One row per batch/expiry-lot split within a single GRN line. + public class GoodsReceiptItemBatch : Template + { + public Guid FK_GoodsReceiptItemId { get; set; } + [ForeignKey("FK_GoodsReceiptItemId")] + public GoodsReceiptItem GoodsReceiptItem { get; set; } + + public Guid FK_ItemBatchId { get; set; } + [ForeignKey("FK_ItemBatchId")] + public ItemBatch ItemBatch { get; set; } + + public int Quantity { get; set; } + } +} diff --git a/Distribution/Models/GoodsReceiptItemSerial.cs b/Distribution/Models/GoodsReceiptItemSerial.cs new file mode 100644 index 0000000..62d7180 --- /dev/null +++ b/Distribution/Models/GoodsReceiptItemSerial.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// One row per serial number received within a single GRN line. + public class GoodsReceiptItemSerial : Template + { + public Guid FK_GoodsReceiptItemId { get; set; } + [ForeignKey("FK_GoodsReceiptItemId")] + public GoodsReceiptItem GoodsReceiptItem { get; set; } + + public Guid FK_ItemSerialNumberId { get; set; } + [ForeignKey("FK_ItemSerialNumberId")] + public ItemSerialNumber ItemSerialNumber { get; set; } + } +} diff --git a/Distribution/Models/Item.cs b/Distribution/Models/Item.cs index f38de1c..48361ae 100644 --- a/Distribution/Models/Item.cs +++ b/Distribution/Models/Item.cs @@ -32,6 +32,13 @@ namespace Distribution.Models /// Cost/purchase price, used to value free-goods scheme redemptions for getSchemeROI. public decimal? CostPrice { get; set; } + public bool TracksSerialNumbers { get; set; } = false; + public bool TracksBatches { get; set; } = false; + public bool TracksExpiryDate { get; set; } = false; + public bool HasWarranty { get; set; } = false; + public int? WarrantyPeriodDays { get; set; } + public bool AllowCustomWarrantyPeriod { get; set; } = false; + public SyncSource SyncSource { get; set; } = SyncSource.Manual; public int? ExternalErpItemId { get; set; } public DateTime? LastSyncedAt { get; set; } diff --git a/Distribution/Models/ItemBatch.cs b/Distribution/Models/ItemBatch.cs new file mode 100644 index 0000000..e548679 --- /dev/null +++ b/Distribution/Models/ItemBatch.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// + /// Backs both batch/lot tracking and expiry-only tracking for an item. BatchNumber is null + /// when the item tracks an expiry date but not a batch number (TracksExpiryDate without + /// TracksBatches). + /// + public class ItemBatch : Template + { + public Guid FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item Item { get; set; } + + public string? BatchNumber { get; set; } + public DateTime? ManufactureDate { get; set; } + public DateTime? ExpiryDate { get; set; } + } +} diff --git a/Distribution/Models/ItemSerialNumber.cs b/Distribution/Models/ItemSerialNumber.cs new file mode 100644 index 0000000..f734067 --- /dev/null +++ b/Distribution/Models/ItemSerialNumber.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// One row per physical serial-tracked unit. Quantity is always 1. + public class ItemSerialNumber : Template + { + public Guid FK_ItemId { get; set; } + [ForeignKey("FK_ItemId")] + public Item Item { get; set; } + + public string SerialNumber { get; set; } + public SerialStatus Status { get; set; } = SerialStatus.InStock; + + public Guid? CurrentLocationId { get; set; } + public LocationType? CurrentLocationType { get; set; } + + public DateTime ReceivedAt { get; set; } + + /// Snapshotted at receipt when Item.AllowCustomWarrantyPeriod is true; otherwise null and the system default applies when the warranty starts. + public int? WarrantyPeriodDaysOverride { get; set; } + + /// Set when the serial is sold/delivered: sale date + (override ?? system default). Null while in stock or if Item.HasWarranty is false. + public DateTime? WarrantyExpiresAt { get; set; } + } +} diff --git a/Distribution/Models/Region.cs b/Distribution/Models/Region.cs index 877ee14..2800aee 100644 --- a/Distribution/Models/Region.cs +++ b/Distribution/Models/Region.cs @@ -2,6 +2,7 @@ { public class Region : Template { + public string Code { get; set; } = string.Empty; public string Name { get; set; } } } diff --git a/Distribution/Models/StockBalance.cs b/Distribution/Models/StockBalance.cs index 92192fe..917764b 100644 --- a/Distribution/Models/StockBalance.cs +++ b/Distribution/Models/StockBalance.cs @@ -13,6 +13,16 @@ namespace Distribution.Models public Guid LocationId { get; set; } public LocationType LocationType { get; set; } + /// Null for items that don't track batches/expiry — no behavior change for existing items. + public Guid? FK_ItemBatchId { get; set; } + [ForeignKey("FK_ItemBatchId")] + public ItemBatch? ItemBatch { get; set; } + + /// Null for items that don't track serial numbers. + public Guid? FK_ItemSerialNumberId { get; set; } + [ForeignKey("FK_ItemSerialNumberId")] + public ItemSerialNumber? ItemSerialNumber { get; set; } + public decimal Quantity { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.Now; } diff --git a/Distribution/Models/StockMovement.cs b/Distribution/Models/StockMovement.cs index e18c31e..d9384a4 100644 --- a/Distribution/Models/StockMovement.cs +++ b/Distribution/Models/StockMovement.cs @@ -16,6 +16,16 @@ namespace Distribution.Models public Guid? ToLocationId { get; set; } public LocationType? ToLocationType { get; set; } + /// Null for items that don't track batches/expiry — no behavior change for existing items. + public Guid? FK_ItemBatchId { get; set; } + [ForeignKey("FK_ItemBatchId")] + public ItemBatch? ItemBatch { get; set; } + + /// Null for items that don't track serial numbers. Quantity is always 1 when set. + public Guid? FK_ItemSerialNumberId { get; set; } + [ForeignKey("FK_ItemSerialNumberId")] + public ItemSerialNumber? ItemSerialNumber { get; set; } + public decimal Quantity { get; set; } public MovementType Type { get; set; } diff --git a/Distribution/Program.cs b/Distribution/Program.cs index 2fbc6bf..19dc98e 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -30,6 +30,7 @@ using Distribution.Features.Sync; using Distribution.Features.Uom; using Distribution.Features.User; using Distribution.Features.Vehicle; +using Distribution.Features.ProductReport; using Distribution.Features.Stock; using Distribution.Features.SysConfig; using Distribution.Features.Warehouse; @@ -62,6 +63,8 @@ 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(); @@ -75,6 +78,7 @@ builder.Services.AddScoped( builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -109,6 +113,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/Distribution/docs/refactor_docs/item_tracking_config.md b/Distribution/docs/refactor_docs/item_tracking_config.md new file mode 100644 index 0000000..63b322f --- /dev/null +++ b/Distribution/docs/refactor_docs/item_tracking_config.md @@ -0,0 +1,286 @@ +# [Feature] Item Tracking Configuration — Serial Numbers, Batches, Warranty, Expiry + +**Status**: Implemented (see [item_tracking_plan.md](./item_tracking_plan.md) for per-phase status). Open items: apply migration `AddDirectGoodsReceipt`; build the sale/delivery step that starts warranty. +**Touches**: `Item` catalogue, GRN intake, stock ledger (`StockBalance`/`StockMovement`), new +Configuration page (`distribution_fe/app/(dashboard)/configuration`) + +--- + +## Goal + +Let specific catalogue items opt into serial-number tracking, batch/lot tracking, and/or expiry-date +tracking, with an optional warranty period (system default or per-item custom override), while: + +1. Each feature (serial tracking, batch tracking, expiry-date tracking, warranty) can be turned + **on/off system-wide** from the new Configuration page, independent of any single item. +2. Each **item** can independently track serial numbers, batches, expiry dates, any combination of + the three, or none of them — never forced into a tracking mode it doesn't need. Expiry-date + tracking does not require batch tracking — a perishable item can carry a single expiry date per + goods receipt without ever being assigned a batch number. +3. Items that track none of these continue to work exactly as today — this must be additive, not a + breaking change to the existing stock ledger or GRN flow. +4. Batches and/or expiry-only lots carry an **expiry date**; serials carry a **warranty expiry**, + derived from a period in days that is either the system default or a per-item custom value (only + if the item is flagged to allow a custom override). + +--- + +## Current State (as found in the codebase, before implementation) + +- **`Item`** (`Models/Item.cs`) has no tracking-related fields today — just SKU, category, UOM, + pricing, `ReorderLevel` (added in the most recent migration, `20260928101132_AddItemReorderLevel`). +- **Stock ledger** (`Models/StockBalance.cs`, `Models/StockMovement.cs`) is keyed purely on + `FK_ItemId` + `LocationId`/`LocationType`. There is no concept of a batch or a serial unit + anywhere in the ledger — one aggregate quantity per item per location. +- **GRN** (`Features/Grn/GrnService.cs`, `Models/GoodsReceiptItem.cs`) records only + `AcceptedQuantity`/`RejectedQuantity` per PO line. No batch number, expiry, or serial capture + point exists anywhere in intake. +- **No existing entity** anywhere in the backend represents a batch, lot, serial number, or + warranty period — this is a net-new domain concept, not a rename/extension of something existing. +- **`SysConfig`** (`Models/SysConfig.cs`, `Features/SysConfig/*`) is a generic key/value store + already used for one system-wide toggle (`InvoiceCreationMode`, see + `SysConfigService.KeyInvoiceCreationMode`). This is the existing mechanism for global on/off + switches and is the natural home for the new feature toggles — no new entity needed for that part. + +--- + +## Two Layers of Configuration + +### 1. Global feature toggles (configuration page) — reuse `SysConfig` + +New well-known keys, following the exact pattern `KeyInvoiceCreationMode` already uses +(`SysConfigService.cs:8`, validated in `ValidateValue`): + +| Key | Values | Meaning | +|---|---|---| +| `Feature.SerialNumberTracking` | `"true"` / `"false"` | Whether any item is allowed to track serial numbers. | +| `Feature.BatchTracking` | `"true"` / `"false"` | Whether any item is allowed to track batches/lots. | +| `Feature.ExpiryDateTracking` | `"true"` / `"false"` | Whether any item is allowed to require an expiry date at intake — independent of batch tracking. | +| `Feature.WarrantyTracking` | `"true"` / `"false"` | Whether warranty periods apply at all. | +| `Feature.CustomWarrantyOverride` | `"true"` / `"false"` | Whether individual items may override the system default warranty period. | +| `Feature.DefaultWarrantyPeriodDays` | integer string | System-wide default warranty length in days, used when an item has warranty enabled but no custom override. | + +`SysConfigService` gains typed getters (`GetSerialTrackingEnabledAsync()`, etc.) mirroring +`GetInvoiceCreationModeAsync()`, plus validation in `ValidateValue` for the boolean/int keys. +`SysConfigController` already exposes generic `GET/PUT /api/sys-config/{key}`, so the +configuration page frontend needs no new backend endpoints for this layer — just calls to the +existing key/value routes. + +Turning a global toggle **off** does not delete per-item settings or existing batch/serial data — +it only hides the corresponding controls in the Item Sheet and disables new intake of that kind +(enforced in `ItemService`/`GrnService`, not just the UI). + +### 2. Per-item settings — new columns on `Item` + +| Column | Type | Notes | +|---|---|---| +| `TracksSerialNumbers` | `bool` | Default `false`. Can only be set `true` if `Feature.SerialNumberTracking` is on. | +| `TracksBatches` | `bool` | Default `false`. Can only be set `true` if `Feature.BatchTracking` is on. | +| `TracksExpiryDate` | `bool` | Default `false`. Can only be set `true` if `Feature.ExpiryDateTracking` is on. Independent of `TracksBatches` — see below. | +| `HasWarranty` | `bool` | Default `false`. Can only be set `true` if `Feature.WarrantyTracking` is on. | +| `WarrantyPeriodDays` | `int?` | Only meaningful if `HasWarranty`. Null = use `Feature.DefaultWarrantyPeriodDays`. | +| `AllowCustomWarrantyPeriod` | `bool` | Default `false`. Gates whether `WarrantyPeriodDays` may be set on this item at all — requires `Feature.CustomWarrantyOverride` on. | + +`TracksSerialNumbers`, `TracksBatches`, and `TracksExpiryDate` are independent booleans, so every +combination (none / any one / any two / all three) falls out naturally — no extra enum needed. An +item can have `TracksExpiryDate = true` with `TracksBatches = false`: it gets a per-receipt expiry +date without ever needing a batch number (see `ItemBatch` below, whose `BatchNumber` becomes +optional to support this case). + +Validation lives in `ItemService.CreateAsync`/`UpdateAsync`: reject the request if a per-item flag +is `true` while its global feature is off, with a message like *"Serial number tracking is +disabled system-wide."* + +--- + +## New Entities — Batches and Serial Numbers + +### `ItemBatch` + +| Column | Type | Notes | +|---|---|---| +| Id | Guid | PK | +| FK_ItemId | Guid | FK → Item. Item must have `TracksBatches = true` and/or `TracksExpiryDate = true`. | +| BatchNumber | string? | Supplier/internal lot code. **Required if `TracksBatches`, otherwise null** — an expiry-only item (`TracksExpiryDate` with `TracksBatches = false`) gets a row here purely to carry the expiry date, with no batch number. | +| ManufactureDate | DateTime? | Optional. | +| ExpiryDate | DateTime? | Required if `TracksExpiryDate`. Drives expiry alerts and (later phase) dispatch blocking. | +| IsActive | bool | Standard soft-delete/versioning flag. | +| CreatedAt | DateTime | From `Template`. | + +`ItemBatch` is the single mechanism behind both "batch tracking" and "expiry-date tracking" — a +batch is a subdivision of one item's stock, not a new stock-holding location, and quantity for a +batch/lot still lives in the ledger (see below). The row only exists at all when the item tracks +batches or expiry dates (or both); an item that tracks neither never gets `ItemBatch` rows. + +### `ItemSerialNumber` + +| Column | Type | Notes | +|---|---|---| +| Id | Guid | PK | +| FK_ItemId | Guid | FK → Item. Item must have `TracksSerialNumbers = true`. | +| SerialNumber | string | Unique per item. | +| Status | enum `SerialStatus { InStock, Sold, Returned, Damaged }` | Current lifecycle state. | +| CurrentLocationId / CurrentLocationType | Guid? / `LocationType?` | Mirrors the ledger's location model — where this specific unit currently sits. | +| ReceivedAt | DateTime | When it entered stock (GRN). | +| WarrantyPeriodDaysOverride | int? | Snapshotted at receipt from `Item.WarrantyPeriodDays` when `Item.HasWarranty` and `Item.AllowCustomWarrantyPeriod`; otherwise null (system default applies when the warranty starts). | +| WarrantyExpiresAt | DateTime? | **Set at sale/delivery, not receipt**: sale date + (override ?? `Feature.DefaultWarrantyPeriodDays`). Null while in stock or if `Item.HasWarranty` is false. *(Original design computed this at receipt; changed so warehouse time doesn't consume the customer's warranty.)* | +| IsActive | bool | Standard flag. | + +A serial number represents exactly one physical unit — quantity is always 1, so serials are +tracked as rows, not as a quantity column. + +--- + +## Stock Ledger Changes (additive, nullable) + +`StockBalance` and `StockMovement` (`Models/StockBalance.cs`, `Models/StockMovement.cs`) each gain: + +| Column | Type | Notes | +|---|---|---| +| `FK_ItemBatchId` | Guid? | Null for items that don't track batches — **no behavior change for existing items.** | +| `FK_ItemSerialNumberId` | Guid? | Null for items that don't track serials. | + +- Untracked items: both columns stay null, ledger behaves exactly as it does today (one aggregate + row per item+location). +- Batch-tracked items: one `StockBalance` row per (Item, Location, Batch) instead of per (Item, + Location) — the existing `StockRepository.GetBalanceAsync`/`AddBalanceAsync` calls in + `GrnService.CreateAsync` (`Features/Grn/GrnService.cs:106-122`) need a batch-aware overload. +- Serial-tracked items: quantity is always 1 per movement; a `StockMovement` moves one serial from + one location to another and updates `ItemSerialNumber.CurrentLocationId/Type` + `Status` in the + same transaction. A running `StockBalance` row can still exist (count of serials at a location) + for consistency with existing balance-reporting UI, kept in sync alongside the serial's own + status. + +--- + +## GRN Changes (intake is where batches/serials are born) + +`GoodsReceiptItem` (`Models/GoodsReceiptItem.cs`) needs a capture point matching the item's +tracking mode: + +- **Batch-tracked item**: the GRN line must supply a batch number (+ expiry date if the item also + has `TracksExpiryDate`) instead of a bare accepted quantity. New table `GoodsReceiptItemBatch` + (FK_GoodsReceiptItemId, BatchNumber?, ExpiryDate?, Quantity) — a single PO line can be split across + multiple batches in one receipt. +- **Expiry-only item** (`TracksExpiryDate` without `TracksBatches`): the GRN line must supply an + expiry date but no batch number — same `GoodsReceiptItemBatch` table, `BatchNumber` left null. +- **Serial-tracked item**: the GRN line must supply exactly `AcceptedQuantity` serial numbers. New + table `GoodsReceiptItemSerial` (FK_GoodsReceiptItemId, SerialNumber). +- **None of the above**: unchanged — today's `AcceptedQuantity`/`RejectedQuantity` flow stays as-is. + +`GrnService.CreateAsync` (`Features/Grn/GrnService.cs:44`) needs a branch per tracking mode when +writing `StockBalance`/`StockMovement`: create/lookup the `ItemBatch` (or create each +`ItemSerialNumber`) before writing the ledger rows, and validate the request shape matches the +item's `TracksSerialNumbers`/`TracksBatches`/`TracksExpiryDate` flags (e.g. reject a batch number on +an item that doesn't track batches, or a missing expiry date on an item that requires one). + +--- + +## Frontend Changes + +- **configuration page** (`distribution_fe/app/(dashboard)/configuration/page.tsx`, currently a + placeholder): add an "Item Tracking" section with toggles for the five `Feature.*` keys and a + number input for the default warranty period, following the same form pattern as + `drivers/compliance/page.tsx` (a single-purpose settings form backed by a GET on load / PUT on + save) but hitting the generic `/api/sys-config/{key}` routes instead of a dedicated controller. +- **Item Sheet** (`catalogue/items/_components/item-sheet.tsx`): add a "Tracking" `FieldGroup` with + `Track serial numbers` / `Track batches` / `Track expiry date` checkboxes, a `Has warranty` checkbox revealing a + warranty-period number input, and (only when `Feature.CustomWarrantyOverride` is on) an + `Allow custom warranty period` checkbox. Checkboxes for a disabled global feature are hidden + entirely, not just disabled, to avoid confusing item editors. +- **GRN entry** — built as two full pages (against a PO: `/procurement/grn/po/new`; direct: + `/procurement/grn/direct/new`). Tracked lines show a status chip that opens a panel for serial + entry (scan / auto-generate / type-paste-edit) or batch rows (batch no. with Auto, expiry, qty). + Save is disabled until every tracked line is complete. Details in the plan, Phases 10–12. +- **configuration page**: a single Products card on `/configuration` holds the switches and warranty + defaults (no separate sub-page). **Item view** shows tracking badges. + +--- + +## Direct Goods Receipt (no purchase order) + +A goods receipt may exist without a PO. `GoodsReceipt.FK_PurchaseOrderId` and +`GoodsReceiptItem.FK_PurchaseOrderItemId` are nullable; direct lines carry `FK_ItemId` instead; +an optional `FK_SupplierId` identifies the supplier. `POST /api/goods-receipts/direct` creates one. +Serial/batch/expiry capture, stock balances and movements are identical to a PO receipt (shared +`PostReceiptAsync`); the only differences are that no PO quantity or status is updated and no PO +validation applies. See the plan, Phase 11. + +--- + +## Process — How Item Tracking Works End to End + +### 1. Setup (once) +1. **Configuration → Products card**: switch on the capabilities the business needs (serial, batch, + expiry, warranty; optionally per-item custom warranty) and set the default warranty days. Changes + save immediately and are stored in `SysConfig` (`Feature.*` keys). +2. **Catalogue → Items → edit item**: tick `Track serial numbers` / `Track batches` / + `Track expiry date` and, if applicable, `Has warranty` (+ `Use a custom warranty period` and days). + A flag can only be set if its global feature is on. Untracked items are unaffected. + +### 2. Receiving goods (where serials / batches are entered) +Entered **only at goods receipt**, never on the item or PO forms. + +| Item tracks | GRN line needs | Stored as | +|---|---|---| +| nothing | accepted / rejected qty | one `StockBalance` row per item+warehouse | +| serial numbers | exactly *accepted qty* unique serials | one `ItemSerialNumber` (InStock) + one `StockBalance` (qty 1) + one `StockMovement` per unit | +| batches | batch rows (batch no., [expiry], qty) summing to accepted qty | `ItemBatch` (reused by item + batch no.) + `StockBalance` per (item, warehouse, batch) + `StockMovement` per batch | +| expiry only | lot rows (expiry, qty), **no** batch number | fresh `ItemBatch` per row carrying only the expiry date | +| serial + batch/expiry | serials only (serial wins; batch/expiry not linked — follow-up) | as serial | + +Rejected quantity never touches stock. Everything posts in one unit of work and rolls back on any +failure. A serial that already exists for the item, or a mismatch with the item's flags, is rejected. + +**Two ways to receive**: *against a PO* (lines come from the PO, PO received-quantities and status +update) or *direct* (no PO; item chosen per line, optional supplier). The UI exposes them as two +pages and both share the same serial/batch entry panel. + +### 3. Entering serials / batches in the UI +- Each tracked line shows a chip (amber until complete, green ✓ when complete) → opens the panel. +- **Serials**: scan (Enter adds one), auto-generate (prefix + start number, fills only missing units), + or type/paste one per line; every entry stays editable; duplicates are flagged. +- **Batches**: add rows; batch number can be typed, scanned or auto-generated; expiry date warns if + expired or within 30 days; quantities must add up to the accepted quantity. +- Save is disabled until all tracked lines are complete; the server re-validates. +- Prefer real supplier serials/batch numbers where available — auto-generated values suit internal tags. + +### 4. Warranty lifecycle +1. *Receipt*: serial created `InStock`; only the item's custom period (if any) is snapshotted. +2. *Sale/delivery* (**not built yet**): serial → `Sold`; `WarrantyExpiresAt = sale date + (override ?? + default days)`. +3. *Claim / return* (future): `WarrantyExpiresAt` decides eligibility; status moves to + `Returned` / `Damaged`. + +### 5. Viewing +GRN view shows batches (with expiry) and an expandable serial list per line; the item view shows +tracking badges; the receipts list marks direct receipts. + +--- + +## Explicitly Out of Scope for This Phase + +- **Picking/dispatch by batch or serial** (loading sheets, delivery tasks, sales returns) — this + doc only covers catalogue configuration and stock intake (GRN) + ledger schema. Selecting *which* + batch or serial goes out on a given ride/loading sheet is a separate follow-up doc once this + foundation exists. +- **FEFO (first-expiry-first-out) enforcement** at dispatch time. +- **Expired-batch blocking** — whether dispatch should hard-block an expired batch or just warn is + an open question below, deferred to the dispatch-side follow-up doc. + +--- + +## Open Questions + +1. Should an expired `ItemBatch` hard-block further dispatch, or just surface a warning? (Decision + deferred to the dispatch follow-up doc, but the `ExpiryDate` column needs to exist now either way.) +2. Is `WarrantyPeriodDays` always in days, or should the Item Sheet offer months/years and convert? +~~Resolved:~~ warranty timing — decided to start at sale, not receipt (see Process §4). + +3. Can serial numbers ever be added outside a GRN (e.g. a manual stock adjustment for an + already-tracked item), or is GRN the only entry point in this phase? +4. When batch tracking splits one item's stock into multiple `StockBalance` rows per location, does + the existing Stock Balances page (`stock/_components/stock-balances-client.tsx`) need a + drill-down, or is an aggregated total (sum across batches) sufficient for now? +5. Naming for the migration: proposed `AddItemTrackingConfig`, continuing from the most recent + migration `20260928101132_AddItemReorderLevel`. diff --git a/Distribution/docs/refactor_docs/item_tracking_plan.md b/Distribution/docs/refactor_docs/item_tracking_plan.md new file mode 100644 index 0000000..d442185 --- /dev/null +++ b/Distribution/docs/refactor_docs/item_tracking_plan.md @@ -0,0 +1,419 @@ +# Item Tracking Configuration — Implementation Plan + +**Status**: Implemented — serial/batch/expiry capture on both receipt flows (against a PO and direct), config + item UI done. Pending: DB migration `AddDirectGoodsReceipt`, and the sale/delivery step that starts warranty (not built) +**Design doc**: [item_tracking_config.md](./item_tracking_config.md) + +| Phase | Title | Status | +|-------|-------|--------| +| 1 | Enums | ✅ Done | +| 2 | Global feature toggles (`SysConfig`) | ✅ Done | +| 3 | `Item` per-item tracking fields | ✅ Done | +| 4 | New entities — `ItemBatch`, `ItemSerialNumber` | ✅ Done | +| 5 | Stock ledger — batch/serial columns | ✅ Done | +| 6 | GRN intake — batch/serial capture | ✅ Done (serial path takes precedence when an item tracks both serials and batches — see `GrnService.ValidateLineShape`). **Warranty is no longer computed here** — see "Warranty timing" below | +| 7 | Migration | ✅ Done — `20261001080630_AddItemTrackingConfig`, applied | +| 8 | configuration page UI | ✅ Done — single Products card on `/configuration` (no separate `/configuration/products` route) | +| 9 | Item Sheet UI | ✅ Done (also fixed pre-existing `reorderLevel` drift in `types/api.ts`/`ItemBody`) | +| 10 | GRN form UI — serial/batch entry | ✅ Done — see Phase 10 | +| 11 | Direct goods receipt (no PO) — backend | ✅ Code done · ⏳ migration `AddDirectGoodsReceipt` generated, **not yet applied** | +| 12 | GRN pages — two routes (against PO / direct) | ✅ Done | +| 13 | Sale/delivery step — start warranty, mark serial `Sold` | ❌ Not built (needs a decision on which document triggers it) | + +--- + +## Scope + +Add opt-in serial-number and batch/lot tracking to catalogue items, each independently on/off +system-wide via the new configuration page, with warranty periods (system default or per-item +custom override) and batch expiry dates. Fully additive to the existing schema — items that track +neither must behave exactly as they do today. See the design doc for full rationale, entity shapes, +and open questions; this file only sequences the work. + +--- + +## Execution Order + +``` +Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5 → Phase 6 → Phase 7 (migration) +→ Phase 8, 9, 10 (frontend, independent of each other, after Phase 7) +``` + +Backend phases 2–6 touch shared files (`Item.cs`, `AppDBContext.cs`) in sequence and should not be +parallelized. Frontend phases 8–10 have no dependencies on each other. + +--- + +## Phase 1 — Enums + +**File**: `Models/Enums.cs` + +Add: + +```csharp +public enum SerialStatus { InStock, Sold, Returned, Damaged } +``` + +--- + +## Phase 2 — Global feature toggles (`SysConfig`) + +**Files**: `Features/SysConfig/SysConfigService.cs`, `Features/SysConfig/SysConfigController.cs` + +### 2.1 New well-known keys + +In `SysConfigService`, alongside `KeyInvoiceCreationMode`: + +```csharp +public const string KeySerialNumberTrackingEnabled = "Feature.SerialNumberTracking"; +public const string KeyBatchTrackingEnabled = "Feature.BatchTracking"; +public const string KeyExpiryDateTrackingEnabled = "Feature.ExpiryDateTracking"; +public const string KeyWarrantyTrackingEnabled = "Feature.WarrantyTracking"; +public const string KeyCustomWarrantyOverrideEnabled = "Feature.CustomWarrantyOverride"; +public const string KeyDefaultWarrantyPeriodDays = "Feature.DefaultWarrantyPeriodDays"; +``` + +### 2.2 Typed getters + +Mirror `GetInvoiceCreationModeAsync()`: + +```csharp +public async Task GetSerialNumberTrackingEnabledAsync() => await GetBoolAsync(KeySerialNumberTrackingEnabled); +public async Task GetBatchTrackingEnabledAsync() => await GetBoolAsync(KeyBatchTrackingEnabled); +public async Task GetExpiryDateTrackingEnabledAsync() => await GetBoolAsync(KeyExpiryDateTrackingEnabled); +public async Task GetWarrantyTrackingEnabledAsync() => await GetBoolAsync(KeyWarrantyTrackingEnabled); +public async Task GetCustomWarrantyOverrideEnabledAsync() => await GetBoolAsync(KeyCustomWarrantyOverrideEnabled); +public async Task GetDefaultWarrantyPeriodDaysAsync() +{ + var config = await repo.GetByKeyAsync(KeyDefaultWarrantyPeriodDays); + return config is not null && int.TryParse(config.Value, out var days) ? days : 0; +} + +private async Task GetBoolAsync(string key) +{ + var config = await repo.GetByKeyAsync(key); + return config is not null && bool.TryParse(config.Value, out var enabled) && enabled; +} +``` + +### 2.3 Validation + +Extend `ValidateValue` to accept `"true"`/`"false"` for the five boolean keys and a non-negative +integer string for `KeyDefaultWarrantyPeriodDays`. + +### 2.4 Controller + +No new routes — the existing generic `GET/PUT /api/sys-config/{key}` already covers reading and +setting these keys (same pattern the frontend will already use for `InvoiceCreationMode`). + +--- + +## Phase 3 — `Item` per-item tracking fields + +**File**: `Models/Item.cs` + +Add after `ReorderLevel`: + +```csharp +public bool TracksSerialNumbers { get; set; } = false; +public bool TracksBatches { get; set; } = false; +public bool TracksExpiryDate { get; set; } = false; +public bool HasWarranty { get; set; } = false; +public int? WarrantyPeriodDays { get; set; } +public bool AllowCustomWarrantyPeriod { get; set; } = false; +``` + +**File**: `Features/Item/ItemDTOs.cs` + +Add the same six fields to `CreateItemRequest`, `UpdateItemRequest`, `ItemSummaryResponse`, and +`ItemDetailResponse` (all optional/defaulted, matching how `ReorderLevel` was added). + +**File**: `Features/Item/ItemService.cs` + +In `CreateAsync`/`UpdateAsync`, before saving, validate each per-item flag against the matching +`SysConfigService` getter from Phase 2 and throw `InvalidOperationException` with a message like +*"Serial number tracking is disabled system-wide."* if a flag is `true` while its feature is off +(same check for `TracksExpiryDate` against `KeyExpiryDateTrackingEnabled`). Same check for +`AllowCustomWarrantyPeriod` against `KeyCustomWarrantyOverrideEnabled`, and for +`WarrantyPeriodDays` requiring `HasWarranty = true`. + +--- + +## Phase 4 — New entities: `ItemBatch`, `ItemSerialNumber` + +**New file**: `Models/ItemBatch.cs` + +```csharp +public class ItemBatch : Template +{ + public Guid FK_ItemId { get; set; } + public Item Item { get; set; } + + // Null when the item tracks expiry dates but not batch numbers (TracksExpiryDate without TracksBatches). + public string? BatchNumber { get; set; } + public DateTime? ManufactureDate { get; set; } + public DateTime? ExpiryDate { get; set; } +} +``` + +This single entity backs both `TracksBatches` and `TracksExpiryDate` — an item can have one, the +other, or both; `ItemBatch` rows only get created at all when at least one of the two is true. + +**New file**: `Models/ItemSerialNumber.cs` + +```csharp +public class ItemSerialNumber : Template +{ + public Guid FK_ItemId { get; set; } + public Item Item { get; set; } + + public string SerialNumber { get; set; } + public SerialStatus Status { get; set; } = SerialStatus.InStock; + + public Guid? CurrentLocationId { get; set; } + public LocationType? CurrentLocationType { get; set; } + + public DateTime ReceivedAt { get; set; } + public int? WarrantyPeriodDaysOverride { get; set; } + public DateTime? WarrantyExpiresAt { get; set; } +} +``` + +**File**: `Data/AppDBContext.cs` — add `DbSet ItemBatches`, +`DbSet ItemSerialNumbers`, unique index on `(FK_ItemId, SerialNumber)` for +`ItemSerialNumber`, and index on `(FK_ItemId, BatchNumber)` for `ItemBatch`. + +--- + +## Phase 5 — Stock ledger: batch/serial columns + +**Files**: `Models/StockBalance.cs`, `Models/StockMovement.cs` + +Add to both: + +```csharp +public Guid? FK_ItemBatchId { get; set; } +[ForeignKey("FK_ItemBatchId")] +public ItemBatch? ItemBatch { get; set; } + +public Guid? FK_ItemSerialNumberId { get; set; } +[ForeignKey("FK_ItemSerialNumberId")] +public ItemSerialNumber? ItemSerialNumber { get; set; } +``` + +**Files**: `Features/Stock/IStockRepository.cs`, `Features/Stock/StockRepository.cs` + +Add batch-aware overloads: `GetBalanceAsync(itemId, locationId, batchId)` alongside the existing +`GetBalanceAsync(itemId, locationId)`, used only when `Item.TracksBatches` is true so untracked +items keep calling the existing method unchanged. + +--- + +## Phase 6 — GRN intake: batch/serial capture + +**New file**: `Models/GoodsReceiptItemBatch.cs` + +```csharp +public class GoodsReceiptItemBatch : Template +{ + public Guid FK_GoodsReceiptItemId { get; set; } + public GoodsReceiptItem GoodsReceiptItem { get; set; } + + public string BatchNumber { get; set; } + public DateTime? ExpiryDate { get; set; } + public int Quantity { get; set; } +} +``` + +**New file**: `Models/GoodsReceiptItemSerial.cs` + +```csharp +public class GoodsReceiptItemSerial : Template +{ + public Guid FK_GoodsReceiptItemId { get; set; } + public GoodsReceiptItem GoodsReceiptItem { get; set; } + + public string SerialNumber { get; set; } +} +``` + +**File**: `Features/Grn/GrnDTOs.cs` — extend the create-GRN line request with optional +`List? Batches` (each carrying `BatchNumber?`, `ExpiryDate?`, `Quantity`) and +`List? SerialNumbers`, validated so exactly one of {bare quantity, batches, serials} is +populated depending on the item's tracking flags. + +**File**: `Features/Grn/GrnService.cs` + +In `CreateAsync` (`Features/Grn/GrnService.cs:44`), branch per line on +`poItem.Item.TracksSerialNumbers` / `TracksBatches` / `TracksExpiryDate`: + +- None of the three: unchanged — today's `AcceptedQuantity`/`RejectedQuantity` path. +- Batches and/or expiry-only: reject if `line.Batches` is null/empty or its quantities don't sum to + `AcceptedQuantity`; reject a populated `BatchNumber` if `TracksBatches` is false, and reject a + missing `ExpiryDate` if `TracksExpiryDate` is true. For each batch entry, create or look up an + `ItemBatch` (by `FK_ItemId` + `BatchNumber` when present, otherwise a fresh expiry-only row), + write one `GoodsReceiptItemBatch`, and write/increment `StockBalance` keyed by + `(ItemId, WarehouseId, BatchId)` plus a `StockMovement` carrying `FK_ItemBatchId`. +- Serials: reject if `line.SerialNumbers.Count != AcceptedQuantity` or any serial number already + exists for the item; create one `ItemSerialNumber` per entry (`Status = InStock`; **no `WarrantyExpiresAt` yet** — see "Warranty timing"), one + `GoodsReceiptItemSerial`, and one `StockMovement` per serial carrying `FK_ItemSerialNumberId` + (quantity always 1). + +--- + +### Warranty timing (changed from the original plan) + +The original plan computed `WarrantyExpiresAt = ReceivedAt + period` at GRN. That made stock sitting in +a warehouse consume the customer's warranty, so it was changed: **the warranty clock starts when the +serial is sold/delivered**, not when it is received. + +- At GRN, `GrnService` only snapshots the item's custom period onto the serial + (`WarrantyPeriodDaysOverride = Item.WarrantyPeriodDays`, and only when `HasWarranty` and + `AllowCustomWarrantyPeriod`). `WarrantyExpiresAt` stays `null`. +- `GrnService` no longer depends on `SysConfigService`. +- When the sale/delivery step exists (Phase 13), it sets + `WarrantyExpiresAt = saleDate + (WarrantyPeriodDaysOverride ?? Feature.DefaultWarrantyPeriodDays)` + and moves the serial to `SerialStatus.Sold`. +- Caveat: with the default `Feature.DefaultWarrantyPeriodDays = 0` and no per-item period, the + warranty would expire the day it starts — set a default or per-item periods before enabling warranty. + +### Shared posting code (refactor done while adding Phase 11) + +`GrnService.PostReceiptAsync(goodsReceipt, lines, warehouseId, userId, po?, ct)` now holds the +unit-of-work block: add the receipt, per line write serials / batches / `StockBalance` / +`StockMovement` (type `PurchaseReceipt`), update the PO when present, commit. Both +`CreateAsync` (PO receipt) and `CreateDirectAsync` (no PO) validate their own input and then call +it, so tracking behaviour is identical in both flows. + +--- + +## Phase 7 — Migration + +(Applied.) Run `dotnet ef migrations add AddItemTrackingConfig` after Phases 3–6 are code-complete, covering: +`Item` new columns, `ItemBatch`, `ItemSerialNumber`, `GoodsReceiptItemBatch`, +`GoodsReceiptItemSerial` tables, and the new nullable FK columns on `StockBalance`/`StockMovement`. +Review the generated migration for the FK-drop pitfall noted in `refactor_plan.md` Phase 5 before +running `dotnet ef database update`. + +--- + +## Phase 8 — configuration page UI + +**Files**: `distribution_fe/app/(dashboard)/configuration/page.tsx`, +`configuration/_components/product-capabilities-card.tsx` + +**As built**: `/configuration` shows one **Products** card whose body holds the switches and warranty +defaults directly (each switch saves immediately via `sysConfig.set`, keys in +`ITEM_TRACKING_KEYS`). There is no separate `/configuration/products` page. The original +description follows. Replace the placeholder with a form (pattern: `drivers/compliance/page.tsx`) that on load calls +`GET /api/sys-config` (or the five individual `GET /api/sys-config/{key}` calls) and on save calls +`PUT /api/sys-config/{key}` once per changed key: + +- Checkbox — "Enable serial number tracking" (`Feature.SerialNumberTracking`) +- Checkbox — "Enable batch/lot tracking" (`Feature.BatchTracking`) +- Checkbox — "Enable expiry date tracking" (`Feature.ExpiryDateTracking`) +- Checkbox — "Enable warranty periods" (`Feature.WarrantyTracking`) +- Checkbox — "Allow items to set a custom warranty period" (`Feature.CustomWarrantyOverride`), only + shown/enabled when warranty tracking is on +- Number input — "Default warranty period (days)" (`Feature.DefaultWarrantyPeriodDays`), only shown + when warranty tracking is on + +Add `sysConfig` helpers to `lib/api/index.ts` (`get(key)`, `set(key, value)`) if not already present. + +--- + +## Phase 9 — Item Sheet UI + +**File**: `distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx` + +Add a "Tracking" `FieldGroup` after the existing fields: + +- Checkbox `TracksSerialNumbers` — rendered only if the global serial-tracking toggle is on +- Checkbox `TracksBatches` — rendered only if the global batch-tracking toggle is on +- Checkbox `TracksExpiryDate` — rendered only if the global expiry-date toggle is on; independent + of `TracksBatches`, so it can be checked on its own for perishables with no batch numbering +- Checkbox `HasWarranty` — rendered only if the global warranty toggle is on +- Number input `WarrantyPeriodDays` — shown only when `HasWarranty` is checked +- Checkbox `AllowCustomWarrantyPeriod` — shown only when `HasWarranty` is checked **and** the + global custom-override toggle is on + +Fetch the four global toggles once (e.g. via a small `useSysConfig` hook or inline `Promise.all` +next to the existing `categoriesApi`/`brandsApi`/`uomsApi` calls) when the sheet opens, so hidden +fields never get submitted as `true`. + +Also update `ItemSummaryResponse`/`ItemDetailResponse` in `distribution_fe/types/api.ts` with the +five new fields (this file was already found to be missing `reorderLevel` from a prior backend +change — fix that drift in the same pass). + +--- + +## Phase 10 — GRN form UI (serial / batch entry) + +**Files** (`distribution_fe/app/(dashboard)/procurement/grn/_components/`): `grn-tracking.tsx` +(shared), `grn-view-sheet.tsx`; types in `types/api.ts`, `lib/api/procurement.ts`. + +- The form loads each line's item (`items.get`) to read its tracking flags — the PO line response + does not carry them. +- Tracked lines show a **status chip** ("Enter serials · 0/50", amber until complete, green "✓" when + complete). Clicking it opens a panel (`LineTracking`): + - **Serial items**: scan field (Enter adds one serial, duplicates rejected), **auto-generate** + (prefix + start number, fills only the missing units, skips taken values), and an editable + list box (type / paste / edit / clear). Live "n of N entered" count, duplicates listed. + - **Batch / expiry items**: rows of batch number (+ **Auto** button), expiry date, quantity; + "Add batch" pre-fills the remaining quantity; warns "Already expired" / "Expires within 30 + days"; live "allocated of accepted" count. Batch-number column hidden if the item does not track + batches; expiry column hidden if it does not track expiry. + - Warranty is a read-only note ("starts at sale") — never an input at GRN. +- `trackingProgress()` drives chip state; **Save is disabled until every tracked line is complete**. + `trackingPayload()` mirrors `GrnService.ValidateLineShape` and builds the request (or an error + toast). Serial tracking takes precedence over batch/expiry, as on the server. +- **GRN view** lists batches per line (batch no., qty, expiry) and serials in an expandable + "n serial numbers" row. **Item view sheet** shows tracking badges (Serial / Batch / Expiry / + Warranty period). + +--- + +## Phase 11 — Direct goods receipt (no purchase order) + +Goods can now be received without a PO. Both flows work independently and share the posting code. + +**Models / migration `AddDirectGoodsReceipt`** (generated; **apply with `dotnet ef database update`**): +- `GoodsReceipt.FK_PurchaseOrderId` → nullable; new optional `FK_SupplierId` (+ `Supplier` nav). +- `GoodsReceiptItem.FK_PurchaseOrderItemId` → nullable; new `FK_ItemId` (+ `Item` nav) set on + direct lines. Existing PO lines resolve their item through `PurchaseOrderItem` (`MapItem`: + `i.Item ?? i.PurchaseOrderItem!.Item`). + +**DTOs** (`GrnDTOs.cs`): `GrnItemRequest.PurchaseOrderItemId` is now `Guid?`, plus +`Guid? ItemId = null` for direct lines; new `CreateDirectGrnRequest(WarehouseId, Items, SupplierId?, +ReceivedByUserId?)`; `GrnResponse` gains `SupplierId`/`SupplierName` and a nullable +`PurchaseOrderId`; `GrnItemResponse.PurchaseOrderItemId` is nullable. + +**Endpoint**: `POST /api/goods-receipts/direct` (`GrnController.CreateDirect`). +`GrnService.CreateDirectAsync` validates: at least one line; supplier (if given) exists and is +active; each line has an `ItemId` of an active item; quantities non-negative and not both zero; +`ValidateLineShape`; serials not already in stock and not repeated across lines of the same +request. It then calls `PostReceiptAsync` with no PO — no PO quantities or status change. + +**Not included (by choice)**: unit cost on direct lines; a received-date input (the server stamps +`DateTime.Now` — also true of the PO flow). + +--- + +## Phase 12 — GRN pages (two routes) + +| Route | Purpose | +|---|---| +| `/procurement/grn` | Receipts list. "Receipt Against PO" and "Direct Receipt" buttons navigate to the pages below. PO column shows "Direct receipt" when there is no PO. | +| `/procurement/grn/po/new` | Pick an issued / partially received PO, warehouse, date, user; PO lines pre-filled with remaining quantity; serial/batch chips on tracked lines. | +| `/procurement/grn/direct/new` | Warehouse, optional supplier, user, then "Add item" rows (item + accepted / rejected + serial/batch chip). | + +Both are full pages (not dialogs), using the shared `grn-tracking.tsx`. The old popup components +`grn-sheet.tsx` / `direct-grn-sheet.tsx` were removed. The GRN view remains a dialog. + +--- + +## Phase 13 — Sale/delivery step (not built) + +Needed to complete warranty: on sale/delivery of a serial, set `Status = Sold`, +`WarrantyExpiresAt`, and (for batch items) consume batch balances (FEFO suggestion). Also still +missing: stock screen drill-down by batch/serial and any warranty-status lookup UI. Requires a +decision on which document (invoice vs delivery) triggers it. diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx new file mode 100644 index 0000000..77122b0 --- /dev/null +++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-form.tsx @@ -0,0 +1,406 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useForm, zodResolver, z } from '@/lib/form' +import { FormField } from '@/components/shared/form-field' +import { Input } from '@/components/ui/input' +import { Checkbox } from '@/components/ui/checkbox' +import { Button } from '@/components/ui/button' +import { FieldGroup } from '@/components/ui/field' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { toast } from '@/components/ui/toast' +import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, sysConfig, ITEM_TRACKING_KEYS, getApiError } from '@/lib/api/index' +import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api' + +const schema = z.object({ + sku: z.string().min(1, 'SKU is required'), + name: z.string().min(1, 'Name is required'), + categoryId: z.string().min(1, 'Category is required'), + brandId: z.string().optional(), + baseUomId: z.string().min(1, 'Base UOM is required'), + salePrice: z.number().min(0, 'Sale price must be 0 or more'), + tracksSerialNumbers: z.boolean(), + tracksBatches: z.boolean(), + tracksExpiryDate: z.boolean(), + hasWarranty: z.boolean(), + warrantyPeriodDays: z.number().min(0).optional(), + allowCustomWarrantyPeriod: z.boolean(), + isActive: z.boolean(), +}) + +type FormValues = z.infer + +interface TrackingToggles { + serialNumberTracking: boolean + batchTracking: boolean + expiryDateTracking: boolean + warrantyTracking: boolean + customWarrantyOverride: boolean +} + +const NO_TRACKING_ENABLED: TrackingToggles = { + serialNumberTracking: false, + batchTracking: false, + expiryDateTracking: false, + warrantyTracking: false, + customWarrantyOverride: false, +} + +const EMPTY_VALUES: FormValues = { + sku: '', + name: '', + categoryId: '', + brandId: undefined, + baseUomId: '', + salePrice: 0, + tracksSerialNumbers: false, + tracksBatches: false, + tracksExpiryDate: false, + hasWarranty: false, + warrantyPeriodDays: undefined, + allowCustomWarrantyPeriod: false, + isActive: true, +} + +interface ItemFormProps { + item: ItemSummaryResponse | null + onSuccess: () => void + onCancel: () => void + twoColumn?: boolean +} + +export function ItemForm({ item, onSuccess, onCancel, twoColumn = false }: ItemFormProps) { + const isEdit = !!item + + const [categories, setCategories] = useState([]) + const [brandsList, setBrandsList] = useState([]) + const [uomsList, setUomsList] = useState([]) + const [tracking, setTracking] = useState(NO_TRACKING_ENABLED) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: EMPTY_VALUES, + }) + + useEffect(() => { + Promise.all([ + categoriesApi.list(), + brandsApi.list(), + uomsApi.list(), + sysConfig.getAll(), + ]).then(([cats, brandsRes, uomsRes, configs]) => { + setCategories(cats) + setBrandsList(brandsRes) + setUomsList(uomsRes) + const byKey = Object.fromEntries(configs.map(c => [c.key, c.value])) + setTracking({ + serialNumberTracking: byKey[ITEM_TRACKING_KEYS.serialNumberTracking] === 'true', + batchTracking: byKey[ITEM_TRACKING_KEYS.batchTracking] === 'true', + expiryDateTracking: byKey[ITEM_TRACKING_KEYS.expiryDateTracking] === 'true', + warrantyTracking: byKey[ITEM_TRACKING_KEYS.warrantyTracking] === 'true', + customWarrantyOverride: byKey[ITEM_TRACKING_KEYS.customWarrantyOverride] === 'true', + }) + }).catch((err) => { + toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) + }) + + form.reset( + item + ? { + sku: item.sku, + name: item.name, + categoryId: item.categoryId, + brandId: item.brandId ?? undefined, + baseUomId: item.baseUomId, + salePrice: item.salePrice, + tracksSerialNumbers: item.tracksSerialNumbers, + tracksBatches: item.tracksBatches, + tracksExpiryDate: item.tracksExpiryDate, + hasWarranty: item.hasWarranty, + warrantyPeriodDays: item.warrantyPeriodDays ?? undefined, + allowCustomWarrantyPeriod: item.allowCustomWarrantyPeriod, + isActive: item.isActive, + } + : EMPTY_VALUES + ) + }, [item]) + + const hasWarranty = form.watch('hasWarranty') + + async function onSubmit(values: FormValues) { + try { + const body = { + ...values, + brandId: values.brandId || null, + warrantyPeriodDays: values.hasWarranty && values.allowCustomWarrantyPeriod ? values.warrantyPeriodDays ?? null : null, + } + if (isEdit) { + await itemsApi.update(item!.id, body) + toast.add({ title: 'Product updated', type: 'success' }) + } else { + await itemsApi.create(body) + toast.add({ title: 'Product created', type: 'success' }) + } + onSuccess() + } catch (err) { + toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' }) + } + } + + return ( +
+ + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + + )} + /> + + ( + field.onChange(parseFloat(e.target.value) || 0)} + onBlur={field.onBlur} + placeholder="0.00" + aria-invalid={invalid} + /> + )} + /> + + {(tracking.serialNumberTracking || tracking.batchTracking || tracking.expiryDateTracking || tracking.warrantyTracking) && ( +
+ Tracking + + {tracking.serialNumberTracking && ( + ( +
+ + +
+ )} + /> + )} + + {tracking.batchTracking && ( + ( +
+ + +
+ )} + /> + )} + + {tracking.expiryDateTracking && ( + ( +
+ + +
+ )} + /> + )} + + {tracking.warrantyTracking && ( + <> + ( +
+ + +
+ )} + /> + + {hasWarranty && tracking.customWarrantyOverride && ( + <> + ( +
+ + +
+ )} + /> + + {form.watch('allowCustomWarrantyPeriod') && ( + ( + field.onChange(e.target.value === '' ? undefined : parseInt(e.target.value, 10) || 0)} + onBlur={field.onBlur} + placeholder="System default" + /> + )} + /> + )} + + )} + + )} +
+ )} + +
+ ( +
+ + +
+ )} + /> +
+
+ +
+ + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx index 7dba95e..17511e4 100644 --- a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx +++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-sheet.tsx @@ -1,290 +1,40 @@ 'use client' -import { useEffect, useState } from 'react' -import { useForm, zodResolver, z } from '@/lib/form' -import { FormField } from '@/components/shared/form-field' -import { Input } from '@/components/ui/input' -import { Checkbox } from '@/components/ui/checkbox' -import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, - DialogFooter, } from '@/components/ui/dialog' -import { FieldGroup } from '@/components/ui/field' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { toast } from '@/components/ui/toast' -import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, getApiError } from '@/lib/api/index' -import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api' +import type { ItemSummaryResponse } from '@/types/api' -const schema = z.object({ - sku: z.string().min(1, 'SKU is required'), - name: z.string().min(1, 'Name is required'), - categoryId: z.string().min(1, 'Category is required'), - brandId: z.string().optional(), - baseUomId: z.string().min(1, 'Base UOM is required'), - salePrice: z.number().min(0, 'Sale price must be 0 or more'), - isActive: z.boolean(), -}) - -type FormValues = z.infer +import { ItemForm } from './item-form' interface ItemSheetProps { open: boolean onClose: () => void - item: ItemSummaryResponse | null + item: ItemSummaryResponse onSuccess: () => void } export function ItemSheet({ open, onClose, item, onSuccess }: ItemSheetProps) { - const isEdit = !!item - - const [categories, setCategories] = useState([]) - const [brandsList, setBrandsList] = useState([]) - const [uomsList, setUomsList] = useState([]) - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - sku: '', - name: '', - categoryId: '', - brandId: undefined, - baseUomId: '', - salePrice: 0, - isActive: true, - }, - }) - - useEffect(() => { - if (open) { - Promise.all([ - categoriesApi.list(), - brandsApi.list(), - uomsApi.list(), - ]).then(([cats, brandsRes, uomsRes]) => { - setCategories(cats) - setBrandsList(brandsRes) - setUomsList(uomsRes) - }).catch((err) => { - toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' }) - }) - - form.reset( - item - ? { - sku: item.sku, - name: item.name, - categoryId: item.categoryId, - brandId: item.brandId ?? undefined, - baseUomId: item.baseUomId, - salePrice: item.salePrice, - isActive: item.isActive, - } - : { - sku: '', - name: '', - categoryId: '', - brandId: undefined, - baseUomId: '', - salePrice: 0, - isActive: true, - } - ) - } - }, [open, item]) - - async function onSubmit(values: FormValues) { - try { - const body = { - ...values, - brandId: values.brandId || null, - } - if (isEdit) { - await itemsApi.update(item!.id, body) - toast.add({ title: 'Item updated', type: 'success' }) - } else { - await itemsApi.create(body) - toast.add({ title: 'Item created', type: 'success' }) - } - onSuccess() - onClose() - } catch (err) { - toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' }) - } - } - return ( !v && onClose()}> - {isEdit ? 'Edit Item' : 'Add Item'} - - {isEdit ? 'Update the item details below.' : 'Add a new catalogue item.'} - + Edit Product + Update the product details below. -
- - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - field.onChange(parseFloat(e.target.value) || 0)} - onBlur={field.onBlur} - placeholder="0.00" - aria-invalid={invalid} - /> - )} - /> - - ( -
- - -
- )} - /> -
- - - - - -
+ { + onSuccess() + onClose() + }} + onCancel={onClose} + />
) diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx index 1381063..9e047b8 100644 --- a/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx +++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/item-view-sheet.tsx @@ -34,7 +34,7 @@ export function ItemViewSheet({ open, onClose, item }: ItemViewSheetProps) { !v && onClose()}> - Item Details + Product Details
@@ -44,6 +44,27 @@ export function ItemViewSheet({ open, onClose, item }: ItemViewSheetProps) { + + {item.tracksSerialNumbers && Serial} + {item.tracksBatches && Batch} + {item.tracksExpiryDate && Expiry} + {item.hasWarranty && ( + + Warranty {item.allowCustomWarrantyPeriod && item.warrantyPeriodDays != null + ? `${item.warrantyPeriodDays}d` + : '(default period)'} + + )} + + ) : ( + '—' + ) + } + /> {item.isActive ? 'Active' : 'Inactive'}} /> diff --git a/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx b/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx index 607a43c..1f5a72f 100644 --- a/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx +++ b/distribution_fe/app/(dashboard)/catalogue/items/_components/items-client.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' +import { useRouter } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon } from '@hugeicons/core-free-icons' @@ -16,6 +17,7 @@ import { ItemSheet } from './item-sheet' import { ItemViewSheet } from './item-view-sheet' export function ItemsClient() { + const router = useRouter() const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) @@ -34,7 +36,7 @@ export function ItemsClient() { setData(getPaginatedItems(res)) setTotal(res.total) } catch (err) { - toast.add({ title: 'Failed to load items', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to load products', description: getApiError(err), type: 'error' }) } finally { setLoading(false) } @@ -44,11 +46,6 @@ export function ItemsClient() { fetchData(page, pageSize) }, [fetchData, page, pageSize]) - function openCreate() { - setEditing(null) - setSheetOpen(true) - } - function openEdit(item: ItemSummaryResponse) { setEditing(item) setSheetOpen(true) @@ -62,10 +59,10 @@ export function ItemsClient() { async function handleDelete(item: ItemSummaryResponse) { try { await itemsApi.remove(item.id) - toast.add({ title: 'Item deactivated', type: 'success' }) + toast.add({ title: 'Product deactivated', type: 'success' }) fetchData(page, pageSize) } catch (err) { - toast.add({ title: 'Failed to deactivate item', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to deactivate product', description: getApiError(err), type: 'error' }) } } @@ -73,7 +70,7 @@ export function ItemsClient() { rowKey: 'id', serverSide: true, stickyHeader: true, - emptyMessage: 'No items yet. Add one to get started.', + emptyMessage: 'No products yet. Add one to get started.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, columns: [ { @@ -137,17 +134,17 @@ export function ItemsClient() { actions: () => [ { action: 'view', - label: 'View item', + label: 'View product', onClick: openView, }, { action: 'edit', - label: 'Edit item', + label: 'Edit product', onClick: openEdit, }, { action: 'cancel', - label: 'Delete item', + label: 'Delete product', onClick: handleDelete, visible: (target) => target.isActive, }, @@ -159,14 +156,14 @@ export function ItemsClient() { <>
-

Items

+

Products

Products available for sale and distribution

-
@@ -181,12 +178,14 @@ export function ItemsClient() { }} /> - setSheetOpen(false)} - item={editing} - onSuccess={() => fetchData(page, pageSize)} - /> + {editing && ( + setSheetOpen(false)} + item={editing} + onSuccess={() => fetchData(page, pageSize)} + /> + )} router.push('/catalogue/items') + + return ( +
+
+ +
+

Add Product

+

Add a new catalogue product.

+
+
+ +
+ +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/configuration/_components/product-capabilities-card.tsx b/distribution_fe/app/(dashboard)/configuration/_components/product-capabilities-card.tsx new file mode 100644 index 0000000..85e4a3c --- /dev/null +++ b/distribution_fe/app/(dashboard)/configuration/_components/product-capabilities-card.tsx @@ -0,0 +1,220 @@ +'use client' + +import { useEffect, useState } from 'react' +import { HugeiconsIcon } from '@hugeicons/react' +import { Package01Icon } from '@hugeicons/core-free-icons' +import { Switch } from '@/components/ui/switch' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { FieldLabel, FieldDescription } from '@/components/ui/field' +import { Separator } from '@/components/ui/separator' +import { toast } from '@/components/ui/toast' +import { sysConfig, ITEM_TRACKING_KEYS, getApiError } from '@/lib/api/index' +import { cn } from '@/lib/utils' +import { useAuth } from '@/contexts/auth-context' + +interface FormState { + serialNumberTracking: boolean + batchTracking: boolean + expiryDateTracking: boolean + warrantyTracking: boolean + customWarrantyOverride: boolean + defaultWarrantyPeriodDays: number +} + +const DEFAULTS: FormState = { + serialNumberTracking: false, + batchTracking: false, + expiryDateTracking: false, + warrantyTracking: false, + customWarrantyOverride: false, + defaultWarrantyPeriodDays: 0, +} + +interface CapabilityToggleProps { + id: string + label: string + description: string + checked: boolean + disabled?: boolean + onCheckedChange: (checked: boolean) => void + className?: string +} + +function CapabilityToggle({ id, label, description, checked, disabled, onCheckedChange, className }: CapabilityToggleProps) { + return ( +
+
+ + {label} + + {description} +
+
+ {checked ? 'ON' : 'OFF'} + onCheckedChange(!!v)} /> +
+
+ ) +} + +/** The Products configuration card: tracking capability switches and warranty defaults, saved immediately. */ +export function ProductCapabilitiesCard() { + const { user } = useAuth() + const [loading, setLoading] = useState(true) + const [form, setForm] = useState(DEFAULTS) + const [pendingKeys, setPendingKeys] = useState>({}) + + useEffect(() => { + sysConfig.getAll() + .then((configs) => { + const byKey = Object.fromEntries(configs.map(c => [c.key, c.value])) + setForm({ + serialNumberTracking: byKey[ITEM_TRACKING_KEYS.serialNumberTracking] === 'true', + batchTracking: byKey[ITEM_TRACKING_KEYS.batchTracking] === 'true', + expiryDateTracking: byKey[ITEM_TRACKING_KEYS.expiryDateTracking] === 'true', + warrantyTracking: byKey[ITEM_TRACKING_KEYS.warrantyTracking] === 'true', + customWarrantyOverride: byKey[ITEM_TRACKING_KEYS.customWarrantyOverride] === 'true', + defaultWarrantyPeriodDays: Number(byKey[ITEM_TRACKING_KEYS.defaultWarrantyPeriodDays] ?? 0), + }) + }) + .catch((err) => toast.add({ title: 'Failed to load configuration', description: getApiError(err), type: 'error' })) + .finally(() => setLoading(false)) + }, []) + + async function updateToggle(formKey: keyof FormState, configKey: string, value: boolean) { + const previous = form[formKey] + setForm(f => ({ ...f, [formKey]: value })) + setPendingKeys(p => ({ ...p, [configKey]: true })) + try { + await sysConfig.set(configKey, String(value), user.id) + } catch (err) { + setForm(f => ({ ...f, [formKey]: previous })) + toast.add({ title: 'Failed to update configuration', description: getApiError(err), type: 'error' }) + } finally { + setPendingKeys(p => ({ ...p, [configKey]: false })) + } + } + + async function updateDefaultWarrantyDays(value: number) { + const previous = form.defaultWarrantyPeriodDays + setForm(f => ({ ...f, defaultWarrantyPeriodDays: value })) + if (value === previous) return + setPendingKeys(p => ({ ...p, [ITEM_TRACKING_KEYS.defaultWarrantyPeriodDays]: true })) + try { + await sysConfig.set(ITEM_TRACKING_KEYS.defaultWarrantyPeriodDays, String(value), user.id) + } catch (err) { + setForm(f => ({ ...f, defaultWarrantyPeriodDays: previous })) + toast.add({ title: 'Failed to update configuration', description: getApiError(err), type: 'error' }) + } finally { + setPendingKeys(p => ({ ...p, [ITEM_TRACKING_KEYS.defaultWarrantyPeriodDays]: false })) + } + } + + return loading ? ( +

Loading...

+ ) : ( + + +
+
+ +
+ Products +
+ + Serial number, batch, expiry and warranty tracking for catalogue items. Each switch applies immediately; + items then opt into the enabled capabilities from the item's edit form. + +
+ +
+ updateToggle('serialNumberTracking', ITEM_TRACKING_KEYS.serialNumberTracking, v)} + /> + + updateToggle('batchTracking', ITEM_TRACKING_KEYS.batchTracking, v)} + /> + + updateToggle('expiryDateTracking', ITEM_TRACKING_KEYS.expiryDateTracking, v)} + /> + + updateToggle('warrantyTracking', ITEM_TRACKING_KEYS.warrantyTracking, v)} + /> +
+ + {form.warrantyTracking && ( + <> + + +
+

+ Warranty defaults +

+ +
+ updateToggle('customWarrantyOverride', ITEM_TRACKING_KEYS.customWarrantyOverride, v)} + /> + +
+ + Default warranty period + +
+ setForm(f => ({ ...f, defaultWarrantyPeriodDays: parseInt(e.target.value, 10) || 0 }))} + onBlur={(e) => updateDefaultWarrantyDays(parseInt(e.target.value, 10) || 0)} + className="max-w-24" + /> + days +
+ + Applied to items that don't set their own warranty period. + +
+
+
+ + )} +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/configuration/page.tsx b/distribution_fe/app/(dashboard)/configuration/page.tsx new file mode 100644 index 0000000..b787920 --- /dev/null +++ b/distribution_fe/app/(dashboard)/configuration/page.tsx @@ -0,0 +1,16 @@ +'use client' + +import { ProductCapabilitiesCard } from './_components/product-capabilities-card' + +export default function ConfigurationPage() { + return ( +
+
+

Configuration

+

System-wide settings, grouped by area

+
+ + +
+ ) +} diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx new file mode 100644 index 0000000..4013883 --- /dev/null +++ b/distribution_fe/app/(dashboard)/network/regions/_components/region-manager-dialog.tsx @@ -0,0 +1,138 @@ +'use client' + +import { useEffect, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { toast } from '@/components/ui/toast' +import { regions as regionsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { RegionResponse, RegionManagerResponse, UserResponse } from '@/types/api' + +const MANAGER_ROLE = 'RegionalManager' + +interface RegionManagerDialogProps { + open: boolean + onClose: () => void + region: RegionResponse | null +} + +export function RegionManagerDialog({ open, onClose, region }: RegionManagerDialogProps) { + const [current, setCurrent] = useState(null) + const [candidates, setCandidates] = useState([]) + const [selected, setSelected] = useState('') + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!open || !region) return + let cancelled = false + setLoading(true) + setSelected('') + setCurrent(null) + Promise.all([ + regionsApi.getManager(region.id).catch(() => null), // 404 = no manager yet + usersApi.list({ pageSize: 500 }), + ]) + .then(([manager, userRes]) => { + if (cancelled) return + setCurrent(manager) + setSelected(manager?.userId ?? '') + setCandidates(getPaginatedItems(userRes).filter((u) => u.isActive && u.roleName === MANAGER_ROLE)) + }) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) + .finally(() => !cancelled && setLoading(false)) + return () => { cancelled = true } + }, [open, region]) + + async function save() { + if (!region || !selected) return + setSaving(true) + try { + await regionsApi.setManager(region.id, selected) + toast.add({ title: 'Region manager assigned', type: 'success' }) + onClose() + } catch (err) { + toast.add({ title: 'Failed to assign manager', description: getApiError(err), type: 'error' }) + } finally { + setSaving(false) + } + } + + async function remove() { + if (!region) return + setSaving(true) + try { + await regionsApi.removeManager(region.id) + toast.add({ title: 'Region manager removed', type: 'success' }) + onClose() + } catch (err) { + toast.add({ title: 'Failed to remove manager', description: getApiError(err), type: 'error' }) + } finally { + setSaving(false) + } + } + + return ( + !v && !saving && onClose()}> + + + Region Manager + + {region ? `${region.code} – ${region.name}. ` : ''} + Each region has one manager, chosen from users with the RegionalManager role. + + + +
+

+ Current manager:{' '} + + {loading ? 'Loading…' : current ? `${current.fullName} (${current.username})` : 'None'} + +

+ + +
+ + + {current && ( + + )} + + + +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/region-sheet.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/region-sheet.tsx index d31ed67..b68aa66 100644 --- a/distribution_fe/app/(dashboard)/network/regions/_components/region-sheet.tsx +++ b/distribution_fe/app/(dashboard)/network/regions/_components/region-sheet.tsx @@ -4,7 +4,6 @@ import { useEffect } from 'react' import { useForm, zodResolver, z } from '@/lib/form' import { FormField } from '@/components/shared/form-field' import { Input } from '@/components/ui/input' -import { Checkbox } from '@/components/ui/checkbox' import { Button } from '@/components/ui/button' import { Dialog, @@ -20,8 +19,8 @@ import { regions as regionsApi, getApiError } from '@/lib/api/index' import type { RegionResponse } from '@/types/api' const schema = z.object({ + code: z.string().max(50, 'Code must be under 50 characters').optional(), name: z.string().min(1, 'Name is required').max(100, 'Name must be under 100 characters'), - isActive: z.boolean(), }) type FormValues = z.infer @@ -30,30 +29,39 @@ interface RegionSheetProps { open: boolean onClose: () => void region: RegionResponse | null + readOnly?: boolean onSuccess: () => void } -export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetProps) { +export function RegionSheet({ open, onClose, region, readOnly = false, onSuccess }: RegionSheetProps) { const isEdit = !!region const form = useForm({ resolver: zodResolver(schema), - defaultValues: { name: '', isActive: true }, + defaultValues: { code: '', name: '' }, }) useEffect(() => { if (open) { - form.reset(region ? { name: region.name, isActive: region.isActive } : { name: '', isActive: true }) + form.reset(region ? { code: region.code, name: region.name } : { code: '', name: '' }) + if (!region) { + // Suggest the next code; the user can overwrite it, or clear it to let the server generate one. + regionsApi.nextCode() + .then((res) => { + if (!form.getValues('code')) form.setValue('code', res.code) + }) + .catch(() => {}) + } } }, [open, region]) async function onSubmit(values: FormValues) { try { if (isEdit) { - await regionsApi.update(region!.id, values) + await regionsApi.update(region!.id, { name: values.name, code: values.code?.trim() || undefined }) toast.add({ title: 'Region updated', type: 'success' }) } else { - await regionsApi.create(values) + await regionsApi.create({ name: values.name, code: values.code?.trim() || undefined }) toast.add({ title: 'Region created', type: 'success' }) } onSuccess() @@ -67,14 +75,37 @@ export function RegionSheet({ open, onClose, region, onSuccess }: RegionSheetPro !v && onClose()}> - {isEdit ? 'Edit Region' : 'Add Region'} + {readOnly ? 'View Region' : isEdit ? 'Edit Region' : 'Add Region'} - {isEdit ? 'Update the region details below.' : 'Add a new geographic region to the distribution network.'} + {readOnly ? 'Region details.' : isEdit ? 'Update the region details below.' : 'Add a new geographic region to the distribution network.'}
+ ( + + )} + /> + {!isEdit && !readOnly && ( +

+ Suggested automatically — edit it, or clear it to auto-generate on save. +

+ )} + )} /> - - ( -
- - -
- )} - />
- + )}
diff --git a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx index 3017e36..2d0b8f2 100644 --- a/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx +++ b/distribution_fe/app/(dashboard)/network/regions/_components/regions-client.tsx @@ -8,11 +8,13 @@ import { DataTable } from '@/components/shared/data-table' import type { TableConfig } from '@/components/shared/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' import { toast } from '@/components/ui/toast' import { regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { RegionResponse } from '@/types/api' import { RegionSheet } from './region-sheet' +import { RegionManagerDialog } from './region-manager-dialog' export function RegionsClient() { const [data, setData] = useState([]) @@ -23,6 +25,10 @@ export function RegionsClient() { const [sheetOpen, setSheetOpen] = useState(false) const [editing, setEditing] = useState(null) + const [viewOnly, setViewOnly] = useState(false) + const [deleting, setDeleting] = useState(null) + const [deleteBusy, setDeleteBusy] = useState(false) + const [managerRegion, setManagerRegion] = useState(null) const fetchData = useCallback(async (p: number, ps: number) => { setLoading(true) @@ -43,24 +49,34 @@ export function RegionsClient() { function openCreate() { setEditing(null) + setViewOnly(false) setSheetOpen(true) } function openEdit(region: RegionResponse) { setEditing(region) + setViewOnly(false) setSheetOpen(true) } - async function toggleActive(region: RegionResponse) { + function openView(region: RegionResponse) { + setEditing(region) + setViewOnly(true) + setSheetOpen(true) + } + + async function confirmDelete() { + if (!deleting) return + setDeleteBusy(true) try { - await regionsApi.update(region.id, { isActive: !region.isActive }) - toast.add({ - title: region.isActive ? 'Region deactivated' : 'Region activated', - type: 'success', - }) + await regionsApi.remove(deleting.id) + toast.add({ title: 'Region deleted', type: 'success' }) + setDeleting(null) fetchData(page, pageSize) } catch (err) { - toast.add({ title: 'Failed to update region', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to delete region', description: getApiError(err), type: 'error' }) + } finally { + setDeleteBusy(false) } } @@ -71,6 +87,13 @@ export function RegionsClient() { emptyMessage: 'No regions yet. Add one to get started.', pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] }, columns: [ + { + key: 'code', + title: 'Code', + fields: [{ key: 'code', primary: true }], + sortable: true, + width: 120, + }, { key: 'name', title: 'Region', @@ -103,6 +126,16 @@ export function RegionsClient() { ], rowActions: { actions: () => [ + { + action: 'confirm', + label: 'Manage region manager', + onClick: (region: RegionResponse) => setManagerRegion(region), + }, + { + action: 'view', + label: 'View region', + onClick: openView, + }, { action: 'edit', label: 'Edit region', @@ -110,19 +143,12 @@ export function RegionsClient() { }, { action: 'deactivate', - label: 'Deactivate region', - onClick: toggleActive, - visible: (target) => target.isActive, - }, - { - action: 'activate', - label: 'Activate region', - onClick: toggleActive, - visible: (target) => !target.isActive, + label: 'Delete region', + onClick: (region: RegionResponse) => setDeleting(region), }, ], }, - }), [pageSize, openEdit, toggleActive]) + }), [pageSize, openEdit, openView]) return ( <> @@ -154,8 +180,34 @@ export function RegionsClient() { open={sheetOpen} onClose={() => setSheetOpen(false)} region={editing} + readOnly={viewOnly} onSuccess={() => fetchData(page, pageSize)} /> + setManagerRegion(null)} + region={managerRegion} + /> + + !v && !deleteBusy && setDeleting(null)}> + + + Delete region? + + {deleting ? `${deleting.code} – ${deleting.name} will be removed from the regions list.` : null} + {' '}This action cannot be undone from this screen. + + + + + + + + ) } diff --git a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx index 64fb407..07fe7db 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx @@ -71,7 +71,7 @@ const STEPS = [ n: 3, label: 'Items', title: 'What should be loaded from Shop POs?', - help: "Expand a shop to tick items and set quantities. If the main warehouse doesn't have enough stock, use \"Take remaining from another warehouse\" to top up the rest.", + help: "Expand a shop to tick items and set quantities. If the main warehouse can't cover an item, a top-up row is added automatically — just pick which other warehouse to take the rest from.", }, { n: 4, @@ -321,10 +321,16 @@ function LoadingSheetForm() { let changed = false const next: Record = {} for (const [key, l] of Object.entries(prev)) { - const extras = l.extras.filter((e) => e.warehouseId !== warehouseId) + let extras = l.extras.filter((e) => e.warehouseId !== warehouseId) const avail = stockByWh[warehouseId]?.[l.itemId] ?? 0 const quantity = Math.min(l.quantity, avail) const checked = l.checked && (quantity > 0 || extras.length > 0) + // Pre-add an empty top-up row if the new main warehouse leaves this line short + // and one isn't already pending, mirroring toggleLine's auto-suggestion. + const shortBy = l.remainingQty - quantity - extras.reduce((sum, e) => sum + (Number(e.quantity) || 0), 0) + if (checked && shortBy > 0 && !extras.some((e) => !e.warehouseId)) { + extras = [...extras, { warehouseId: '', quantity: 0 }] + } if (extras.length !== l.extras.length || quantity !== l.quantity || checked !== l.checked) changed = true next[key] = { ...l, extras, quantity, checked } } @@ -452,7 +458,12 @@ function LoadingSheetForm() { const avail = availIn(warehouseId, line.itemId) if (!stockLoaded || avail <= 0) return const quantity = Math.max(0, Math.min(line.remainingQty - extrasTotal(line), avail)) - setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked, quantity } })) + // The main warehouse can't cover the full remaining quantity, so a top-up row is + // pre-added (still editable/removable) instead of making the user click "Take + // remaining from another warehouse" themselves. + const shortBy = line.remainingQty - quantity - extrasTotal(line) + const extras = shortBy > 0 ? [...line.extras, { warehouseId: '', quantity: 0 }] : line.extras + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked, quantity, extras } })) return } setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } })) @@ -912,10 +923,18 @@ function LoadingSheetForm() { - {shortBy} will stay open on the PO for another sheet - + {line.extras.some((e) => !e.warehouseId) ? ( + + {shortBy} short — pick a warehouse above to top it up, or it stays open on the PO for another sheet. + + ) : ( + <> + {shortBy} will stay open on the PO for another sheet + + + )} )} diff --git a/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-client.tsx b/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-client.tsx index f3a6a97..ca0259d 100644 --- a/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-client.tsx +++ b/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-client.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' +import { useRouter } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon } from '@hugeicons/core-free-icons' import { DataTable } from '@/components/shared/data-table' @@ -9,7 +10,6 @@ import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/toast' import { grn as grnApi, getApiError, getPaginatedItems } from '@/lib/api/index' import type { GrnResponse } from '@/types/api' -import { GrnSheet } from './grn-sheet' import { GrnViewSheet } from './grn-view-sheet' const fmtDate = (v: string) => @@ -18,12 +18,12 @@ const fmtDate = (v: string) => const truncId = (id: string) => id.substring(0, 8) export function GrnClient() { + const router = useRouter() const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) - const [sheetOpen, setSheetOpen] = useState(false) const [viewing, setViewing] = useState(null) const [viewOpen, setViewOpen] = useState(false) @@ -71,7 +71,9 @@ export function GrnClient() { fields: ['purchaseOrderId'], width: 140, cellRender: (row) => ( - {truncId(row.purchaseOrderId)} + row.purchaseOrderId + ? {truncId(row.purchaseOrderId)} + : Direct receipt ), }, { @@ -119,13 +121,19 @@ export function GrnClient() {

Goods Receipts

- Stock received into a warehouse against a purchase order + Stock received into a warehouse, against a purchase order or directly

- +
+ + +
- setSheetOpen(false)} - onSuccess={() => fetchData(page, pageSize)} - /> - setViewOpen(false)} diff --git a/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-tracking.tsx b/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-tracking.tsx new file mode 100644 index 0000000..2d31ab6 --- /dev/null +++ b/distribution_fe/app/(dashboard)/procurement/grn/_components/grn-tracking.tsx @@ -0,0 +1,324 @@ +'use client' + +import { useState } from 'react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { toast } from '@/components/ui/toast' +import type { GrnBatchLineBody } from '@/lib/api/procurement' +import type { ItemSummaryResponse } from '@/types/api' + +export function parseSerials(text: string): string[] { + return text.split(/[\n,]+/).map(s => s.trim()).filter(Boolean) +} + +export type BatchRow = { batchNumber: string; expiryDate: string; quantity: number } + +interface Progress { + kind: 'serial' | 'batch' + done: number + complete: boolean + duplicates: string[] +} + +/** How far a tracked line is from satisfying the server rules; null for untracked items. */ +export function trackingProgress( + info: ItemSummaryResponse | undefined, + accepted: number, + serialText: string, + batches: BatchRow[], +): Progress | null { + if (info?.tracksSerialNumbers) { + const serials = parseSerials(serialText) + const seen = new Set() + const dupes = new Set() + for (const s of serials) { + if (seen.has(s)) dupes.add(s) + seen.add(s) + } + return { + kind: 'serial', + done: serials.length, + duplicates: [...dupes], + complete: accepted === 0 || (serials.length === accepted && dupes.size === 0), + } + } + if (info?.tracksBatches || info?.tracksExpiryDate) { + const done = batches.reduce((s, b) => s + (b.quantity || 0), 0) + const rowsOk = batches.every( + b => + b.quantity > 0 && + (!info.tracksBatches || b.batchNumber.trim() !== '') && + (!info.tracksExpiryDate || b.expiryDate !== ''), + ) + return { kind: 'batch', done, duplicates: [], complete: accepted === 0 || (done === accepted && rowsOk) } + } + return null +} + +interface LineTrackingProps { + info: ItemSummaryResponse + name: string + accepted: number + serialText: string + batches: BatchRow[] + onSerialText: (v: string) => void + onBatches: (v: BatchRow[]) => void +} + +/** + * Compact status chip for one GRN line; clicking it opens a panel for the actual serial/batch + * entry, so long serial lists don't stretch the form. Serial tracking takes precedence over + * batch/expiry (same as the server). Warranty is not entered here — its clock starts at sale. + */ +export function LineTracking({ info, name, accepted, serialText, batches, onSerialText, onBatches }: LineTrackingProps) { + const [panelOpen, setPanelOpen] = useState(false) + const [scanValue, setScanValue] = useState('') + const [genPrefix, setGenPrefix] = useState(`${info.sku}-`) + const [genStart, setGenStart] = useState(1) + const progress = trackingProgress(info, accepted, serialText, batches) + if (!progress) return null + + // Scanner (keyboard-wedge) input: each scan ends with Enter and appends one serial. + const addScanned = () => { + const value = scanValue.trim() + if (!value) return + if (parseSerials(serialText).includes(value)) { + toast.add({ title: `Serial ${value} is already in the list`, type: 'error' }) + } else { + onSerialText(serialText.trim() ? `${serialText.trimEnd()}\n${value}` : value) + } + setScanValue('') + } + + // Appends generated serials (prefix + zero-padded sequence) for the units still missing, + // skipping any value already in the list. They stay editable in the list below. + const generateSerials = () => { + const existing = parseSerials(serialText) + const taken = new Set(existing) + const missing = Math.max(0, accepted - existing.length) + const generated: string[] = [] + for (let n = genStart; generated.length < missing; n++) { + const sn = `${genPrefix}${String(n).padStart(4, '0')}` + if (!taken.has(sn)) generated.push(sn) + } + onSerialText([...existing, ...generated].join('\n')) + setGenStart(genStart + missing) + } + + const isSerial = progress.kind === 'serial' + const noun = isSerial ? 'serials' : info.tracksBatches ? 'batches' : 'expiry lots' + const warrantyNote = info.hasWarranty + ? `Warranty: ${info.warrantyPeriodDays != null && info.allowCustomWarrantyPeriod ? `${info.warrantyPeriodDays} days` : 'system default period'}, starts at sale.` + : null + + const now = new Date() + const today = now.toISOString().slice(0, 10) + const inThirtyDays = new Date(now) + inThirtyDays.setDate(inThirtyDays.getDate() + 30) + const soon = inThirtyDays.toISOString().slice(0, 10) + const update = (i: number, patch: Partial) => + onBatches(batches.map((b, j) => (j === i ? { ...b, ...patch } : b))) + + return ( +
+ + {warrantyNote && {warrantyNote}} + + + + + {isSerial ? 'Serial numbers' : info.tracksBatches ? 'Batches' : 'Expiry lots'} + + {name} ({info.sku}) — accepting {accepted} + + + +
+ {isSerial ? ( + <> + setScanValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault() + addScanned() + } + }} + /> +
+ setGenPrefix(e.target.value)} + /> + setGenStart(Math.max(1, Number(e.target.value)))} + /> + +
+