From 5b2b39b7bf0eb29d95171d8ea0d9476830713ee1 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:24:09 +0530 Subject: [PATCH 01/11] feat: add preview totals endpoint and DTOs for invoice calculations --- .../Features/Invoice/InvoiceController.cs | 7 ++ Distribution/Features/Invoice/InvoiceDTOs.cs | 20 ++++-- .../Features/Invoice/InvoiceService.cs | 69 ++++++++++++++++--- 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/Distribution/Features/Invoice/InvoiceController.cs b/Distribution/Features/Invoice/InvoiceController.cs index 1cc9d80..ac91186 100644 --- a/Distribution/Features/Invoice/InvoiceController.cs +++ b/Distribution/Features/Invoice/InvoiceController.cs @@ -52,6 +52,13 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } } + [HttpPost("{id:guid}/preview-totals")] + public async Task PreviewTotals(Guid id, [FromBody] PreviewInvoiceTotalsRequest request) + { + var result = await service.PreviewTotalsAsync(id, request); + return result is null ? NotFound() : Ok(result); + } + [HttpGet("{id:guid}/versions")] public async Task GetVersions(Guid id) { diff --git a/Distribution/Features/Invoice/InvoiceDTOs.cs b/Distribution/Features/Invoice/InvoiceDTOs.cs index 70673de..224c66e 100644 --- a/Distribution/Features/Invoice/InvoiceDTOs.cs +++ b/Distribution/Features/Invoice/InvoiceDTOs.cs @@ -1,4 +1,5 @@ -using Distribution.Models; +using Distribution.Features.ShopPO; +using Distribution.Models; namespace Distribution.Features.Invoice; @@ -9,7 +10,14 @@ public record PrintInvoiceRequest(Guid? PrintedByUserId = null); // InvoiceItemId set = edit/remove/restore an existing line; ItemId set (InvoiceItemId null) = add a new line. public record InvoiceItemEdit(Guid? InvoiceItemId, Guid? ItemId, int Quantity, decimal? UnitPrice = null, bool IsRemoved = false); -public record UpdateInvoiceItemsRequest(List Items, Guid? EditedByUserId = null); +// DiscountSchemeIds: the percentage-discount scheme chosen in the editor (at most one); null = leave as is. +public record UpdateInvoiceItemsRequest(List Items, Guid? EditedByUserId = null, List? DiscountSchemeIds = null); + +public record PreviewLine(Guid ItemId, int Quantity, decimal UnitPrice, bool IsFree = false, bool IsRemoved = false, Guid? SchemeId = null); +public record PreviewInvoiceTotalsRequest(List Items, List? DiscountSchemeIds = null); +// Quantity = how many free units the scheme still entitles for the lines as typed. +public record FreeEntitlement(Guid SchemeId, int Quantity); +public record InvoiceTotalsPreview(decimal DiscountAmount, decimal TotalAmount, List FreeEntitlements); public record InvoiceVersionItemResponse(Guid? ShopPOItemId, Guid ItemId, string ItemName, int Quantity, decimal UnitPrice, bool IsRemoved); @@ -30,7 +38,10 @@ public record InvoiceItemResponse( int? OrderedQuantity, int Quantity, decimal UnitPrice, - bool IsRemoved); + bool IsRemoved, + bool IsFree = false, + Guid? SchemeId = null, + string? SchemeName = null); public record InvoiceResponse( Guid Id, @@ -49,6 +60,7 @@ public record InvoiceResponse( decimal PaidAmount, bool IsActive, DateTime CreatedAt, - List Items); + List Items, + List? Discounts = null); public record OutstandingPaymentsResponse(int InvoiceCount, decimal OutstandingAmount); diff --git a/Distribution/Features/Invoice/InvoiceService.cs b/Distribution/Features/Invoice/InvoiceService.cs index a8ce1a8..07af3e8 100644 --- a/Distribution/Features/Invoice/InvoiceService.cs +++ b/Distribution/Features/Invoice/InvoiceService.cs @@ -1,4 +1,5 @@ -using Distribution.Features.ShopPO; +using Distribution.Features.Scheme; +using Distribution.Features.ShopPO; using Distribution.Features.Stock; using Distribution.Infra.Authorization; using Distribution.Infra.UoW; @@ -9,6 +10,7 @@ namespace Distribution.Features.Invoice; public class InvoiceService( IInvoiceRepository repo, IShopPORepository shopPoRepo, + SchemeService schemeService, IUnitOfWork uow, ICurrentUserService currentUser) { @@ -25,20 +27,53 @@ public class InvoiceService( i.Items.Where(x => x.IsActive).Select(x => new InvoiceItemResponse(x.Id, x.FK_ShopPOItemId, x.FK_ItemId, x.Item?.Name ?? string.Empty, x.ShopPOItem?.Quantity, - x.Quantity, x.UnitPrice, x.IsRemoved)).ToList()); + x.Quantity, x.UnitPrice, x.IsRemoved, x.IsFree, x.FK_SchemeId, x.SchemeName)).ToList()); private static decimal BillableTotal(IEnumerable items) => items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice); - // TotalAmount = billable lines - scheme discount redeemed on the PO. Re-derived whenever lines change - // and at finalize, so a scheme applied after the invoice was drafted is still picked up before it locks. + // The lines that count toward a percentage discount: active, not struck out. Free issues carry the flag so the + // scheme rules ignore them. + private static List DiscountableLines(IEnumerable items) => + items.Where(i => i.IsActive && !i.IsRemoved) + .Select(i => new ShopPOItem { FK_ItemId = i.FK_ItemId, Quantity = i.Quantity, UnitPrice = i.UnitPrice, IsFree = i.IsFree }) + .ToList(); + + // TotalAmount = billable lines - scheme discount. The discount is re-evaluated against the current lines + // (so adding/removing/re-pricing items moves it, and a slab level can be gained or lost) whenever lines change + // and at print/finalize. The redemption amount is updated with it. private async Task ApplyTotalsAsync(Models.Invoice invoice, IEnumerable items) { - var billable = BillableTotal(items); - invoice.DiscountAmount = Math.Min(await repo.GetSchemeDiscountAsync(invoice.FK_ShopPOId), billable); + var lines = items.ToList(); + // Lowering/removing the paid item that earned a free issue lowers the free quantity with it. + await schemeService.ReconcileInvoiceFreeLinesAsync(invoice.FK_ShopPOId, lines); + var billable = BillableTotal(lines); + var discount = await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, DiscountableLines(lines), persist: true); + invoice.DiscountAmount = Math.Min(discount, billable); invoice.TotalAmount = billable - invoice.DiscountAmount; } + // Live preview for the draft editor: what the discount and total would be for these unsaved lines. Changes nothing. + public async Task PreviewTotalsAsync(Guid id, PreviewInvoiceTotalsRequest request) + { + var invoice = await repo.GetByIdAsync(id); + if (invoice is null) return null; + + var lines = request.Items.Where(l => !l.IsRemoved) + .Select(l => new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice, IsFree = l.IsFree, FK_SchemeId = l.SchemeId }) + .ToList(); + var billable = lines.Sum(l => l.Quantity * l.UnitPrice); + var chosen = request.DiscountSchemeIds is null + ? await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, lines, persist: false) + : await schemeService.CalculateDiscountForSchemesAsync(request.DiscountSchemeIds, lines); + var discount = Math.Min(chosen, billable); + + var schemeIds = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue).Select(l => l.FK_SchemeId!.Value).ToList(); + var entitlements = (await schemeService.GetFreeEntitlementsAsync(schemeIds, lines)) + .Select(e => new FreeEntitlement(e.Key, e.Value)).ToList(); + return new InvoiceTotalsPreview(discount, billable - discount, entitlements); + } + // Immutable copy of the invoice lines as they stand right now. public static InvoiceVersion BuildVersion(Models.Invoice invoice, IEnumerable items, int versionNumber, InvoiceVersionStage stage, Guid userId) => new() @@ -83,13 +118,17 @@ public class InvoiceService( FK_ShopPOItemId = i.Id, FK_ItemId = i.FK_ItemId, Quantity = i.Quantity, - UnitPrice = i.UnitPrice + UnitPrice = i.UnitPrice, + IsFree = i.IsFree, + FK_SchemeId = i.FK_SchemeId, + SchemeName = i.SchemeName }).ToList(); public async Task GetByIdAsync(Guid id) { var invoice = await repo.GetByIdAsync(id); - return invoice is null ? null : ToResponse(invoice); + if (invoice is null) return null; + return ToResponse(invoice) with { Discounts = await schemeService.GetDiscountsForShopPOAsync(invoice.FK_ShopPOId) }; } public async Task> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize) @@ -172,7 +211,12 @@ public class InvoiceService( ValidateQuantity(item, edit.Quantity); item.Quantity = edit.Quantity; } - if (edit.UnitPrice is decimal price) item.UnitPrice = price; + if (edit.UnitPrice is decimal price) + { + if (item.IsFree && price != 0m) + throw new InvalidOperationException("A free-issue line must stay at zero price"); + item.UnitPrice = price; + } } else if (edit.ItemId is Guid itemId) { @@ -201,7 +245,10 @@ public class InvoiceService( throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)"); } - await ApplyTotalsAsync(invoice, invoice.Items.Concat(newLines)); + var allLines = invoice.Items.Concat(newLines).ToList(); + await schemeService.ApplyInvoiceDiscountSelectionAsync( + invoice.FK_ShopPOId, invoice.ShopPO?.FK_StoreId ?? Guid.Empty, allLines, request.DiscountSchemeIds); + await ApplyTotalsAsync(invoice, allLines); await uow.BeginAsync(ct); try @@ -213,7 +260,7 @@ public class InvoiceService( } catch { await uow.RollbackAsync(ct); throw; } - return ToResponse((await repo.GetByIdAsync(id))!); + return (await GetByIdAsync(id))!; } private static void ValidateQuantity(InvoiceItem item, int quantity) -- 2.52.0 From 33dc4adce2840d081929fbc1f98c9f1a0eae0dcf Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:26:15 +0530 Subject: [PATCH 02/11] feat: add promotion suggestions for Shop PO processing --- .../Features/Scheme/ISchemeRepository.cs | 2 + .../Features/Scheme/SchemeController.cs | 8 + Distribution/Features/Scheme/SchemeDTOs.cs | 8 +- .../Features/Scheme/SchemeRepository.cs | 12 + Distribution/Features/Scheme/SchemeService.cs | 370 +++++++++++++++++- 5 files changed, 389 insertions(+), 11 deletions(-) diff --git a/Distribution/Features/Scheme/ISchemeRepository.cs b/Distribution/Features/Scheme/ISchemeRepository.cs index 39d4b86..cfd9764 100644 --- a/Distribution/Features/Scheme/ISchemeRepository.cs +++ b/Distribution/Features/Scheme/ISchemeRepository.cs @@ -10,6 +10,8 @@ public interface ISchemeRepository Task> GetRedemptionsAsync(Guid schemeId); Task AddRedemptionAsync(Models.SchemeRedemption entity); + Task> GetRedemptionsByShopPOAsync(Guid shopPOId); + void RemoveRedemption(Models.SchemeRedemption entity); Task HasRedemptionAsync(Guid schemeId, Guid orderId); Task HasLockedInvoiceAsync(Guid shopPOId); diff --git a/Distribution/Features/Scheme/SchemeController.cs b/Distribution/Features/Scheme/SchemeController.cs index 6858999..99f50fd 100644 --- a/Distribution/Features/Scheme/SchemeController.cs +++ b/Distribution/Features/Scheme/SchemeController.cs @@ -112,6 +112,14 @@ public class SchemeController(SchemeService service) : ControllerBase return result is null ? NotFound() : Ok(result); } + [HttpPost("suggest")] + [Authorize("scheme.view")] + public async Task Suggest(SuggestSchemesRequest request) + { + var result = await service.SuggestAsync(request); + return result is null ? NotFound() : Ok(result); + } + [HttpGet("{id:guid}/spend")] [Authorize("scheme.view")] public async Task GetSpend(Guid id) diff --git a/Distribution/Features/Scheme/SchemeDTOs.cs b/Distribution/Features/Scheme/SchemeDTOs.cs index 28ec165..58078a2 100644 --- a/Distribution/Features/Scheme/SchemeDTOs.cs +++ b/Distribution/Features/Scheme/SchemeDTOs.cs @@ -94,7 +94,13 @@ public record SchemeEligibilityResponse( string? Reason, decimal? DiscountAmount, Guid? FreeItemId, - decimal? FreeQty); + decimal? FreeQty, + string? FreeItemName = null); + +/// A cart line for scheme suggestion before the Shop PO exists. UnitPrice null = resolve from the price list. +public record SuggestLine(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false); + +public record SuggestSchemesRequest(Guid StoreId, List Items); public record SchemeRedemptionResponse( Guid Id, diff --git a/Distribution/Features/Scheme/SchemeRepository.cs b/Distribution/Features/Scheme/SchemeRepository.cs index 12f6ee1..daa154f 100644 --- a/Distribution/Features/Scheme/SchemeRepository.cs +++ b/Distribution/Features/Scheme/SchemeRepository.cs @@ -49,6 +49,18 @@ public class SchemeRepository(AppDBContext context) : ISchemeRepository public async Task AddRedemptionAsync(Models.SchemeRedemption entity) => await context.SchemeRedemptions.AddAsync(entity); + public async Task> GetRedemptionsByShopPOAsync(Guid shopPOId) + { + var list = await context.SchemeRedemptions.Include(r => r.Scheme).Where(r => r.FK_ShopPOId == shopPOId).ToListAsync(); + // Rows added earlier in this unit of work are not in the database yet; rows removed are still there. + foreach (var added in context.SchemeRedemptions.Local.Where(r => r.FK_ShopPOId == shopPOId && !list.Contains(r))) + list.Add(added); + return list.Where(r => context.Entry(r).State != EntityState.Deleted).ToList(); + } + + public void RemoveRedemption(Models.SchemeRedemption entity) => + context.SchemeRedemptions.Remove(entity); + public async Task HasRedemptionAsync(Guid schemeId, Guid shopPOId) => await context.SchemeRedemptions.AnyAsync(r => r.FK_SchemeId == schemeId && r.FK_ShopPOId == shopPOId); diff --git a/Distribution/Features/Scheme/SchemeService.cs b/Distribution/Features/Scheme/SchemeService.cs index cf39dd7..7103586 100644 --- a/Distribution/Features/Scheme/SchemeService.cs +++ b/Distribution/Features/Scheme/SchemeService.cs @@ -1,5 +1,7 @@ using System.Text.Json; +using Distribution.Features.Invoice; using Distribution.Features.Item; +using Distribution.Features.PriceList; using Distribution.Features.ShopPO; using Distribution.Features.Stock; using Distribution.Features.Store; @@ -14,6 +16,8 @@ public class SchemeService( IItemRepository itemRepo, IStoreRepository storeRepo, IStockRepository stockRepo, + IInvoiceRepository invoiceRepo, + PriceListService priceListService, IUnitOfWork uow) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); @@ -261,12 +265,7 @@ public class SchemeService( foreach (var scheme in candidates) { - if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != regionId) - continue; - if (!string.IsNullOrEmpty(scheme.EligibleSegment) && - !string.Equals(scheme.EligibleSegment, store.Segment, StringComparison.OrdinalIgnoreCase)) - continue; - if (shopPO.CreatedAt < scheme.DateRangeFrom || shopPO.CreatedAt > scheme.DateRangeTo) + if (!IsInScope(scheme, store.Segment, regionId, shopPO.CreatedAt)) continue; result.Add(EvaluateRule(scheme, items)); @@ -275,6 +274,328 @@ public class SchemeService( return result; } + private static bool IsInScope(Models.Scheme scheme, string? storeSegment, Guid? storeRegionId, DateTime asOf) + { + if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != storeRegionId) return false; + if (!string.IsNullOrEmpty(scheme.EligibleSegment) && + !string.Equals(scheme.EligibleSegment, storeSegment, StringComparison.OrdinalIgnoreCase)) return false; + return asOf >= scheme.DateRangeFrom && asOf.Date <= scheme.DateRangeTo.Date; + } + + /// + /// Suggestions for a Shop PO that is still being built (nothing saved yet). Returns eligible schemes, + /// plus BuyXGetY/FreeBundle schemes the cart is part-way to, so the UI can nudge ("add 4 more"). + /// + public async Task?> SuggestAsync(SuggestSchemesRequest request) + { + var store = await storeRepo.GetByIdWithRegionAsync(request.StoreId); + if (store is null || !store.IsActive) return null; + + var lines = new List(); + foreach (var l in request.Items.Where(i => !i.IsFree)) + { + var price = l.UnitPrice + ?? (await priceListService.ResolveByStoreAsync(l.ItemId, request.StoreId, DateTime.Now))?.Price + ?? 0m; + lines.Add(new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = price }); + } + + var regionId = store.Route?.FK_RegionId; + var inCart = lines.Select(l => l.FK_ItemId).ToHashSet(); + var result = new List(); + + foreach (var scheme in (await repo.GetAllAsync()).Where(s => s.IsActive)) + { + if (!IsInScope(scheme, store.Segment, regionId, DateTime.Now)) continue; + + var eval = EvaluateRule(scheme, lines); + var relevant = eval.Eligible || scheme.RuleType switch + { + SchemeRuleType.BuyXGetY => scheme.FK_TriggerItemId.HasValue && inCart.Contains(scheme.FK_TriggerItemId.Value), + SchemeRuleType.FreeBundle => scheme.BundleComponents.Any(c => inCart.Contains(c.FK_ItemId)), + _ => false + }; + if (!relevant) continue; + + result.Add(eval with { FreeItemName = scheme.FreeItem?.Name }); + } + + return result; + } + + /// + /// Checks the free lines of a Shop PO that is about to be saved: each must reference an in-scope scheme the + /// paid lines still qualify for, be the scheme's free item, stay within the entitled qty, and be in stock. + /// Stamps the scheme-name snapshot and forces UnitPrice to 0. Read-only; throws InvalidOperationException. + /// + public async Task ValidateFreeLinesAsync(Guid storeId, DateTime asOf, List lines) + { + var freeLines = lines.Where(l => l.IsFree).ToList(); + if (freeLines.Count == 0) return; + + if (freeLines.Any(l => l.FK_SchemeId is null)) + throw new InvalidOperationException("A free-issue line must reference the scheme that granted it."); + if (freeLines.Any(l => l.Quantity <= 0)) + throw new InvalidOperationException("Free-issue quantity must be greater than zero (remove the line instead)."); + + var store = await storeRepo.GetByIdWithRegionAsync(storeId) + ?? throw new InvalidOperationException("Store not found."); + var regionId = store.Route?.FK_RegionId; + + foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value)) + { + var scheme = await repo.GetByIdAsync(group.Key); + if (scheme is null || !scheme.IsActive) + throw new InvalidOperationException("A free-issue line references a scheme that no longer exists."); + if (!IsInScope(scheme, store.Segment, regionId, asOf)) + throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date."); + + var eval = EvaluateRule(scheme, lines); + if (!eval.Eligible || eval.FreeItemId is null || eval.FreeQty is null) + throw new InvalidOperationException(eval.Reason ?? $"Order no longer qualifies for scheme '{scheme.Name}'."); + if (group.Any(l => l.FK_ItemId != eval.FreeItemId)) + throw new InvalidOperationException($"Scheme '{scheme.Name}' grants a different free item."); + + var qty = group.Sum(l => l.Quantity); + if (qty > eval.FreeQty.Value) + throw new InvalidOperationException($"Free quantity {qty} exceeds the {eval.FreeQty.Value:0.##} entitled by scheme '{scheme.Name}'."); + + var available = (await stockRepo.GetBalancesByItemAsync(eval.FreeItemId.Value)).Sum(b => b.Quantity); + if (available < qty) + throw new InvalidOperationException($"Insufficient stock to give {qty} free of scheme '{scheme.Name}'."); + + foreach (var l in group) + { + l.UnitPrice = 0m; + l.SchemeName = scheme.Name; + } + } + } + + /// + /// Checks the percentage-discount schemes (FlatPercent / SlabDiscount) the user accepted for a Shop PO that is + /// about to be saved: one at most, in scope, and the paid lines must still qualify. Read-only; throws. + /// + public async Task ValidateDiscountSchemesAsync(Guid storeId, DateTime asOf, List lines, List? schemeIds) + { + if (schemeIds is not { Count: > 0 }) return; + if (schemeIds.Distinct().Count() > 1) + throw new InvalidOperationException("Only one discount scheme can be applied to an order."); + + var store = await storeRepo.GetByIdWithRegionAsync(storeId) + ?? throw new InvalidOperationException("Store not found."); + + var scheme = await repo.GetByIdAsync(schemeIds[0]); + if (scheme is null || !scheme.IsActive) + throw new InvalidOperationException("The selected discount scheme no longer exists."); + if (scheme.RuleType is not (SchemeRuleType.FlatPercent or SchemeRuleType.SlabDiscount)) + throw new InvalidOperationException($"Scheme '{scheme.Name}' is not a discount scheme."); + if (!IsInScope(scheme, store.Segment, store.Route?.FK_RegionId, asOf)) + throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date."); + + var eval = EvaluateRule(scheme, lines); + if (!eval.Eligible) + throw new InvalidOperationException(eval.Reason ?? $"Order does not qualify for scheme '{scheme.Name}'."); + } + + /// + /// Re-evaluates the percentage discounts redeemed against a Shop PO for the given (invoice) lines and returns + /// their total. With persist = true the redemption amounts are updated too (the caller commits), so spend/ROI and + /// the invoice stay in step; a discount the lines no longer qualify for drops to 0 and comes back if they requalify. + /// + public async Task CalculateDiscountAsync(Guid shopPOId, IEnumerable lines, bool persist) + { + var priced = lines.ToList(); + decimal total = 0m; + foreach (var r in (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue)) + { + var scheme = await repo.GetByIdAsync(r.FK_SchemeId); + var eval = scheme is null ? null : EvaluateRule(scheme, priced); + var amount = eval is { Eligible: true, DiscountAmount: not null } ? eval.DiscountAmount.Value : 0m; + if (persist) r.DiscountAmount = amount; + total += amount; + } + return total; + } + + // How many free units each scheme currently entitles for these paid lines (0 when no longer eligible). + private async Task> FreeEntitlementsAsync(IEnumerable schemeIds, List paidLines) + { + var result = new Dictionary(); + foreach (var id in schemeIds.Distinct()) + { + var scheme = await repo.GetByIdAsync(id); + var eval = scheme is null ? null : EvaluateRule(scheme, paidLines); + result[id] = eval is { Eligible: true, FreeQty: not null } ? (int)Math.Floor(eval.FreeQty.Value) : 0; + } + return result; + } + + /// + /// Free-issue entitlement per scheme for unsaved invoice lines (editor preview). Lines carry IsFree / scheme id; + /// only the paid ones count toward the scheme rules. + /// + public async Task> GetFreeEntitlementsAsync(IEnumerable schemeIds, IEnumerable lines) => + await FreeEntitlementsAsync(schemeIds, lines.Where(l => !l.IsFree).ToList()); + + /// + /// Keeps an invoice's free-issue lines within what the remaining paid lines still earn: when the paid quantity of the + /// qualifying item is lowered (or removed), the free quantity is reduced to match (a line that earns nothing is struck + /// out) and the PO's redemption is resized. Stages changes only. Returns true if any free line was changed. + /// + public async Task ReconcileInvoiceFreeLinesAsync(Guid shopPOId, List lines) + { + var freeLines = lines.Where(l => l.IsActive && l.IsFree && l.FK_SchemeId.HasValue).ToList(); + if (freeLines.Count == 0) return false; + + var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree) + .Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice }) + .ToList(); + var entitled = await FreeEntitlementsAsync(freeLines.Select(l => l.FK_SchemeId!.Value), paid); + var redemptions = await repo.GetRedemptionsByShopPOAsync(shopPOId); + var changed = false; + + foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value)) + { + var remaining = entitled[group.Key]; + foreach (var line in group.Where(l => !l.IsRemoved).OrderBy(l => l.CreatedAt)) + { + if (line.Quantity <= remaining) { remaining -= line.Quantity; continue; } + + if (remaining == 0) line.IsRemoved = true; + else line.Quantity = remaining; + remaining = 0; + changed = true; + } + + var redemption = redemptions.FirstOrDefault(r => r.FK_SchemeId == group.Key && r.FK_FreeItemId.HasValue); + if (redemption is not null) + redemption.FreeQty = group.Where(l => !l.IsRemoved).Sum(l => l.Quantity); + } + + return changed; + } + + /// + /// Discount the given schemes would give for these lines, without touching any redemption (editor preview). + /// + public async Task CalculateDiscountForSchemesAsync(IEnumerable schemeIds, IEnumerable lines) + { + var priced = lines.ToList(); + decimal total = 0m; + foreach (var id in schemeIds.Distinct()) + { + var scheme = await repo.GetByIdAsync(id); + var eval = scheme is null ? null : EvaluateRule(scheme, priced); + if (eval is { Eligible: true, DiscountAmount: not null }) total += eval.DiscountAmount.Value; + } + return total; + } + + /// + /// Applies the discount choice made on a Draft invoice (the percentage scheme the user accepted, or none) to the + /// PO's redemptions: newly chosen schemes are validated and added, unchosen ones removed. The amount itself is + /// then set by CalculateDiscountAsync when the totals are re-derived. Stages changes only. Null = leave as is. + /// + public async Task ApplyInvoiceDiscountSelectionAsync(Guid shopPOId, Guid storeId, List lines, List? schemeIds) + { + if (schemeIds is null) return; + if (schemeIds.Distinct().Count() > 1) + throw new InvalidOperationException("Only one discount scheme can be applied to an order."); + + var existing = (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue).ToList(); + foreach (var r in existing.Where(r => !schemeIds.Contains(r.FK_SchemeId))) + repo.RemoveRedemption(r); + + var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree) + .Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice }) + .ToList(); + var added = schemeIds.Where(id => existing.All(r => r.FK_SchemeId != id)).ToList(); + await ValidateDiscountSchemesAsync(storeId, DateTime.Now, paid, added); + + foreach (var id in added) + await repo.AddRedemptionAsync(new SchemeRedemption { FK_SchemeId = id, FK_ShopPOId = shopPOId, DiscountAmount = 0m }); + } + + /// Discounts already redeemed against a Shop PO, for display and for editing. + public async Task> GetDiscountsForShopPOAsync(Guid shopPOId) => + (await repo.GetRedemptionsByShopPOAsync(shopPOId)) + .Where(r => r.DiscountAmount.HasValue) + .Select(r => new ShopPODiscountResponse(r.FK_SchemeId, r.Scheme?.Name ?? string.Empty, r.DiscountAmount!.Value)) + .ToList(); + + /// + /// Brings the Shop PO's scheme redemptions in line with its saved lines: free-goods redemptions follow the + /// free lines (created, resized or removed), discount redemptions are recomputed or dropped if the order no + /// longer qualifies. Stages changes only — the caller owns the commit. + /// + public async Task SyncRedemptionsAsync(Guid shopPOId, List lines, List? discountSchemeIds = null) + { + var existing = await repo.GetRedemptionsByShopPOAsync(shopPOId); + var freeBySchemes = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue) + .GroupBy(l => l.FK_SchemeId!.Value).ToDictionary(g => g.Key, g => g.ToList()); + + foreach (var r in existing) + { + if (r.FK_FreeItemId.HasValue) + { + if (!freeBySchemes.ContainsKey(r.FK_SchemeId)) repo.RemoveRedemption(r); + } + else if (r.DiscountAmount.HasValue) + { + // A null list means "leave the accepted discounts alone" (just recompute them). + if (discountSchemeIds is not null && !discountSchemeIds.Contains(r.FK_SchemeId)) + { + repo.RemoveRedemption(r); + continue; + } + + var scheme = await repo.GetByIdAsync(r.FK_SchemeId); + var eval = scheme is null ? null : EvaluateRule(scheme, lines); + if (eval is { Eligible: true, DiscountAmount: not null }) r.DiscountAmount = eval.DiscountAmount; + else repo.RemoveRedemption(r); + } + } + + foreach (var schemeId in discountSchemeIds ?? []) + { + if (existing.Any(r => r.FK_SchemeId == schemeId && r.DiscountAmount.HasValue)) continue; + var scheme = await repo.GetByIdAsync(schemeId); + var eval = scheme is null ? null : EvaluateRule(scheme, lines); + if (eval is { Eligible: true, DiscountAmount: not null }) + await repo.AddRedemptionAsync(new SchemeRedemption + { + FK_SchemeId = schemeId, + FK_ShopPOId = shopPOId, + DiscountAmount = eval.DiscountAmount + }); + } + + foreach (var (schemeId, group) in freeBySchemes) + { + var freeItem = await itemRepo.GetByIdAsync(group[0].FK_ItemId) + ?? throw new InvalidOperationException("Free item not found."); + var qty = group.Sum(l => l.Quantity); + var redemption = existing.FirstOrDefault(r => r.FK_SchemeId == schemeId && r.FK_FreeItemId.HasValue); + + if (redemption is null) + await repo.AddRedemptionAsync(new SchemeRedemption + { + FK_SchemeId = schemeId, + FK_ShopPOId = shopPOId, + FK_FreeItemId = freeItem.Id, + FreeQty = qty, + FreeItemCostValue = freeItem.CostPrice ?? 0m + }); + else + { + redemption.FK_FreeItemId = freeItem.Id; + redemption.FreeQty = qty; + redemption.FreeItemCostValue = freeItem.CostPrice ?? 0m; + } + } + } + /// /// Scope-only check (region, segment, date range as of today) with no ShopPO to evaluate line /// items against — used by Store Management's `checkStoreSchemeEligibility(storeId, schemeId)` @@ -299,14 +620,18 @@ public class SchemeService( return (false, $"Scheme is only available to the '{scheme.EligibleSegment}' segment."); var today = DateTime.Now; - if (today < scheme.DateRangeFrom || today > scheme.DateRangeTo) + if (today < scheme.DateRangeFrom || today.Date > scheme.DateRangeTo.Date) return (false, "Scheme is not currently active (outside its date range)."); return (true, null); } - private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List items) + private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List allItems) { + // Free-issue lines (added by a previous redemption) must not count toward a trigger/bundle + // quantity, otherwise the free units inflate the multiplier. + var items = allItems.Where(i => !i.IsFree && i.UnitPrice > 0).ToList(); + switch (scheme.RuleType) { case SchemeRuleType.BuyXGetY: @@ -414,7 +739,10 @@ public class SchemeService( FK_ShopPOId = shopPOId, FK_ItemId = freeItem.Id, Quantity = (int)eligibility.FreeQty.Value, - UnitPrice = 0m + UnitPrice = 0m, + IsFree = true, + FK_SchemeId = scheme.Id, + SchemeName = scheme.Name }); redemption.FK_FreeItemId = freeItem.Id; @@ -425,7 +753,27 @@ public class SchemeService( await uow.BeginAsync(ct); try { - if (newItems.Count > 0) await shopPORepo.AddItemsAsync(newItems); + if (newItems.Count > 0) + { + await shopPORepo.AddItemsAsync(newItems); + + // A still-draft invoice was copied from the PO before this line existed, so mirror it there. + if (shopPO.InvoiceId is Guid invoiceId && + await invoiceRepo.GetByIdAsync(invoiceId) is { Status: InvoiceStatus.Draft }) + { + await invoiceRepo.AddItemsAsync(newItems.Select(n => new InvoiceItem + { + FK_InvoiceId = invoiceId, + FK_ShopPOItemId = n.Id, + FK_ItemId = n.FK_ItemId, + Quantity = n.Quantity, + UnitPrice = 0m, + IsFree = true, + FK_SchemeId = n.FK_SchemeId, + SchemeName = n.SchemeName + })); + } + } await repo.AddRedemptionAsync(redemption); await uow.CommitAsync(ct); } @@ -487,6 +835,8 @@ public class SchemeService( if (freeItemId is null || freeQty is null or <= 0) throw new InvalidOperationException("FreeItemId and a positive FreeQty are required."); + if (freeQty.Value != decimal.Truncate(freeQty.Value)) + throw new InvalidOperationException("FreeQty must be a whole number (order lines hold whole units)."); if (await itemRepo.GetByIdAsync(freeItemId.Value) is null) throw new InvalidOperationException("Free item not found."); } -- 2.52.0 From 855eb44118a356cf3a0658eb235a8732183c0c6a Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:27:14 +0530 Subject: [PATCH 03/11] feat: enhance Shop PO handling with discount scheme support and free issue tracking --- Distribution/Features/ShopPO/ShopPODTOs.cs | 16 ++-- Distribution/Features/ShopPO/ShopPOService.cs | 75 ++++++++++++------- Distribution/Models/ShopPOItem.cs | 6 ++ 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/Distribution/Features/ShopPO/ShopPODTOs.cs b/Distribution/Features/ShopPO/ShopPODTOs.cs index 39bc7af..a242507 100644 --- a/Distribution/Features/ShopPO/ShopPODTOs.cs +++ b/Distribution/Features/ShopPO/ShopPODTOs.cs @@ -2,13 +2,18 @@ namespace Distribution.Features.ShopPO; -public record CreateShopPORequest(Guid StoreId, List Items, Guid? RaisedByUserId = null); -public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null); -public record UpdateShopPOItemsRequest(List Items); +// DiscountSchemeIds: the FlatPercent/SlabDiscount scheme the user accepted (at most one). Null = leave as is. +public record CreateShopPORequest(Guid StoreId, List Items, Guid? RaisedByUserId = null, List? DiscountSchemeIds = null); +// IsFree + SchemeId mark a free-issue line the user accepted from a scheme suggestion; its price is forced to 0. +public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false, Guid? SchemeId = null); +public record UpdateShopPOItemsRequest(List Items, List? DiscountSchemeIds = null); public record ConfirmShopPORequest(Guid? ConfirmedByUserId = null); public record CancelShopPORequest(string Reason, Guid? CancelledByUserId = null); -public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive); +public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive, + bool IsFree = false, Guid? SchemeId = null, string? SchemeName = null); + +public record ShopPODiscountResponse(Guid SchemeId, string SchemeName, decimal Amount); public record ShopPOResponse( Guid Id, @@ -34,4 +39,5 @@ public record ShopPOResponse( DateTime? CancelledAt = null, string? CancelReason = null, string? CancelledByName = null, - string? CancelledByRole = null); + string? CancelledByRole = null, + List? Discounts = null); diff --git a/Distribution/Features/ShopPO/ShopPOService.cs b/Distribution/Features/ShopPO/ShopPOService.cs index e427f9e..f21437c 100644 --- a/Distribution/Features/ShopPO/ShopPOService.cs +++ b/Distribution/Features/ShopPO/ShopPOService.cs @@ -1,6 +1,7 @@ using Distribution.Features.Invoice; using Distribution.Features.Notifications; using Distribution.Features.PriceList; +using Distribution.Features.Scheme; using Distribution.Features.Stock; using Distribution.Infra.UoW; using Distribution.Models; @@ -11,9 +12,30 @@ public class ShopPOService( IShopPORepository repo, IInvoiceRepository invoiceRepo, PriceListService priceListService, + SchemeService schemeService, INotificationService notifications, IUnitOfWork uow) { + // Paid lines are priced from the request or the price list; accepted free-issue lines are always priced 0. + private async Task BuildLineAsync(ShopPOItemRequest line, Guid storeId) + { + if (line.IsFree) + return new ShopPOItem + { + FK_ItemId = line.ItemId, + Quantity = line.Quantity, + UnitPrice = 0m, + IsFree = true, + FK_SchemeId = line.SchemeId + }; + + var unitPrice = line.UnitPrice + ?? (await priceListService.ResolveByStoreAsync(line.ItemId, storeId, DateTime.Now))?.Price + ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); + + return new ShopPOItem { FK_ItemId = line.ItemId, Quantity = line.Quantity, UnitPrice = unitPrice }; + } + // Tells the person who raised the order (and their supervisor) what happened to it, unless they did it themselves. private Task NotifyOrderAsync(string type, Models.ShopPO po, Guid actingUserId, string? reason, CancellationToken ct) => notifications.NotifyAsync( @@ -22,14 +44,15 @@ public class ShopPOService( new NotificationArgs(new { StoreName = po.Store?.StoreName, Reason = reason }, "ShopPO", po.Id, "/sales/orders", actingUserId), ct); - private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary? users = null) => + private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary? users = null, List? discounts = null) => new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty, p.Store?.StoreAddress, p.Store?.ContactNumber, p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status, p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt, p.Items.Where(i => i.IsActive).Select(i => new ShopPOItemResponse(i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty, - i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).ToList(), + i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive, + i.IsFree, i.FK_SchemeId, i.SchemeName)).ToList(), p.IsActive, p.CreatedAt, users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null, users != null && users.TryGetValue(p.RaisedById, out var r) ? r.Role : null, @@ -37,7 +60,8 @@ public class ShopPOService( users != null && p.ConfirmedBy is Guid cb2 && users.TryGetValue(cb2, out var c2) ? c2.Role : null, p.CancelledBy, p.CancelledAt, p.CancelReason, users != null && p.CancelledBy is Guid xb && users.TryGetValue(xb, out var x) ? x.Name : null, - users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null); + users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null, + discounts ?? []); private static IEnumerable UserIds(Models.ShopPO p) { @@ -62,7 +86,7 @@ public class ShopPOService( var po = await repo.GetByIdAsync(id); if (po is null) return null; var users = await repo.GetUserInfoAsync(UserIds(po)); - return ToResponse(po, users); + return ToResponse(po, users, await schemeService.GetDiscountsForShopPOAsync(po.Id)); } public async Task> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId) @@ -82,20 +106,14 @@ public class ShopPOService( foreach (var line in request.Items) { - var unitPrice = line.UnitPrice - ?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price - ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); - - items.Add(new ShopPOItem - { - FK_ItemId = line.ItemId, - Quantity = line.Quantity, - QuantityFulfilled = 0, - UnitPrice = unitPrice - }); - total += line.Quantity * unitPrice; + var item = await BuildLineAsync(line, request.StoreId); + items.Add(item); + total += item.Quantity * item.UnitPrice; } + await schemeService.ValidateFreeLinesAsync(request.StoreId, DateTime.Now, items); + await schemeService.ValidateDiscountSchemesAsync(request.StoreId, DateTime.Now, items, request.DiscountSchemeIds); + var po = new Models.ShopPO { FK_StoreId = request.StoreId, @@ -110,6 +128,7 @@ public class ShopPOService( await repo.AddAsync(po); foreach (var item in items) item.FK_ShopPOId = po.Id; await repo.AddItemsAsync(items); + await schemeService.SyncRedemptionsAsync(po.Id, items, request.DiscountSchemeIds); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } @@ -137,27 +156,22 @@ public class ShopPOService( foreach (var line in request.Items) { - var unitPrice = line.UnitPrice - ?? (await priceListService.ResolveByStoreAsync(line.ItemId, po.FK_StoreId, DateTime.Now))?.Price - ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); - - newItems.Add(new ShopPOItem - { - FK_ShopPOId = id, - FK_ItemId = line.ItemId, - Quantity = line.Quantity, - QuantityFulfilled = 0, - UnitPrice = unitPrice - }); - total += line.Quantity * unitPrice; + var item = await BuildLineAsync(line, po.FK_StoreId); + item.FK_ShopPOId = id; + newItems.Add(item); + total += item.Quantity * item.UnitPrice; } + await schemeService.ValidateFreeLinesAsync(po.FK_StoreId, po.CreatedAt, newItems); + await schemeService.ValidateDiscountSchemesAsync(po.FK_StoreId, po.CreatedAt, newItems, request.DiscountSchemeIds); + po.TotalAmount = total; await uow.BeginAsync(ct); try { await repo.ReplaceItemsAsync(id, newItems); + await schemeService.SyncRedemptionsAsync(id, newItems, request.DiscountSchemeIds); repo.Update(po); await uow.CommitAsync(ct); } @@ -212,6 +226,9 @@ public class ShopPOService( CreatedBy = userId, TotalAmount = po.TotalAmount }; + // Start the invoice at the discounted total so it matches what print/finalize will recompute. + invoice.DiscountAmount = Math.Min(await invoiceRepo.GetSchemeDiscountAsync(po.Id), po.TotalAmount); + invoice.TotalAmount = po.TotalAmount - invoice.DiscountAmount; await uow.BeginAsync(ct); try diff --git a/Distribution/Models/ShopPOItem.cs b/Distribution/Models/ShopPOItem.cs index 0b7b7ec..4118879 100644 --- a/Distribution/Models/ShopPOItem.cs +++ b/Distribution/Models/ShopPOItem.cs @@ -15,5 +15,11 @@ namespace Distribution.Models public int Quantity { get; set; } public int QuantityFulfilled { get; set; } = 0; public decimal UnitPrice { get; set; } + + /// Free issue granted by a scheme (UnitPrice 0); shown in the Free Issue column. + public bool IsFree { get; set; } + public Guid? FK_SchemeId { get; set; } + /// Scheme name when it was applied, so later scheme edits don't rewrite history. + public string? SchemeName { get; set; } } } -- 2.52.0 From 3a503abe6a27439d6fbf5389759053123fb0ef0e Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:28:46 +0530 Subject: [PATCH 04/11] feat: Add Free Issue Lines to ShopPOItems and InvoiceItems --- ...261022063408_AddFreeIssueLines.Designer.cs | 6107 +++++++++++++++++ .../20261022063408_AddFreeIssueLines.cs | 99 + .../Migrations/AppDBContextModelSnapshot.cs | 22 + Distribution/Models/InvoiceItem.cs | 5 + 4 files changed, 6233 insertions(+) create mode 100644 Distribution/Migrations/20261022063408_AddFreeIssueLines.Designer.cs create mode 100644 Distribution/Migrations/20261022063408_AddFreeIssueLines.cs diff --git a/Distribution/Migrations/20261022063408_AddFreeIssueLines.Designer.cs b/Distribution/Migrations/20261022063408_AddFreeIssueLines.Designer.cs new file mode 100644 index 0000000..19a3ad0 --- /dev/null +++ b/Distribution/Migrations/20261022063408_AddFreeIssueLines.Designer.cs @@ -0,0 +1,6107 @@ +// +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("20261022063408_AddFreeIssueLines")] + partial class AddFreeIssueLines + { + /// + 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.BankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AccountNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("AccountType") + .HasColumnType("int"); + + b.Property("BankName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CashAccountTypeName") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("AccountType", "AccountName") + .IsUnique(); + + b.ToTable("BankAccounts"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + AccountName = "Petty Cash", + AccountType = 0, + CashAccountTypeName = "Petty Cash", + CreatedAt = new DateTime(2026, 10, 19, 0, 0, 0, 0, DateTimeKind.Unspecified), + CurrencyCode = "LKR", + IsActive = true, + IsDefault = true + }); + }); + + 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.ChequeBook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("ChequeBookNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("EndChequeNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("FK_BankAccountId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedDate") + .HasColumnType("datetime(6)"); + + b.Property("StartChequeNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalLeaves") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ChequeBookNo") + .IsUnique(); + + b.HasIndex("FK_BankAccountId"); + + b.ToTable("ChequeBooks"); + }); + + modelBuilder.Entity("Distribution.Models.ChequePage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CancelReason") + .HasColumnType("longtext"); + + b.Property("ChequeNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ClearedByBank") + .HasColumnType("tinyint(1)"); + + b.Property("ClearedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrencyCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("FK_ChequeBookId") + .HasColumnType("char(36)"); + + b.Property("IsAccountPayee") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsCrossCheque") + .HasColumnType("tinyint(1)"); + + b.Property("IsPostDated") + .HasColumnType("tinyint(1)"); + + b.Property("IssueDate") + .HasColumnType("datetime(6)"); + + b.Property("IssueStatus") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("PayeeId") + .HasColumnType("int"); + + b.Property("PayeeName") + .HasColumnType("longtext"); + + b.Property("PayeeType") + .HasColumnType("int"); + + b.Property("PrintedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrintedBy") + .HasColumnType("longtext"); + + b.Property("Purpose") + .HasColumnType("longtext"); + + b.Property("ReferenceNo") + .HasColumnType("longtext"); + + b.Property("VoucherId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ChequeNo") + .IsUnique(); + + b.HasIndex("FK_ChequeBookId"); + + b.ToTable("ChequePages"); + }); + + 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("DiscountPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + 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("NbtPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("VatPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + 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), + DiscountPercent = 0m, + 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, + NbtPercent = 0m, + ReceivedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + VatPercent = 0m + }); + }); + + 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("DiscountPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderItemId") + .HasColumnType("char(36)"); + + b.Property("FreeQuantity") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("NbtPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RejectedQuantity") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("VatPercent") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + 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), + DiscountPercent = 0m, + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000001"), + FreeQuantity = 0, + IsActive = true, + NbtPercent = 0m, + RejectedQuantity = 0, + UnitCost = 0m, + VatPercent = 0m + }, + new + { + Id = new Guid("44000000-0000-0000-0000-000000000002"), + AcceptedQuantity = 98, + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + DiscountPercent = 0m, + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderItemId = new Guid("42000000-0000-0000-0000-000000000002"), + FreeQuantity = 0, + IsActive = true, + NbtPercent = 0m, + RejectedQuantity = 2, + UnitCost = 0m, + VatPercent = 0m + }); + }); + + 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("DiscountAmount") + .HasColumnType("decimal(65,30)"); + + 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"), + DiscountAmount = 0m, + 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"), + DiscountAmount = 0m, + 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.InvoiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsFree") + .HasColumnType("tinyint(1)"); + + b.Property("IsRemoved") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("SchemeName") + .HasColumnType("longtext"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ShopPOItemId"); + + b.ToTable("InvoiceItems"); + }); + + modelBuilder.Entity("Distribution.Models.InvoiceVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("VersionNumber") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceId", "VersionNumber") + .IsUnique(); + + b.ToTable("InvoiceVersions"); + }); + + modelBuilder.Entity("Distribution.Models.InvoiceVersionItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_InvoiceVersionId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRemoved") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceVersionId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ShopPOItemId"); + + b.ToTable("InvoiceVersionItems"); + }); + + 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.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)"); + + b.Property("EntityId") + .HasColumnType("char(36)"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Link") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Severity") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.HasKey("Id"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Distribution.Models.NotificationRecipient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_NotificationId") + .HasColumnType("char(36)"); + + b.Property("FK_UserId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("ReadAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_NotificationId", "FK_UserId") + .IsUnique(); + + b.HasIndex("FK_UserId", "IsRead"); + + b.ToTable("NotificationRecipients"); + }); + + 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_BankAccountId") + .HasColumnType("char(36)"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("FK_ReceivedChequeId") + .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_BankAccountId"); + + b.HasIndex("FK_InvoiceId"); + + b.HasIndex("FK_ReceivedChequeId"); + + 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"); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0000-0000-000000000001"), + Code = "orders.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View shop POs, invoices, payments and returns", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000002"), + Code = "rides.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage delivery rides and delivery tasks", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000003"), + Code = "loading.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage loading sheets", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000004"), + Code = "stores.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View store accounts, credit limits and balances", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000005"), + Code = "sales_visits.checkin", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Check in and out of scheduled store visits", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000006"), + Code = "items.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage catalogue products", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000007"), + Code = "categories.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage product categories and brands", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000008"), + Code = "warehouses.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View stock balances, movements, warehouses and reports", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000009"), + Code = "purchase_orders.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage purchase orders, GRNs and supplier claims", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000010"), + Code = "pricelists.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage regional price lists", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000011"), + Code = "schemes.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage pricing schemes and promotions", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000012"), + Code = "suppliers.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage supplier accounts", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000013"), + Code = "driver_profiles.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage driver profiles and licensing", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000014"), + Code = "compliance.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Manage driver compliance rules", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000015"), + Code = "users.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage staff user accounts", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000016"), + Code = "roles.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage roles and their permission sets", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000017"), + Code = "regions.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage regions and region configuration", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000018"), + Code = "routes.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage delivery routes", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000019"), + Code = "vehicles.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View and manage fleet vehicles", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000020"), + Code = "claim.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View agency claims and settlement status", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000021"), + Code = "claim.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and settle agency claims", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000022"), + Code = "delivery-task.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View delivery tasks within a ride", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000023"), + Code = "delivery-task.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Confirm and fail delivery tasks", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000024"), + Code = "driver.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View driver profiles and license details", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000025"), + Code = "driver.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and update driver profiles", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000026"), + Code = "grn.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View goods receipt notes", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000027"), + Code = "grn.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and confirm goods receipts", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000028"), + Code = "loading-sheet.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View loading sheets and their items", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000029"), + Code = "loading-sheet.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create, edit, and confirm loading sheets", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000030"), + Code = "price-list.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View regional price lists", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000031"), + Code = "price-list.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and manage item price lists", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000032"), + Code = "purchase-order.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View purchase orders and line items", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000033"), + Code = "purchase-order.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create, confirm, and close purchase orders", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000034"), + Code = "purchase-return.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and manage purchase returns to suppliers", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000035"), + Code = "region.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View regions and their configuration", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000036"), + Code = "region.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and update regions, targets, and budgets", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000037"), + Code = "return.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View order returns", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000038"), + Code = "return.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and manage order returns", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000039"), + Code = "route.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View delivery routes", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000040"), + Code = "route.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and manage delivery routes", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000041"), + Code = "scheme.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View pricing schemes and promotions", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000042"), + Code = "scheme.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and configure pricing schemes", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000043"), + Code = "stock.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View stock balances and movements", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000044"), + Code = "stock.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Transfer stock and record damage or adjustments", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000045"), + Code = "store.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View store accounts and balances", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000046"), + Code = "store.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and update store accounts", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000047"), + Code = "supplier-claim.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View supplier claims", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000048"), + Code = "supplier-claim.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Create and resolve supplier claims", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000049"), + Code = "sys-config.view", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "View system configuration settings", + IsActive = true + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000050"), + Code = "sys-config.manage", + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Update system configuration settings", + IsActive = true + }); + }); + + 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.ReceivedCheque", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AccountHolderName") + .HasColumnType("longtext"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("ChequeDate") + .HasColumnType("datetime(6)"); + + b.Property("ChequeNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("longtext"); + + b.Property("DepositDate") + .HasColumnType("datetime(6)"); + + b.Property("DrawerBankBranch") + .HasColumnType("longtext"); + + b.Property("DrawerBankName") + .HasColumnType("longtext"); + + b.Property("FK_DepositBankAccountId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("ReceivedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReceivedFromId") + .HasColumnType("int"); + + b.Property("ReceivedFromName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ReceivedFromType") + .HasColumnType("int"); + + b.Property("ReferenceId") + .HasColumnType("int"); + + b.Property("ReferenceType") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ChequeNo"); + + b.HasIndex("FK_DepositBankAccountId"); + + b.ToTable("ReceivedCheques"); + }); + + modelBuilder.Entity("Distribution.Models.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Regions"); + + b.HasData( + new + { + Id = new Guid("24000000-0000-0000-0000-000000000001"), + Code = "REG-0001", + 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"), + Code = "REG-0002", + 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" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000007"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Level = 100, + Name = "SuperAdmin" + }); + }); + + 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"); + + b.HasData( + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("8a6922a3-2196-4eb7-bf2d-965cc0c5aed0"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("ea2cb1db-ef8c-466e-b0fc-60ebf8b64165"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("2d32e0b5-6a0d-44d8-b3b9-9a1d7b2bee57"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("fc6e623e-7905-441d-b218-b39f353c4098"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("cd139715-08bc-4654-b43b-7b8ccf41107a"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("d2ab35a5-cd5a-4b62-8db9-26c791747438"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000007"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("b2d39a05-a0d0-4dcd-8f6f-32f19214f854"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000008"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("bb280703-75a3-4303-8825-744dadd25f19"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000009"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("41e4235b-d2aa-4e1d-976a-01447f08c346"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000010"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("b5b0791f-9fe5-4a16-bb4f-9f2b9d6bad8a"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000011"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("d69c93a3-0314-46e6-8e81-8383b4493863"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000012"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("567394d6-299d-4340-9096-f24b897b119f"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000013"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("7fa26dce-f2ab-419e-bf5e-664612f056c4"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000014"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("406fa373-d5f1-4432-a49e-4c2cb3902265"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000015"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("394b36ba-ff10-46ce-b9cf-7d38171e1c0e"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000016"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("18ab609a-3210-4e00-b316-cdaf08702b6d"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000017"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("f080d122-f3d5-4aa9-883d-a74932ff067d"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000018"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("f57ee0a9-c42a-44a4-808d-26cf1a2a7fdd"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000019"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("5dbb7d55-3a70-4d7d-ac5b-bcfc50b9745a"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000020"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("57d32a05-6eaf-4d33-9c70-482601ed4850"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000021"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("d1e57675-60a9-45e3-a140-7a2aeb20774c"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000022"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("05f09100-5bfe-4a48-8dba-61083eb27674"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000023"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("5d0b7f1c-d477-47fc-ac34-aa7a89c0a0e7"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000024"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("b1da7157-5066-4e37-9007-e0e62d65d74d"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000025"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("f7d7e59f-df0d-4407-8a01-674b519167ed"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000026"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("5bbd2a66-c916-443b-8dac-cbbea50cea00"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000027"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("4ae14ca8-5686-4287-9b29-bad7e63614f8"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000028"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("51d514dd-a970-43ff-bd77-e28c9a042794"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000029"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("8a3834af-1889-4260-a19b-9dc0a5f9248c"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000030"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("5c46f385-0897-4e1c-9e80-7898bd17b223"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000031"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("faae7511-c574-4427-9d39-34228ad73811"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000032"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("ac2213bf-9743-4a8d-9aec-54a7a70681b7"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000033"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("203b5092-5239-48a2-9c61-d66048db8106"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000034"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("570586b7-f2c8-48d2-9cf1-fa21103b13d9"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000035"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("47a4985f-4b44-4cb5-ad9d-ef6095051be3"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000036"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("feffd0e4-69fc-46fe-8ccf-089d3ad49e6d"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000037"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("8ca877ff-1806-493f-befa-df422de44a94"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000038"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("dfc1fcaf-846f-4483-a827-2eda1c5e46d3"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000039"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("36a9ca26-29d1-4e08-bd0f-521d2f1c5b6e"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000040"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("6793e270-6f34-4b36-9b11-db70dae0cad6"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000041"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("dfee8dfb-616f-435f-b966-69126c5f6ca7"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000042"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("e88bdb37-0613-4133-9725-a3f4176662f1"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000043"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("3e41df03-4e8e-4a8b-bd1e-9efee28e4f43"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000044"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("9710fd44-ae77-46af-997b-ae0dba17dc69"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000045"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("dcd043e6-0fca-4fcd-a3d8-06da2c7bff55"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000046"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("01bd8af1-e052-4603-91e7-c52515d797e2"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000047"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("c2e4c574-9071-4a02-a40e-566cac669995"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000048"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("8177815b-7034-4b22-931d-ba7e5ab1d7c0"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000049"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("cb213e4b-47b5-40cb-a5be-f61387042e5f"), + IsActive = true + }, + new + { + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FK_PermissionId = new Guid("20000000-0000-0000-0000-000000000050"), + CreatedAt = new DateTime(2026, 8, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), + Id = new Guid("3a175321-a85a-4ba3-8379-29283c0648bd"), + IsActive = true + }); + }); + + 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_SchemeId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsFree") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityFulfilled") + .HasColumnType("int"); + + b.Property("SchemeName") + .HasColumnType("longtext"); + + 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, + IsFree = false, + 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, + IsFree = false, + 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, + IsFree = false, + 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, + IsFree = false, + 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.StoreCreditMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Direction") + .HasColumnType("int"); + + b.Property("FK_InvoiceId") + .HasColumnType("char(36)"); + + b.Property("FK_PaymentId") + .HasColumnType("char(36)"); + + b.Property("FK_StoreId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OccurredAt") + .HasColumnType("datetime(6)"); + + b.Property("RecordedBy") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FK_InvoiceId"); + + b.HasIndex("FK_PaymentId"); + + b.HasIndex("FK_StoreId", "OccurredAt"); + + b.ToTable("StoreCreditMovements"); + }); + + 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.SupplierPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_BankAccountId") + .HasColumnType("char(36)"); + + b.Property("FK_ChequePageId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Method") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("PaidAt") + .HasColumnType("datetime(6)"); + + b.Property("PaidByUserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FK_BankAccountId"); + + b.HasIndex("FK_ChequePageId"); + + b.HasIndex("FK_SupplierId"); + + b.ToTable("SupplierPayments"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierPaymentGRN", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllocatedAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierPaymentId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_SupplierPaymentId"); + + b.ToTable("SupplierPaymentGRNs"); + }); + + 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("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, + 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, + 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, + 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, + 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, + 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, + 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, + Username = "channa.desilva" + }, + new + { + Id = new Guid("00000001-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 10, 13, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_RoleId = new Guid("10000000-0000-0000-0000-000000000007"), + FullName = "Super Admin", + IsActive = true, + Username = "superadmin" + }); + }); + + 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.ChequeBook", b => + { + b.HasOne("Distribution.Models.BankAccount", "BankAccount") + .WithMany() + .HasForeignKey("FK_BankAccountId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BankAccount"); + }); + + modelBuilder.Entity("Distribution.Models.ChequePage", b => + { + b.HasOne("Distribution.Models.ChequeBook", "ChequeBook") + .WithMany("Pages") + .HasForeignKey("FK_ChequeBookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChequeBook"); + }); + + 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.InvoiceItem", b => + { + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany("Items") + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPOItem", "ShopPOItem") + .WithMany() + .HasForeignKey("FK_ShopPOItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Invoice"); + + b.Navigation("Item"); + + b.Navigation("ShopPOItem"); + }); + + modelBuilder.Entity("Distribution.Models.InvoiceVersion", b => + { + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany() + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Distribution.Models.InvoiceVersionItem", b => + { + b.HasOne("Distribution.Models.InvoiceVersion", "InvoiceVersion") + .WithMany("Items") + .HasForeignKey("FK_InvoiceVersionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ShopPOItem", "ShopPOItem") + .WithMany() + .HasForeignKey("FK_ShopPOItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("InvoiceVersion"); + + b.Navigation("Item"); + + b.Navigation("ShopPOItem"); + }); + + 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.NotificationRecipient", b => + { + b.HasOne("Distribution.Models.Notification", "Notification") + .WithMany("Recipients") + .HasForeignKey("FK_NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "User") + .WithMany() + .HasForeignKey("FK_UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Distribution.Models.Payment", b => + { + b.HasOne("Distribution.Models.BankAccount", "BankAccount") + .WithMany() + .HasForeignKey("FK_BankAccountId"); + + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.ReceivedCheque", "ReceivedCheque") + .WithMany() + .HasForeignKey("FK_ReceivedChequeId"); + + b.Navigation("BankAccount"); + + b.Navigation("Invoice"); + + b.Navigation("ReceivedCheque"); + }); + + 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.ReceivedCheque", b => + { + b.HasOne("Distribution.Models.BankAccount", "DepositBankAccount") + .WithMany() + .HasForeignKey("FK_DepositBankAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DepositBankAccount"); + }); + + 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.StoreCreditMovement", b => + { + b.HasOne("Distribution.Models.Invoice", "Invoice") + .WithMany() + .HasForeignKey("FK_InvoiceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Payment", "Payment") + .WithMany() + .HasForeignKey("FK_PaymentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Store", "Store") + .WithMany() + .HasForeignKey("FK_StoreId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Invoice"); + + b.Navigation("Payment"); + + b.Navigation("Store"); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.HasOne("Distribution.Models.Category", "Category") + .WithMany() + .HasForeignKey("FK_CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany() + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("FK_PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "RaisedByUser") + .WithMany() + .HasForeignKey("RaisedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedByUserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GoodsReceipt"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("RaisedByUser"); + + b.Navigation("ResolvedByUser"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.HasOne("Distribution.Models.Item", "Item") + .WithMany() + .HasForeignKey("FK_ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SupplierClaim", "SupplierClaim") + .WithMany("Items") + .HasForeignKey("FK_SupplierClaimId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("SupplierClaim"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierPayment", b => + { + b.HasOne("Distribution.Models.BankAccount", "BankAccount") + .WithMany() + .HasForeignKey("FK_BankAccountId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.ChequePage", "ChequePage") + .WithMany() + .HasForeignKey("FK_ChequePageId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Distribution.Models.Supplier", "Supplier") + .WithMany() + .HasForeignKey("FK_SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BankAccount"); + + b.Navigation("ChequePage"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierPaymentGRN", b => + { + b.HasOne("Distribution.Models.GoodsReceipt", "GoodsReceipt") + .WithMany() + .HasForeignKey("FK_GoodsReceiptId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Distribution.Models.SupplierPayment", "SupplierPayment") + .WithMany("Allocations") + .HasForeignKey("FK_SupplierPaymentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GoodsReceipt"); + + b.Navigation("SupplierPayment"); + }); + + 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.ChequeBook", b => + { + b.Navigation("Pages"); + }); + + 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("Items"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.InvoiceVersion", b => + { + b.Navigation("Items"); + }); + + 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.Notification", b => + { + b.Navigation("Recipients"); + }); + + 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"); + }); + + modelBuilder.Entity("Distribution.Models.SupplierPayment", b => + { + b.Navigation("Allocations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Distribution/Migrations/20261022063408_AddFreeIssueLines.cs b/Distribution/Migrations/20261022063408_AddFreeIssueLines.cs new file mode 100644 index 0000000..2fe6857 --- /dev/null +++ b/Distribution/Migrations/20261022063408_AddFreeIssueLines.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddFreeIssueLines : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FK_SchemeId", + table: "ShopPOItems", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "IsFree", + table: "ShopPOItems", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SchemeName", + table: "ShopPOItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "FK_SchemeId", + table: "InvoiceItems", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "IsFree", + table: "InvoiceItems", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SchemeName", + table: "InvoiceItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + // Existing free-issue lines were plain zero-priced lines; flag them from their scheme redemption. + migrationBuilder.Sql(@" +UPDATE ShopPOItems i +JOIN SchemeRedemptions r ON r.FK_ShopPOId = i.FK_ShopPOId AND r.FK_FreeItemId = i.FK_ItemId +JOIN Schemes s ON s.Id = r.FK_SchemeId +SET i.IsFree = 1, i.FK_SchemeId = r.FK_SchemeId, i.SchemeName = s.Name +WHERE i.UnitPrice = 0 AND i.IsActive = 1;"); + + migrationBuilder.Sql(@" +UPDATE InvoiceItems ii +JOIN ShopPOItems pi ON ii.FK_ShopPOItemId = pi.Id +SET ii.IsFree = pi.IsFree, ii.FK_SchemeId = pi.FK_SchemeId, ii.SchemeName = pi.SchemeName +WHERE pi.IsFree = 1;"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FK_SchemeId", + table: "ShopPOItems"); + + migrationBuilder.DropColumn( + name: "IsFree", + table: "ShopPOItems"); + + migrationBuilder.DropColumn( + name: "SchemeName", + table: "ShopPOItems"); + + migrationBuilder.DropColumn( + name: "FK_SchemeId", + table: "InvoiceItems"); + + migrationBuilder.DropColumn( + name: "IsFree", + table: "InvoiceItems"); + + migrationBuilder.DropColumn( + name: "SchemeName", + table: "InvoiceItems"); + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index d3256bc..016f5df 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -932,18 +932,27 @@ namespace Distribution.Migrations b.Property("FK_ItemId") .HasColumnType("char(36)"); + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + b.Property("FK_ShopPOItemId") .HasColumnType("char(36)"); b.Property("IsActive") .HasColumnType("tinyint(1)"); + b.Property("IsFree") + .HasColumnType("tinyint(1)"); + b.Property("IsRemoved") .HasColumnType("tinyint(1)"); b.Property("Quantity") .HasColumnType("int"); + b.Property("SchemeName") + .HasColumnType("longtext"); + b.Property("UnitPrice") .HasColumnType("decimal(65,30)"); @@ -3717,18 +3726,27 @@ namespace Distribution.Migrations b.Property("FK_ItemId") .HasColumnType("char(36)"); + b.Property("FK_SchemeId") + .HasColumnType("char(36)"); + b.Property("FK_ShopPOId") .HasColumnType("char(36)"); b.Property("IsActive") .HasColumnType("tinyint(1)"); + b.Property("IsFree") + .HasColumnType("tinyint(1)"); + b.Property("Quantity") .HasColumnType("int"); b.Property("QuantityFulfilled") .HasColumnType("int"); + b.Property("SchemeName") + .HasColumnType("longtext"); + b.Property("UnitPrice") .HasColumnType("decimal(65,30)"); @@ -3748,6 +3766,7 @@ namespace Distribution.Migrations FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), IsActive = true, + IsFree = false, Quantity = 30, QuantityFulfilled = 30, UnitPrice = 60m @@ -3759,6 +3778,7 @@ namespace Distribution.Migrations FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), IsActive = true, + IsFree = false, Quantity = 24, QuantityFulfilled = 24, UnitPrice = 50m @@ -3770,6 +3790,7 @@ namespace Distribution.Migrations FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), IsActive = true, + IsFree = false, Quantity = 10, QuantityFulfilled = 0, UnitPrice = 120m @@ -3781,6 +3802,7 @@ namespace Distribution.Migrations FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), IsActive = true, + IsFree = false, Quantity = 20, QuantityFulfilled = 20, UnitPrice = 120m diff --git a/Distribution/Models/InvoiceItem.cs b/Distribution/Models/InvoiceItem.cs index b56f15f..4081d87 100644 --- a/Distribution/Models/InvoiceItem.cs +++ b/Distribution/Models/InvoiceItem.cs @@ -19,5 +19,10 @@ namespace Distribution.Models public int Quantity { get; set; } public decimal UnitPrice { get; set; } public bool IsRemoved { get; set; } + + /// Free issue copied from the Shop PO line (UnitPrice 0). + public bool IsFree { get; set; } + public Guid? FK_SchemeId { get; set; } + public string? SchemeName { get; set; } } } -- 2.52.0 From 58247a1a0bc1b4285a63feb5b9bc19b212a734b4 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:36:00 +0530 Subject: [PATCH 05/11] feat: Add SchemeForm component for creating and editing schemes with validation. --- .../pricing/schemes/[id]/edit/page.tsx | 30 + .../(dashboard)/pricing/schemes/[id]/page.tsx | 30 + .../schemes/_components/scheme-form.tsx | 602 ++++++++++++++++++ .../schemes/_components/scheme-labels.ts | 61 ++ .../schemes/_components/scheme-sheet.tsx | 265 -------- .../schemes/_components/scheme-view-sheet.tsx | 87 --- .../schemes/_components/schemes-client.tsx | 39 +- .../(dashboard)/pricing/schemes/new/page.tsx | 5 + 8 files changed, 740 insertions(+), 379 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/[id]/edit/page.tsx create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/[id]/page.tsx create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-form.tsx create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-labels.ts delete mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-sheet.tsx delete mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-view-sheet.tsx create mode 100644 distribution_fe/app/(dashboard)/pricing/schemes/new/page.tsx diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/[id]/edit/page.tsx new file mode 100644 index 0000000..fc3a0a8 --- /dev/null +++ b/distribution_fe/app/(dashboard)/pricing/schemes/[id]/edit/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { toast } from '@/components/ui/toast' +import { schemes as schemesApi, getApiError } from '@/lib/api/index' +import type { SchemeResponse } from '@/types/api' +import { SchemeForm } from '../../_components/scheme-form' + +export default function EditSchemePage() { + const router = useRouter() + const { id } = useParams<{ id: string }>() + const [scheme, setScheme] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + schemesApi + .get(id) + .then(setScheme) + .catch((err) => { + toast.add({ title: 'Failed to load scheme', description: getApiError(err), type: 'error' }) + router.push('/pricing/schemes') + }) + .finally(() => setLoading(false)) + }, [id, router]) + + if (loading) return

Loading...

+ if (!scheme) return

Scheme not found.

+ return +} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/[id]/page.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/[id]/page.tsx new file mode 100644 index 0000000..fdc2341 --- /dev/null +++ b/distribution_fe/app/(dashboard)/pricing/schemes/[id]/page.tsx @@ -0,0 +1,30 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { toast } from '@/components/ui/toast' +import { schemes as schemesApi, getApiError } from '@/lib/api/index' +import type { SchemeResponse } from '@/types/api' +import { SchemeForm } from '../_components/scheme-form' + +export default function SchemeDetailsPage() { + const router = useRouter() + const { id } = useParams<{ id: string }>() + const [scheme, setScheme] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + schemesApi + .get(id) + .then(setScheme) + .catch((err) => { + toast.add({ title: 'Failed to load scheme', description: getApiError(err), type: 'error' }) + router.push('/pricing/schemes') + }) + .finally(() => setLoading(false)) + }, [id, router]) + + if (loading) return

Loading...

+ if (!scheme) return

Scheme not found.

+ return +} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-form.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-form.tsx new file mode 100644 index 0000000..431efc1 --- /dev/null +++ b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-form.tsx @@ -0,0 +1,602 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import { useFieldArray, useWatch } from 'react-hook-form' +import { HugeiconsIcon } from '@hugeicons/react' +import { Add01Icon, ArrowLeft01Icon, Delete02Icon } from '@hugeicons/core-free-icons' +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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Button } from '@/components/ui/button' +import { FieldGroup } from '@/components/ui/field' +import { toast } from '@/components/ui/toast' +import { + schemes as schemesApi, + regions as regionsApi, + items as itemsApi, + getApiError, + getPaginatedItems, +} from '@/lib/api/index' +import type { SchemeResponse, RegionResponse, ItemSummaryResponse, SchemeRuleType } from '@/types/api' +import { RULE_META, parseRuleConfig } from './scheme-labels' + +const RULE_ORDER: SchemeRuleType[] = ['BuyXGetY', 'FreeBundle', 'FlatPercent', 'SlabDiscount'] + +const schema = z + .object({ + name: z.string(), + ruleType: z.enum(['BuyXGetY', 'SlabDiscount', 'FlatPercent', 'FreeBundle']), + regionId: z.string().optional(), + eligibleSegment: z.string().optional(), + dateRangeFrom: z.string().min(1, 'Required'), + dateRangeTo: z.string().min(1, 'Required'), + isActive: z.boolean(), + // Buy & get free + triggerItemId: z.string(), + triggerQty: z.number(), + // Buy & get free + combo gift: what the store receives + freeItemId: z.string(), + freeQty: z.number(), + // Combo gift: the products that must be bought together + bundle: z.array(z.object({ itemId: z.string(), qty: z.number() })), + // Flat discount + discountPercent: z.number(), + // Spend more, save more + tiers: z.array(z.object({ minOrderAmount: z.number(), discountPercent: z.number() })), + }) + .superRefine((v, ctx) => { + const fail = (path: (string | number)[], message: string) => ctx.addIssue({ code: 'custom', path, message }) + const wholeQty = (n: number) => Number.isInteger(n) && n >= 1 + const pct = (n: number) => n > 0 && n <= 100 + + if (v.dateRangeFrom && v.dateRangeTo && v.dateRangeFrom > v.dateRangeTo) + fail(['dateRangeTo'], 'End date must not be before the start date') + + if (v.ruleType === 'BuyXGetY') { + if (!v.triggerItemId) fail(['triggerItemId'], 'Choose the product the store must buy') + if (!wholeQty(v.triggerQty)) fail(['triggerQty'], 'Enter a whole number, 1 or more') + } + if (v.ruleType === 'BuyXGetY' || v.ruleType === 'FreeBundle') { + if (!v.freeItemId) fail(['freeItemId'], 'Choose the free product') + if (!wholeQty(v.freeQty)) fail(['freeQty'], 'Enter a whole number, 1 or more') + } + if (v.ruleType === 'FreeBundle') { + if (v.bundle.length === 0) fail(['bundle'], 'Add at least one product') + v.bundle.forEach((b, i) => { + if (!b.itemId) fail(['bundle', i, 'itemId'], 'Choose a product') + if (!wholeQty(b.qty)) fail(['bundle', i, 'qty'], 'Whole number, 1 or more') + }) + const ids = v.bundle.map((b) => b.itemId).filter(Boolean) + if (new Set(ids).size !== ids.length) fail(['bundle'], 'Each product can only be listed once') + } + if (v.ruleType === 'FlatPercent' && !pct(v.discountPercent)) + fail(['discountPercent'], 'Enter a percentage between 1 and 100') + if (v.ruleType === 'SlabDiscount') { + if (v.tiers.length === 0) fail(['tiers'], 'Add at least one level') + v.tiers.forEach((t, i) => { + if (!(t.minOrderAmount > 0)) fail(['tiers', i, 'minOrderAmount'], 'Enter an amount above 0') + if (!pct(t.discountPercent)) fail(['tiers', i, 'discountPercent'], '1 to 100') + }) + } + }) + +type FormValues = z.infer + +const EMPTY: FormValues = { + name: '', + ruleType: 'BuyXGetY', + regionId: '', + eligibleSegment: '', + dateRangeFrom: '', + dateRangeTo: '', + isActive: true, + triggerItemId: '', + triggerQty: 10, + freeItemId: '', + freeQty: 1, + bundle: [{ itemId: '', qty: 1 }], + discountPercent: 10, + tiers: [{ minOrderAmount: 5000, discountPercent: 5 }], +} + +function toFormValues(scheme: SchemeResponse): FormValues { + const cfg = parseRuleConfig(scheme) + return { + ...EMPTY, + name: scheme.name, + ruleType: scheme.ruleType, + regionId: scheme.regionId ?? '', + eligibleSegment: scheme.eligibleSegment ?? '', + dateRangeFrom: scheme.dateRangeFrom.split('T')[0], + dateRangeTo: scheme.dateRangeTo.split('T')[0], + isActive: scheme.isActive, + triggerItemId: scheme.triggerItemId ?? '', + triggerQty: scheme.triggerQty ?? EMPTY.triggerQty, + freeItemId: scheme.freeItemId ?? '', + freeQty: scheme.freeQty ?? EMPTY.freeQty, + bundle: scheme.bundleComponents.length + ? scheme.bundleComponents.map((c) => ({ itemId: c.itemId, qty: c.qty })) + : EMPTY.bundle, + discountPercent: cfg.discountPercent || EMPTY.discountPercent, + tiers: cfg.tiers.length ? cfg.tiers : EMPTY.tiers, + } +} + +interface SchemeFormProps { + /** null = create a new scheme, otherwise edit this one. */ + scheme: SchemeResponse | null + /** Show the scheme exactly as the edit page does, but locked, with an Edit button. */ + readOnly?: boolean +} + +export function SchemeForm({ scheme, readOnly = false }: SchemeFormProps) { + const router = useRouter() + const goBack = () => router.push('/pricing/schemes') + const isEdit = !!scheme + const [regionsList, setRegionsList] = useState([]) + const [itemsList, setItemsList] = useState([]) + + const form = useForm({ resolver: zodResolver(schema), defaultValues: EMPTY }) + const bundle = useFieldArray({ control: form.control, name: 'bundle' }) + const tiers = useFieldArray({ control: form.control, name: 'tiers' }) + const v = useWatch({ control: form.control }) as FormValues + const ruleType = v.ruleType ?? 'BuyXGetY' + + useEffect(() => { + form.reset(scheme ? toFormValues(scheme) : EMPTY) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scheme]) + + useEffect(() => { + Promise.all([regionsApi.list({ pageSize: 100 }), itemsApi.list({ pageSize: 200 })]) + .then(([regions, items]) => { + setRegionsList(getPaginatedItems(regions)) + setItemsList(getPaginatedItems(items)) + }) + .catch(() => toast.add({ title: 'Failed to load regions or products', type: 'error' })) + }, []) + + const itemName = (id?: string) => itemsList.find((i) => i.id === id)?.name + + // Live sentence so the user can check the offer reads the way they intended. + const preview = useMemo(() => { + const n = (x?: number) => (x && x > 0 ? x : '?') + if (ruleType === 'BuyXGetY') { + const buy = itemName(v.triggerItemId) ?? 'a product' + const free = itemName(v.freeItemId) ?? 'a product' + return `Buy ${n(v.triggerQty)} × ${buy}, get ${n(v.freeQty)} × ${free} free. Repeats: buying ${n(v.triggerQty && v.triggerQty * 2)} earns ${n(v.freeQty && v.freeQty * 2)} free.` + } + if (ruleType === 'FreeBundle') { + const parts = (v.bundle ?? []).map((b) => `${n(b.qty)} × ${itemName(b.itemId) ?? 'a product'}`).join(' + ') + return `Buy ${parts || 'products'} together, get ${n(v.freeQty)} × ${itemName(v.freeItemId) ?? 'a product'} free (once per order).` + } + if (ruleType === 'FlatPercent') return `${n(v.discountPercent)}% off the whole order.` + const levels = [...(v.tiers ?? [])] + .sort((a, b) => a.minOrderAmount - b.minOrderAmount) + .map((t) => `${n(t.discountPercent)}% off from Rs. ${(t.minOrderAmount || 0).toLocaleString()}`) + return levels.length ? `${levels.join('; ')}. The highest level the order reaches applies.` : 'Add at least one level.' + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [v, ruleType, itemsList]) + + async function onSubmit(values: FormValues) { + const isGoods = values.ruleType === 'BuyXGetY' || values.ruleType === 'FreeBundle' + const ruleConfig = + values.ruleType === 'FlatPercent' + ? JSON.stringify({ discountPercent: values.discountPercent }) + : values.ruleType === 'SlabDiscount' + ? JSON.stringify({ tiers: values.tiers }) + : null + // The name is optional: when left blank, describe the offer itself (e.g. "Buy 10 Soap, get 1 Soap free"). + const n = (id: string) => itemName(id) ?? 'item' + const autoName = + values.ruleType === 'BuyXGetY' + ? `Buy ${values.triggerQty} ${n(values.triggerItemId)}, get ${values.freeQty} ${n(values.freeItemId)} free` + : values.ruleType === 'FreeBundle' + ? `Buy ${values.bundle.map((b) => `${b.qty} ${n(b.itemId)}`).join(' + ')}, get ${values.freeQty} ${n(values.freeItemId)} free` + : values.ruleType === 'FlatPercent' + ? `${values.discountPercent}% off the order` + : `Up to ${Math.max(...values.tiers.map((t) => t.discountPercent))}% off by order value` + const common = { + name: values.name.trim() || autoName, + ruleConfig, + regionId: values.regionId || null, + eligibleSegment: values.eligibleSegment?.trim() || null, + dateRangeFrom: values.dateRangeFrom, + dateRangeTo: values.dateRangeTo, + isActive: values.isActive, + } + const goods = isGoods + ? { + triggerItemId: values.ruleType === 'BuyXGetY' ? values.triggerItemId : null, + triggerQty: values.ruleType === 'BuyXGetY' ? values.triggerQty : undefined, + freeItemId: values.freeItemId, + freeQty: values.freeQty, + } + : {} + + try { + if (!scheme) { + await schemesApi.create({ + ...common, + ruleType: values.ruleType, + ...goods, + bundleComponents: + values.ruleType === 'FreeBundle' ? values.bundle.map((b) => ({ itemId: b.itemId, qty: b.qty })) : undefined, + }) + toast.add({ title: 'Scheme created', type: 'success' }) + } else { + // The rule type is fixed once created; update the basics, then the free-goods rule and bundle rows. + await schemesApi.update(scheme.id, { ...common, agentLimitExempt: scheme.agentLimitExempt }) + if (isGoods) { + await schemesApi.setFreeGoodsRule(scheme.id, { + triggerItemId: goods.triggerItemId, + triggerQty: goods.triggerQty, + freeItemId: values.freeItemId, + freeQty: values.freeQty, + }) + } + if (values.ruleType === 'FreeBundle') { + const keep = new Set(values.bundle.map((b) => b.itemId)) + for (const old of scheme.bundleComponents) { + if (!keep.has(old.itemId)) await schemesApi.removeBundleComponent(scheme.id, old.itemId) + } + for (const b of values.bundle) await schemesApi.upsertBundleComponent(scheme.id, b.itemId, b.qty) + } + toast.add({ title: 'Scheme updated', type: 'success' }) + } + goBack() + } catch (err) { + toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' }) + } + } + + const itemSelect = (value: string, onChange: (v: string) => void, placeholder: string, invalid?: boolean) => ( + + ) + + const numberInput = (value: number, onChange: (n: number) => void, invalid?: boolean, props?: { min?: number; max?: number; step?: string }) => ( + onChange(Number(e.target.value))} + aria-invalid={invalid} + {...props} + /> + ) + + return ( +
+
+ +
+

+ {readOnly ? 'Scheme Details' : isEdit ? 'Edit Scheme' : 'Add Scheme'} +

+

+ {readOnly + ? 'How this offer is set up.' + : isEdit ? 'Update the offer below.' : 'Pick the kind of offer, then fill in the details.'} +

+
+ {readOnly && scheme && ( + + )} +
+ +
+
+ + ( + + )} + /> + +
+

What kind of offer is it?

+
+ {RULE_ORDER.map((type) => { + const selected = ruleType === type + return ( + + ) + })} +
+ {isEdit &&

The kind of offer can't be changed after it is created.

} +
+ + {/* ── Buy & get free ─────────────────────────────────────────── */} + {ruleType === 'BuyXGetY' && ( +
+

The store buys

+
+ + itemSelect(field.value as string, (id) => { + field.onChange(id) + if (!form.getValues('freeItemId')) form.setValue('freeItemId', id) + }, 'Select product…', invalid) + } + /> + numberInput(field.value as number, field.onChange, invalid, { min: 1 })} + /> +
+

…and gets free

+
+ itemSelect(field.value as string, field.onChange, 'Select product…', invalid)} + /> + numberInput(field.value as number, field.onChange, invalid, { min: 1 })} + /> +
+
+ )} + + {/* ── Combo gift ─────────────────────────────────────────────── */} + {ruleType === 'FreeBundle' && ( +
+
+

The store must buy all of these

+ +
+ {bundle.fields.map((row, index) => ( +
+ itemSelect(field.value as string, field.onChange, 'Select product…', invalid)} + /> + numberInput(field.value as number, field.onChange, invalid, { min: 1 })} + /> + +
+ ))} + {form.formState.errors.bundle?.message && ( +

{form.formState.errors.bundle.message}

+ )} +

…and gets this free

+
+ itemSelect(field.value as string, field.onChange, 'Select product…', invalid)} + /> + numberInput(field.value as number, field.onChange, invalid, { min: 1 })} + /> +
+
+ )} + + {/* ── Flat discount ──────────────────────────────────────────── */} + {ruleType === 'FlatPercent' && ( +
+ numberInput(field.value as number, field.onChange, invalid, { min: 1, max: 100, step: '0.5' })} + /> +
+ )} + + {/* ── Spend more, save more ──────────────────────────────────── */} + {ruleType === 'SlabDiscount' && ( +
+
+

Discount levels

+ +
+ {tiers.fields.map((row, index) => ( +
+ numberInput(field.value as number, field.onChange, invalid, { min: 0 })} + /> + numberInput(field.value as number, field.onChange, invalid, { min: 1, max: 100, step: '0.5' })} + /> + +
+ ))} + {form.formState.errors.tiers?.message && ( +

{form.formState.errors.tiers.message}

+ )} +
+ )} + +
+ Summary: + {preview} +
+ +
+ ( + + )} + /> + ( + + )} + /> +
+ + ( +
+ + +
+ )} + /> + +
+

Where does it apply? (optional)

+
+ ( + + )} + /> + ( + + )} + /> +
+
+
+
+ + {!readOnly &&
+ + +
} +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-labels.ts b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-labels.ts new file mode 100644 index 0000000..37f7f65 --- /dev/null +++ b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-labels.ts @@ -0,0 +1,61 @@ +import type { SchemeResponse, SchemeRuleType } from '@/types/api' + +// Plain-language names for the four rule types, so users don't need to know the technical names. +export const RULE_META: Record = { + BuyXGetY: { + label: 'Buy & get free', + description: 'The store buys a quantity of a product and gets free units.', + example: 'Buy 10 Soap, get 1 Soap free', + }, + FreeBundle: { + label: 'Combo gift', + description: 'The store buys a set of products together and gets a free gift.', + example: 'Buy Soap + Shampoo, get a Toothbrush free', + }, + FlatPercent: { + label: 'Flat discount', + description: 'A fixed percentage off the whole order.', + example: '10% off every order', + }, + SlabDiscount: { + label: 'Spend more, save more', + description: 'The discount grows as the order value reaches higher levels.', + example: '5% off above Rs. 5,000, 8% off above Rs. 10,000', + }, +} + +export interface SlabTierConfig { + minOrderAmount: number + discountPercent: number +} + +export function parseRuleConfig(scheme: Pick) { + try { + const cfg = scheme.ruleConfig ? JSON.parse(scheme.ruleConfig) : null + if (scheme.ruleType === 'FlatPercent') return { discountPercent: Number(cfg?.discountPercent) || 0, tiers: [] as SlabTierConfig[] } + if (scheme.ruleType === 'SlabDiscount') return { discountPercent: 0, tiers: (cfg?.tiers ?? []) as SlabTierConfig[] } + } catch { + // Malformed config — fall through to empty. + } + return { discountPercent: 0, tiers: [] as SlabTierConfig[] } +} + +const money = (n: number) => `Rs. ${n.toLocaleString()}` + +/** One-sentence description of what a scheme does, built from its saved rule. */ +export function describeScheme(scheme: SchemeResponse): string { + switch (scheme.ruleType) { + case 'BuyXGetY': + return `Buy ${scheme.triggerQty ?? '?'} ${scheme.triggerItemName ?? 'item'}, get ${scheme.freeQty ?? '?'} ${scheme.freeItemName ?? 'item'} free` + case 'FreeBundle': { + const parts = scheme.bundleComponents.map((c) => `${c.qty} ${c.itemName}`).join(' + ') || 'items' + return `Buy ${parts}, get ${scheme.freeQty ?? '?'} ${scheme.freeItemName ?? 'item'} free` + } + case 'FlatPercent': + return `${parseRuleConfig(scheme).discountPercent}% off the order` + case 'SlabDiscount': { + const tiers = [...parseRuleConfig(scheme).tiers].sort((a, b) => a.minOrderAmount - b.minOrderAmount) + return tiers.length ? tiers.map((t) => `${t.discountPercent}% off from ${money(t.minOrderAmount)}`).join(', ') : 'No levels set' + } + } +} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-sheet.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-sheet.tsx deleted file mode 100644 index 3ad9406..0000000 --- a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-sheet.tsx +++ /dev/null @@ -1,265 +0,0 @@ -'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 { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '@/components/ui/dialog' -import { FieldGroup } from '@/components/ui/field' -import { toast } from '@/components/ui/toast' -import { schemes as schemesApi, regions as regionsApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { SchemeResponse, RegionResponse } from '@/types/api' - -const schema = z.object({ - name: z.string().min(1, 'Name is required'), - ruleType: z.enum(['BuyXGetY', 'SlabDiscount', 'FlatPercent', 'FreeBundle']), - regionId: z.string().optional(), - dateRangeFrom: z.string().min(1, 'Required'), - dateRangeTo: z.string().min(1, 'Required'), - isActive: z.boolean(), -}) - -type FormValues = z.infer - -interface SchemeSheetProps { - open: boolean - onClose: () => void - scheme: SchemeResponse | null - onSuccess: () => void -} - -export function SchemeSheet({ open, onClose, scheme, onSuccess }: SchemeSheetProps) { - const isEdit = !!scheme - const [regionsList, setRegionsList] = useState([]) - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: '', - ruleType: 'FlatPercent', - regionId: '', - dateRangeFrom: '', - dateRangeTo: '', - isActive: true, - }, - }) - - useEffect(() => { - if (open) { - form.reset( - scheme - ? { - name: scheme.name, - ruleType: scheme.ruleType, - regionId: scheme.regionId ?? '', - dateRangeFrom: scheme.dateRangeFrom.split('T')[0], - dateRangeTo: scheme.dateRangeTo.split('T')[0], - isActive: scheme.isActive, - } - : { - name: '', - ruleType: 'FlatPercent', - regionId: '', - dateRangeFrom: '', - dateRangeTo: '', - isActive: true, - } - ) - } - }, [open, scheme]) - - useEffect(() => { - if (open) { - regionsApi.list({ pageSize: 100 }) - .then((res) => setRegionsList(getPaginatedItems(res))) - .catch(() => { - toast.add({ title: 'Failed to load regions', type: 'error' }) - }) - } - }, [open]) - - async function onSubmit(values: FormValues) { - try { - const body = { - name: values.name, - ruleType: values.ruleType, - regionId: values.regionId || null, - dateRangeFrom: values.dateRangeFrom, - dateRangeTo: values.dateRangeTo, - isActive: values.isActive, - } - if (isEdit) { - await schemesApi.update(scheme!.id, body) - toast.add({ title: 'Scheme updated', type: 'success' }) - } else { - await schemesApi.create(body) - toast.add({ title: 'Scheme created', type: 'success' }) - } - onSuccess() - onClose() - } catch (err) { - toast.add({ title: 'Failed to save', description: getApiError(err), type: 'error' }) - } - } - - return ( - !v && onClose()}> - - - {isEdit ? 'Edit Scheme' : 'Add Scheme'} - - {isEdit ? 'Update the scheme details below.' : 'Create a new promotion or discount scheme.'} - - - -
- - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( - - )} - /> - - ( -
- - -
- )} - /> -
- - - - - -
-
-
- ) -} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-view-sheet.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-view-sheet.tsx deleted file mode 100644 index 3ad58fa..0000000 --- a/distribution_fe/app/(dashboard)/pricing/schemes/_components/scheme-view-sheet.tsx +++ /dev/null @@ -1,87 +0,0 @@ -'use client' - -import { Badge } from '@/components/ui/badge' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import type { SchemeResponse } from '@/types/api' - -const fmtDate = (v: string) => - new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) - -interface SchemeViewSheetProps { - open: boolean - onClose: () => void - scheme: SchemeResponse | null -} - -function Row({ label, value }: { label: string; value: React.ReactNode }) { - return ( -
- {label} - {value} -
- ) -} - -export function SchemeViewSheet({ open, onClose, scheme }: SchemeViewSheetProps) { - if (!scheme) return null - - return ( - !v && onClose()}> - - - Scheme Details - - -
- - {scheme.ruleType}} /> - - - {scheme.isActive ? 'Active' : 'Inactive'}} /> - - - {scheme.baselineExpectedSales > 0 && ( - - )} - {scheme.ruleConfig && {scheme.ruleConfig}} />} - {scheme.triggerItemName && ( - - )} - {scheme.freeItemName && ( - - )} - -
- - {scheme.bundleComponents.length > 0 && ( -
-

Bundle Components

-
- - - - - - - - - {scheme.bundleComponents.map((c) => ( - - - - - ))} - -
ItemQty
{c.itemName} ({c.sku}){c.qty}
-
-
- )} -
-
- ) -} diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/_components/schemes-client.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/_components/schemes-client.tsx index e8e2b2a..19e5372 100644 --- a/distribution_fe/app/(dashboard)/pricing/schemes/_components/schemes-client.tsx +++ b/distribution_fe/app/(dashboard)/pricing/schemes/_components/schemes-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' @@ -12,8 +13,7 @@ import { toast } from '@/components/ui/toast' import { schemes as schemesApi, getApiError } from '@/lib/api/index' import type { SchemeResponse, SchemeRuleType } from '@/types/api' -import { SchemeSheet } from './scheme-sheet' -import { SchemeViewSheet } from './scheme-view-sheet' +import { RULE_META, describeScheme } from './scheme-labels' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -26,14 +26,11 @@ const RULE_TYPE_VARIANT: Record = { } export function SchemesClient() { + const router = useRouter() const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [pageSize, setPageSize] = useState(20) - const [sheetOpen, setSheetOpen] = useState(false) - const [editing, setEditing] = useState(null) - const [viewing, setViewing] = useState(null) - const [viewOpen, setViewOpen] = useState(false) const fetchData = useCallback(async () => { setLoading(true) @@ -52,18 +49,15 @@ export function SchemesClient() { }, [fetchData]) function openCreate() { - setEditing(null) - setSheetOpen(true) + router.push('/pricing/schemes/new') } function openEdit(scheme: SchemeResponse) { - setEditing(scheme) - setSheetOpen(true) + router.push(`/pricing/schemes/${scheme.id}/edit`) } function openView(scheme: SchemeResponse) { - setViewing(scheme) - setViewOpen(true) + router.push(`/pricing/schemes/${scheme.id}`) } async function handleDelete(scheme: SchemeResponse) { @@ -91,11 +85,14 @@ export function SchemesClient() { }, { key: 'ruleType', - title: 'Rule Type', + title: 'Offer', fields: ['ruleType'], - width: 160, + minWidth: 280, cellRender: (row) => ( - {row.ruleType} +
+ {RULE_META[row.ruleType].label} + {describeScheme(row)} +
), }, { @@ -177,18 +174,6 @@ export function SchemesClient() { }} /> - setSheetOpen(false)} - scheme={editing} - onSuccess={fetchData} - /> - - setViewOpen(false)} - scheme={viewing} - /> ) } diff --git a/distribution_fe/app/(dashboard)/pricing/schemes/new/page.tsx b/distribution_fe/app/(dashboard)/pricing/schemes/new/page.tsx new file mode 100644 index 0000000..ea2f986 --- /dev/null +++ b/distribution_fe/app/(dashboard)/pricing/schemes/new/page.tsx @@ -0,0 +1,5 @@ +import { SchemeForm } from '../_components/scheme-form' + +export default function NewSchemePage() { + return +} -- 2.52.0 From b1bda1448e6dbcc7d5f53229a196603f3218bbce Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:37:03 +0530 Subject: [PATCH 06/11] feat: Implement Free Issue functionality in Shop PO and Invoice management --- .../app/(dashboard)/sales/[id]/edit/page.tsx | 39 ++- .../app/(dashboard)/sales/[id]/page.tsx | 23 +- .../sales/_components/free-issue-panel.tsx | 293 ++++++++++++++++++ .../sales/invoices/[id]/edit/page.tsx | 106 ++++++- .../(dashboard)/sales/invoices/[id]/page.tsx | 35 ++- .../app/(dashboard)/sales/new/page.tsx | 22 +- .../_components/invoice-action-dialog.tsx | 12 +- 7 files changed, 509 insertions(+), 21 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/sales/_components/free-issue-panel.tsx diff --git a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx index 31c5eda..c8773c6 100644 --- a/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/[id]/edit/page.tsx @@ -31,6 +31,7 @@ import { } from '@/lib/api/index' import type { InvoiceCreationMode } from '@/lib/api/sys-config' import type { ItemSummaryResponse, ShopPOResponse, UserResponse } from '@/types/api' +import { FreeIssuePanel, type FreeLine } from '../../_components/free-issue-panel' const lineItemSchema = z.object({ itemId: z.string().min(1, 'Item is required'), @@ -60,6 +61,8 @@ export default function EditShopPOPage() { const [confirmedByUserId, setConfirmedByUserId] = useState('') const [confirming, setConfirming] = useState(false) const [mode, setMode] = useState(DEFAULT_INVOICE_CREATION_MODE) + const [freeLines, setFreeLines] = useState([]) + const [discountIds, setDiscountIds] = useState([]) useEffect(() => { invoiceCreationModeApi.get().then(setMode).catch(() => {}) @@ -91,9 +94,21 @@ export default function EditShopPOPage() { setUsersList(users) form.reset({ items: poRes.items - .filter((i) => i.isActive) + .filter((i) => i.isActive && !i.isFree) .map((i) => ({ itemId: i.itemId, quantity: i.quantity, unitPrice: i.unitPrice })), }) + setDiscountIds(poRes.discounts.map((d) => d.schemeId)) + setFreeLines( + poRes.items + .filter((i) => i.isActive && i.isFree && i.schemeId) + .map((i) => ({ + itemId: i.itemId, + itemName: i.itemName, + quantity: i.quantity, + schemeId: i.schemeId!, + schemeName: i.schemeName ?? 'Scheme', + })) + ) }) .catch((err) => { toast.add({ title: 'Failed to load Shop PO', description: getApiError(err), type: 'error' }) @@ -138,7 +153,13 @@ export default function EditShopPOPage() { async function onSubmit(values: FormValues) { try { - await shopPosApi.updateItems(id, { items: values.items }) + await shopPosApi.updateItems(id, { + discountSchemeIds: discountIds, + items: [ + ...values.items, + ...freeLines.map((l) => ({ itemId: l.itemId, quantity: l.quantity, isFree: true, schemeId: l.schemeId })), + ], + }) toast.add({ title: 'Shop PO updated', type: 'success' }) router.push('/sales') } catch (err) { @@ -296,6 +317,20 @@ export default function EditShopPOPage() { + ({ + itemId: l.itemId, + quantity: Number(l.quantity) || 0, + unitPrice: Number(l.unitPrice) || 0, + }))} + freeLines={freeLines} + onChange={setFreeLines} + discountIds={discountIds} + onDiscountChange={setDiscountIds} + disabled={locked} + /> +

Order Summary

diff --git a/distribution_fe/app/(dashboard)/sales/[id]/page.tsx b/distribution_fe/app/(dashboard)/sales/[id]/page.tsx index 9c24006..853eac1 100644 --- a/distribution_fe/app/(dashboard)/sales/[id]/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/[id]/page.tsx @@ -83,7 +83,7 @@ export default function ShopPOViewPage() { if (!po) return null - const activeItems = po.items.filter((i) => i.isActive) + const activeItems = po.items.filter((i) => i.isActive).sort((a, b) => Number(a.isFree) - Number(b.isFree)) return (
@@ -155,6 +155,7 @@ export default function ShopPOViewPage() { Item Quantity + Free Issue Fulfilled Unit Price Line Total @@ -163,15 +164,21 @@ export default function ShopPOViewPage() { {activeItems.length === 0 ? ( - + No items on this PO. ) : ( activeItems.map((item) => ( - {item.itemName} - {item.quantity} + + {item.itemName} + {item.isFree && item.schemeName && ( + {item.schemeName} + )} + + {item.isFree ? '-' : item.quantity} + {item.isFree ? item.quantity : '-'} {item.quantityFulfilled} {fmtAmount(item.unitPrice)} {fmtAmount(item.quantity * item.unitPrice)} @@ -181,8 +188,14 @@ export default function ShopPOViewPage() { {activeItems.length > 0 && ( + {po.discounts.map((d) => ( + + Scheme discount ({d.schemeName}) + -{fmtAmount(d.amount)} + + ))} - + Total diff --git a/distribution_fe/app/(dashboard)/sales/_components/free-issue-panel.tsx b/distribution_fe/app/(dashboard)/sales/_components/free-issue-panel.tsx new file mode 100644 index 0000000..075b5f5 --- /dev/null +++ b/distribution_fe/app/(dashboard)/sales/_components/free-issue-panel.tsx @@ -0,0 +1,293 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { HugeiconsIcon } from '@hugeicons/react' +import { Add01Icon, Delete02Icon } from '@hugeicons/core-free-icons' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { toast } from '@/components/ui/toast' +import { schemes as schemesApi } from '@/lib/api/index' +import type { SchemeSuggestion } from '@/types/api' + +export interface FreeLine { + itemId: string + itemName: string + quantity: number + schemeId: string + schemeName: string +} + +interface PaidLine { + itemId: string + quantity: number + unitPrice: number +} + +interface Props { + storeId: string + paidLines: PaidLine[] + freeLines: FreeLine[] + onChange: (lines: FreeLine[]) => void + /** Accepted percentage-discount scheme ids (at most one). */ + discountIds: string[] + onDiscountChange: (ids: string[]) => void + disabled?: boolean +} + +const isFreeGoods = (s: SchemeSuggestion) => s.ruleType === 'BuyXGetY' || s.ruleType === 'FreeBundle' +const isDiscount = (s: SchemeSuggestion) => s.ruleType === 'FlatPercent' || s.ruleType === 'SlabDiscount' +const money = (n: number) => `Rs. ${n.toLocaleString()}` + +/** + * Suggests free-issue schemes for the paid lines being entered. Nothing is added automatically: the user + * accepts a suggestion, after which the free line stays editable. If a paid-quantity change shrinks the + * entitlement, accepted free quantities are trimmed to match (and the user is told). + */ +export function FreeIssuePanel({ storeId, paidLines, freeLines, onChange, discountIds, onDiscountChange, disabled }: Props) { + const [suggestions, setSuggestions] = useState([]) + const freeRef = useRef(freeLines) + freeRef.current = freeLines + const discountRef = useRef(discountIds) + discountRef.current = discountIds + + const paidKey = JSON.stringify( + paidLines.filter((l) => l.itemId && l.quantity > 0).map((l) => [l.itemId, l.quantity, l.unitPrice]) + ) + + useEffect(() => { + if (!storeId) { + setSuggestions([]) + return + } + const lines = paidLines.filter((l) => l.itemId && l.quantity > 0) + if (lines.length === 0) { + setSuggestions([]) + return + } + let cancelled = false + const timer = setTimeout(() => { + schemesApi + .suggest({ + storeId, + items: lines.map((l) => ({ + itemId: l.itemId, + quantity: l.quantity, + unitPrice: l.unitPrice > 0 ? l.unitPrice : null, + })), + }) + .then((result) => { + if (cancelled) return + setSuggestions(result) + reconcile(result) + }) + .catch(() => { + // Suggestions are advisory; the PO can still be saved without them. + if (!cancelled) setSuggestions([]) + }) + }, 400) + return () => { + cancelled = true + clearTimeout(timer) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [storeId, paidKey]) + + // Trim or drop accepted free lines that the current paid lines no longer entitle. + function reconcile(result: SchemeSuggestion[]) { + const current = freeRef.current + let changed = false + const next: FreeLine[] = [] + for (const line of current) { + const s = result.find((r) => r.schemeId === line.schemeId) + const entitled = s?.eligible ? Math.floor(s.freeQty ?? 0) : 0 + if (entitled <= 0) { + changed = true + continue + } + const used = next.filter((n) => n.schemeId === line.schemeId).reduce((sum, n) => sum + n.quantity, 0) + const allowed = Math.max(0, entitled - used) + if (allowed <= 0) { + changed = true + continue + } + if (line.quantity > allowed) { + changed = true + next.push({ ...line, quantity: allowed }) + } else { + next.push(line) + } + } + if (changed) { + onChange(next) + toast.add({ + title: 'Free issue adjusted', + description: 'Free quantities were reduced to match what the current order qualifies for.', + type: 'warning', + }) + } + + // An accepted discount the order no longer qualifies for is dropped. + const stillValid = discountRef.current.filter((id) => result.some((r) => r.schemeId === id && r.eligible)) + if (stillValid.length !== discountRef.current.length) { + onDiscountChange(stillValid) + toast.add({ title: 'Discount removed', description: 'The order no longer qualifies for the discount.', type: 'warning' }) + } + } + + function accept(s: SchemeSuggestion, remaining: number) { + if (!s.freeItemId) return + const existing = freeLines.find((l) => l.schemeId === s.schemeId) + if (existing) { + onChange(freeLines.map((l) => (l === existing ? { ...l, quantity: l.quantity + remaining } : l))) + return + } + onChange([ + ...freeLines, + { + itemId: s.freeItemId, + itemName: s.freeItemName ?? 'Free item', + quantity: remaining, + schemeId: s.schemeId, + schemeName: s.schemeName, + }, + ]) + } + + function setQuantity(index: number, quantity: number) { + onChange(freeLines.map((l, i) => (i === index ? { ...l, quantity } : l))) + } + + const goods = suggestions.filter(isFreeGoods) + const offers = goods + .filter((s) => s.eligible && s.freeItemId) + .map((s) => { + const accepted = freeLines.filter((l) => l.schemeId === s.schemeId).reduce((sum, l) => sum + l.quantity, 0) + return { s, remaining: Math.floor(s.freeQty ?? 0) - accepted } + }) + .filter((o) => o.remaining > 0) + const nudges = goods.filter((s) => !s.eligible && s.reason) + const discountOffers = suggestions.filter((s) => isDiscount(s) && s.eligible && s.discountAmount != null) + const applied = discountOffers.filter((s) => discountIds.includes(s.schemeId)) + const available = discountOffers.filter((s) => !discountIds.includes(s.schemeId)) + const orderTotal = paidLines.reduce((sum, l) => sum + (l.quantity || 0) * (l.unitPrice || 0), 0) + + if (offers.length === 0 && nudges.length === 0 && freeLines.length === 0 && discountOffers.length === 0) return null + + return ( +
+

Offers & Free Issues

+

+ Suggested from active schemes. Add them to the order, then adjust the quantity if needed. +

+ + {(applied.length > 0 || available.length > 0) && ( +
+ {applied.map((s) => ( +
+
+

{s.schemeName}

+

+ Discount applied: -{money(s.discountAmount!)} + {' '}({money(orderTotal)} → {money(Math.max(0, orderTotal - s.discountAmount!))} on the invoice) +

+
+ +
+ ))} + {available.map((s) => ( +
+
+

{s.schemeName}

+

+ Saves {money(s.discountAmount!)} on this order +

+
+ +
+ ))} +
+ )} + + {offers.length > 0 && ( +
+ {offers.map(({ s, remaining }) => ( +
+
+

{s.schemeName}

+

+ Qualifies for {remaining} free {s.freeItemName ?? 'item'} +

+
+ +
+ ))} +
+ )} + + {nudges.length > 0 && ( +
    + {nudges.map((s) => ( +
  • + {s.schemeName}: {s.reason} +
  • + ))} +
+ )} + + {freeLines.length > 0 && ( +
+ + + + + + + + + + + {freeLines.map((line, index) => ( + + + + + + + ))} + +
Free itemSchemeFree qty
{line.itemName} + {line.schemeName} + + setQuantity(index, Math.max(1, Math.floor(Number(e.target.value) || 1)))} + /> + + +
+
+ )} +
+ ) +} diff --git a/distribution_fe/app/(dashboard)/sales/invoices/[id]/edit/page.tsx b/distribution_fe/app/(dashboard)/sales/invoices/[id]/edit/page.tsx index 05ea17b..54ac70a 100644 --- a/distribution_fe/app/(dashboard)/sales/invoices/[id]/edit/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/invoices/[id]/edit/page.tsx @@ -20,11 +20,12 @@ import { invoices as invoicesApi, users as usersApi, items as itemsApi, + schemes as schemesApi, priceLists as priceListsApi, getApiError, getPaginatedItems, } from '@/lib/api/index' -import type { InvoiceItemResponse, InvoiceResponse, ItemSummaryResponse, UserResponse } from '@/types/api' +import type { InvoiceItemResponse, InvoiceResponse, ItemSummaryResponse, SchemeSuggestion, UserResponse } from '@/types/api' interface AddedLine { key: string @@ -59,6 +60,11 @@ export default function InvoiceEditPage() { const [userId, setUserId] = useState('') const [usersList, setUsersList] = useState([]) const [saving, setSaving] = useState(false) + // Scheme discount for the lines as currently typed, recalculated by the server (null until the first answer). + const [previewDiscount, setPreviewDiscount] = useState(null) + // The percentage-discount scheme applied to this invoice's order (at most one), and the ones it currently qualifies for. + const [discountIds, setDiscountIds] = useState([]) + const [discountOffers, setDiscountOffers] = useState([]) useEffect(() => { invoicesApi.get(invoiceId) @@ -69,6 +75,7 @@ export default function InvoiceEditPage() { return } setFull(inv) + setDiscountIds(inv.discounts.map((d) => d.schemeId)) setQty(Object.fromEntries(inv.items.map((i) => [i.id, String(i.quantity)]))) setPrice(Object.fromEntries(inv.items.map((i) => [i.id, String(i.unitPrice)]))) setRemoved(Object.fromEntries(inv.items.map((i) => [i.id, i.isRemoved]))) @@ -104,11 +111,65 @@ export default function InvoiceEditPage() { quantity: Number(a.qty) || 0, unitPrice: Number(a.price) || 0, isRemoved: false, + isFree: false, + schemeId: null, + schemeName: null, })) const items = [...existing, ...extra] - const totalAmount = items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0) - return { ...full, items, totalAmount } - }, [full, qty, price, removed, added]) + const subtotal = items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0) + const discountAmount = Math.min(previewDiscount ?? full.discountAmount, subtotal) + return { ...full, items, discountAmount, totalAmount: subtotal - discountAmount } + }, [full, qty, price, removed, added, previewDiscount]) + + // Adding, removing or re-pricing lines can change a percentage/slab discount, so ask the server to recalculate it. + const previewKey = JSON.stringify( + live?.items.map((i) => [i.itemId, i.quantity, i.unitPrice, i.isFree, i.isRemoved]) ?? [], + ) + useEffect(() => { + if (!full || !live) return + let cancelled = false + const timer = setTimeout(() => { + invoicesApi + .previewTotals(full.id, live.items.map((i) => ({ + itemId: i.itemId, quantity: i.quantity, unitPrice: i.unitPrice, isFree: i.isFree, isRemoved: i.isRemoved, schemeId: i.schemeId, + })), discountIds) + .then((r) => { + if (cancelled) return + setPreviewDiscount(r.discountAmount) + // Lowering the paid item that earned a free issue lowers the free quantity with it. + const left = new Map(r.freeEntitlements.map((e) => [e.schemeId, e.quantity])) + let reduced = false + for (const it of live.items) { + if (!it.isFree || !it.schemeId || it.isRemoved) continue + const allowed = left.get(it.schemeId) ?? 0 + if (it.quantity <= allowed) { left.set(it.schemeId, allowed - it.quantity); continue } + reduced = true + left.set(it.schemeId, 0) + if (allowed === 0) setRemoved((x) => ({ ...x, [it.id]: true })) + else setQty((x) => ({ ...x, [it.id]: String(allowed) })) + } + if (reduced) toast.add({ title: 'Free issue reduced', description: 'The free quantity was lowered to match the paid quantity.', type: 'warning' }) + }) + .catch(() => { /* keep the last known discount; the save recalculates it anyway */ }) + }, 400) + return () => { cancelled = true; clearTimeout(timer) } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [full?.id, previewKey, discountIds]) + + // Which percentage discounts the lines as typed qualify for (so one can be applied or removed here). + useEffect(() => { + if (!full || !live) return + const paid = live.items.filter((i) => !i.isFree && !i.isRemoved && i.quantity > 0) + let cancelled = false + const timer = setTimeout(() => { + schemesApi + .suggest({ storeId: full.storeId, items: paid.map((i) => ({ itemId: i.itemId, quantity: i.quantity, unitPrice: i.unitPrice > 0 ? i.unitPrice : null })) }) + .then((r) => { if (!cancelled) setDiscountOffers(r.filter((x) => (x.ruleType === 'FlatPercent' || x.ruleType === 'SlabDiscount') && x.eligible)) }) + .catch(() => { if (!cancelled) setDiscountOffers([]) }) + }, 400) + return () => { cancelled = true; clearTimeout(timer) } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [full?.id, previewKey]) // Items not already on the invoice (a deleted line can be restored with Undo instead). const availableItems = catalogue.filter((c) => !live?.items.some((i) => i.itemId === c.id)) @@ -196,6 +257,7 @@ export default function InvoiceEditPage() { ...newLines.map((a) => ({ itemId: a.itemId, quantity: Number(a.qty), unitPrice: Number(a.price) })), ], userId, + discountIds, ) toast.add({ title: 'Invoice updated', type: 'success' }) // Navigating away right after a successful save; clear the unsaved-changes guard first. @@ -236,6 +298,36 @@ export default function InvoiceEditPage() {

Loading invoice…

) : ( <> + {(discountOffers.length > 0 || discountIds.length > 0) && ( +
+

Scheme discount

+ {discountOffers.map((o) => { + const applied = discountIds.includes(o.schemeId) + return ( +
+ + {o.schemeName}{' '} + saves Rs. {(o.discountAmount ?? 0).toLocaleString()} on these lines + + {applied ? ( + + ) : ( + + )} +
+ ) + })} + {discountIds.length > 0 && !discountOffers.some((o) => discountIds.includes(o.schemeId)) && ( +
+ An applied discount no longer qualifies for these lines (it counts as Rs. 0). + +
+ )} +
+ )} + ( @@ -248,7 +340,9 @@ export default function InvoiceEditPage() { : setQty((q) => ({ ...q, [item.id]: e.target.value }))} /> )} - renderUnitPrice={(item) => ( + renderUnitPrice={(item) => item.isFree ? ( + Free + ) : (
-
diff --git a/distribution_fe/app/(dashboard)/sales/invoices/[id]/page.tsx b/distribution_fe/app/(dashboard)/sales/invoices/[id]/page.tsx index 670245a..07a05a4 100644 --- a/distribution_fe/app/(dashboard)/sales/invoices/[id]/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/invoices/[id]/page.tsx @@ -199,6 +199,7 @@ export default function InvoiceViewPage() { Item Ordered Billed + Free Issue Unit price Line total @@ -206,19 +207,23 @@ export default function InvoiceViewPage() { {invoice.items.length === 0 ? ( - No items. + No items. ) : ( - invoice.items.map((i) => ( + [...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)).map((i) => ( {i.itemName} + {i.isFree && {i.schemeName ?? "Free issue"}} {i.orderedQuantity === null && Added} {i.isRemoved && Removed} - {i.orderedQuantity ?? '—'} - - {i.quantity} + {i.isFree ? '-' : (i.orderedQuantity ?? '-')} + + {i.isFree ? '-' : i.quantity} + + + {i.isFree ? i.quantity : '-'} {fmtAmount(i.unitPrice)} {i.isRemoved ? '—' : fmtAmount(i.quantity * i.unitPrice)} @@ -226,6 +231,26 @@ export default function InvoiceViewPage() { )) )} + {invoice.items.length > 0 && ( + + + Subtotal + + {fmtAmount(invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0))} + + + {invoice.discountAmount > 0 && ( + + Scheme discount + -{fmtAmount(invoice.discountAmount)} + + )} + + Total + {fmtAmount(invoice.totalAmount)} + + + )}
diff --git a/distribution_fe/app/(dashboard)/sales/new/page.tsx b/distribution_fe/app/(dashboard)/sales/new/page.tsx index 2ebc9e1..e11141c 100644 --- a/distribution_fe/app/(dashboard)/sales/new/page.tsx +++ b/distribution_fe/app/(dashboard)/sales/new/page.tsx @@ -29,6 +29,7 @@ import { getPaginatedItems, } from '@/lib/api/index' import type { ItemSummaryResponse, StoreResponse, UserResponse } from '@/types/api' +import { FreeIssuePanel, type FreeLine } from '../_components/free-issue-panel' const lineItemSchema = z.object({ itemId: z.string().min(1, 'Item is required'), @@ -57,6 +58,8 @@ export default function NewShopPOPage() { const [usersList, setUsersList] = useState([]) const [priceMinByIndex, setPriceMinByIndex] = useState>({}) const [priceRetailByIndex, setPriceRetailByIndex] = useState>({}) + const [freeLines, setFreeLines] = useState([]) + const [discountIds, setDiscountIds] = useState([]) const form = useForm({ resolver: zodResolver(schema), @@ -125,9 +128,13 @@ export default function NewShopPOPage() { async function onSubmit(values: FormValues) { try { await shopPosApi.create({ + discountSchemeIds: discountIds, storeId: values.storeId, raisedByUserId: values.raisedByUserId, - items: values.items, + items: [ + ...values.items, + ...freeLines.map((l) => ({ itemId: l.itemId, quantity: l.quantity, isFree: true, schemeId: l.schemeId })), + ], }) toast.add({ title: 'Shop PO created', type: 'success' }) router.push('/sales') @@ -343,6 +350,19 @@ export default function NewShopPOPage() {
+ ({ + itemId: l.itemId, + quantity: Number(l.quantity) || 0, + unitPrice: Number(l.unitPrice) || 0, + }))} + freeLines={freeLines} + onChange={setFreeLines} + discountIds={discountIds} + onDiscountChange={setDiscountIds} + /> +

Order Summary

diff --git a/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx b/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx index 8859339..a924bd7 100644 --- a/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx +++ b/distribution_fe/app/(dashboard)/sales/orders/_components/invoice-action-dialog.tsx @@ -1,6 +1,8 @@ 'use client' import { useEffect, useState } from 'react' +import { HugeiconsIcon } from '@hugeicons/react' +import { Delete02Icon, Undo02Icon } from '@hugeicons/core-free-icons' import { useForm, zodResolver, z } from '@/lib/form' import { FormField } from '@/components/shared/form-field' import { @@ -164,8 +166,14 @@ export function InvoiceActionDialog({ open, onClose, onSuccess, invoice, mode }: /> - -- 2.52.0 From 801276559d324dd39478a3b26cb5e4742a9efe35 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:38:09 +0530 Subject: [PATCH 07/11] feat: Enhance pricing and sales API with free goods and discount scheme support --- distribution_fe/lib/api/pricing.ts | 14 ++++++++++++++ distribution_fe/lib/api/sales.ts | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/distribution_fe/lib/api/pricing.ts b/distribution_fe/lib/api/pricing.ts index ea75cb1..3be1a32 100644 --- a/distribution_fe/lib/api/pricing.ts +++ b/distribution_fe/lib/api/pricing.ts @@ -4,6 +4,7 @@ import type { PriceResolution, SchemeResponse, SchemeRuleType, + SchemeSuggestion, } from '@/types/api' // ─── Price Lists ────────────────────────────────────────────────────────────── @@ -89,4 +90,17 @@ export const schemes = { remove: (id: string) => api.delete(`/schemes/${id}`), + + setFreeGoodsRule: (id: string, body: { triggerItemId?: string | null; triggerQty?: number | null; freeItemId: string; freeQty: number }) => + api.put(`/schemes/${id}/free-goods-rule`, body).then(r => r.data), + + upsertBundleComponent: (id: string, itemId: string, qty: number) => + api.put(`/schemes/${id}/bundle-components/${itemId}`, { qty }).then(r => r.data), + + removeBundleComponent: (id: string, itemId: string) => + api.delete(`/schemes/${id}/bundle-components/${itemId}`).then(r => r.data), + + /** Schemes the lines of a not-yet-saved Shop PO qualify for (or are close to). */ + suggest: (body: { storeId: string; items: { itemId: string; quantity: number; unitPrice?: number | null }[] }) => + api.post('/schemes/suggest', body).then(r => r.data), } diff --git a/distribution_fe/lib/api/sales.ts b/distribution_fe/lib/api/sales.ts index cb07213..4ed8840 100644 --- a/distribution_fe/lib/api/sales.ts +++ b/distribution_fe/lib/api/sales.ts @@ -21,16 +21,22 @@ export interface ShopPOItemBody { itemId: string quantity: number unitPrice?: number | null + /** Free issue accepted from a scheme suggestion; priced 0 by the server. */ + isFree?: boolean + schemeId?: string | null } export interface CreateShopPOBody { storeId: string items: ShopPOItemBody[] raisedByUserId?: string | null + /** The one flat/slab discount scheme the user accepted. */ + discountSchemeIds?: string[] } export interface UpdateShopPOItemsBody { items: ShopPOItemBody[] + discountSchemeIds?: string[] } export interface ShopPOListParams extends PageParams { @@ -97,8 +103,12 @@ export const invoices = { finalize: (id: string, finalizedByUserId?: string | null, adjustments?: { invoiceItemId: string; adjustedQuantity: number; isRemoved?: boolean }[]) => api.post(`/invoices/${id}/finalize`, { finalizedByUserId, adjustments }).then(r => r.data), - updateItems: (id: string, items: { invoiceItemId?: string; itemId?: string; quantity: number; unitPrice?: number | null; isRemoved?: boolean }[], editedByUserId?: string | null) => - api.put(`/invoices/${id}/items`, { items, editedByUserId }).then(r => r.data), + updateItems: (id: string, items: { invoiceItemId?: string; itemId?: string; quantity: number; unitPrice?: number | null; isRemoved?: boolean }[], editedByUserId?: string | null, discountSchemeIds?: string[]) => + api.put(`/invoices/${id}/items`, { items, editedByUserId, discountSchemeIds }).then(r => r.data), + + /** What the discount and total would be for these unsaved lines (draft editor preview). */ + previewTotals: (id: string, items: { itemId: string; quantity: number; unitPrice: number; isFree?: boolean; isRemoved?: boolean; schemeId?: string | null }[], discountSchemeIds?: string[]) => + api.post<{ discountAmount: number; totalAmount: number; freeEntitlements: { schemeId: string; quantity: number }[] }>(`/invoices/${id}/preview-totals`, { items, discountSchemeIds }).then(r => r.data), versions: (id: string) => api.get(`/invoices/${id}/versions`).then(r => r.data), -- 2.52.0 From 87e3112a9a0951d4fb2eb4b217b3b6feac9aa86d Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:39:00 +0530 Subject: [PATCH 08/11] feat: Implement free issue tracking and scheme discount handling in invoices and Shop POs --- Distribution/docs/17_promotions_schemes.md | 99 +++++++++++++++++-- .../refactor_docs/order_invoice_lifecycle.md | 9 +- .../components/invoice/invoice-template.tsx | 63 +++++++++--- distribution_fe/types/api.ts | 26 +++++ 4 files changed, 172 insertions(+), 25 deletions(-) diff --git a/Distribution/docs/17_promotions_schemes.md b/Distribution/docs/17_promotions_schemes.md index f3ef70b..6636f36 100644 --- a/Distribution/docs/17_promotions_schemes.md +++ b/Distribution/docs/17_promotions_schemes.md @@ -76,7 +76,7 @@ order must contain every component in at least its listed `Qty` before the schem SchemeRedemption ├── Id (Guid) — PK ├── FK_SchemeId (Guid) — required -├── FK_OrderId (Guid?) — nullable (see FieldInvoiceId below) +├── FK_ShopPOId (Guid?) — nullable (see FieldInvoiceId below); the Shop PO the scheme was applied to ├── FieldInvoiceId (Guid?) — placeholder for the future field-invoice module; no FieldInvoice │ entity exists yet, so this is an unconstrained Guid for now ├── DiscountAmount (decimal?) — set for SlabDiscount/FlatPercent redemptions @@ -87,8 +87,8 @@ SchemeRedemption │ later changes ├── RedeemedAt (DateTime) ``` -At least one of `DiscountAmount` or `FreeQty` is set per redemption; `Scheme`/`Order` restrict-delete -(a scheme or order with redemptions can't be hard-deleted, and soft-delete leaves history intact). +At least one of `DiscountAmount` or `FreeQty` is set per redemption; `Scheme`/`ShopPO` restrict-delete +(a scheme or Shop PO with redemptions can't be hard-deleted, and soft-delete leaves history intact). ### Item (extended) ``` @@ -124,8 +124,10 @@ columns/child table after the first migration had already been applied to the de | — | `DELETE /api/schemes/{id}/free-goods-rule` | Clears it | | — | `PUT /api/schemes/{id}/bundle-components/{itemId}` | Upserts one FreeBundle required-item row | | — | `DELETE /api/schemes/{id}/bundle-components/{itemId}` | Removes one FreeBundle required-item row | -| `evaluateSchemeEligibility(storeId, orderId)` | `GET /api/schemes/eligible?storeId=...&orderId=...` | Returns eligibility for every in-scope scheme, not just one | +| — | `POST /api/schemes/suggest` | Body `{ storeId, items:[{itemId, quantity, unitPrice?}] }`; scheme suggestions for a Shop PO that is not saved yet | +| `evaluateSchemeEligibility(storeId, shopPOId)` | `GET /api/schemes/eligible?storeId=...&orderId=...` (`orderId` is the Shop PO id) | Returns eligibility for every in-scope scheme, not just one | | `applySchemeDiscount(orderId, schemeId)` | `POST /api/orders/{id}/apply-scheme` | Adds a zero-price order line for free-goods payouts | +| — | `POST /api/invoices/{id}/preview-totals` | Draft-editor preview of discount, total and free entitlement for unsaved lines | | `trackSchemeSpend(schemeId)` | `GET /api/schemes/{id}/spend` | Discount spend + free-goods cost spend | | `getSchemeROI(schemeId)` | `GET /api/schemes/{id}/roi` | Uses `BaselineExpectedSales` | @@ -216,13 +218,18 @@ Use this to add/edit one bundle component after creation. [08_sales_agent_controls.md](./08_sales_agent_controls.md) unless `AgentLimitExempt = true`. (Enforcement against the actual ceiling is deferred — 08_sales_agent_controls.md's discount ceiling isn't implemented in code yet; this is recorded as the target behavior.) -2. `evaluateSchemeEligibility` matches a scheme to a store/order only if: the scheme's `RegionId` is +2. `evaluateSchemeEligibility` matches a scheme to a store/Shop PO only if: the scheme's `RegionId` is null or matches the store's region; `EligibleSegment` is null or matches `Store.Segment`; and the - order's date falls within `[DateRangeFrom, DateRangeTo]`. -3. `BuyXGetY` eligibility: the order must contain at least `TriggerQty` of `TriggerItem`; the payout + Shop PO's date falls within `[DateRangeFrom, DateRangeTo]`. `DateRangeTo` is inclusive of the + whole calendar day (compared by date, so a scheme ending `2026-08-31` still applies on the 31st). +3. `BuyXGetY` eligibility: the Shop PO must contain at least `TriggerQty` of `TriggerItem`; the payout scales with how many multiples of `TriggerQty` were bought (`FreeQty × floor(boughtQty / TriggerQty)`). -4. `FreeBundle` eligibility: the order must contain every `SchemeBundleComponent` in at least its - listed `Qty`; payout is the flat `FreeQty` of `FreeItem` (no multiplier). + Only **paid** lines (`UnitPrice > 0`) count toward `boughtQty` — free lines added by an earlier + redemption never count toward a trigger, so "buy 10 get 1 of the same item" can't self-inflate. +4. `FreeBundle` eligibility: the Shop PO must contain every `SchemeBundleComponent` in at least its + listed `Qty` (paid lines only, same as rule 3); payout is the flat `FreeQty` of `FreeItem` (no multiplier). + `FreeQty` must be a whole number (validated on create / `PUT .../free-goods-rule`), because + Shop PO lines hold whole units. 5. `applySchemeDiscount` adds a zero-price `OrderItem` line for the free-goods payout (`UnitPrice = 0`, `Quantity = FreeQty`), rather than only recording a discount amount — the free line still needs to be picked, delivered, and stock-tracked like any other sold line. @@ -240,6 +247,80 @@ Use this to add/edit one bundle component after creation. --- +## Offers on the PO and Invoice (implemented) + +Nothing is added silently: the system **suggests**, the user **accepts**, and what was accepted stays **editable**. + +### 1. On the Shop PO (new and edit screens) +1. While paid lines are entered, the screen calls `POST /api/schemes/suggest` (`{ storeId, items:[{itemId, quantity, unitPrice?}] }`). + The server returns the in-scope schemes (region, segment, date) the lines qualify for, plus Buy & get free / + Combo gift schemes the cart is part-way to (`reason` doubles as a nudge, e.g. "Requires 10 of the trigger item; + ShopPO has 6"). +2. The **Offers & Free Issues** panel shows: + - **Free goods** (BuyXGetY / FreeBundle): "Qualifies for N free X" with **Add free**. The accepted free line is + editable (qty, remove) and is saved as a line with `isFree = true` and the `schemeId`. + - **Percentage discount** (FlatPercent / SlabDiscount): "Saves Rs. X" with **Apply discount**. Only one discount + per order. The accepted scheme ids are sent as `discountSchemeIds` on create / update-items. +3. If a later paid-qty change shrinks what the order earns, the screen trims the free qty / drops the discount and + says so. The server enforces the same rules on save: free qty within the entitlement, free item in stock, scheme + active and in scope, at most one discount, order still qualifies. +4. Saving keeps `SchemeRedemption` in step with the lines: created or resized per free line, removed with its line, + and discount redemptions created, recomputed, or dropped if the order stops qualifying. +5. `GET /api/shop-pos/{id}` returns `discounts[]` (scheme, amount) so the edit screen starts with the right state and + the PO view shows a "Scheme discount" row above the total. `PO.TotalAmount` stays the full lines total. + +### 2. On the invoice +- The invoice copies the PO lines, including free lines (`IsFree`, `FK_SchemeId`, `SchemeName`). Free lines show in a + **Free Issue** column (a "-" in the cell that does not apply), are listed **after all paid lines**, are zero-priced + (price cannot be edited) and can be lowered like any line while the invoice is Draft. +- `Invoice.TotalAmount = billable lines - DiscountAmount`. When the invoice is created from a PO it starts at the + discounted total. +- **Draft invoice editor** (`/sales/invoices/{id}/edit`): + - A **Scheme discount** box lists the flat / slab discounts the current lines qualify for with **Apply discount** / + **Remove**. This also covers a PO that was confirmed before a discount was applied. `PUT /api/invoices/{id}/items` + accepts `discountSchemeIds` (null = leave as is). + - `POST /api/invoices/{id}/preview-totals` (`{ items:[{itemId, quantity, unitPrice, isFree, isRemoved, schemeId}], discountSchemeIds? }`) + returns the discount, total and per-scheme free entitlement for the unsaved lines; the editor calls it (debounced) + so the Scheme discount row and Total update while typing. It changes nothing. +- **Recalculation.** On every line change (add, remove, restore, re-price, quantity) and again at print and finalize, + `InvoiceService.ApplyTotalsAsync` first reconciles free lines, then re-evaluates the discount against the current lines: + - FlatPercent stays the set % of the current lines total; SlabDiscount can move up or down a level, or drop to 0 and + return if the lines requalify. Free lines and struck-out lines are ignored. The redemption's `DiscountAmount` is + updated with it, so scheme spend / ROI stay correct. + - **Free lines only shrink automatically.** If the paid quantity that earned a free issue is lowered or removed, the free + quantity is reduced to what is still earned (a line that earns nothing is struck out, keeping its quantity so it can be + restored) and the PO's free-goods redemption is resized. Free quantity is never raised automatically; a person raises it, + up to what the scheme earns. +- The invoice page's Items table ends with **Subtotal / Scheme discount / Total**, and the printed A4 and receipt copies show + Subtotal, Scheme discount, Total (Paid / Balance Due use the discounted total). +- The older `POST /api/orders/{id}/apply-scheme` endpoint still works for an already-saved PO and flags the line the same way + (and mirrors a free line onto a still-Draft invoice). + +### 3. Scheme setup screens +`/pricing/schemes/new`, `/pricing/schemes/{id}` (read-only) and `/pricing/schemes/{id}/edit` are full pages (no modal) built on one +form. Rule types are shown in plain words: **Buy & get free** (BuyXGetY), **Combo gift** (FreeBundle), **Flat discount** +(FlatPercent), **Spend more, save more** (SlabDiscount); the fields adapt to the type (product dropdowns, quantities, % and +levels) with a live summary sentence. Required fields come first; Active, then the optional Region / Store segment last. The scheme +name is optional (generated from the offer when blank). The rule type cannot change after creation. + +### Data +`ShopPOItem` and `InvoiceItem` gained `IsFree`, `FK_SchemeId` (plain Guid, no FK constraint) and a `SchemeName` snapshot. Migration +`AddFreeIssueLines` also back-fills existing zero-priced redemption lines. + +### Not done yet +`SchemeRedemption.FK_InvoiceId` (`FieldInvoiceId` is still an unused placeholder); invoice-version snapshots do not record `IsFree`; +no hint or one-click "add more free units" when the paid quantity rises; scope (date / region) is checked when a discount is first +applied, not re-checked on later invoice edits; Slab "close to the next level" hints are not shown. + +## Known Gaps vs. Enterprise Practice + +Scheme code, lifecycle (Draft/Approved/Active/Expired), priority and stackable/exclusive flags; +budget and per-store caps; triggers on product groups/brands/categories or order value; tiered +free-goods slabs; funding source (company vs supplier — ties into SupplierClaims and GRN +`FreeQuantity`); stock reservation for the free item (the current check only sums balances). + +--- + ## Open Questions - What counts as "incremental sales" for `getSchemeROI` — **resolved for v1**: a manual diff --git a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md index 1ebcc60..870963d 100644 --- a/Distribution/docs/refactor_docs/order_invoice_lifecycle.md +++ b/Distribution/docs/refactor_docs/order_invoice_lifecycle.md @@ -95,6 +95,7 @@ InvoiceItem — the live billing lines ├── FK_InvoiceId, FK_ShopPOItemId, FK_ItemId ├── Quantity (int) — what is billed now (0..ShopPOItem.Quantity) ├── UnitPrice (decimal) +├── IsFree, FK_SchemeId, SchemeName — free issue copied from the PO line (UnitPrice 0); see 17_promotions_schemes.md └── IsActive, CreatedAt — from Template InvoiceVersion — immutable snapshot of the invoice @@ -145,7 +146,8 @@ invoice for the preview, the draft editor and the print tab: `*** PRINTED ***`, `qty x price` lines, heavy-ruled TOTAL, Paid/Balance, payments, "Thank you!"). - Money `Rs. 12,450.00`, quantities up to 3 decimals, dates `dd/MM/yyyy`. - Deleted lines are shown **struck through in every status** and excluded from all totals. -- Not included (no data in the app): company/"From" block, logo, bank details, discount, tax, due date. +- Scheme discount: shown as Subtotal / Scheme discount / Total (`Invoice.DiscountAmount`), re-derived from the current lines on every edit, print and finalize (see 17_promotions_schemes.md). Free-issue lines get a **Free Issue** column and come after the paid lines. +- Not included (no data in the app): company/"From" block, logo, bank details, tax, due date. **Print tab** — `/invoices/{id}/print` (own route group `(print)`, no dashboard chrome). Has a paper-size selector (A4 / 80mm / 58mm), sets `@page` with `margin: 0` (so the browser's own @@ -290,8 +292,9 @@ Draft → Printed → Finalized also unresolved. Both cases need explicit cascade rules before implementation. 6. **Exact adjustment payload for `POST /finalize`** — *partly resolved:* quantity adjustments (`{ shopPOItemId, adjustedQuantity }`) are implemented and persisted on the invoice lines. - Returns, damage and discounts are still open, as is the payload shape for them. "Free issues" handling (items - given to the shop at no charge) is a known case that needs its own field(s) in the payload. + Returns and damage are still open, as is the payload shape for them. Scheme discounts and free issues are now handled + by the invoice itself (re-evaluated on every edit, print and finalize; free lines only ever shrink automatically) — see + [17_promotions_schemes.md](../17_promotions_schemes.md). --- diff --git a/distribution_fe/components/invoice/invoice-template.tsx b/distribution_fe/components/invoice/invoice-template.tsx index ad7e9a5..261ece6 100644 --- a/distribution_fe/components/invoice/invoice-template.tsx +++ b/distribution_fe/components/invoice/invoice-template.tsx @@ -34,9 +34,11 @@ const statusLabel = (invoice: InvoiceResponse) => // Removed lines stay on the document (struck through) but are excluded from every total. function totals(invoice: InvoiceResponse, payments: PaymentResponse[]) { - const total = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0) + const subtotal = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0) + const discount = Math.min(invoice.discountAmount ?? 0, subtotal) + const total = subtotal - discount const paid = payments.reduce((s, p) => s + p.amount, 0) - return { total, paid, balance: total - paid } + return { subtotal, discount, total, paid, balance: total - paid } } export function InvoiceTemplate({ paper = 'A4', ...props }: InvoiceTemplateProps) { @@ -56,9 +58,12 @@ function A4Sheet({ showAgentEdits, className, }: Omit) { - const { total, paid, balance } = totals(invoice, payments) + const { subtotal, discount, total, paid, balance } = totals(invoice, payments) const status = statusLabel(invoice) const hasRemoved = invoice.items.some((i) => i.isRemoved) + const hasFree = invoice.items.some((i) => i.isFree) + // Free issues are listed after every paid line. + const rows = [...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)) return (
Item Qty + {hasFree && Free Issue} Unit Price Amount {renderActions && } @@ -118,15 +124,25 @@ function A4Sheet({ No items. ) : ( - invoice.items.map((item) => ( + rows.map((item) => ( - {item.itemName} - - {renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)} + + {item.itemName} + {item.isFree && item.schemeName && ( + Free issue · {item.schemeName} + )} + + {item.isFree ? '-' : renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)} + + {hasFree && ( + + {item.isFree ? (renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)) : '-'} + + )} {renderUnitPrice && !item.isRemoved ? renderUnitPrice(item) : fmtMoney(item.unitPrice)} @@ -160,8 +176,14 @@ function A4Sheet({
Subtotal
-
{fmtMoney(total)}
+
{fmtMoney(subtotal)}
+ {discount > 0 && ( +
+
Scheme discount
+
-{fmtMoney(discount)}
+
+ )}
Total
{fmtMoney(total)}
@@ -225,7 +247,7 @@ function ReceiptSheet({ showAgentEdits, className, }: Omit & { paper: Exclude }) { - const { total, paid, balance } = totals(invoice, payments) + const { subtotal, discount, total, paid, balance } = totals(invoice, payments) const status = statusLabel(invoice) const narrow = paper === '58mm' const rule =
@@ -252,12 +274,21 @@ function ReceiptSheet({
{rule}
- {invoice.items.map((i) => ( + {[...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)).map((i) => (

{i.itemName}

- {fmtQty(i.quantity)} x {num(i.unitPrice)} - {num(i.quantity * i.unitPrice)} + {i.isFree ? ( + <> + {fmtQty(i.quantity)} FREE{i.schemeName ? ` (${i.schemeName})` : ''} + {num(0)} + + ) : ( + <> + {fmtQty(i.quantity)} x {num(i.unitPrice)} + {num(i.quantity * i.unitPrice)} + + )}

))} @@ -265,8 +296,14 @@ function ReceiptSheet({ {rule}

Subtotal - {fmtMoney(total)} + {fmtMoney(subtotal)}

+ {discount > 0 && ( +

+ Scheme discount + -{fmtMoney(discount)} +

+ )}

TOTAL diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index 5ec5790..5a8289e 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -661,6 +661,18 @@ export interface SchemeResponse { createdAt: string } +export interface SchemeSuggestion { + schemeId: string + schemeName: string + ruleType: SchemeRuleType + eligible: boolean + reason: string | null + discountAmount: number | null + freeItemId: string | null + freeQty: number | null + freeItemName: string | null +} + // ─── Suppliers ──────────────────────────────────────────────────────────────── export interface SupplierResponse { @@ -973,6 +985,15 @@ export interface ShopPOItemResponse { quantityFulfilled: number unitPrice: number isActive: boolean + isFree: boolean + schemeId: string | null + schemeName: string | null +} + +export interface ShopPODiscount { + schemeId: string + schemeName: string + amount: number } export interface ShopPOResponse { @@ -1000,6 +1021,7 @@ export interface ShopPOResponse { cancelReason: string | null cancelledByName: string | null cancelledByRole: string | null + discounts: ShopPODiscount[] } export interface InvoiceItemResponse { @@ -1011,6 +1033,9 @@ export interface InvoiceItemResponse { quantity: number unitPrice: number isRemoved: boolean + isFree: boolean + schemeId: string | null + schemeName: string | null } export interface InvoiceVersionItemResponse { @@ -1050,6 +1075,7 @@ export interface InvoiceResponse { isActive: boolean createdAt: string items: InvoiceItemResponse[] + discounts: ShopPODiscount[] } export interface OutstandingPaymentsResponse { -- 2.52.0 From 7d7e9f13185d5ea002643cd3bc96835d0b7a9391 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 16:49:14 +0530 Subject: [PATCH 09/11] feat: Create ViewRolePage component and remove RoleViewSheet for role details display --- .../app/(dashboard)/users/roles/[id]/page.tsx | 117 ++++++++++++++++++ .../roles/_components/role-view-sheet.tsx | 77 ------------ .../users/roles/_components/roles-client.tsx | 13 +- 3 files changed, 118 insertions(+), 89 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/users/roles/[id]/page.tsx delete mode 100644 distribution_fe/app/(dashboard)/users/roles/_components/role-view-sheet.tsx diff --git a/distribution_fe/app/(dashboard)/users/roles/[id]/page.tsx b/distribution_fe/app/(dashboard)/users/roles/[id]/page.tsx new file mode 100644 index 0000000..6e0c424 --- /dev/null +++ b/distribution_fe/app/(dashboard)/users/roles/[id]/page.tsx @@ -0,0 +1,117 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { HugeiconsIcon } from '@hugeicons/react' +import { ArrowLeft01Icon, PencilEdit01Icon } from '@hugeicons/core-free-icons' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { toast } from '@/components/ui/toast' +import { roles as rolesApi, getApiError } from '@/lib/api/index' +import { PERMISSION_CATEGORIES } from '@/config/permissions' +import type { RoleResponse, PermissionResponse } from '@/types/api' + +const fmtDate = (v: string) => + new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + +// 'orders.view' -> 'View Orders', 'loading-sheet.manage' -> 'Manage Loading Sheet' +function permissionLabel(code: string): string { + const [resource, action] = code.split('.') + const title = (s: string) => s.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') + return action ? `${title(action)} ${title(resource)}` : title(resource) +} + +function Field({ label, children }: { label: string; children: React.ReactNode }) { + return ( +

+ {label} + {children} +
+ ) +} + +export default function ViewRolePage() { + const { id } = useParams<{ id: string }>() + const router = useRouter() + const goBack = () => router.push('/users/roles') + + const [role, setRole] = useState(null) + const [permissions, setPermissions] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + Promise.all([rolesApi.get(id), rolesApi.getPermissions(id)]) + .then(([r, perms]) => { setRole(r); setPermissions(perms) }) + .catch((err) => toast.add({ title: 'Failed to load role', description: getApiError(err), type: 'error' })) + .finally(() => setLoading(false)) + }, [id]) + + return ( +
+
+
+ +
+

Role Details

+

View role details and permissions.

+
+
+ {role && ( + + )} +
+ +
+ {loading && ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ ))} +
+ )} + {!loading && role && ( +
+
+ {role.name} + {role.level} + + {role.isActive ? 'Active' : 'Inactive'} + + {fmtDate(role.createdAt)} +
+ +
+

Permissions

+ {permissions.length === 0 ? ( +

No permissions assigned to this role.

+ ) : ( +
+ {PERMISSION_CATEGORIES.map((cat) => { + const catPerms = permissions.filter((p) => cat.codes.includes(p.code)) + if (catPerms.length === 0) return null + return ( +
+ {cat.label} +
+ {catPerms.map((p) => ( + {permissionLabel(p.code)} + ))} +
+
+ ) + })} +
+ )} +
+
+ )} +
+
+ ) +} diff --git a/distribution_fe/app/(dashboard)/users/roles/_components/role-view-sheet.tsx b/distribution_fe/app/(dashboard)/users/roles/_components/role-view-sheet.tsx deleted file mode 100644 index c6405b1..0000000 --- a/distribution_fe/app/(dashboard)/users/roles/_components/role-view-sheet.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import { useEffect, useState } from 'react' -import { Badge } from '@/components/ui/badge' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { toast } from '@/components/ui/toast' -import { roles as rolesApi, getApiError } from '@/lib/api/index' -import type { RoleResponse, PermissionResponse } from '@/types/api' - -const fmtDate = (v: string) => - new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) - -interface RoleViewSheetProps { - open: boolean - onClose: () => void - role: RoleResponse | null -} - -function Row({ label, value }: { label: string; value: React.ReactNode }) { - return ( -
- {label} - {value} -
- ) -} - -export function RoleViewSheet({ open, onClose, role }: RoleViewSheetProps) { - const [permissions, setPermissions] = useState([]) - const [loading, setLoading] = useState(false) - - useEffect(() => { - if (!open || !role) return - setLoading(true) - rolesApi.getPermissions(role.id) - .then(setPermissions) - .catch((err) => toast.add({ title: 'Failed to load permissions', description: getApiError(err), type: 'error' })) - .finally(() => setLoading(false)) - }, [open, role]) - - if (!role) return null - - return ( - !v && onClose()}> - - - Role Details - - -
- - - {role.isActive ? 'Active' : 'Inactive'}} /> - -
- -
-

Permissions

- {loading ? ( -

Loading…

- ) : permissions.length === 0 ? ( -

No permissions assigned to this role.

- ) : ( -
- {permissions.map((p) => ( -
- {p.code} - {p.description && {p.description}} -
- ))} -
- )} -
-
-
- ) -} diff --git a/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx b/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx index f757c5e..d8e4d79 100644 --- a/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx +++ b/distribution_fe/app/(dashboard)/users/roles/_components/roles-client.tsx @@ -13,8 +13,6 @@ import { toast } from '@/components/ui/toast' import { roles as rolesApi, getApiError } from '@/lib/api/index' import type { RoleResponse } from '@/types/api' -import { RoleViewSheet } from './role-view-sheet' - export function RolesClient() { const router = useRouter() const [data, setData] = useState([]) @@ -22,9 +20,6 @@ export function RolesClient() { const [total, setTotal] = useState(0) const [pageSize, setPageSize] = useState(20) - const [viewOpen, setViewOpen] = useState(false) - const [viewing, setViewing] = useState(null) - const fetchData = useCallback(async () => { setLoading(true) try { @@ -42,7 +37,7 @@ export function RolesClient() { function openCreate() { router.push('/users/roles/new') } function openEdit(role: RoleResponse) { router.push(`/users/roles/${role.id}/edit`) } - function openView(role: RoleResponse) { setViewing(role); setViewOpen(true) } + function openView(role: RoleResponse) { router.push(`/users/roles/${role.id}`) } async function handleDeactivate(role: RoleResponse) { try { @@ -120,12 +115,6 @@ export function RolesClient() { totalCount={total} onServerSideChange={({ page: p }) => setPageSize(p.pageSize)} /> - - setViewOpen(false)} - role={viewing} - /> ) } -- 2.52.0 From 455ce699add2fb21394059ffcab19659eef8bd35 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 17:32:28 +0530 Subject: [PATCH 10/11] feat: Implement Payment Report feature with repository, service, and controller --- .../PaymentReport/IPaymentReportRepository.cs | 13 +++ .../PaymentReport/PaymentReportController.cs | 17 +++ .../PaymentReport/PaymentReportDTOs.cs | 42 +++++++ .../PaymentReport/PaymentReportRepository.cs | 59 ++++++++++ .../PaymentReport/PaymentReportService.cs | 105 ++++++++++++++++++ Distribution/Program.cs | 2 + 6 files changed, 238 insertions(+) create mode 100644 Distribution/Features/PaymentReport/IPaymentReportRepository.cs create mode 100644 Distribution/Features/PaymentReport/PaymentReportController.cs create mode 100644 Distribution/Features/PaymentReport/PaymentReportDTOs.cs create mode 100644 Distribution/Features/PaymentReport/PaymentReportRepository.cs create mode 100644 Distribution/Features/PaymentReport/PaymentReportService.cs diff --git a/Distribution/Features/PaymentReport/IPaymentReportRepository.cs b/Distribution/Features/PaymentReport/IPaymentReportRepository.cs new file mode 100644 index 0000000..964b585 --- /dev/null +++ b/Distribution/Features/PaymentReport/IPaymentReportRepository.cs @@ -0,0 +1,13 @@ +namespace Distribution.Features.PaymentReport; + +public interface IPaymentReportRepository +{ + /// Active payments collected within [From, To] (whole days). + Task> GetPaymentsAsync(PaymentReportFilter f); + + /// Finalized, non-cancelled invoices up to the end of 'To', each with cash/cheque paid as of then. + Task> GetInvoiceBalancesAsync(PaymentReportFilter f); + + /// Sum of TotalAmount of invoices finalized within [From, To]. + Task GetInvoicedAsync(PaymentReportFilter f); +} diff --git a/Distribution/Features/PaymentReport/PaymentReportController.cs b/Distribution/Features/PaymentReport/PaymentReportController.cs new file mode 100644 index 0000000..d61679f --- /dev/null +++ b/Distribution/Features/PaymentReport/PaymentReportController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.PaymentReport; + +[ApiController] +[Route("api/reports/payments")] +public class PaymentReportController(PaymentReportService service) : ControllerBase +{ + [HttpGet("analytics")] + public async Task Analytics( + [FromQuery] DateTime? from, [FromQuery] DateTime? to, [FromQuery] Guid? regionId) + { + var f = PaymentReportService.BuildFilter(from, to, regionId); + var error = PaymentReportService.Validate(f); + return error is null ? Ok(await service.GetAnalyticsAsync(f)) : BadRequest(new { message = error }); + } +} diff --git a/Distribution/Features/PaymentReport/PaymentReportDTOs.cs b/Distribution/Features/PaymentReport/PaymentReportDTOs.cs new file mode 100644 index 0000000..d13f7f9 --- /dev/null +++ b/Distribution/Features/PaymentReport/PaymentReportDTOs.cs @@ -0,0 +1,42 @@ +using Distribution.Models; + +namespace Distribution.Features.PaymentReport; + +public record PaymentReportFilter(DateTime From, DateTime To, Guid? RegionId); + +public record PaymentSummary( + decimal Invoiced, + decimal Collected, + decimal CreditExtended, + decimal Outstanding, + decimal Overdue, + decimal? CollectionRatePct, + int PaymentCount); + +public record MethodBreakdown(string Method, int Count, decimal Amount, decimal SharePct); + +public record PaymentTrendPoint(string Period, decimal Cash, decimal Cheque, decimal Credit); + +public record AgingBucket(string Bucket, int Invoices, decimal Balance); + +public record StoreOutstanding(Guid StoreId, string StoreName, int Invoices, decimal Balance, decimal Overdue); + +public record AccountCollection(Guid? AccountId, string AccountName, int Count, decimal Amount); + +public record ChequePipeline(string Status, int Count, decimal Amount); + +public record PaymentAnalyticsResponse( + DateTime From, + DateTime To, + PaymentSummary Summary, + List ByMethod, + List Trend, + List Aging, + List TopOutstanding, + List ByAccount, + List Cheques); + +// Repository row shapes +public record PaymentRow(PaymentMethod Method, decimal Amount, DateTime CollectedAt, Guid? AccountId, string? AccountName, string? ChequeStatus); + +public record InvoiceBalanceRow(Guid StoreId, string StoreName, DateTime FinalizedAt, decimal Total, decimal Paid); diff --git a/Distribution/Features/PaymentReport/PaymentReportRepository.cs b/Distribution/Features/PaymentReport/PaymentReportRepository.cs new file mode 100644 index 0000000..99dd21b --- /dev/null +++ b/Distribution/Features/PaymentReport/PaymentReportRepository.cs @@ -0,0 +1,59 @@ +using Distribution.Data; +using Distribution.Models; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.PaymentReport; + +public class PaymentReportRepository(AppDBContext db) : IPaymentReportRepository +{ + public async Task> GetPaymentsAsync(PaymentReportFilter f) + { + var start = f.From.Date; + var end = f.To.Date.AddDays(1); + var q = db.Payments.AsNoTracking() + .Where(p => p.IsActive && p.CollectedAt >= start && p.CollectedAt < end + && p.Invoice.ShopPO.Status != ShopPOStatus.Cancelled); + if (f.RegionId.HasValue) + q = q.Where(p => p.Invoice.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); + + return await q.Select(p => new PaymentRow( + p.Method, p.Amount, p.CollectedAt, + p.FK_BankAccountId, + p.BankAccount == null ? null : p.BankAccount.AccountName, + p.ReceivedCheque == null ? null : p.ReceivedCheque.Status.ToString())) + .ToListAsync(); + } + + public async Task> GetInvoiceBalancesAsync(PaymentReportFilter f) + { + var end = f.To.Date.AddDays(1); + var q = db.Invoices.AsNoTracking() + .Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized + && i.ShopPO.Status != ShopPOStatus.Cancelled && i.FinalizedAt < end); + if (f.RegionId.HasValue) + q = q.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); + + // Credit payments do not reduce the receivable (same rule as the dashboard). + return await q.Select(i => new InvoiceBalanceRow( + i.ShopPO.Store.Id, + i.ShopPO.Store.StoreName, + i.FinalizedAt!.Value, + i.TotalAmount, + i.Payments.Where(p => p.IsActive && p.Method != PaymentMethod.Credit && p.CollectedAt < end) + .Sum(p => (decimal?)p.Amount) ?? 0m)) + .ToListAsync(); + } + + public async Task GetInvoicedAsync(PaymentReportFilter f) + { + var start = f.From.Date; + var end = f.To.Date.AddDays(1); + var q = db.Invoices.AsNoTracking() + .Where(i => i.IsActive && i.Status == InvoiceStatus.Finalized + && i.ShopPO.Status != ShopPOStatus.Cancelled + && i.FinalizedAt >= start && i.FinalizedAt < end); + if (f.RegionId.HasValue) + q = q.Where(i => i.ShopPO.Store.Route.FK_RegionId == f.RegionId.Value); + return await q.SumAsync(i => (decimal?)i.TotalAmount) ?? 0m; + } +} diff --git a/Distribution/Features/PaymentReport/PaymentReportService.cs b/Distribution/Features/PaymentReport/PaymentReportService.cs new file mode 100644 index 0000000..fc614c3 --- /dev/null +++ b/Distribution/Features/PaymentReport/PaymentReportService.cs @@ -0,0 +1,105 @@ +using Distribution.Models; + +namespace Distribution.Features.PaymentReport; + +public class PaymentReportService(IPaymentReportRepository repo) +{ + public const int MaxRangeDays = 366; + private const int DailyGranularityMaxDays = 92; + private const int OverdueDays = 30; // matches the dashboard receivables definition + private const int TopStores = 10; + + public static PaymentReportFilter BuildFilter(DateTime? from, DateTime? to, Guid? regionId) + { + var end = (to ?? DateTime.Now).Date; + var start = (from ?? end.AddDays(-29)).Date; + return new PaymentReportFilter(start, end, regionId); + } + + public static string? Validate(PaymentReportFilter f) + { + if (f.To < f.From) return "'to' must not be before 'from'."; + if ((f.To - f.From).TotalDays > MaxRangeDays) return $"Date range cannot exceed {MaxRangeDays} days."; + return null; + } + + private static decimal Pct(decimal part, decimal whole) => whole > 0 ? Math.Round(part / whole * 100, 2) : 0m; + + public async Task GetAnalyticsAsync(PaymentReportFilter f) + { + var payments = await repo.GetPaymentsAsync(f); + var balances = await repo.GetInvoiceBalancesAsync(f); + var invoiced = await repo.GetInvoicedAsync(f); + var asOf = f.To.Date.AddDays(1); + + var collected = payments.Where(p => p.Method != PaymentMethod.Credit).Sum(p => p.Amount); + var credit = payments.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount); + var total = payments.Sum(p => p.Amount); + + var open = balances + .Select(b => (b.StoreId, b.StoreName, Age: (asOf - b.FinalizedAt).TotalDays, Balance: Math.Max(0m, b.Total - b.Paid))) + .Where(b => b.Balance > 0) + .ToList(); + var outstanding = open.Sum(b => b.Balance); + var overdue = open.Where(b => b.Age > OverdueDays).Sum(b => b.Balance); + + var summary = new PaymentSummary( + invoiced, collected, credit, outstanding, overdue, + invoiced > 0 ? Math.Round(collected / invoiced * 100, 2) : null, + payments.Count); + + var byMethod = Enum.GetValues().Select(m => + { + var rows = payments.Where(p => p.Method == m).ToList(); + var amount = rows.Sum(p => p.Amount); + return new MethodBreakdown(m.ToString(), rows.Count, amount, Pct(amount, total)); + }).ToList(); + + var daily = (f.To - f.From).TotalDays <= DailyGranularityMaxDays; + var trend = payments + .GroupBy(p => daily ? p.CollectedAt.ToString("yyyy-MM-dd") : p.CollectedAt.ToString("yyyy-MM")) + .OrderBy(g => g.Key) + .Select(g => new PaymentTrendPoint(g.Key, + g.Where(p => p.Method == PaymentMethod.Cash).Sum(p => p.Amount), + g.Where(p => p.Method == PaymentMethod.Cheque).Sum(p => p.Amount), + g.Where(p => p.Method == PaymentMethod.Credit).Sum(p => p.Amount))) + .ToList(); + + var buckets = new (string Name, Func In)[] + { + ("0-30 days", a => a <= 30), + ("31-60 days", a => a > 30 && a <= 60), + ("61-90 days", a => a > 60 && a <= 90), + ("90+ days", a => a > 90), + }; + var aging = buckets.Select(b => + { + var rows = open.Where(o => b.In(o.Age)).ToList(); + return new AgingBucket(b.Name, rows.Count, rows.Sum(r => r.Balance)); + }).ToList(); + + var topOutstanding = open + .GroupBy(o => (o.StoreId, o.StoreName)) + .Select(g => new StoreOutstanding(g.Key.StoreId, g.Key.StoreName, g.Count(), + g.Sum(x => x.Balance), g.Where(x => x.Age > OverdueDays).Sum(x => x.Balance))) + .OrderByDescending(s => s.Balance) + .Take(TopStores) + .ToList(); + + var byAccount = payments + .Where(p => p.Method == PaymentMethod.Cash) + .GroupBy(p => (p.AccountId, Name: p.AccountName ?? "Unassigned")) + .Select(g => new AccountCollection(g.Key.AccountId, g.Key.Name, g.Count(), g.Sum(p => p.Amount))) + .OrderByDescending(a => a.Amount) + .ToList(); + + var cheques = payments + .Where(p => p.Method == PaymentMethod.Cheque) + .GroupBy(p => p.ChequeStatus ?? "Unknown") + .Select(g => new ChequePipeline(g.Key, g.Count(), g.Sum(p => p.Amount))) + .OrderByDescending(c => c.Amount) + .ToList(); + + return new PaymentAnalyticsResponse(f.From, f.To, summary, byMethod, trend, aging, topOutstanding, byAccount, cheques); + } +} diff --git a/Distribution/Program.cs b/Distribution/Program.cs index 059eeb3..1afd464 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -79,6 +79,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(); -- 2.52.0 From 932d09c135570c36d990a8aeaf0db86bab243358 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Thu, 22 Oct 2026 17:33:06 +0530 Subject: [PATCH 11/11] feat: Add Payment Analytics page and integrate payment reports API --- .../(dashboard)/payments/analytics/page.tsx | 266 ++++++++++++++++++ distribution_fe/config/nav.ts | 2 +- distribution_fe/lib/api/index.ts | 2 +- distribution_fe/lib/api/reports.ts | 25 ++ 4 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/payments/analytics/page.tsx diff --git a/distribution_fe/app/(dashboard)/payments/analytics/page.tsx b/distribution_fe/app/(dashboard)/payments/analytics/page.tsx new file mode 100644 index 0000000..9ea4884 --- /dev/null +++ b/distribution_fe/app/(dashboard)/payments/analytics/page.tsx @@ -0,0 +1,266 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis } from 'recharts' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart' +import { Input } from '@/components/ui/input' +import { Skeleton } from '@/components/ui/skeleton' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { paymentReports, getApiError } from '@/lib/api/index' +import type { PaymentAnalyticsResponse } from '@/lib/api/reports' + +import { daysFromToday, fmtCompact, fmtMoney, fmtNum, fmtShortDay } from '@/app/(dashboard)/network/warehouses/_components/dashboard/format' +import { EmptyNote, Metric, Section } from '@/app/(dashboard)/network/warehouses/_components/dashboard/shared' + +const PRESETS = [ + { label: '7 days', days: -6 }, + { label: '30 days', days: -29 }, + { label: '90 days', days: -89 }, + { label: '1 year', days: -364 }, +] + +const flowConfig = { + cash: { label: 'Cash', color: 'var(--chart-1)' }, + cheque: { label: 'Cheque', color: 'var(--chart-2)' }, + credit: { label: 'Credit', color: 'var(--chart-3)' }, +} satisfies ChartConfig + +const METHOD_COLOR: Record = { + Cash: 'var(--chart-1)', + Cheque: 'var(--chart-2)', + Credit: 'var(--chart-3)', +} + +const methodConfig = { + Cash: { label: 'Cash', color: METHOD_COLOR.Cash }, + Cheque: { label: 'Cheque', color: METHOD_COLOR.Cheque }, + Credit: { label: 'Credit', color: METHOD_COLOR.Credit }, +} satisfies ChartConfig + +const balanceConfig = { balance: { label: 'Outstanding', color: 'var(--chart-4)' } } satisfies ChartConfig + +const periodLabel = (p: string) => (p.length === 10 ? fmtShortDay(p) : p) + +export default function PaymentAnalyticsPage() { + const [from, setFrom] = useState(() => daysFromToday(-29)) + const [to, setTo] = useState(() => daysFromToday(0)) + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!from || !to) return + let cancelled = false + setLoading(true) + setError(null) + paymentReports.analytics({ from, to }) + .then(res => { if (!cancelled) setData(res) }) + .catch(err => { if (!cancelled) setError(getApiError(err)) }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, [from, to]) + + const s = data?.summary + const hasFlow = !!data && data.byMethod.some(m => m.amount > 0) + + return ( +
+
+
+

Payment Analytics

+

Money flow by method, collections and outstanding receivables.

+
+
+ {PRESETS.map(p => ( + + ))} + setFrom(e.target.value)} className="w-40" aria-label="From date" /> + setTo(e.target.value)} className="w-40" aria-label="To date" /> +
+
+ + {error && ( +
{error}
+ )} + + {!error && !data && loading && ( +
+ + +
+ )} + + {data && s && ( +
+
+ + + + 0 ? 'warning' : undefined} /> + 0 ? 'danger' : undefined} /> + +
+ +
+
+
+ {!hasFlow ? ( + No payments in this period. + ) : ( + + + + + + periodLabel(String(l))} formatter={(v) => fmtMoney(Number(v))} />} /> + + + + + + )} +
+
+ +
+ {!hasFlow ? ( + No payments in this period. + ) : ( +
+ + + fmtMoney(Number(v))} />} /> + + {data.byMethod.map(m => )} + + + +
    + {data.byMethod.map(m => ( +
  • + + + {m.method} · {fmtNum(m.count)} + + {fmtMoney(m.amount)} ({m.sharePct}%) +
  • + ))} +
+
+ )} +
+
+ +
+
+ {s.outstanding === 0 ? ( + Nothing outstanding. + ) : ( + + + + + + fmtMoney(Number(v))} />} /> + + + + )} +
+ +
+ {data.topOutstanding.length === 0 ? ( + No outstanding balances. + ) : ( + + + + Store + Invoices + Outstanding + Overdue + + + + {data.topOutstanding.map(r => ( + + {r.storeName} + {fmtNum(r.invoices)} + {fmtMoney(r.balance)} + + {r.overdue > 0 ? {fmtMoney(r.overdue)} : '—'} + + + ))} + +
+ )} +
+
+ +
+
+ {data.byAccount.length === 0 ? ( + No cash payments in this period. + ) : ( + + + + Account + Payments + Amount + + + + {data.byAccount.map(a => ( + + {a.accountName} + {fmtNum(a.count)} + {fmtMoney(a.amount)} + + ))} + +
+ )} +
+ +
+ {data.cheques.length === 0 ? ( + No cheque payments in this period. + ) : ( + + + + Status + Cheques + Amount + + + + {data.cheques.map(c => ( + + {c.status} + {fmtNum(c.count)} + {fmtMoney(c.amount)} + + ))} + +
+ )} +
+
+
+ )} +
+ ) +} diff --git a/distribution_fe/config/nav.ts b/distribution_fe/config/nav.ts index 98dcabf..54cc6ab 100644 --- a/distribution_fe/config/nav.ts +++ b/distribution_fe/config/nav.ts @@ -65,7 +65,6 @@ export const NAV_GROUPS: NavGroup[] = [ items: [ { title: 'Shop POs', href: '/sales', icon: ClipboardListIcon, description: 'Purchase orders raised by sales agents on behalf of stores', permission: 'orders.view' }, { title: 'Invoices', href: '/sales/orders', icon: DollarSquareIcon, description: 'Billing documents generated from confirmed Shop POs', permission: 'orders.view' }, - { title: 'Payments', href: '/sales/payments', icon: BankIcon, description: 'Cash and credit payments collected against invoices', permission: 'orders.view' }, ], }, { @@ -131,6 +130,7 @@ export const NAV_GROUPS: NavGroup[] = [ title: 'Accounts', icon: BankIcon, items: [ + { title: 'Analytics', href: '/payments/analytics', icon: BankIcon, description: 'Overview of bank accounts, cheque books and received cheques' }, { title: 'Bank Accounts', href: '/payments/bank-accounts', icon: BankIcon, description: 'Cash and bank accounts used for payments and deposits' }, { title: 'Cheque Books', href: '/payments/cheque-books', icon: ClipboardListIcon, description: 'Cheque books issued from the company’s own supply' }, { title: 'Received Cheques', href: '/payments/received-cheques', icon: DollarSquareIcon, description: 'Cheques received from customers, suppliers, or others' }, diff --git a/distribution_fe/lib/api/index.ts b/distribution_fe/lib/api/index.ts index 0c45d86..ed57b7e 100644 --- a/distribution_fe/lib/api/index.ts +++ b/distribution_fe/lib/api/index.ts @@ -8,7 +8,7 @@ export { drivers, compliance } from './drivers' export { purchaseOrders, grn, purchaseReturns } from './procurement' export { rides, loadingSheets, deliveryTasks } from './operations' export { stock } from './stock' -export { productReports, stockReports, dashboardReports } from './reports' +export { productReports, stockReports, dashboardReports, paymentReports } from './reports' export { shopPos, invoices, payments } from './sales' export { saleReturns, claims, supplierClaims } from './returns-claims' export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS, invoiceCreationMode, INVOICE_CREATION_MODE_KEY, DEFAULT_INVOICE_CREATION_MODE } from './sys-config' diff --git a/distribution_fe/lib/api/reports.ts b/distribution_fe/lib/api/reports.ts index a620d4e..098689d 100644 --- a/distribution_fe/lib/api/reports.ts +++ b/distribution_fe/lib/api/reports.ts @@ -206,3 +206,28 @@ export const dashboardReports = { inventory: (params?: { from?: string; to?: string; warehouseId?: string; categoryId?: string }) => api.get('/reports/dashboard/inventory', { params }).then(r => r.data), } + +export interface PaymentAnalyticsResponse { + from: string + to: string + summary: { + invoiced: number + collected: number + creditExtended: number + outstanding: number + overdue: number + collectionRatePct: number | null + paymentCount: number + } + byMethod: Array<{ method: 'Cash' | 'Cheque' | 'Credit'; count: number; amount: number; sharePct: number }> + trend: Array<{ period: string; cash: number; cheque: number; credit: number }> + aging: Array<{ bucket: string; invoices: number; balance: number }> + topOutstanding: Array<{ storeId: string; storeName: string; invoices: number; balance: number; overdue: number }> + byAccount: Array<{ accountId: string | null; accountName: string; count: number; amount: number }> + cheques: Array<{ status: string; count: number; amount: number }> +} + +export const paymentReports = { + analytics: (params?: { from?: string; to?: string; regionId?: string }) => + api.get('/reports/payments/analytics', { params }).then(r => r.data), +} -- 2.52.0