From d1c3d4e22fe393af7c8232060c224966c4c5ea66 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 20 Oct 2026 16:45:48 +0530 Subject: [PATCH 1/4] feat: implement notification system with repository, service, and controller layers --- .../Notifications/INotificationRepository.cs | 29 ++++ .../Notifications/NotificationCleanupJob.cs | 63 +++++++ .../Notifications/NotificationController.cs | 57 ++++++ .../Notifications/NotificationDTOs.cs | 28 +++ .../Features/Notifications/NotificationHub.cs | 22 +++ .../NotificationRecipientResolver.cs | 53 ++++++ .../Notifications/NotificationRepository.cs | 156 +++++++++++++++++ .../Notifications/NotificationService.cs | 162 ++++++++++++++++++ .../Notifications/NotificationTypes.cs | 49 ++++++ .../Features/Notifications/RecipientRule.cs | 37 ++++ 10 files changed, 656 insertions(+) create mode 100644 Distribution/Features/Notifications/INotificationRepository.cs create mode 100644 Distribution/Features/Notifications/NotificationCleanupJob.cs create mode 100644 Distribution/Features/Notifications/NotificationController.cs create mode 100644 Distribution/Features/Notifications/NotificationDTOs.cs create mode 100644 Distribution/Features/Notifications/NotificationHub.cs create mode 100644 Distribution/Features/Notifications/NotificationRecipientResolver.cs create mode 100644 Distribution/Features/Notifications/NotificationRepository.cs create mode 100644 Distribution/Features/Notifications/NotificationService.cs create mode 100644 Distribution/Features/Notifications/NotificationTypes.cs create mode 100644 Distribution/Features/Notifications/RecipientRule.cs diff --git a/Distribution/Features/Notifications/INotificationRepository.cs b/Distribution/Features/Notifications/INotificationRepository.cs new file mode 100644 index 0000000..ac86a80 --- /dev/null +++ b/Distribution/Features/Notifications/INotificationRepository.cs @@ -0,0 +1,29 @@ +namespace Distribution.Features.Notifications; + +public interface INotificationRepository +{ + // ── Inbox ──────────────────────────────────────────────────────────── + Task AddAsync(Models.Notification notification); + /// Stops tracking a notification (and its recipients) whose save failed. + void Discard(Models.Notification notification); + + Task GetInboxCountAsync(Guid userId, bool unreadOnly); + Task> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize); + Task GetUnreadCountAsync(Guid userId); + Task GetRecipientAsync(Guid userId, Guid recipientId); + Task> GetUnreadRecipientsAsync(Guid userId); + void RemoveRecipient(Models.NotificationRecipient recipient); + + // ── Recipient resolution ───────────────────────────────────────────── + Task> FilterActiveUserIdsAsync(IEnumerable userIds); + Task> GetUserIdsByPermissionAsync(string permissionCode); + Task> GetUserIdsByScopeAsync(ScopeKind kind, Guid id); + Task GetReportsToUserIdAsync(Guid userId); + Task GetRouteOwnerUserIdAsync(ScopeKind kind, Guid id); + + // ── Retention ──────────────────────────────────────────────────────── + Task> GetExpiredReadRecipientsAsync(DateTime cutoff, int take); + Task> GetOrphanNotificationsAsync(int take); + void RemoveRecipients(IEnumerable recipients); + void RemoveNotifications(IEnumerable notifications); +} diff --git a/Distribution/Features/Notifications/NotificationCleanupJob.cs b/Distribution/Features/Notifications/NotificationCleanupJob.cs new file mode 100644 index 0000000..fd43b1a --- /dev/null +++ b/Distribution/Features/Notifications/NotificationCleanupJob.cs @@ -0,0 +1,63 @@ +using Distribution.Infra.UoW; + +namespace Distribution.Features.Notifications; + +/// +/// Daily retention purge: deletes read inbox rows older than Notifications:RetentionDays +/// (default 90), then any notification left with no recipients. +/// +public class NotificationCleanupJob( + IServiceScopeFactory scopes, + IConfiguration config, + ILogger logger) : BackgroundService +{ + private const int BatchSize = 500; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Let the app finish starting before the first run. + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + try { await RunOnceAsync(stoppingToken); } + catch (OperationCanceledException) { break; } + catch (Exception ex) { logger.LogError(ex, "Notification cleanup failed."); } + + await Task.Delay(TimeSpan.FromHours(24), stoppingToken); + } + } + + private async Task RunOnceAsync(CancellationToken ct) + { + var retentionDays = config.GetValue("Notifications:RetentionDays", 90); + var cutoff = DateTime.Now.AddDays(-retentionDays); + + using var scope = scopes.CreateScope(); + var repo = scope.ServiceProvider.GetRequiredService(); + var uow = scope.ServiceProvider.GetRequiredService(); + + var removed = 0; + while (true) + { + var expired = await repo.GetExpiredReadRecipientsAsync(cutoff, BatchSize); + if (expired.Count == 0) break; + + repo.RemoveRecipients(expired); + await uow.CommitAsync(ct); + removed += expired.Count; + } + + while (true) + { + var orphans = await repo.GetOrphanNotificationsAsync(BatchSize); + if (orphans.Count == 0) break; + + repo.RemoveNotifications(orphans); + await uow.CommitAsync(ct); + } + + if (removed > 0) + logger.LogInformation("Notification cleanup removed {Count} read inbox rows older than {Days} days.", removed, retentionDays); + } +} diff --git a/Distribution/Features/Notifications/NotificationController.cs b/Distribution/Features/Notifications/NotificationController.cs new file mode 100644 index 0000000..2429290 --- /dev/null +++ b/Distribution/Features/Notifications/NotificationController.cs @@ -0,0 +1,57 @@ +using Distribution.Infra.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Distribution.Features.Notifications; + +/// Every endpoint acts on the signed-in user's own notifications only. +[ApiController] +[Route("api/notifications")] +public class NotificationController(NotificationService service, ICurrentUserService currentUser) : ControllerBase +{ + [HttpGet] + public async Task GetInbox( + [FromQuery] bool unreadOnly = false, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + return Ok(await service.GetInboxAsync(userId, unreadOnly, page, pageSize)); + } + + [HttpGet("unread-count")] + public async Task GetUnreadCount() + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + return Ok(await service.GetUnreadCountAsync(userId)); + } + + [HttpPatch("{id:guid}/read")] + public async Task MarkRead(Guid id, CancellationToken ct) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + try + { + await service.MarkReadAsync(userId, id, ct); + return NoContent(); + } + catch (KeyNotFoundException ex) { return NotFound(new { error = ex.Message }); } + } + + [HttpPatch("read-all")] + public async Task MarkAllRead(CancellationToken ct) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + await service.MarkAllReadAsync(userId, ct); + return NoContent(); + } + + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id, CancellationToken ct) + { + if (currentUser.UserId is not { } userId) return Unauthorized(); + try + { + await service.DeleteAsync(userId, id, ct); + return NoContent(); + } + catch (KeyNotFoundException ex) { return NotFound(new { error = ex.Message }); } + } +} diff --git a/Distribution/Features/Notifications/NotificationDTOs.cs b/Distribution/Features/Notifications/NotificationDTOs.cs new file mode 100644 index 0000000..5eb65e0 --- /dev/null +++ b/Distribution/Features/Notifications/NotificationDTOs.cs @@ -0,0 +1,28 @@ +using Distribution.Models; + +namespace Distribution.Features.Notifications; + +/// Object whose public properties fill the {Token} placeholders in the type's templates. +/// The acting user; excluded from the recipients. +public record NotificationArgs( + object? Values, + string EntityType, + Guid EntityId, + string Link, + Guid? CreatedByUserId = null); + +/// The caller's own recipient row id — use it for read / delete. +public record NotificationResponse( + Guid Id, + string Type, + NotificationSeverity Severity, + string Title, + string Message, + string EntityType, + Guid EntityId, + string Link, + DateTime CreatedAt, + bool IsRead, + DateTime? ReadAt); + +public record UnreadCountResponse(int Count); diff --git a/Distribution/Features/Notifications/NotificationHub.cs b/Distribution/Features/Notifications/NotificationHub.cs new file mode 100644 index 0000000..fc83279 --- /dev/null +++ b/Distribution/Features/Notifications/NotificationHub.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace Distribution.Features.Notifications; + +/// +/// Server → client only. The server pushes a "notification" event to each recipient; clients never +/// invoke hub methods. Users are addressed by their id (see ). +/// +[Authorize] +public class NotificationHub : Hub +{ + public const string Path = "/hubs/notifications"; + public const string ClientEvent = "notification"; +} + +/// Maps a SignalR connection to the "UserId" claim that AuthMiddleware puts on the principal. +public class NotificationUserIdProvider : IUserIdProvider +{ + public string? GetUserId(HubConnectionContext connection) => + connection.User?.FindFirst("UserId")?.Value; +} diff --git a/Distribution/Features/Notifications/NotificationRecipientResolver.cs b/Distribution/Features/Notifications/NotificationRecipientResolver.cs new file mode 100644 index 0000000..ad7b279 --- /dev/null +++ b/Distribution/Features/Notifications/NotificationRecipientResolver.cs @@ -0,0 +1,53 @@ +namespace Distribution.Features.Notifications; + +public class NotificationRecipientResolver(INotificationRepository repo) +{ + /// + /// Expands the rules into a de-duplicated list of active user ids, never including + /// . + /// + public async Task> ResolveAsync(IEnumerable rules, Guid? actingUserId) + { + var ids = new HashSet(); + + foreach (var rule in rules) + ids.UnionWith(await ResolveRuleAsync(rule)); + + if (actingUserId is { } actor) + ids.Remove(actor); + + if (ids.Count == 0) return []; + return await repo.FilterActiveUserIdsAsync(ids); + } + + private async Task> ResolveRuleAsync(RecipientRule rule) + { + switch (rule) + { + case UsersRule r: + return r.Ids; + + case PermissionRule r: + return await repo.GetUserIdsByPermissionAsync(r.Code); + + case ScopeRule r: + return await repo.GetUserIdsByScopeAsync(r.Kind, r.Id); + + case PermissionInScopeRule r: + var withPermission = await repo.GetUserIdsByPermissionAsync(r.Code); + var inScope = await repo.GetUserIdsByScopeAsync(r.Kind, r.Id); + return withPermission.Intersect(inScope); + + case ReportsToRule r: + var supervisor = await repo.GetReportsToUserIdAsync(r.UserId); + return supervisor is { } s ? [s] : []; + + case RouteOwnerRule r: + var owner = await repo.GetRouteOwnerUserIdAsync(r.Kind, r.Id); + return owner is { } o ? [o] : []; + + default: + throw new NotSupportedException($"Unknown recipient rule {rule.GetType().Name}."); + } + } +} diff --git a/Distribution/Features/Notifications/NotificationRepository.cs b/Distribution/Features/Notifications/NotificationRepository.cs new file mode 100644 index 0000000..b766edc --- /dev/null +++ b/Distribution/Features/Notifications/NotificationRepository.cs @@ -0,0 +1,156 @@ +using Distribution.Data; +using Microsoft.EntityFrameworkCore; + +namespace Distribution.Features.Notifications; + +public class NotificationRepository(AppDBContext context) : INotificationRepository +{ + // ── Inbox ──────────────────────────────────────────────────────────── + + public async Task AddAsync(Models.Notification notification) => + await context.Notifications.AddAsync(notification); + + public void Discard(Models.Notification notification) + { + foreach (var recipient in notification.Recipients) + context.Entry(recipient).State = EntityState.Detached; + context.Entry(notification).State = EntityState.Detached; + } + + private IQueryable InboxQuery(Guid userId, bool unreadOnly) + { + var query = context.NotificationRecipients.Where(r => r.FK_UserId == userId && r.IsActive); + return unreadOnly ? query.Where(r => !r.IsRead) : query; + } + + public Task GetInboxCountAsync(Guid userId, bool unreadOnly) => + InboxQuery(userId, unreadOnly).CountAsync(); + + public Task> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize) => + InboxQuery(userId, unreadOnly) + .Include(r => r.Notification) + .OrderByDescending(r => r.Notification.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .AsNoTracking() + .ToListAsync(); + + public Task GetUnreadCountAsync(Guid userId) => + InboxQuery(userId, unreadOnly: true).CountAsync(); + + public Task GetRecipientAsync(Guid userId, Guid recipientId) => + context.NotificationRecipients + .FirstOrDefaultAsync(r => r.Id == recipientId && r.FK_UserId == userId && r.IsActive); + + public Task> GetUnreadRecipientsAsync(Guid userId) => + InboxQuery(userId, unreadOnly: true).ToListAsync(); + + public void RemoveRecipient(Models.NotificationRecipient recipient) => + context.NotificationRecipients.Remove(recipient); + + // ── Recipient resolution ───────────────────────────────────────────── + + public Task> FilterActiveUserIdsAsync(IEnumerable userIds) + { + var ids = userIds.ToList(); + return context.Users + .Where(u => u.IsActive && ids.Contains(u.Id)) + .Select(u => u.Id) + .ToListAsync(); + } + + public Task> GetUserIdsByPermissionAsync(string permissionCode) => + context.Users + .Where(u => u.IsActive && context.RolePermissions.Any(rp => + rp.FK_RoleId == u.FK_RoleId && rp.IsActive && + rp.Permission.IsActive && rp.Permission.Code == permissionCode)) + .Select(u => u.Id) + .ToListAsync(); + + public async Task> GetUserIdsByScopeAsync(ScopeKind kind, Guid id) + { + Guid? regionId = null; + Guid? routeId = null; + Guid? storeId = null; + + switch (kind) + { + case ScopeKind.Region: + regionId = id; + break; + case ScopeKind.Route: + routeId = id; + regionId = await context.Routes.Where(r => r.Id == id) + .Select(r => (Guid?)r.FK_RegionId).FirstOrDefaultAsync(); + break; + case ScopeKind.Store: + storeId = id; + routeId = await context.Stores.Where(s => s.Id == id) + .Select(s => (Guid?)s.FK_RouteId).FirstOrDefaultAsync(); + if (routeId is { } rid) + regionId = await context.Routes.Where(r => r.Id == rid) + .Select(r => (Guid?)r.FK_RegionId).FirstOrDefaultAsync(); + break; + } + + var userIds = new HashSet(); + + if (regionId is { } region) + userIds.UnionWith(await context.UserRegionScopes + .Where(s => s.IsActive && s.FK_RegionId == region) + .Select(s => s.FK_UserId).ToListAsync()); + + if (routeId is { } route) + userIds.UnionWith(await context.UserRouteScopes + .Where(s => s.IsActive && s.FK_RouteId == route) + .Select(s => s.FK_UserId).ToListAsync()); + + if (storeId is { } store) + userIds.UnionWith(await context.UserStoreScopes + .Where(s => s.IsActive && s.FK_StoreId == store) + .Select(s => s.FK_UserId).ToListAsync()); + + return userIds.ToList(); + } + + public Task GetReportsToUserIdAsync(Guid userId) => + context.Users.Where(u => u.Id == userId) + .Select(u => u.FK_ReportsToUserId) + .FirstOrDefaultAsync(); + + public async Task GetRouteOwnerUserIdAsync(ScopeKind kind, Guid id) + { + Guid? routeId = kind switch + { + ScopeKind.Route => id, + ScopeKind.Store => await context.Stores.Where(s => s.Id == id) + .Select(s => (Guid?)s.FK_RouteId).FirstOrDefaultAsync(), + _ => null + }; + if (routeId is not { } rid) return null; + + return await context.Routes.Where(r => r.Id == rid && r.IsActive) + .Select(r => r.FK_OwnerUserId) + .FirstOrDefaultAsync(); + } + + // ── Retention ──────────────────────────────────────────────────────── + + public Task> GetExpiredReadRecipientsAsync(DateTime cutoff, int take) => + context.NotificationRecipients + .Where(r => r.IsRead && r.ReadAt != null && r.ReadAt < cutoff) + .Take(take) + .ToListAsync(); + + public Task> GetOrphanNotificationsAsync(int take) => + context.Notifications + .Where(n => !n.Recipients.Any()) + .Take(take) + .ToListAsync(); + + public void RemoveRecipients(IEnumerable recipients) => + context.NotificationRecipients.RemoveRange(recipients); + + public void RemoveNotifications(IEnumerable notifications) => + context.Notifications.RemoveRange(notifications); +} diff --git a/Distribution/Features/Notifications/NotificationService.cs b/Distribution/Features/Notifications/NotificationService.cs new file mode 100644 index 0000000..3c2cd3f --- /dev/null +++ b/Distribution/Features/Notifications/NotificationService.cs @@ -0,0 +1,162 @@ +using System.Text.RegularExpressions; +using Distribution.Features.Stock; +using Distribution.Infra.UoW; +using Microsoft.AspNetCore.SignalR; + +namespace Distribution.Features.Notifications; + +public interface INotificationService +{ + /// + /// Creates a notification for every resolved recipient and pushes it live. Best-effort: this method + /// never throws, so a notification problem can never fail the business action that triggered it. + /// Call it only AFTER the owning service's uow.CommitAsync() has succeeded. + /// + Task NotifyAsync(string type, IReadOnlyList recipients, NotificationArgs args, CancellationToken ct = default); +} + +public class NotificationService( + INotificationRepository repo, + NotificationRecipientResolver resolver, + IHubContext hub, + IUnitOfWork uow, + ILogger logger) : INotificationService +{ + private static readonly Regex TokenPattern = new(@"\{(\w+)\}", RegexOptions.Compiled); + + // ── Trigger side ───────────────────────────────────────────────────── + + public async Task NotifyAsync(string type, IReadOnlyList recipients, NotificationArgs args, CancellationToken ct = default) + { + try + { + if (!NotificationTypes.All.TryGetValue(type, out var definition)) + { + logger.LogError("Notification type {Type} is not registered in NotificationTypes; skipped.", type); + return; + } + + var userIds = await resolver.ResolveAsync(recipients, args.CreatedByUserId); + if (userIds.Count == 0) return; + + var values = ToDictionary(args.Values); + var notification = new Models.Notification + { + Type = type, + Severity = definition.Severity, + Title = Truncate(Render(definition.TitleTemplate, values), 150), + Message = Truncate(Render(definition.MessageTemplate, values), 500), + EntityType = args.EntityType, + EntityId = args.EntityId, + Link = args.Link, + CreatedByUserId = args.CreatedByUserId, + CreatedAt = DateTime.Now, + Recipients = userIds.Select(id => new Models.NotificationRecipient { FK_UserId = id }).ToList() + }; + + await repo.AddAsync(notification); + try + { + await uow.CommitAsync(ct); + } + catch + { + repo.Discard(notification); + throw; + } + + await PushAsync(notification); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to create {Type} notification for {EntityType} {EntityId}.", + type, args.EntityType, args.EntityId); + } + } + + // Separate from persistence: a failed push still leaves the notification in the inbox. + private async Task PushAsync(Models.Notification notification) + { + foreach (var recipient in notification.Recipients) + { + try + { + await hub.Clients.User(recipient.FK_UserId.ToString()) + .SendAsync(NotificationHub.ClientEvent, ToResponse(notification, recipient)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to push notification {NotificationId} to user {UserId}.", + notification.Id, recipient.FK_UserId); + } + } + } + + // ── Inbox side ─────────────────────────────────────────────────────── + + public async Task> GetInboxAsync(Guid userId, bool unreadOnly, int page, int pageSize) + { + page = Math.Max(1, page); + pageSize = Math.Clamp(pageSize, 1, 100); + + var total = await repo.GetInboxCountAsync(userId, unreadOnly); + var rows = await repo.GetInboxAsync(userId, unreadOnly, page, pageSize); + return new PagedResponse(page, pageSize, total, + rows.Select(r => ToResponse(r.Notification, r)).ToList()); + } + + public async Task GetUnreadCountAsync(Guid userId) => + new(await repo.GetUnreadCountAsync(userId)); + + public async Task MarkReadAsync(Guid userId, Guid recipientId, CancellationToken ct = default) + { + var recipient = await repo.GetRecipientAsync(userId, recipientId) + ?? throw new KeyNotFoundException("Notification not found."); + if (recipient.IsRead) return; + + recipient.IsRead = true; + recipient.ReadAt = DateTime.Now; + await uow.CommitAsync(ct); + } + + public async Task MarkAllReadAsync(Guid userId, CancellationToken ct = default) + { + var unread = await repo.GetUnreadRecipientsAsync(userId); + if (unread.Count == 0) return; + + var now = DateTime.Now; + foreach (var recipient in unread) + { + recipient.IsRead = true; + recipient.ReadAt = now; + } + await uow.CommitAsync(ct); + } + + public async Task DeleteAsync(Guid userId, Guid recipientId, CancellationToken ct = default) + { + var recipient = await repo.GetRecipientAsync(userId, recipientId) + ?? throw new KeyNotFoundException("Notification not found."); + + repo.RemoveRecipient(recipient); + await uow.CommitAsync(ct); + } + + // ── Helpers ────────────────────────────────────────────────────────── + + private static NotificationResponse ToResponse(Models.Notification n, Models.NotificationRecipient r) => + new(r.Id, n.Type, n.Severity, n.Title, n.Message, n.EntityType, n.EntityId, n.Link, + n.CreatedAt, r.IsRead, r.ReadAt); + + private static Dictionary ToDictionary(object? values) => + values is null + ? [] + : values.GetType().GetProperties() + .ToDictionary(p => p.Name, p => p.GetValue(values)?.ToString() ?? string.Empty); + + private static string Render(string template, Dictionary values) => + TokenPattern.Replace(template, m => values.TryGetValue(m.Groups[1].Value, out var v) ? v : string.Empty).Trim(); + + private static string Truncate(string value, int max) => + value.Length <= max ? value : value[..(max - 1)] + "…"; +} diff --git a/Distribution/Features/Notifications/NotificationTypes.cs b/Distribution/Features/Notifications/NotificationTypes.cs new file mode 100644 index 0000000..c196a47 --- /dev/null +++ b/Distribution/Features/Notifications/NotificationTypes.cs @@ -0,0 +1,49 @@ +using Distribution.Models; + +namespace Distribution.Features.Notifications; + +public record NotificationTypeDefinition(NotificationSeverity Severity, string TitleTemplate, string MessageTemplate); + +/// +/// Registry of every notification type. Services must pass one of these constants to +/// — never a string literal. Templates use {Token} +/// placeholders filled from . +/// +public static class NotificationTypes +{ + public const string RideCancelled = "RideCancelled"; + public const string RideDriverAssigned = "RideDriverAssigned"; + public const string DeliveryTaskFailed = "DeliveryTaskFailed"; + public const string DeliveryTaskPartiallyDelivered = "DeliveryTaskPartiallyDelivered"; + public const string SupplierClaimApproved = "SupplierClaimApproved"; + public const string SupplierClaimRejected = "SupplierClaimRejected"; + public const string ShopPOConfirmed = "ShopPOConfirmed"; + public const string ShopPOCancelled = "ShopPOCancelled"; + public const string StockLow = "StockLow"; + public const string CreditLimitBreached = "CreditLimitBreached"; + + public static readonly IReadOnlyDictionary All = + new Dictionary + { + [RideCancelled] = new(NotificationSeverity.Critical, + "Ride cancelled", "A ride was cancelled ({CancelType}). {Reason}"), + [RideDriverAssigned] = new(NotificationSeverity.Info, + "Ride assigned", "You have been assigned to a ride."), + [DeliveryTaskFailed] = new(NotificationSeverity.Warning, + "Delivery failed", "Delivery to {StoreName} failed. {Note}"), + [DeliveryTaskPartiallyDelivered] = new(NotificationSeverity.Warning, + "Partial delivery", "Delivery to {StoreName} was only partly delivered."), + [SupplierClaimApproved] = new(NotificationSeverity.Success, + "Supplier claim approved", "Your supplier claim was approved."), + [SupplierClaimRejected] = new(NotificationSeverity.Warning, + "Supplier claim rejected", "Your supplier claim was rejected."), + [ShopPOConfirmed] = new(NotificationSeverity.Info, + "Shop order confirmed", "Shop order for {StoreName} was confirmed."), + [ShopPOCancelled] = new(NotificationSeverity.Warning, + "Shop order cancelled", "Shop order for {StoreName} was cancelled: {Reason}"), + [StockLow] = new(NotificationSeverity.Warning, + "Low stock", "{ItemName} is down to {Quantity} at {Location}."), + [CreditLimitBreached] = new(NotificationSeverity.Warning, + "Credit limit exceeded", "{StoreName} has exceeded its credit limit."), + }; +} diff --git a/Distribution/Features/Notifications/RecipientRule.cs b/Distribution/Features/Notifications/RecipientRule.cs new file mode 100644 index 0000000..528cf55 --- /dev/null +++ b/Distribution/Features/Notifications/RecipientRule.cs @@ -0,0 +1,37 @@ +namespace Distribution.Features.Notifications; + +public enum ScopeKind { Region, Route, Store } + +/// +/// Who a notification goes to. There is deliberately no "everyone" rule — every trigger must say who +/// needs to know. Scope coverage follows Region → Route → Store: a region scope covers its routes +/// and their stores, a route scope covers its stores. +/// +public abstract record RecipientRule +{ + /// These specific users (creator, driver, approver, route owner …). + public static RecipientRule Users(params Guid[] ids) => new UsersRule(ids); + + /// Every active user whose role holds the permission code. + public static RecipientRule Permission(string code) => new PermissionRule(code); + + /// Every active user whose scope covers the region / route / store. + public static RecipientRule Scope(ScopeKind kind, Guid id) => new ScopeRule(kind, id); + + /// Users who hold the permission AND whose scope covers the region / route / store. + public static RecipientRule PermissionInScope(string code, ScopeKind kind, Guid id) => + new PermissionInScopeRule(code, kind, id); + + /// The user's supervisor (User.FK_ReportsToUserId). + public static RecipientRule ReportsTo(Guid userId) => new ReportsToRule(userId); + + /// The owner of the route () or of the store's route (). + public static RecipientRule RouteOwner(ScopeKind kind, Guid id) => new RouteOwnerRule(kind, id); +} + +public sealed record UsersRule(IReadOnlyCollection Ids) : RecipientRule; +public sealed record PermissionRule(string Code) : RecipientRule; +public sealed record ScopeRule(ScopeKind Kind, Guid Id) : RecipientRule; +public sealed record PermissionInScopeRule(string Code, ScopeKind Kind, Guid Id) : RecipientRule; +public sealed record ReportsToRule(Guid UserId) : RecipientRule; +public sealed record RouteOwnerRule(ScopeKind Kind, Guid Id) : RecipientRule; From a0df2a1bbf12d23766be5d33a855bd4720a07978 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 20 Oct 2026 16:49:05 +0530 Subject: [PATCH 2/4] feat: integrate notification system across PO,store,supplier claims,delivery task and ride --- Distribution/Data/AppDBContext.cs | 26 ++++++++++ .../DeliveryRide/DeliveryTaskService.cs | 25 +++++++++ Distribution/Features/Ride/RideService.cs | 52 ++++++++++++++++++- Distribution/Features/ShopPO/ShopPOService.cs | 16 ++++++ Distribution/Features/Store/StoreService.cs | 16 +++++- .../SupplierClaims/SupplierClaimService.cs | 14 +++++ 6 files changed, 147 insertions(+), 2 deletions(-) diff --git a/Distribution/Data/AppDBContext.cs b/Distribution/Data/AppDBContext.cs index 1fb615b..58ee863 100644 --- a/Distribution/Data/AppDBContext.cs +++ b/Distribution/Data/AppDBContext.cs @@ -71,6 +71,8 @@ public class AppDBContext(DbContextOptions options) : DbContext(op public DbSet ReceivedCheques { get; set; } public DbSet SupplierPayments { get; set; } public DbSet SupplierPaymentGRNs { get; set; } + public DbSet Notifications { get; set; } + public DbSet NotificationRecipients { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -707,6 +709,30 @@ public class AppDBContext(DbContextOptions options) : DbContext(op modelBuilder.Entity() .HasIndex(a => a.FK_GoodsReceiptId); + // --- Notifications --- + + modelBuilder.Entity() + .HasIndex(n => new { n.EntityType, n.EntityId }); + + modelBuilder.Entity() + .HasOne(r => r.Notification) + .WithMany(n => n.Recipients) + .HasForeignKey(r => r.FK_NotificationId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(r => r.User) + .WithMany() + .HasForeignKey(r => r.FK_UserId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasIndex(r => new { r.FK_UserId, r.IsRead }); + + modelBuilder.Entity() + .HasIndex(r => new { r.FK_NotificationId, r.FK_UserId }) + .IsUnique(); + Seed.RoleAndPermissionSeed.Apply(modelBuilder); Seed.MasterDataSeed.Apply(modelBuilder); Seed.OperationsDataSeed.Apply(modelBuilder); diff --git a/Distribution/Features/DeliveryRide/DeliveryTaskService.cs b/Distribution/Features/DeliveryRide/DeliveryTaskService.cs index f68079e..b9de773 100644 --- a/Distribution/Features/DeliveryRide/DeliveryTaskService.cs +++ b/Distribution/Features/DeliveryRide/DeliveryTaskService.cs @@ -1,4 +1,5 @@ using Distribution.Features.LoadingEvent; +using Distribution.Features.Notifications; using Distribution.Features.Ride; using Distribution.Features.ShopPO; using Distribution.Features.Stock; @@ -13,8 +14,18 @@ public class DeliveryTaskService( IShopPORepository shopPORepo, ILoadingSheetRepository loadingSheetRepo, StockMover mover, + INotificationService notifications, IUnitOfWork uow) { + private const string DeliveriesLink = "/operations/deliveries"; + + // Route owner + managers whose scope covers the store being delivered to. + private static List DeliveryRecipients(Guid storeId) => + [ + RecipientRule.RouteOwner(ScopeKind.Store, storeId), + RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Store, storeId), + ]; + private static DeliveryTaskResponse ToResponse(DeliveryTask t) => new(t.Id, t.FK_RideId, @@ -181,6 +192,13 @@ public class DeliveryTaskService( } catch { await uow.RollbackAsync(ct); throw; } + if (!fullyDelivered && shopPO.Store is not null) + await notifications.NotifyAsync( + NotificationTypes.DeliveryTaskPartiallyDelivered, + DeliveryRecipients(shopPO.FK_StoreId), + new NotificationArgs(new { shopPO.Store.StoreName }, "DeliveryTask", task.Id, DeliveriesLink, userId), + ct); + var saved = await repo.GetByIdAsync(taskId); return ToResponse(saved!); } @@ -204,6 +222,13 @@ public class DeliveryTaskService( } catch { await uow.RollbackAsync(ct); throw; } + if (task.ShopPO?.Store is not null) + await notifications.NotifyAsync( + NotificationTypes.DeliveryTaskFailed, + DeliveryRecipients(task.ShopPO.FK_StoreId), + new NotificationArgs(new { task.ShopPO.Store.StoreName, Note = note }, "DeliveryTask", task.Id, DeliveriesLink), + ct); + return ToResponse(task); } } diff --git a/Distribution/Features/Ride/RideService.cs b/Distribution/Features/Ride/RideService.cs index 2e1291d..7fd50ad 100644 --- a/Distribution/Features/Ride/RideService.cs +++ b/Distribution/Features/Ride/RideService.cs @@ -1,4 +1,6 @@ -using Distribution.Features.Stock; +using Distribution.Features.DeliveryRide; +using Distribution.Features.Notifications; +using Distribution.Features.Stock; using Distribution.Features.DriverComplianceConfig; using Distribution.Features.DriverProfile; using Distribution.Infra.UoW; @@ -10,9 +12,14 @@ public class RideService( IRideRepository rideRepo, IDriverProfileRepository driverRepo, IDriverComplianceConfigRepository complianceRepo, + IDeliveryTaskRepository taskRepo, StockMover mover, + INotificationService notifications, + ILogger logger, IUnitOfWork uow) { + private const string RidesLink = "/operations/rides"; + public async Task> GetAllAsync(RideStatus? status, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize) { var total = await rideRepo.GetCountAsync(status, driverProfileId, vehicleId); @@ -72,6 +79,12 @@ public class RideService( try { rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } + await notifications.NotifyAsync( + NotificationTypes.RideDriverAssigned, + [RecipientRule.Users(driver.FK_UserId)], + new NotificationArgs(null, "Ride", rideId, RidesLink), + ct); + var saved = await rideRepo.GetByIdAsync(rideId); return ToResponse(saved!); } @@ -125,9 +138,46 @@ public class RideService( } catch { await uow.RollbackAsync(ct); throw; } + await NotifyRideCancelledAsync(ride, request, userId, ct); + return ToResponse(ride); } + // A ride has no route of its own; the people to tell are the owners and managers of the routes + // its delivery tasks go to. Best-effort: the ride is already cancelled, so nothing here may throw. + private async Task NotifyRideCancelledAsync(DeliveryRun ride, CancelRideRequest request, Guid userId, CancellationToken ct) + { + try + { + var tasks = await taskRepo.GetByRideIdAsync(ride.Id); + var routeIds = tasks + .Where(t => t.ShopPO?.Store is not null) + .Select(t => t.ShopPO.Store.FK_RouteId) + .Distinct() + .ToList(); + + var recipients = new List(); + foreach (var routeId in routeIds) + { + recipients.Add(RecipientRule.RouteOwner(ScopeKind.Route, routeId)); + recipients.Add(RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Route, routeId)); + } + if (recipients.Count == 0) return; + + await notifications.NotifyAsync( + NotificationTypes.RideCancelled, + recipients, + new NotificationArgs( + new { CancelType = request.TypeCode, Reason = request.Reason }, + "Ride", ride.Id, RidesLink, userId), + ct); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to build RideCancelled notification for ride {RideId}.", ride.Id); + } + } + private async Task CheckDriverComplianceAsync(Guid driverProfileId) { var driver = await driverRepo.GetByIdAsync(driverProfileId) diff --git a/Distribution/Features/ShopPO/ShopPOService.cs b/Distribution/Features/ShopPO/ShopPOService.cs index 98a04a9..d0a544e 100644 --- a/Distribution/Features/ShopPO/ShopPOService.cs +++ b/Distribution/Features/ShopPO/ShopPOService.cs @@ -1,4 +1,5 @@ using Distribution.Features.Invoice; +using Distribution.Features.Notifications; using Distribution.Features.PriceList; using Distribution.Features.Stock; using Distribution.Infra.UoW; @@ -10,8 +11,17 @@ public class ShopPOService( IShopPORepository repo, IInvoiceRepository invoiceRepo, PriceListService priceListService, + INotificationService notifications, IUnitOfWork uow) { + // 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( + type, + [RecipientRule.Users(po.RaisedById), RecipientRule.ReportsTo(po.RaisedById)], + 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) => new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty, p.Store?.StoreAddress, p.Store?.ContactNumber, @@ -168,6 +178,8 @@ public class ShopPOService( try { repo.Update(po); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } + await NotifyOrderAsync(NotificationTypes.ShopPOConfirmed, po, confirmedBy, null, ct); + return await ToResponseWithUserAsync(po); } @@ -204,6 +216,8 @@ public class ShopPOService( } catch { await uow.RollbackAsync(ct); throw; } + await NotifyOrderAsync(NotificationTypes.ShopPOConfirmed, po, userId, null, ct); + var saved = await repo.GetByIdAsync(id); return await ToResponseWithUserAsync(saved!); } @@ -231,6 +245,8 @@ public class ShopPOService( try { repo.Update(po); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } + await NotifyOrderAsync(NotificationTypes.ShopPOCancelled, po, cancelledBy, po.CancelReason, ct); + return await ToResponseWithUserAsync(po); } } diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs index 5e47179..2a52177 100644 --- a/Distribution/Features/Store/StoreService.cs +++ b/Distribution/Features/Store/StoreService.cs @@ -1,3 +1,4 @@ +using Distribution.Features.Notifications; using Distribution.Features.Scheme; using Distribution.Features.Stock; using Distribution.Infra.Authorization; @@ -6,7 +7,7 @@ using Distribution.Models; namespace Distribution.Features.Store; -public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService, Route.IRouteRepository routeRepo) +public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess, SchemeService schemeService, Route.IRouteRepository routeRepo, INotificationService notifications) { private const int GlobalScopeRoleLevel = 50; private const int CreditApproverRoleLevel = 20; @@ -147,6 +148,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe var store = await repo.GetByIdAsync(id); if (store is null || !store.IsActive) return null; if (!await HasStoreScopeAsync(id)) return null; + var wasWithinLimit = store.OutstandingBalance <= store.CreditLimit; store.OutstandingBalance = request.NewBalance; await uow.BeginAsync(ct); try @@ -159,6 +161,18 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserSe await uow.RollbackAsync(ct); throw; } + + // Only on the crossing, so repeated updates while over the limit don't re-alert. + if (store.CreditLimit > 0 && wasWithinLimit && store.OutstandingBalance > store.CreditLimit) + await notifications.NotifyAsync( + NotificationTypes.CreditLimitBreached, + [ + RecipientRule.RouteOwner(ScopeKind.Store, store.Id), + RecipientRule.PermissionInScope("store.manage", ScopeKind.Store, store.Id), + ], + new NotificationArgs(new { store.StoreName }, "Store", store.Id, $"/stores/{store.Id}", currentUser.UserId), + ct); + return ToResponse(store); } diff --git a/Distribution/Features/SupplierClaims/SupplierClaimService.cs b/Distribution/Features/SupplierClaims/SupplierClaimService.cs index 64b1b28..3f92b55 100644 --- a/Distribution/Features/SupplierClaims/SupplierClaimService.cs +++ b/Distribution/Features/SupplierClaims/SupplierClaimService.cs @@ -1,4 +1,5 @@ using Distribution.Features.Grn; +using Distribution.Features.Notifications; using Distribution.Features.Po; using Distribution.Infra.UoW; using Distribution.Models; @@ -9,6 +10,7 @@ public class SupplierClaimService( ISupplierClaimRepository repo, IPoRepository poRepo, IGrnRepository grnRepo, + INotificationService notifications, IUnitOfWork uow) { private static SupplierClaimItemResponse MapItem(SupplierClaimItem i) => @@ -149,9 +151,19 @@ public class SupplierClaimService( } catch { await uow.RollbackAsync(ct); throw; } + await NotifyDecisionAsync(NotificationTypes.SupplierClaimApproved, claim, approverId, ct); + return Map(claim); } + // Tells whoever raised the claim how it was decided (never the person who decided it). + private Task NotifyDecisionAsync(string type, SupplierClaim claim, Guid decidedBy, CancellationToken ct) => + notifications.NotifyAsync( + type, + [RecipientRule.Users(claim.RaisedByUserId)], + new NotificationArgs(null, "SupplierClaim", claim.Id, "/returns-claims/supplier-claims", decidedBy), + ct); + public async Task RejectAsync(Guid id, RejectSupplierClaimRequest request, Guid approverId, CancellationToken ct = default) { var claim = await repo.GetByIdAsync(id) @@ -172,6 +184,8 @@ public class SupplierClaimService( } catch { await uow.RollbackAsync(ct); throw; } + await NotifyDecisionAsync(NotificationTypes.SupplierClaimRejected, claim, approverId, ct); + return Map(claim); } } From 6b9f27573b62b0496a703459ad22122a39a3dd08 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 20 Oct 2026 16:51:01 +0530 Subject: [PATCH 3/4] Introduced NotificationService for managing notification logic and recipient resolution. - Integrated SignalR for real-time notification delivery to users. --- .../Services/NotificationServiceTests.cs | 209 + ...0261020075213_AddNotifications.Designer.cs | 5820 +++++++++++++++++ .../20261020075213_AddNotifications.cs | 99 + .../Migrations/AppDBContextModelSnapshot.cs | 211 +- Distribution/Models/Notification.cs | 33 + Distribution/Models/NotificationRecipient.cs | 19 + Distribution/Program.cs | 11 + Distribution/docs/21_notifications_alerts.md | 331 +- 8 files changed, 6649 insertions(+), 84 deletions(-) create mode 100644 Distribution.Tests/Services/NotificationServiceTests.cs create mode 100644 Distribution/Migrations/20261020075213_AddNotifications.Designer.cs create mode 100644 Distribution/Migrations/20261020075213_AddNotifications.cs create mode 100644 Distribution/Models/Notification.cs create mode 100644 Distribution/Models/NotificationRecipient.cs diff --git a/Distribution.Tests/Services/NotificationServiceTests.cs b/Distribution.Tests/Services/NotificationServiceTests.cs new file mode 100644 index 0000000..be3d8f1 --- /dev/null +++ b/Distribution.Tests/Services/NotificationServiceTests.cs @@ -0,0 +1,209 @@ +using Distribution.Features.Notifications; +using Distribution.Infra.UoW; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Distribution.Tests.Services; + +public class NotificationServiceTests +{ + private readonly Mock _repo = new(); + private readonly Mock _uow = new(); + private readonly Mock> _hub = new(); + private readonly Mock _clients = new(); + private readonly Mock _proxy = new(); + private readonly NotificationService _sut; + + public NotificationServiceTests() + { + _repo.Setup(r => r.FilterActiveUserIdsAsync(It.IsAny>())) + .ReturnsAsync((IEnumerable ids) => ids.ToList()); + _hub.SetupGet(h => h.Clients).Returns(_clients.Object); + _clients.Setup(c => c.User(It.IsAny())).Returns(_proxy.Object); + + _sut = new NotificationService( + _repo.Object, + new NotificationRecipientResolver(_repo.Object), + _hub.Object, + _uow.Object, + NullLogger.Instance); + } + + private static NotificationArgs Args(Guid? actor = null) => + new(new { StoreName = "Fort Mini Mart" }, "DeliveryTask", Guid.NewGuid(), "/operations/delivery-tasks/1", actor); + + // Registry + + [Fact] + public void EveryNotificationTypeConstant_HasADefinition() + { + var constants = typeof(NotificationTypes) + .GetFields() + .Where(f => f.IsLiteral && f.FieldType == typeof(string)) + .Select(f => (string)f.GetRawConstantValue()!) + .ToList(); + + Assert.NotEmpty(constants); + Assert.Equal(constants.Count, constants.Distinct().Count()); + Assert.All(constants, c => Assert.True(NotificationTypes.All.ContainsKey(c), $"{c} has no definition")); + } + + // NotifyAsync + + [Fact] + public async Task NotifyAsync_DoesNothing_WhenTypeIsNotRegistered() + { + await _sut.NotifyAsync("NotARealType", [RecipientRule.Users(Guid.NewGuid())], Args()); + + _repo.Verify(r => r.AddAsync(It.IsAny()), Times.Never); + _uow.Verify(u => u.CommitAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task NotifyAsync_DoesNothing_WhenThereAreNoRecipients() + { + await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users()], Args()); + + _repo.Verify(r => r.AddAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task NotifyAsync_ExcludesTheActingUser() + { + var actor = Guid.NewGuid(); + var other = Guid.NewGuid(); + Models.Notification? saved = null; + _repo.Setup(r => r.AddAsync(It.IsAny())) + .Callback(n => saved = n) + .Returns(Task.CompletedTask); + + await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, + [RecipientRule.Users(actor, other)], Args(actor)); + + Assert.NotNull(saved); + var recipient = Assert.Single(saved!.Recipients); + Assert.Equal(other, recipient.FK_UserId); + } + + [Fact] + public async Task NotifyAsync_RendersTemplate_CommitsAndPushesToEachRecipient() + { + var a = Guid.NewGuid(); + var b = Guid.NewGuid(); + Models.Notification? saved = null; + _repo.Setup(r => r.AddAsync(It.IsAny())) + .Callback(n => saved = n) + .Returns(Task.CompletedTask); + + await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(a, b)], Args()); + + Assert.Equal("Delivery to Fort Mini Mart failed.", saved!.Message); + Assert.Equal(Models.NotificationSeverity.Warning, saved.Severity); + _uow.Verify(u => u.CommitAsync(It.IsAny()), Times.Once); + _clients.Verify(c => c.User(a.ToString()), Times.Once); + _clients.Verify(c => c.User(b.ToString()), Times.Once); + _proxy.Verify(p => p.SendCoreAsync(NotificationHub.ClientEvent, + It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task NotifyAsync_DoesNotThrow_WhenSavingFails_AndDiscardsTheEntity() + { + _uow.Setup(u => u.CommitAsync(It.IsAny())).ThrowsAsync(new InvalidOperationException("db down")); + + var ex = await Record.ExceptionAsync(() => + _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())], Args())); + + Assert.Null(ex); + _repo.Verify(r => r.Discard(It.IsAny()), Times.Once); + _proxy.Verify(p => p.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task NotifyAsync_DoesNotThrow_WhenPushFails_AndKeepsTheSavedNotification() + { + _proxy.Setup(p => p.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("socket gone")); + + var ex = await Record.ExceptionAsync(() => + _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())], Args())); + + Assert.Null(ex); + _uow.Verify(u => u.CommitAsync(It.IsAny()), Times.Once); + _repo.Verify(r => r.Discard(It.IsAny()), Times.Never); + } + + // Recipient rules + + [Fact] + public async Task PermissionInScope_ResolvesToTheIntersectionOfPermissionAndScope() + { + var both = Guid.NewGuid(); + var permissionOnly = Guid.NewGuid(); + var scopeOnly = Guid.NewGuid(); + var storeId = Guid.NewGuid(); + _repo.Setup(r => r.GetUserIdsByPermissionAsync("store.manage")).ReturnsAsync([both, permissionOnly]); + _repo.Setup(r => r.GetUserIdsByScopeAsync(ScopeKind.Store, storeId)).ReturnsAsync([both, scopeOnly]); + + var ids = await new NotificationRecipientResolver(_repo.Object) + .ResolveAsync([RecipientRule.PermissionInScope("store.manage", ScopeKind.Store, storeId)], actingUserId: null); + + Assert.Equal([both], ids); + } + + [Fact] + public async Task ReportsTo_ResolvesToTheSupervisor_AndIsEmptyWhenThereIsNone() + { + var user = Guid.NewGuid(); + var supervisor = Guid.NewGuid(); + var resolver = new NotificationRecipientResolver(_repo.Object); + + _repo.Setup(r => r.GetReportsToUserIdAsync(user)).ReturnsAsync(supervisor); + Assert.Equal([supervisor], await resolver.ResolveAsync([RecipientRule.ReportsTo(user)], null)); + + _repo.Setup(r => r.GetReportsToUserIdAsync(user)).ReturnsAsync((Guid?)null); + Assert.Empty(await resolver.ResolveAsync([RecipientRule.ReportsTo(user)], null)); + } + + [Fact] + public async Task RouteOwner_ResolvesToTheOwner_AndIsEmptyWhenTheRouteHasNone() + { + var storeId = Guid.NewGuid(); + var owner = Guid.NewGuid(); + var resolver = new NotificationRecipientResolver(_repo.Object); + + _repo.Setup(r => r.GetRouteOwnerUserIdAsync(ScopeKind.Store, storeId)).ReturnsAsync(owner); + Assert.Equal([owner], await resolver.ResolveAsync([RecipientRule.RouteOwner(ScopeKind.Store, storeId)], null)); + + _repo.Setup(r => r.GetRouteOwnerUserIdAsync(ScopeKind.Store, storeId)).ReturnsAsync((Guid?)null); + Assert.Empty(await resolver.ResolveAsync([RecipientRule.RouteOwner(ScopeKind.Store, storeId)], null)); + } + + [Fact] + public async Task OptionalTokenAtTheEndOfATemplate_LeavesNoTrailingSpace() + { + Models.Notification? saved = null; + _repo.Setup(r => r.AddAsync(It.IsAny())) + .Callback(n => saved = n) + .Returns(Task.CompletedTask); + + await _sut.NotifyAsync(NotificationTypes.DeliveryTaskFailed, [RecipientRule.Users(Guid.NewGuid())], + new NotificationArgs(new { StoreName = "Fort Mini Mart", Note = (string?)null }, "DeliveryTask", Guid.NewGuid(), "/x")); + + Assert.Equal("Delivery to Fort Mini Mart failed.", saved!.Message); + } + + [Fact] + public async Task Resolve_DeduplicatesUsersAcrossRules() + { + var user = Guid.NewGuid(); + _repo.Setup(r => r.GetUserIdsByPermissionAsync("stock.manage")).ReturnsAsync([user]); + + var ids = await new NotificationRecipientResolver(_repo.Object) + .ResolveAsync([RecipientRule.Users(user), RecipientRule.Permission("stock.manage")], null); + + Assert.Equal([user], ids); + } +} diff --git a/Distribution/Migrations/20261020075213_AddNotifications.Designer.cs b/Distribution/Migrations/20261020075213_AddNotifications.Designer.cs new file mode 100644 index 0000000..2bd8aea --- /dev/null +++ b/Distribution/Migrations/20261020075213_AddNotifications.Designer.cs @@ -0,0 +1,5820 @@ +// +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("20261020075213_AddNotifications")] + partial class AddNotifications + { + /// + 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("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.Property("FinalizedAt") + .HasColumnType("datetime(6)"); + + b.Property("FinalizedBy") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PaymentStatus") + .HasColumnType("int"); + + b.Property("PrintedAt") + .HasColumnType("datetime(6)"); + + b.Property("PrintedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("Invoices"); + + b.HasData( + new + { + Id = new Guid("34500000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 8, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + FinalizedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FinalizedBy = new Guid("29000000-0000-0000-0000-000000000001"), + IsActive = true, + PaymentStatus = 2, + PrintedAt = new DateTime(2026, 8, 25, 8, 45, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000002"), + Status = 2, + TotalAmount = 3000m + }, + new + { + Id = new Guid("34500000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + PaymentStatus = 1, + PrintedAt = new DateTime(2026, 8, 31, 9, 15, 0, 0, DateTimeKind.Unspecified), + PrintedBy = new Guid("29000000-0000-0000-0000-000000000003"), + Status = 1, + TotalAmount = 2400m + }); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllowCustomWarrantyPeriod") + .HasColumnType("tinyint(1)"); + + b.Property("CostPrice") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ExternalErpItemId") + .HasColumnType("int"); + + b.Property("FK_BaseUomId") + .HasColumnType("char(36)"); + + b.Property("FK_BrandId") + .HasColumnType("char(36)"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("FK_SubCategoryId") + .HasColumnType("char(36)"); + + b.Property("HasWarranty") + .HasColumnType("tinyint(1)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReorderLevel") + .HasColumnType("decimal(65,30)"); + + b.Property("SalePrice") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SyncSource") + .HasColumnType("int"); + + b.Property("TracksBatches") + .HasColumnType("tinyint(1)"); + + b.Property("TracksExpiryDate") + .HasColumnType("tinyint(1)"); + + b.Property("TracksSerialNumbers") + .HasColumnType("tinyint(1)"); + + b.Property("WarrantyPeriodDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_BaseUomId"); + + b.HasIndex("FK_BrandId"); + + b.HasIndex("FK_CategoryId"); + + b.HasIndex("FK_SubCategoryId"); + + b.ToTable("Items"); + + b.HasData( + new + { + Id = new Guid("23000000-0000-0000-0000-000000000001"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 500ml bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 500ml", + SalePrice = 60m, + Sku = "BEV-COKE-500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000002"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Coca-Cola 1.5L bottle", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000001"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000001"), + HasWarranty = false, + IsActive = true, + Name = "Coca-Cola 1.5L", + SalePrice = 120m, + Sku = "BEV-COKE-1500", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000003"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Classic Salted 52g pack", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Classic 52g", + SalePrice = 50m, + Sku = "SNK-LAYS-52", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }, + new + { + Id = new Guid("23000000-0000-0000-0000-000000000004"), + AllowCustomWarrantyPeriod = false, + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + Description = "Lays Family Pack 150g", + FK_BaseUomId = new Guid("22000000-0000-0000-0000-000000000001"), + FK_BrandId = new Guid("21000000-0000-0000-0000-000000000002"), + FK_CategoryId = new Guid("20000000-0000-0000-0000-000000000002"), + HasWarranty = false, + IsActive = true, + Name = "Lays Family Pack 150g", + SalePrice = 150m, + Sku = "SNK-LAYS-FAM", + SyncSource = 0, + TracksBatches = false, + TracksExpiryDate = false, + TracksSerialNumbers = false + }); + }); + + modelBuilder.Entity("Distribution.Models.ItemBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BatchNumber") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ManufactureDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "BatchNumber"); + + b.ToTable("ItemBatches"); + }); + + modelBuilder.Entity("Distribution.Models.ItemSerialNumber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentLocationId") + .HasColumnType("char(36)"); + + b.Property("CurrentLocationType") + .HasColumnType("int"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerialNumber") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("WarrantyExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("WarrantyPeriodDaysOverride") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId", "SerialNumber") + .IsUnique(); + + b.ToTable("ItemSerialNumbers"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_RideId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("PopulatedAt") + .HasColumnType("datetime(6)"); + + b.Property("PopulatedBy") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_RideId"); + + b.ToTable("LoadingSheets"); + + b.HasData( + new + { + Id = new Guid("35000000-0000-0000-0000-000000000001"), + ConfirmedAt = new DateTime(2026, 8, 25, 7, 45, 0, 0, DateTimeKind.Unspecified), + ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_RideId = new Guid("31000000-0000-0000-0000-000000000001"), + IsActive = true, + PopulatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + PopulatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + Status = 1 + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOItemId") + .HasColumnType("char(36)"); + + b.Property("FK_WarehouseId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("QuantityDispatched") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_LoadingSheetId"); + + b.HasIndex("FK_ShopPOItemId"); + + b.HasIndex("FK_WarehouseId"); + + b.ToTable("LoadingSheetItems"); + + b.HasData( + new + { + Id = new Guid("36000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 50m + }, + new + { + Id = new Guid("36000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_LoadingSheetId = new Guid("35000000-0000-0000-0000-000000000001"), + FK_WarehouseId = new Guid("27000000-0000-0000-0000-000000000001"), + IsActive = true, + QuantityDispatched = 30m + }); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheetOrder", b => + { + b.Property("FK_LoadingSheetId") + .HasColumnType("char(36)"); + + b.Property("FK_ShopPOId") + .HasColumnType("char(36)"); + + b.HasKey("FK_LoadingSheetId", "FK_ShopPOId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("LoadingSheetOrders"); + }); + + modelBuilder.Entity("Distribution.Models.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("e748cecd-7b03-41ba-96f4-e7e80d8b39f5"), + 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("6ec338c9-dbe1-40b0-895c-228bcb439815"), + 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("eb390164-72ff-4b03-8d09-184f25fe2e0b"), + 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("e8b5c2b0-cdba-42f0-b1f0-32a892036c0a"), + 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("48340fe7-3037-4ab1-8800-59763bce0855"), + 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("d43d5a8b-4a0a-4a7a-9b34-7c0561ed9303"), + 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("41c7bd2c-0253-4315-8594-811aec35f15c"), + 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("87052d54-7319-473e-a46f-62d6477ef7d8"), + 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("c0b0185e-3530-4cc8-9e6b-b51cd1187dda"), + 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("6e649c16-703b-4420-ae23-8840d1dc247b"), + 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("efe29c4c-353a-4b25-9279-6566c8ca1b3d"), + 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("3aa9f6cd-0075-4ab2-88c4-c49e177d443a"), + 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("258dfadf-292e-410c-a173-217983becc2b"), + 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("c253907f-f84e-4ea5-af4a-0db56b89b82a"), + 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("c47972c0-6a5f-47cf-ae30-cbf753a6fe94"), + 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("ee4b11f3-78d4-4e6f-bb22-95f3f18be7e5"), + 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("a0a39f4e-95c9-42bc-9803-a510fa4863d7"), + 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("29e55c7f-92cb-47fd-b05c-ce038284ff39"), + 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("7b675ce2-3949-4e1a-ad6e-e1a047d84ece"), + 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("835c1000-3998-444c-b9bf-53b2ef810437"), + 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("3c9d0a5d-059e-4e0b-b1a8-698ce2a076ae"), + 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("f12fc85a-5a91-4e23-81de-0b497d4bc246"), + 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("08005497-616c-4279-aa8a-162762d57a88"), + 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("666a9e41-1c11-4af3-8f72-c80ba3734a48"), + 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("87cd58d6-68f2-4de0-a0d8-5951a2281184"), + 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("a7a0cb93-598b-4e21-9bab-ae5ba4da71d3"), + 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("86bf0611-4798-4209-a5e4-a0064a5e95cd"), + 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("bd386f32-0f7a-4954-9e85-15956b4fb718"), + 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("9dd219a4-da77-49f5-8ea1-097dbba231ea"), + 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("5caf3706-bde2-43bf-b9a8-d351500e6cba"), + 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("63ba1d73-9819-4471-a89b-1eacade54d84"), + 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("58398d5c-4687-4a7f-a205-d6a3ff365c6f"), + 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("72761e2e-cd4f-44d1-b446-d5ab974374d4"), + 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("66c1a66f-5d2d-4532-8f47-441a9a9df5c3"), + 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("6e37280f-b1cc-4aba-a8df-2f42ee1958a0"), + 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("dd4149c0-8746-4ceb-92c2-27cec640fca2"), + 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("e51495d1-bb65-4345-8156-73c791589d1f"), + 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("ccf5af23-acd7-4bdb-a2ea-ec409a970add"), + 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("11e001a5-7a47-4778-996b-0f2aa1e4ae73"), + 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("6b1edf33-51c0-4cae-ba2f-9737e0c91e42"), + 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("0a54915c-1660-4657-96fb-e55d01e8a31a"), + 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("646a6638-8520-4fd8-8608-aa7e29bb874d"), + 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("8fe75d07-1c6c-4019-becc-d5e82f2cb777"), + 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("05732dc6-a967-481b-b055-76ff68c5728e"), + 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("864da17e-470f-41f5-a053-07f112c090ea"), + 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("f8c31d84-f57f-4515-86f2-733442d9ea14"), + 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("ba7e7d95-51f7-4ebb-b911-9bc18a104a39"), + 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("b4cfa315-1b8a-4f61-af60-5389391f973c"), + 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("a03e5eff-1925-4ddf-904b-32be0daed97b"), + 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("be5ff487-33d9-4c63-81f4-fd7f6553c5bf"), + 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_ShopPOId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("QuantityFulfilled") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ShopPOId"); + + b.ToTable("ShopPOItems"); + + b.HasData( + new + { + Id = new Guid("34000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 30, + QuantityFulfilled = 30, + UnitPrice = 60m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"), + IsActive = true, + Quantity = 24, + QuantityFulfilled = 24, + UnitPrice = 50m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"), + IsActive = true, + Quantity = 10, + QuantityFulfilled = 0, + UnitPrice = 120m + }, + new + { + Id = new Guid("34000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"), + IsActive = true, + Quantity = 20, + QuantityFulfilled = 20, + UnitPrice = 120m + }); + }); + + modelBuilder.Entity("Distribution.Models.StockBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + + b.Property("LocationId") + .HasColumnType("char(36)"); + + b.Property("LocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemBatchId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.HasIndex("FK_ItemId", "LocationId", "FK_ItemBatchId", "FK_ItemSerialNumberId") + .IsUnique(); + + b.ToTable("StockBalances"); + + b.HasData( + new + { + Id = new Guid("61000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 150m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"), + LocationId = new Guid("27000000-0000-0000-0000-000000000001"), + LocationType = 0, + Quantity = 90m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = new Guid("61000000-0000-0000-0000-000000000003"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + LocationId = new Guid("28000000-0000-0000-0000-000000000001"), + LocationType = 1, + Quantity = 20m, + UpdatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("Distribution.Models.StockMovement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("char(36)"); + + b.Property("FK_ItemBatchId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_ItemSerialNumberId") + .HasColumnType("char(36)"); + + b.Property("FromLocationId") + .HasColumnType("char(36)"); + + b.Property("FromLocationType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("ReasonCode") + .HasColumnType("longtext"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.Property("ToLocationId") + .HasColumnType("char(36)"); + + b.Property("ToLocationType") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemBatchId"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_ItemSerialNumberId"); + + b.ToTable("StockMovements"); + + b.HasData( + new + { + Id = new Guid("62000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + FromLocationType = 2, + Quantity = 200m, + ReferenceId = new Guid("43000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + ToLocationType = 0, + Type = 6 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 25, 7, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000001"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("27000000-0000-0000-0000-000000000001"), + FromLocationType = 0, + Quantity = 50m, + ReferenceId = new Guid("35000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + ToLocationType = 1, + Type = 5 + }, + new + { + Id = new Guid("62000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified), + CreatedBy = new Guid("29000000-0000-0000-0000-000000000002"), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"), + FromLocationId = new Guid("28000000-0000-0000-0000-000000000001"), + FromLocationType = 1, + Quantity = 30m, + ReferenceId = new Guid("32000000-0000-0000-0000-000000000001"), + ToLocationId = new Guid("00000000-0000-0000-0000-000000000000"), + ToLocationType = 2, + Type = 0 + }); + }); + + modelBuilder.Entity("Distribution.Models.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreditLimit") + .HasColumnType("decimal(65,30)"); + + b.Property("FK_RouteId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Latitude") + .HasColumnType("decimal(65,30)"); + + b.Property("Longitude") + .HasColumnType("decimal(65,30)"); + + b.Property("OneTimeAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("OutstandingBalance") + .HasColumnType("decimal(65,30)"); + + b.Property("OwnerName") + .HasColumnType("longtext"); + + b.Property("Segment") + .HasColumnType("longtext"); + + b.Property("StoreAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StoreName") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_RouteId"); + + b.ToTable("Stores"); + + b.HasData( + new + { + Id = new Guid("26000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 50000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.9344m, + Longitude = 79.8428m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Saman Kumara", + Segment = "Gold", + StoreAddress = "12 York Street, Colombo Fort", + StoreName = "Fort Mini Mart" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000002"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 30000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000001"), + IsActive = true, + Latitude = 6.8508m, + Longitude = 79.8654m, + OneTimeAmount = 0m, + OutstandingBalance = 5000m, + OwnerName = "Nadeesha Silva", + Segment = "Silver", + StoreAddress = "45 Galle Road, Dehiwala", + StoreName = "Dehiwala Grocery" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000003"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 40000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 6.9890m, + Longitude = 79.8912m, + OneTimeAmount = 0m, + OutstandingBalance = 1200m, + OwnerName = "Ruwan Gunasekara", + Segment = "Gold", + StoreAddress = "88 Negombo Road, Wattala", + StoreName = "Wattala Supermarket" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000004"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 25000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000002"), + IsActive = true, + Latitude = 7.2094m, + Longitude = 79.8380m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Chamari Peiris", + StoreAddress = "5 Beach Road, Negombo", + StoreName = "Negombo Beach Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000005"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 35000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000003"), + IsActive = true, + Latitude = 7.2906m, + Longitude = 80.6337m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Dinesh Wickramasinghe", + Segment = "Key Account", + StoreAddress = "3 Lake Road, Kandy", + StoreName = "Kandy Lake View Store" + }, + new + { + Id = new Guid("26000000-0000-0000-0000-000000000006"), + CreatedAt = new DateTime(2026, 8, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), + CreditAmount = 0m, + CreditLimit = 20000m, + FK_RouteId = new Guid("25000000-0000-0000-0000-000000000004"), + IsActive = true, + Latitude = 7.4675m, + Longitude = 80.6234m, + OneTimeAmount = 0m, + OutstandingBalance = 0m, + OwnerName = "Ishara Bandara", + StoreAddress = "20 Main Street, Matale", + StoreName = "Matale Central Store" + }); + }); + + modelBuilder.Entity("Distribution.Models.SubCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExternalErpId") + .HasColumnType("int"); + + b.Property("FK_CategoryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FK_CategoryId"); + + b.ToTable("SubCategories"); + }); + + modelBuilder.Entity("Distribution.Models.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactInfo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PerformanceScore") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.ToTable("Suppliers"); + + b.HasData( + new + { + Id = new Guid("38000000-0000-0000-0000-000000000001"), + ContactInfo = "+94 11 234 5678", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Ceylon Beverages Ltd", + PerformanceScore = 4.2m + }, + new + { + Id = new Guid("38000000-0000-0000-0000-000000000002"), + ContactInfo = "+94 11 876 5432", + CreatedAt = new DateTime(2026, 8, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), + IsActive = true, + Name = "Snack Foods (Pvt) Ltd" + }); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EvidenceUrl") + .HasColumnType("longtext"); + + b.Property("FK_GoodsReceiptId") + .HasColumnType("char(36)"); + + b.Property("FK_PurchaseOrderId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("RaisedByUserId") + .HasColumnType("char(36)"); + + b.Property("ResolutionAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("ResolutionNotes") + .HasColumnType("longtext"); + + b.Property("ResolutionType") + .HasColumnType("int"); + + b.Property("ResolvedAt") + .HasColumnType("datetime(6)"); + + b.Property("ResolvedByUserId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FK_GoodsReceiptId"); + + b.HasIndex("FK_PurchaseOrderId"); + + b.HasIndex("RaisedByUserId"); + + b.HasIndex("ResolvedByUserId"); + + b.ToTable("SupplierClaims"); + + b.HasData( + new + { + Id = new Guid("55000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + Details = "2 units of Coke 1.5L received damaged", + FK_GoodsReceiptId = new Guid("43000000-0000-0000-0000-000000000001"), + FK_PurchaseOrderId = new Guid("41000000-0000-0000-0000-000000000001"), + IsActive = true, + RaisedByUserId = new Guid("29000000-0000-0000-0000-000000000001"), + ResolutionAmount = 160m, + ResolutionNotes = "Credited against next invoice", + ResolutionType = 1, + ResolvedAt = new DateTime(2026, 8, 16, 0, 0, 0, 0, DateTimeKind.Unspecified), + ResolvedByUserId = new Guid("29000000-0000-0000-0000-000000000006"), + Status = 1 + }); + }); + + modelBuilder.Entity("Distribution.Models.SupplierClaimItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FK_ItemId") + .HasColumnType("char(36)"); + + b.Property("FK_SupplierClaimId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IssueType") + .HasColumnType("int"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.HasKey("Id"); + + b.HasIndex("FK_ItemId"); + + b.HasIndex("FK_SupplierClaimId"); + + b.ToTable("SupplierClaimItems"); + + b.HasData( + new + { + Id = new Guid("56000000-0000-0000-0000-000000000001"), + CreatedAt = new DateTime(2026, 8, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), + FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"), + FK_SupplierClaimId = new Guid("55000000-0000-0000-0000-000000000001"), + IsActive = true, + IssueType = 0, + Quantity = 2m + }); + }); + + modelBuilder.Entity("Distribution.Models.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.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.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("Payments"); + }); + + modelBuilder.Entity("Distribution.Models.Item", b => + { + b.Navigation("Conversions"); + }); + + modelBuilder.Entity("Distribution.Models.LoadingSheet", b => + { + b.Navigation("Items"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Distribution.Models.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/20261020075213_AddNotifications.cs b/Distribution/Migrations/20261020075213_AddNotifications.cs new file mode 100644 index 0000000..48d4bc6 --- /dev/null +++ b/Distribution/Migrations/20261020075213_AddNotifications.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Distribution.Migrations +{ + /// + public partial class AddNotifications : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Type = table.Column(type: "varchar(60)", maxLength: 60, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Severity = table.Column(type: "int", nullable: false), + Title = table.Column(type: "varchar(150)", maxLength: 150, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Message = table.Column(type: "varchar(500)", maxLength: 500, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EntityType = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + EntityId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Link = table.Column(type: "varchar(200)", maxLength: 200, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedByUserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "NotificationRecipients", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_NotificationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FK_UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + IsRead = table.Column(type: "tinyint(1)", nullable: false), + ReadAt = table.Column(type: "datetime(6)", nullable: true), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsActive = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_NotificationRecipients", x => x.Id); + table.ForeignKey( + name: "FK_NotificationRecipients_Notifications_FK_NotificationId", + column: x => x.FK_NotificationId, + principalTable: "Notifications", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_NotificationRecipients_Users_FK_UserId", + column: x => x.FK_UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_NotificationRecipients_FK_NotificationId_FK_UserId", + table: "NotificationRecipients", + columns: new[] { "FK_NotificationId", "FK_UserId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_NotificationRecipients_FK_UserId_IsRead", + table: "NotificationRecipients", + columns: new[] { "FK_UserId", "IsRead" }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_EntityType_EntityId", + table: "Notifications", + columns: new[] { "EntityType", "EntityId" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "NotificationRecipients"); + + migrationBuilder.DropTable( + name: "Notifications"); + + } + } +} diff --git a/Distribution/Migrations/AppDBContextModelSnapshot.cs b/Distribution/Migrations/AppDBContextModelSnapshot.cs index 0b3f708..6c1be9d 100644 --- a/Distribution/Migrations/AppDBContextModelSnapshot.cs +++ b/Distribution/Migrations/AppDBContextModelSnapshot.cs @@ -1278,6 +1278,93 @@ namespace Distribution.Migrations 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") @@ -2528,7 +2615,7 @@ namespace Distribution.Migrations 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("30f658ac-2c92-4cd8-ad47-10b0def2699c"), + Id = new Guid("1ce4ca63-cda9-4e52-a33a-d9f817b3af42"), IsActive = true }, new @@ -2536,7 +2623,7 @@ namespace Distribution.Migrations 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("42ab2c52-eaf8-4a17-b67a-ccd66569e56a"), + Id = new Guid("fd551395-791e-4ec0-8acc-90fee0fdf0e4"), IsActive = true }, new @@ -2544,7 +2631,7 @@ namespace Distribution.Migrations 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("8afec825-27b3-4153-ad06-f8bcdbf93da8"), + Id = new Guid("eed0b3b9-c142-4d85-a03c-a4d5047ed839"), IsActive = true }, new @@ -2552,7 +2639,7 @@ namespace Distribution.Migrations 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("4b427620-e99d-4068-a416-c3788d5bd9b5"), + Id = new Guid("fe36f257-7b58-4afe-a950-33e54623a3b0"), IsActive = true }, new @@ -2560,7 +2647,7 @@ namespace Distribution.Migrations 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("361f44ef-1cd1-48a9-be4d-4c068aacef1e"), + Id = new Guid("b7fd5318-8487-4b86-9651-cdafbcd20776"), IsActive = true }, new @@ -2568,7 +2655,7 @@ namespace Distribution.Migrations 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("11fc03ae-92ee-4336-ae57-29aac731ad51"), + Id = new Guid("8f84fb31-063f-49b7-bec8-3a65f1bbc66e"), IsActive = true }, new @@ -2576,7 +2663,7 @@ namespace Distribution.Migrations 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("0b1f2b73-ef60-4634-864f-22a6c0cd1373"), + Id = new Guid("8f181d4a-4eab-4b3a-88bf-a228f544969a"), IsActive = true }, new @@ -2584,7 +2671,7 @@ namespace Distribution.Migrations 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("2380abf3-2bcd-4a9f-b447-5f1685df2fc8"), + Id = new Guid("fd54d83b-b79c-4c47-bc0d-7c728cab064f"), IsActive = true }, new @@ -2592,7 +2679,7 @@ namespace Distribution.Migrations 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("240e4d6a-8306-40ba-aaec-8e05cd0347db"), + Id = new Guid("4306aada-781c-4618-b10c-a937b4760892"), IsActive = true }, new @@ -2600,7 +2687,7 @@ namespace Distribution.Migrations 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("cfde0aa6-e859-4f9c-8291-b0d886fcd4c9"), + Id = new Guid("4b74304c-e3a0-4366-9eca-91cb71ba4553"), IsActive = true }, new @@ -2608,7 +2695,7 @@ namespace Distribution.Migrations 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("dae772c8-26ec-4b5c-8d7a-f3196510bf2c"), + Id = new Guid("ef3b60c8-5698-4757-93c7-ce54759f3c7b"), IsActive = true }, new @@ -2616,7 +2703,7 @@ namespace Distribution.Migrations 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("3512c0a4-a5ae-4002-a779-7d3cc600ab61"), + Id = new Guid("90761b9f-0244-43da-bec6-c2e1e9c9103e"), IsActive = true }, new @@ -2624,7 +2711,7 @@ namespace Distribution.Migrations 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("f9e2a348-159e-47a2-b7be-a60f0254873f"), + Id = new Guid("6bd386df-32b4-434b-8d16-7f1555f92848"), IsActive = true }, new @@ -2632,7 +2719,7 @@ namespace Distribution.Migrations 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("c8b4c181-5b08-45d2-818b-74bceeab78a8"), + Id = new Guid("5e64183c-9fe3-48f1-a236-91df9de1378a"), IsActive = true }, new @@ -2640,7 +2727,7 @@ namespace Distribution.Migrations 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("8fb4ff4c-c5bd-4afd-a08e-02861d63cb7b"), + Id = new Guid("b0e3f065-ccbe-4a6a-9c29-29986868e587"), IsActive = true }, new @@ -2648,7 +2735,7 @@ namespace Distribution.Migrations 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("edb3b711-d623-439d-811e-afaef8d0680d"), + Id = new Guid("cd191ac2-c1a5-454f-80a6-ffa5770a3699"), IsActive = true }, new @@ -2656,7 +2743,7 @@ namespace Distribution.Migrations 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("f33e4c62-d69f-4a91-8b62-acbd5351ea2a"), + Id = new Guid("dba4d3dd-60a1-4445-a327-22026ca35f77"), IsActive = true }, new @@ -2664,7 +2751,7 @@ namespace Distribution.Migrations 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("c7c9d16c-e343-43e9-9e27-f38f903a5d00"), + Id = new Guid("c6a75c84-7992-46b5-a1f9-cdf1bf3e77a4"), IsActive = true }, new @@ -2672,7 +2759,7 @@ namespace Distribution.Migrations 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("f5f4760a-8a45-46ca-8093-743c95b0313f"), + Id = new Guid("a5d1be1b-2fe9-49f9-9256-9da61a7589ae"), IsActive = true }, new @@ -2680,7 +2767,7 @@ namespace Distribution.Migrations 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("c5434801-146a-4fd9-9c29-a1145e7f7f00"), + Id = new Guid("bdbeb46e-3a54-4ae2-90a6-e9b4450b83d3"), IsActive = true }, new @@ -2688,7 +2775,7 @@ namespace Distribution.Migrations 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("6c085135-78a5-4e65-a380-ee090b07e2bb"), + Id = new Guid("eeab7811-ff26-4e4f-9813-6c10cd6c6504"), IsActive = true }, new @@ -2696,7 +2783,7 @@ namespace Distribution.Migrations 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("bbde2785-a3df-4f81-bad1-0bc5afe5de57"), + Id = new Guid("c31fc81f-3e25-422f-81dd-dc6621aad5a4"), IsActive = true }, new @@ -2704,7 +2791,7 @@ namespace Distribution.Migrations 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("4421a0fb-bdd4-45a9-8dc0-d9b70f775e97"), + Id = new Guid("11d9afa6-fecd-494a-9264-cd57f7edc552"), IsActive = true }, new @@ -2712,7 +2799,7 @@ namespace Distribution.Migrations 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("dba9dcb4-3b94-4ddc-89f3-552cfc109478"), + Id = new Guid("3e704029-4b4b-40b0-b202-97fbf266ec31"), IsActive = true }, new @@ -2720,7 +2807,7 @@ namespace Distribution.Migrations 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("d8f74160-c150-4d9c-aba6-e45e628c684e"), + Id = new Guid("fcaea017-aae4-4a42-92d2-da2b994169d9"), IsActive = true }, new @@ -2728,7 +2815,7 @@ namespace Distribution.Migrations 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("495fb68b-9f01-4538-9561-fefce0d39558"), + Id = new Guid("a58856db-d72b-4d06-a56b-b34861e94551"), IsActive = true }, new @@ -2736,7 +2823,7 @@ namespace Distribution.Migrations 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("3bdece9b-a798-4223-a23c-9c9dd10d042d"), + Id = new Guid("2795cbf1-de83-48d2-b955-818e30d31659"), IsActive = true }, new @@ -2744,7 +2831,7 @@ namespace Distribution.Migrations 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("6fa616eb-99fe-46b9-8305-908765d037fd"), + Id = new Guid("cd60d268-87bf-44a0-9414-cfbd7dc8b30c"), IsActive = true }, new @@ -2752,7 +2839,7 @@ namespace Distribution.Migrations 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("7a07148a-1327-4e97-befc-b2045eacad21"), + Id = new Guid("7b435bfb-7fd4-4f26-a746-df77fbb69b64"), IsActive = true }, new @@ -2760,7 +2847,7 @@ namespace Distribution.Migrations 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("852e86d7-aa57-4a77-81c0-054f3fb4d7f7"), + Id = new Guid("519222bb-2021-42ec-a888-ffbbef3ebebf"), IsActive = true }, new @@ -2768,7 +2855,7 @@ namespace Distribution.Migrations 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("3b767929-c973-41fb-a340-4ee5feb65619"), + Id = new Guid("e629ea0e-70e6-4ff5-8f5e-dffbcac162ba"), IsActive = true }, new @@ -2776,7 +2863,7 @@ namespace Distribution.Migrations 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("63619bbd-0352-41bd-9e1c-a3f7908fb5d2"), + Id = new Guid("34d421af-e9cb-4eb5-ab13-afd5d26749f9"), IsActive = true }, new @@ -2784,7 +2871,7 @@ namespace Distribution.Migrations 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("f6b4c8bf-a9b3-4179-9f57-a8584678c0d1"), + Id = new Guid("9b2af108-27a1-4cfc-a0d5-ad6a54ef9804"), IsActive = true }, new @@ -2792,7 +2879,7 @@ namespace Distribution.Migrations 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("bb47b8ca-f7cf-40fe-8945-fc532a439b53"), + Id = new Guid("431ed966-2f97-45bb-a28e-330ebfbd90ee"), IsActive = true }, new @@ -2800,7 +2887,7 @@ namespace Distribution.Migrations 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("52126271-ae3b-4b58-a9d2-50e2dd894087"), + Id = new Guid("11ed5a9f-a14f-4e7a-ade1-d9b39e30c1e5"), IsActive = true }, new @@ -2808,7 +2895,7 @@ namespace Distribution.Migrations 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("981e3358-85bb-47fe-a1f5-3a3a02cf47b1"), + Id = new Guid("ec51a647-7a5a-4bab-a44c-aadced5448ea"), IsActive = true }, new @@ -2816,7 +2903,7 @@ namespace Distribution.Migrations 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("749408da-fcee-458d-9144-8d8dee928ba0"), + Id = new Guid("87cd6a37-e18a-4c43-aeeb-ab6d7f866e6b"), IsActive = true }, new @@ -2824,7 +2911,7 @@ namespace Distribution.Migrations 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("c157857c-fb0e-41fd-a414-c0f57a44b120"), + Id = new Guid("ca2ad12a-65d0-41b7-9ae8-9ac8df117d01"), IsActive = true }, new @@ -2832,7 +2919,7 @@ namespace Distribution.Migrations 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("3217cdbc-76c9-4505-b9f3-23de91a6212b"), + Id = new Guid("817d6c94-11c3-4733-9fe5-c19f56316200"), IsActive = true }, new @@ -2840,7 +2927,7 @@ namespace Distribution.Migrations 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("63526c52-18ba-40e0-9889-3bd3b7fc8161"), + Id = new Guid("5b7f95c1-ddb3-4ce8-b62f-11f3c37c44b2"), IsActive = true }, new @@ -2848,7 +2935,7 @@ namespace Distribution.Migrations 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("260cf2fa-1ca6-4cd5-92ae-8c479a80e2f0"), + Id = new Guid("a84f7909-8f45-4ca3-a10f-2fc17680dfc0"), IsActive = true }, new @@ -2856,7 +2943,7 @@ namespace Distribution.Migrations 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("130ab09c-8a36-4705-8235-9d00f75dd684"), + Id = new Guid("2af26741-1110-4853-82aa-70639060c087"), IsActive = true }, new @@ -2864,7 +2951,7 @@ namespace Distribution.Migrations 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("41794774-e919-4f4e-a42a-5ae408d3407f"), + Id = new Guid("f2267efb-00c3-49f2-8f5d-c9cabd6cc1ec"), IsActive = true }, new @@ -2872,7 +2959,7 @@ namespace Distribution.Migrations 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("2ff23d04-d1f6-4aa3-a48e-1c82bb9ad12a"), + Id = new Guid("6026dc21-65d6-4754-8e08-7a8588b41020"), IsActive = true }, new @@ -2880,7 +2967,7 @@ namespace Distribution.Migrations 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("d36c81c0-8e9b-41ea-8e09-464a327a16d2"), + Id = new Guid("769408c0-fa97-4234-9ea8-a4e5a1a847be"), IsActive = true }, new @@ -2888,7 +2975,7 @@ namespace Distribution.Migrations 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("6c51dfc1-63e0-46a6-b2bb-27c21c708b51"), + Id = new Guid("bace0a50-82ca-4d44-88f2-d0ecef753b0a"), IsActive = true }, new @@ -2896,7 +2983,7 @@ namespace Distribution.Migrations 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("076333d5-b1f3-4b89-86fd-5a61a4451c88"), + Id = new Guid("9043c1e0-cf7b-4ac3-affd-52b0706d1753"), IsActive = true }, new @@ -2904,7 +2991,7 @@ namespace Distribution.Migrations 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("7e834f3e-1424-4aaa-816e-d62da6df672b"), + Id = new Guid("b4572222-2bcc-4d18-aa82-d5fdb2a3f4ae"), IsActive = true }, new @@ -2912,7 +2999,7 @@ namespace Distribution.Migrations 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("699fa06b-a7ff-4dbd-98a7-a29c2c361665"), + Id = new Guid("2f6ab72b-a3f2-4a47-8681-0ee575c9998f"), IsActive = true }, new @@ -2920,7 +3007,7 @@ namespace Distribution.Migrations 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("00df5735-54c6-4cdf-8e2c-a64cd90ede15"), + Id = new Guid("395971d4-1369-45d4-8b54-28c1a33b7e0c"), IsActive = true }); }); @@ -4967,6 +5054,25 @@ namespace Distribution.Migrations 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") @@ -5659,6 +5765,11 @@ namespace Distribution.Migrations b.Navigation("Orders"); }); + modelBuilder.Entity("Distribution.Models.Notification", b => + { + b.Navigation("Recipients"); + }); + modelBuilder.Entity("Distribution.Models.PriceList", b => { b.Navigation("Items"); diff --git a/Distribution/Models/Notification.cs b/Distribution/Models/Notification.cs new file mode 100644 index 0000000..834eec7 --- /dev/null +++ b/Distribution/Models/Notification.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace Distribution.Models +{ + public enum NotificationSeverity { Info, Success, Warning, Critical } + + /// One row per event. Who it was delivered to lives in . + public class Notification : Template + { + [MaxLength(60)] + public string Type { get; set; } = string.Empty; + + public NotificationSeverity Severity { get; set; } + + [MaxLength(150)] + public string Title { get; set; } = string.Empty; + + [MaxLength(500)] + public string Message { get; set; } = string.Empty; + + [MaxLength(50)] + public string EntityType { get; set; } = string.Empty; + + public Guid EntityId { get; set; } + + [MaxLength(200)] + public string Link { get; set; } = string.Empty; + + public Guid? CreatedByUserId { get; set; } + + public ICollection Recipients { get; set; } = new List(); + } +} diff --git a/Distribution/Models/NotificationRecipient.cs b/Distribution/Models/NotificationRecipient.cs new file mode 100644 index 0000000..929320e --- /dev/null +++ b/Distribution/Models/NotificationRecipient.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Distribution.Models +{ + /// One row per user per notification, so read state is per user. + public class NotificationRecipient : Template + { + public Guid FK_NotificationId { get; set; } + [ForeignKey("FK_NotificationId")] + public Notification Notification { get; set; } + + public Guid FK_UserId { get; set; } + [ForeignKey("FK_UserId")] + public User User { get; set; } + + public bool IsRead { get; set; } + public DateTime? ReadAt { get; set; } + } +} diff --git a/Distribution/Program.cs b/Distribution/Program.cs index 595e772..87be043 100644 --- a/Distribution/Program.cs +++ b/Distribution/Program.cs @@ -12,6 +12,7 @@ using Distribution.Features.ReceivedCheque; using Distribution.Features.DriverComplianceConfig; using Distribution.Features.DriverProfile; using Distribution.Features.Grn; +using Distribution.Features.Notifications; using Distribution.Features.Item; using Distribution.Features.ShopPO; using Distribution.Features.Invoice; @@ -141,6 +142,15 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => sp.GetRequiredService()); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Services.AddSignalR() + .AddJsonProtocol(options => options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter())); + builder.Services.AddMemoryCache(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); @@ -192,5 +202,6 @@ app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); +app.MapHub(NotificationHub.Path); app.Run(); diff --git a/Distribution/docs/21_notifications_alerts.md b/Distribution/docs/21_notifications_alerts.md index c339b8d..8df94b9 100644 --- a/Distribution/docs/21_notifications_alerts.md +++ b/Distribution/docs/21_notifications_alerts.md @@ -1,67 +1,330 @@ # 21 — Notifications & Alerts -**Status**: Not Started -**Depends On**: 02 — Stock Ledger, 12 — Store Management, 08 — Sales Agent Controls +**Status**: v1 in-app built — backend, SignalR hub, frontend bell + view-all page, 10 unit tests. EF migration `AddNotifications` is generated but **not applied** to any database. Six of the seven Phase 1 triggers are wired into the services (see **Triggers**); `StockLow` is deferred. The temporary test endpoint and button were removed. +**Depends On**: 02 — Stock Ledger, 12 — Store Management, 08 — Sales Agent Controls, 15 — Procurement, 18 — Returns & Claims, 19 — Finance & Accounting --- ## Goal -Covers [mainPlan.md §15](./mainPlan.md#15-notifications--alerts) in full -— a cross-cutting delivery mechanism (email/SMS/push) triggered by events other steps already -produce. +Real-time **in-app** notifications (bell + toast) triggered by events other modules already produce +([mainPlan.md §15](./mainPlan.md#15-notifications--alerts)). Email / SMS / push are out of scope for v1. + +Real-time uses **SignalR**. A notification is **saved first, pushed second**, so offline users see +what they missed on their next load. --- -## Background / Context +## Implemented now — who gets which notification -No notification channel exists in this codebase today. This step is purely the delivery mechanism -and trigger wiring; the underlying conditions it watches for are already defined elsewhere -(reorder point in [02_stock_ledger.md](./02_stock_ledger.md), credit limit in -[12_store_management.md](./12_store_management.md), route deviation logic in -[09_fraud_controls.md](./09_fraud_controls.md)). +Every notification is **in-app only** (bell + toast + `/notifications`), pushed live over SignalR. + +| # | When this happens | Where in the app | Notification the user sees | Who receives it | Who does **not** | Level | Click opens | +|---|---|---|---|---|---|---|---| +| 1 | Shop order **confirmed** | `ShopPOService.ConfirmAsync`, `ConfirmAndCreateInvoiceAsync` | **Shop order confirmed** — "Shop order for *Store* was confirmed." | The user who raised the order + that user's supervisor | The person who confirmed it | Info | `/sales/orders` | +| 2 | Shop order **cancelled** | `ShopPOService.CancelAsync` | **Shop order cancelled** — "Shop order for *Store* was cancelled: *reason*" | The user who raised the order + that user's supervisor | The person who cancelled it | Warning | `/sales/orders` | +| 3 | Supplier claim **approved** | `SupplierClaimService.ApproveAsync` | **Supplier claim approved** | The user who raised the claim | The approver | Success | `/returns-claims/supplier-claims` | +| 4 | Supplier claim **rejected** | `SupplierClaimService.RejectAsync` | **Supplier claim rejected** | The user who raised the claim | The person who rejected it | Warning | `/returns-claims/supplier-claims` | +| 5 | **Driver assigned** to a ride | `RideService.AssignDriverAsync` | **Ride assigned** — "You have been assigned to a ride." | The assigned driver | Everyone else | Info | `/operations/rides` | +| 6 | Ride **cancelled** | `RideService.CancelAsync` | **Ride cancelled** — "A ride was cancelled (*type*). *reason*" | The owner of every route in the ride, plus users with `delivery-task.manage` whose scope covers those routes | The person who cancelled it | Critical | `/operations/rides` | +| 7 | Delivery **failed** | `DeliveryTaskService.FailAsync` | **Delivery failed** — "Delivery to *Store* failed. *note*" | The owner of the store's route, plus users with `delivery-task.manage` whose scope covers the store | Nobody is excluded (this action does not record who did it) | Warning | `/operations/deliveries` | +| 8 | Delivery **partly delivered** | `DeliveryTaskService.ConfirmAsync` | **Partial delivery** — "Delivery to *Store* was only partly delivered." | Same as #7 | The person who confirmed it | Warning | `/operations/deliveries` | +| 9 | Store **credit limit crossed** | `StoreService.UpdateOutstandingBalanceAsync` | **Credit limit exceeded** — "*Store* has exceeded its credit limit." | The owner of the store's route, plus users with `store.manage` whose scope covers the store | The person who updated the balance. Fires only when the balance goes from within the limit to above it | Warning | `/stores/{id}` | + +**Not wired yet:** low stock (`StockLow`), cheque returned / bounced / due, expiring batches, overdue payments, and the Phase 2 list below. + +### Three rules that decide who sees what + +1. **You are never notified of your own action.** +2. **Receiving needs no permission** — any logged-in user gets the bell and the page. Permissions only matter for *doing* the action, and for the "managers in scope" recipients (they need the permission **and** a scope covering the store or route). +3. **SuperAdmin usually acts, rarely receives.** It holds every permission but has no scope rows in the seed, so it is only a recipient when it created the record (orders, claims) or owns the route. + +### With two logins — SuperAdmin and a sales rep + +| Who does it | Action | Who gets the notification | +|---|---|---| +| Rep creates, **SuperAdmin** confirms or cancels | Shop order | **Rep** (creator), plus the rep's supervisor if they have one | +| **SuperAdmin** creates, rep confirms or cancels | Shop order | **SuperAdmin** (creator) | +| SuperAdmin | Sets a store's outstanding balance above its limit | The **rep**, if the rep owns that store's route. SuperAdmin never (actor) | +| SuperAdmin | Assigns a driver to a ride | The **rep**, only if the rep is that driver | +| SuperAdmin | Approves or rejects a supplier claim | Whoever raised the claim — the **rep** only if the rep raised it | +| Anyone | Fails a delivery / cancels a ride | The **rep**, if the rep owns a route involved. SuperAdmin only if it has a scope on that route | + +Seed route owners: `nimal.perera` owns the Colombo and Negombo routes; `kamal.silva` owns Kandy and Matale. + +### Quick test (two browsers, one login each, both on a dashboard page) + +1. As the **rep**, create a shop order (Sales → New). +2. As **SuperAdmin**, confirm it, then create a second one and cancel it with a reason. +3. The rep sees a toast, a red badge on the bell and a new row on `/notifications` — "Shop order confirmed" and "Shop order cancelled: *reason*". Clicking one marks it read and opens `/sales/orders`. +4. Reverse it: SuperAdmin creates an order and the rep confirms or cancels it — SuperAdmin is notified. +5. Credit limit: there is no screen for it, so use Swagger (`/swagger`): `PUT /api/stores/{id}/outstanding-balance` with `{ "newBalance": 60000 }` on a store whose limit is lower (Fort Mini Mart: limit 50,000). A second call with a higher value does not alert again; drop the balance back under the limit and set it over again to re-trigger. --- -## Data Model Changes +## Flow -### Notification - -`RecipientUserId`, `Type`, `Payload` (JSON), `Channel` (`Email`, `SMS`, `Push`, `InApp`), `SentAt`, -`ReadAt` (nullable). +``` +Service action ──► uow.CommitAsync() (business transaction, unchanged) + │ + ▼ only after a successful commit +NotificationService.NotifyAsync(type, recipientRule, args, entity) + 1. resolve recipients (users | permission | scope | reports-to) + 2. insert Notification + NotificationRecipient rows + 3. hub.Clients.Users(ids).SendAsync("notification", dto) + ▼ +Frontend provider: connection.on("notification") → badge + toast → click opens Link + marks read +``` --- -## API Endpoints +## Decisions -| Function reference | Endpoint intent | +| Question | Decision | |---|---| -| `sendLowStockAlert(warehouseId, sku)` | Internal — triggered when a stock movement crosses the reorder point set in [02_stock_ledger.md](./02_stock_ledger.md) | -| `sendCreditLimitBreachAlert(storeId)` | Internal — triggered on order/invoice creation when `Store.OutstandingBalance` exceeds `Store.CreditLimit` | -| `sendApprovalPendingNotification(approverId, requestId)` | Internal — triggered by `submitOrderForApproval` ([08_sales_agent_controls.md](./08_sales_agent_controls.md)) and return approval ([18_returns_claims.md](./18_returns_claims.md)) | -| `sendRouteDeviationAlert(routeId, salesRepId)` | Internal — triggered from [09_fraud_controls.md](./09_fraud_controls.md)'s anomaly flag. The source data now exists (`RouteVisitLog.IsDeviationFlagged`, see [11_route_sales_management.md](./11_route_sales_management.md)), but no anomaly flag or notification reads it yet. | -| — | `GET /api/notifications` — recipient's own notification inbox | -| — | `PUT /api/notifications/{id}/read` | +| First batch of triggers | The seven Phase 1 triggers below. Cheque triggers wait until finance permissions exist in the seed | +| Channels | In-app only (SignalR). Email / SMS / push deferred | +| Per-type config table + admin screen | **Not in v1.** Every type is always enabled; add `NotificationModuleConfig` when someone needs to mute a type | +| Scheduled alerts (Phase 1b) | Second pass. Only the retention cleanup job ships in v1 | +| CORS / instances | Existing `Cors:AllowedOrigins` + `AllowCredentials()` already suit SignalR. Single API instance, no Redis backplane | +| Recipients | By **permission code** and scope, not role name. The seeded roles are `SalesRep`, `Supervisor`, `AreaManager`, `AreaSupervisor`, `RegionalManager`, `RegionalAgency`, `SuperAdmin`; there is no Accounts / operations role | +| Ids | `User`, `Permission` and entity ids are **Guid** (see seeds) | --- -## Business Rules +## Data Model -- Every `send*` function in §15 is a trigger, not a user-facing endpoint — the only client-facing - endpoints are inbox read/list. -- Notification delivery failures (e.g. SMS provider down) must not roll back the business - transaction that triggered them — fire-and-forget, logged separately. +**Notification** — one row per event + +| Field | Type | Notes | +|---|---|---| +| `Id` | Guid PK | | +| `Type` | varchar(60) | `NotificationTypes` constant; frontend maps it to an icon | +| `Severity` | enum | `Info`, `Success`, `Warning`, `Critical` | +| `Title` / `Message` | varchar(150) / varchar(500) | Built from the type's templates | +| `EntityType` / `EntityId` | varchar(50) / Guid | The related record | +| `Link` | varchar(200) | Frontend route, e.g. `/payments/received-cheques/88` | +| `CreatedByUserId` | FK User, null | Null for system / scheduled events | +| `CreatedAt` | datetime | `DateTime.Now` | +| `DedupeKey` | — | **Not in v1.** Added with the Phase 1b scheduled jobs, e.g. `ItemBatchExpiring:77:2026-10-20` | + +**NotificationRecipient** — one row per user per event (read state is per user) + +| Field | Type | +|---|---| +| `Id` | Guid PK | +| `NotificationId` | FK | +| `UserId` | FK | +| `IsRead` / `ReadAt` | bool / datetime null | + +Index `(UserId, IsRead)`; the bell orders by the joined `Notification.CreatedAt DESC`. + +No config table in v1 — every type is enabled. (Later: `NotificationModuleConfig` with `Type` PK + +`InAppEnabled`, missing row = enabled.) + +Example (failed delivery): +``` +Notification: Type=DeliveryTaskFailed, Severity=Warning, + Title="Delivery failed", Message="Delivery to Fort Mini Mart failed.", + EntityType=DeliveryTask, EntityId=, Link=/operations/delivery-tasks/ +NotificationRecipient: (that notification, Nimal Perera - route owner, unread) + (that notification, a delivery-task.manage user in Western region, unread) +``` + +--- + +## Recipients + +Every trigger names its rule. There is **no "notify everyone"** option. + +| Rule | Resolves to | +|---|---| +| `Users(ids)` | Those users (creator, driver, approver, claim owner) | +| `Permission(code)` | Active users whose role has that permission code (`User.FK_RoleId` → `RolePermission` → `Permission.Code`), e.g. `delivery-task.manage` | +| `Scope(kind, id)` | Users whose `UserStoreScope` / `UserRegionScope` / `UserRouteScope` covers the record | +| `PermissionInScope(code, kind, id)` | Intersection of the two (e.g. `store.manage` users for that store's region) | +| `ReportsTo(userId)` | The user's supervisor via `User.FK_ReportsToUserId` | +| `RouteOwner(kind, id)` | The owner (`Route.FK_OwnerUserId`) of the route, or of the store's route when `kind` is `Store` | + +`SuperAdmin` holds every permission, so it receives `Permission` rules automatically; use +`PermissionInScope` where that would be too noisy. + +The resolver always drops inactive users and the acting user, and de-duplicates. + +--- + +## Triggers + +Each is called **after** the owning service's `uow.CommitAsync()`. + +**Phase 1 — must have** + +| Type | Raised by | When | Recipients | Severity | +|---|---|---|---|---| +| Type | Status | Raised by | When | Recipients | Severity | +|---|---|---|---|---|---| +| `SupplierClaimApproved` / `Rejected` | ✅ wired | `SupplierClaimService.ApproveAsync` / `RejectAsync` | Decision | `Users(RaisedByUserId)` (not the decider) | Success / Warning | +| `RideCancelled` | ✅ wired | `RideService.CancelAsync` | Cancelled (message carries `CancelRequestType` + reason) | For each distinct route in the ride's delivery tasks: `RouteOwner(Route)` + `PermissionInScope(delivery-task.manage, Route)`. A ride has no route of its own, so routes come from its tasks' stores | Critical | +| `RideDriverAssigned` | ✅ wired | `RideService.AssignDriverAsync` | Driver assigned | `Users(driver.FK_UserId)` | Info | +| `DeliveryTaskFailed` | ✅ wired | `DeliveryTaskService.FailAsync` | Task failed | `RouteOwner(Store)` + `PermissionInScope(delivery-task.manage, Store)` | Warning | +| `DeliveryTaskPartiallyDelivered` | ✅ wired | `DeliveryTaskService.ConfirmAsync` | Confirmed with `fullyDelivered == false` (a separate type: "failed" would be the wrong wording) | same as above | Warning | +| `ShopPOConfirmed` / `Cancelled` | ✅ wired | `ShopPOService.ConfirmAsync`, `ConfirmAndCreateInvoiceAsync` / `CancelAsync` | Status change | `Users(RaisedById)` + `ReportsTo(RaisedById)` (not the actor) | Info / Warning | +| `CreditLimitBreached` | ✅ wired | `StoreService.UpdateOutstandingBalanceAsync` | Balance crosses from ≤ to > `CreditLimit` (crossing only). This is the only place `OutstandingBalance` is ever written — invoices and payments do not update it | `RouteOwner(Store)` + `PermissionInScope(store.manage, Store)` | Warning | +| `StockLow` | ⏸ deferred | Stock movement | Crosses to ≤ `Item.ReorderLevel` | Undecided — warehouses have no region / scope link, so `PermissionInScope` cannot target them. Needs a per-request tracker + action filter because stock moves from many call sites that commit separately | Warning | + +**Waiting on finance permissions** (not in the seed yet — add a cheque permission first) + +`ReceivedChequeReturned` (`ReceivedChequeService.UpdateStatusAsync`, action `Return`, Critical) · +`ChequePageBounced` (`ChequeBookService.UpdatePageStatusAsync`, action `Bounce`, Critical) + +**Phase 1b — scheduled, second pass** (daily `BackgroundService`, uses `DedupeKey`) + +`ReceivedChequeDueSoon` (≤ 2 days) · `ItemBatchExpiring` (≤ N days) · `PaymentOverdue` · +`DriverComplianceExpiring` + +**Phase 2 — later** + +`PoCancelled` / `PoClosed` · `GrnShortOrDamaged` · `LoadingSheetConfirmed` · `InvoiceFinalized` · +`ReturnCreated` / `ClaimSettled` · `PurchaseReturnCreated` · `ApprovalPending` · +`RouteDeviationFlagged` · `RoleAssigned` / `ScopeChanged` · `SyncFailed` + +Excluded on purpose: plain master-data CRUD (brand, category, UOM, vehicle, region). + +--- + +## API + +| Endpoint | Purpose | +|---|---| +| `GET /api/notifications?unreadOnly=&page=&pageSize=` | Caller's inbox, newest first | +| `GET /api/notifications/unread-count` | Badge count | +| `PATCH /api/notifications/{id}/read` | Mark one read (own row only) | +| `PATCH /api/notifications/read-all` | Mark all read | +| `DELETE /api/notifications/{id}` | Remove caller's recipient row | +| `WS /hubs/notifications` | SignalR; server → client event `notification` | + +--- + +## Backend structure + +``` +Features/Notifications/ (namespace Distribution.Features.Notifications — "Notification" alone clashes with Models.Notification) + NotificationController.cs thin, no UoW + NotificationService.cs resolve → persist → push; owns uow.CommitAsync() + INotificationRepository.cs / NotificationRepository.cs + NotificationRecipientResolver.cs Users / Permission / Scope / PermissionInScope / ReportsTo + NotificationTypes.cs constants + definition (severity, title/message template) + NotificationDTOs.cs + NotificationHub.cs SignalR hub + NotificationCleanupJob.cs daily retention purge + NotificationScheduleJob.cs Phase 1b alerts (second pass) +Models/Notification.cs, NotificationRecipient.cs +``` + +### Code reference + +**Registry** — services never pass string literals: +```csharp +public static class NotificationTypes +{ + public const string DeliveryTaskFailed = "DeliveryTaskFailed"; + // ... + public static readonly IReadOnlyDictionary All = new Dictionary + { + [DeliveryTaskFailed] = new(NotificationSeverity.Warning, + "Delivery failed", "Delivery to {StoreName} failed."), + }; +} +``` + +**Service contract** — never throws; failures are logged: +```csharp +public interface INotificationService +{ + // recipients = union of the rules, then resolved, filtered (inactive, actor) and de-duplicated + Task NotifyAsync(string type, IReadOnlyList recipients, NotificationArgs args, CancellationToken ct = default); +} +// RecipientRule: Users(ids) | Permission(code) | Scope(kind, id) | PermissionInScope(code, kind, id) | ReportsTo(userId) | RouteOwner(kind, id) +// NotificationArgs: Values (template tokens), EntityType, EntityId (Guid), Link, CreatedByUserId (Guid?) +``` + +**Calling it from an existing service:** +```csharp +await _uow.CommitAsync(ct); +await _notifications.NotifyAsync(NotificationTypes.DeliveryTaskFailed, + [RecipientRule.Users(route.FK_OwnerUserId), + RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Region, route.FK_RegionId)], + new(new { StoreName = store.StoreName }, "DeliveryTask", task.Id, + $"/operations/delivery-tasks/{task.Id}", currentUserId), ct); +``` + +**Program.cs:** +```csharp +builder.Services.AddSignalR(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); +// CORS: AllowCredentials() with explicit origins +app.MapHub("/hubs/notifications"); +``` +The hub authenticates through the existing `access-token` cookie (read by `AuthMiddleware`); the +client connects with `withCredentials: true`. A `?access_token=` query fallback is not needed unless +the token ever moves out of the cookie. Users are addressed by their `UserId` claim +(`NotificationUserIdProvider`). Each recipient gets its own push because the DTO `Id` is the +recipient row id. + +**Frontend** (`contexts/notification-context.tsx`): +```tsx +const connection = new HubConnectionBuilder() + .withUrl(`${API}/hubs/notifications`, { withCredentials: true }) + .withAutomaticReconnect().build(); + +connection.on("notification", (n: NotificationDto) => { + setUnread((c) => c + 1); + setItems((p) => [n, ...p]); + toast(n.title, { description: n.message, action: { label: "Open", onClick: () => router.push(n.link) } }); +}); +connection.onreconnected(refetch); // catch up on anything missed +``` +Also: `lib/api/notifications.ts`, bell + popover in the dashboard header, `/notifications` "view all" page. + +--- + +## Rules + +- Notify **after** a successful commit, inside try/catch + log. A notification failure must never + fail or roll back the business action. +- Saving and the hub push are separate try/catch blocks (a failed push still leaves the inbox entry). +- Unknown type: throws in dev/test, logged and skipped in production. A unit test checks that every + constant has a definition. +- No per-type config in v1: every type is always delivered. +- Click-through is mandatory: `EntityType`, `EntityId` and `Link` are required; clicking marks read. +- Retention: `NotificationCleanupJob` deletes read recipients older than 90 days (SysConfig), then + events with no recipients left. +- No Email/SMS switches in the UI until a sender exists. +- `AGENTS.md`: `uow.CommitAsync()` only in services, never `SaveChangesAsync()`, `DateTime.Now` only. --- ## Open Questions -- Which channels are actually needed at launch — in-app only, or SMS/email/push from day one? This - significantly changes the infrastructure needed (third-party provider integration, see - [22_integration_api.md](./22_integration_api.md)). +Resolved — see **Decisions**. Remaining for later: + +1. Add a cheque / finance permission to the seed, then enable the two cheque triggers. +2. Email / SMS / push: see [22_integration_api.md](./22_integration_api.md). +3. If the API ever runs on more than one instance, add a SignalR Redis backplane. +4. Per-type mute (`NotificationModuleConfig` + admin screen) when requested. --- -## Implementation Notes +## Build order -> Fill this section in during planning discussion before writing any code. +1. Entities (`Notification`, `NotificationRecipient`) + EF migration. +2. `Features/Notifications/` (repository, resolver, service, controller, DTOs, `NotificationTypes`) + retention job. +3. SignalR hub, `Program.cs` registration, user-id provider. +4. Frontend: API client, provider, bell, toast, click-through, view-all page. + +5. Phase 1 triggers — six wired (recipient resolution is unit-tested; the per-service calls are not, + because `Distribution.Tests` does not currently compile). `StockLow` deferred. +6. Later: `StockLow`, cheque triggers, Phase 1b scheduled alerts, Phase 2 triggers. + +Remaining before this works end to end: apply the `AddNotifications` migration. From 209ee7da2d6fcde76e7bf39ca5c839f92d30bc48 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 20 Oct 2026 16:53:15 +0530 Subject: [PATCH 4/4] fe integration for notification system with context, components, and API integration --- distribution_fe/app/(dashboard)/layout.tsx | 37 ++-- .../_components/notifications-client.tsx | 133 +++++++++++++ .../app/(dashboard)/notifications/page.tsx | 5 + .../components/layout/notification-bell.tsx | 122 ++++++++++++ distribution_fe/components/ui/popover.tsx | 50 +++++ .../contexts/notification-context.tsx | 135 +++++++++++++ distribution_fe/lib/api/index.ts | 1 + distribution_fe/lib/api/notifications.ts | 24 +++ distribution_fe/package-lock.json | 182 +++++++++++++++++- distribution_fe/package.json | 1 + distribution_fe/types/notifications.ts | 23 +++ 11 files changed, 696 insertions(+), 17 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/notifications/_components/notifications-client.tsx create mode 100644 distribution_fe/app/(dashboard)/notifications/page.tsx create mode 100644 distribution_fe/components/layout/notification-bell.tsx create mode 100644 distribution_fe/components/ui/popover.tsx create mode 100644 distribution_fe/contexts/notification-context.tsx create mode 100644 distribution_fe/lib/api/notifications.ts create mode 100644 distribution_fe/types/notifications.ts diff --git a/distribution_fe/app/(dashboard)/layout.tsx b/distribution_fe/app/(dashboard)/layout.tsx index 8ae0431..5c8cc8e 100644 --- a/distribution_fe/app/(dashboard)/layout.tsx +++ b/distribution_fe/app/(dashboard)/layout.tsx @@ -2,6 +2,8 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/s import { AppSidebar } from '@/components/layout/app-sidebar' import { BottomNav } from '@/components/layout/bottom-nav' import { ThemeToggle } from '@/components/layout/theme-toggle' +import { NotificationBell } from '@/components/layout/notification-bell' +import { NotificationProvider } from '@/contexts/notification-context' import { TopLoader } from '@/components/shared/top-loader' export default function DashboardLayout({ @@ -10,21 +12,24 @@ export default function DashboardLayout({ children: React.ReactNode }) { return ( - - - - -
- -
- -
-
-
- {children} -
-
- -
+ + + + + +
+ +
+ + +
+
+
+ {children} +
+
+ +
+
) } diff --git a/distribution_fe/app/(dashboard)/notifications/_components/notifications-client.tsx b/distribution_fe/app/(dashboard)/notifications/_components/notifications-client.tsx new file mode 100644 index 0000000..fda89c6 --- /dev/null +++ b/distribution_fe/app/(dashboard)/notifications/_components/notifications-client.tsx @@ -0,0 +1,133 @@ +'use client' + +import { useCallback, useEffect, useState } from 'react' +import { Trash2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { EmptyState } from '@/components/shared/empty-state' +import { NotificationRow } from '@/components/layout/notification-bell' +import { useNotifications } from '@/contexts/notification-context' +import { getApiError } from '@/lib/api' +import { notifications } from '@/lib/api/notifications' +import { toast } from '@/components/ui/toast' +import type { NotificationResponse } from '@/types/notifications' + +const PAGE_SIZE = 20 + +type Filter = 'all' | 'unread' + +export function NotificationsClient() { + const { recent, unreadCount, open, markAllRead, refresh } = useNotifications() + + const [filter, setFilter] = useState('all') + const [items, setItems] = useState([]) + const [page, setPage] = useState(1) + const [total, setTotal] = useState(0) + const [loading, setLoading] = useState(true) + + // The caller decides whether to show the loading state (it is true on first render). + const load = useCallback(async (targetPage: number, reset: boolean) => { + try { + const result = await notifications.list({ + unreadOnly: filter === 'unread', + page: targetPage, + pageSize: PAGE_SIZE, + }) + setItems(prev => (reset ? result.items : [...prev, ...result.items])) + setPage(targetPage) + setTotal(result.total) + } catch (error) { + toast.add({ title: 'Could not load notifications', description: getApiError(error), type: 'error' }) + } finally { + setLoading(false) + } + }, [filter]) + + // Reload when the filter changes or a new notification is pushed in. + const latestId = recent[0]?.id + useEffect(() => { void load(1, true) }, [load, latestId]) + + async function handleOpen(n: NotificationResponse) { + setItems(prev => prev.map(x => (x.id === n.id ? { ...x, isRead: true } : x))) + await open(n) + } + + async function handleMarkAllRead() { + await markAllRead() + await load(1, true) + } + + async function handleDelete(n: NotificationResponse) { + try { + await notifications.remove(n.id) + setItems(prev => prev.filter(x => x.id !== n.id)) + setTotal(t => Math.max(0, t - 1)) + void refresh() + } catch (error) { + toast.add({ title: 'Could not delete notification', description: getApiError(error), type: 'error' }) + } + } + + const hasMore = items.length < total + + return ( +
+
+
+

Notifications

+

+ {unreadCount > 0 ? `${unreadCount} unread` : 'You\'re all caught up'} +

+
+
+
+ {(['all', 'unread'] as const).map(f => ( + + ))} +
+ +
+
+ +
+ {items.length === 0 && !loading ? ( + + ) : ( +
+ {items.map(n => ( +
+ { void handleOpen(item) }} /> + +
+ ))} +
+ )} +
+ + {hasMore && ( + + )} +
+ ) +} diff --git a/distribution_fe/app/(dashboard)/notifications/page.tsx b/distribution_fe/app/(dashboard)/notifications/page.tsx new file mode 100644 index 0000000..141fc91 --- /dev/null +++ b/distribution_fe/app/(dashboard)/notifications/page.tsx @@ -0,0 +1,5 @@ +import { NotificationsClient } from './_components/notifications-client' + +export default function NotificationsPage() { + return +} diff --git a/distribution_fe/components/layout/notification-bell.tsx b/distribution_fe/components/layout/notification-bell.tsx new file mode 100644 index 0000000..f7af6f9 --- /dev/null +++ b/distribution_fe/components/layout/notification-bell.tsx @@ -0,0 +1,122 @@ +'use client' + +import Link from 'next/link' +import { useState } from 'react' +import { Bell } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { useNotifications } from '@/contexts/notification-context' +import { cn } from '@/lib/utils' +import type { NotificationResponse, NotificationSeverity } from '@/types/notifications' + +const SEVERITY_DOT: Record = { + Info: 'bg-sky-500', + Success: 'bg-emerald-500', + Warning: 'bg-amber-500', + Critical: 'bg-red-500', +} + +export function timeAgo(iso: string): string { + const seconds = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000)) + if (seconds < 60) return 'just now' + const minutes = Math.floor(seconds / 60) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 7) return `${days}d ago` + return new Date(iso).toLocaleDateString() +} + +export function NotificationRow({ + notification: n, + onOpen, +}: { + notification: NotificationResponse + onOpen: (n: NotificationResponse) => void +}) { + return ( + + ) +} + +export function NotificationBell() { + const { recent, unreadCount, open, markAllRead } = useNotifications() + const [isOpen, setIsOpen] = useState(false) + + return ( + + 0 ? `Notifications (${unreadCount} unread)` : 'Notifications'} + /> + } + > + + {unreadCount > 0 && ( + + {unreadCount > 99 ? '99+' : unreadCount} + + )} + + + +
+ Notifications + +
+ +
+ {recent.length === 0 ? ( +

You're all caught up.

+ ) : ( + recent.map(n => ( + { setIsOpen(false); void open(item) }} + /> + )) + )} +
+ +
+ setIsOpen(false)} + className="block rounded-xl px-3 py-2 text-center text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" + > + View all + +
+
+
+ ) +} diff --git a/distribution_fe/components/ui/popover.tsx b/distribution_fe/components/ui/popover.tsx new file mode 100644 index 0000000..6e6ca70 --- /dev/null +++ b/distribution_fe/components/ui/popover.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import { Popover as PopoverPrimitive } from "@base-ui/react/popover" + +import { cn } from "@/lib/utils" + +function Popover({ ...props }: PopoverPrimitive.Root.Props) { + return +} + +function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) { + return +} + +function PopoverContent({ + align = "end", + alignOffset = 0, + side = "bottom", + sideOffset = 6, + className, + ...props +}: PopoverPrimitive.Popup.Props & + Pick< + PopoverPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/distribution_fe/contexts/notification-context.tsx b/distribution_fe/contexts/notification-context.tsx new file mode 100644 index 0000000..683f2ca --- /dev/null +++ b/distribution_fe/contexts/notification-context.tsx @@ -0,0 +1,135 @@ +'use client' + +import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react' +import { useRouter } from 'next/navigation' +import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr' +import { useAuth } from '@/contexts/auth-context' +import { notifications, NOTIFICATION_HUB_URL } from '@/lib/api/notifications' +import { toast } from '@/components/ui/toast' +import type { NotificationResponse, NotificationSeverity } from '@/types/notifications' + +const RECENT_COUNT = 20 + +const TOAST_TYPE: Record = { + Info: 'info', + Success: 'success', + Warning: 'warning', + Critical: 'error', +} + +interface NotificationContextValue { + /** The most recent notifications, newest first (what the bell popover shows). */ + recent: NotificationResponse[] + unreadCount: number + /** Marks the notification read (if needed) and navigates to its link. */ + open: (n: NotificationResponse) => Promise + markAllRead: () => Promise + /** Re-fetches the recent list and the unread count. */ + refresh: () => Promise +} + +const NotificationContext = createContext(null) + +export function NotificationProvider({ children }: { children: ReactNode }) { + const router = useRouter() + const { user } = useAuth() + const [recent, setRecent] = useState([]) + const [unreadCount, setUnreadCount] = useState(0) + + const refresh = useCallback(async () => { + try { + const [page, count] = await Promise.all([ + notifications.list({ page: 1, pageSize: RECENT_COUNT }), + notifications.unreadCount(), + ]) + setRecent(page.items) + setUnreadCount(count) + } catch { + // The bell is a convenience; never break the page if it can't load. + } + }, []) + + const markRead = useCallback(async (n: NotificationResponse) => { + if (n.isRead) return + setRecent(prev => prev.map(x => (x.id === n.id ? { ...x, isRead: true } : x))) + setUnreadCount(c => Math.max(0, c - 1)) + try { + await notifications.markRead(n.id) + } catch { + await refresh() + } + }, [refresh]) + + const open = useCallback(async (n: NotificationResponse) => { + await markRead(n) + if (n.link) router.push(n.link) + }, [markRead, router]) + + const markAllRead = useCallback(async () => { + setRecent(prev => prev.map(x => ({ ...x, isRead: true }))) + setUnreadCount(0) + try { + await notifications.markAllRead() + } catch { + await refresh() + } + }, [refresh]) + + // Keep the latest `open` for the long-lived socket handler without reconnecting. + const openRef = useRef(open) + useEffect(() => { openRef.current = open }, [open]) + + useEffect(() => { + if (!user) return + + void refresh() + + const connection = new HubConnectionBuilder() + .withUrl(NOTIFICATION_HUB_URL, { withCredentials: true }) + .withAutomaticReconnect() + .configureLogging(LogLevel.Warning) + .build() + + connection.on('notification', (n: NotificationResponse) => { + setRecent(prev => [n, ...prev.filter(x => x.id !== n.id)].slice(0, RECENT_COUNT)) + setUnreadCount(c => c + 1) + toast.add({ + title: n.title, + description: n.message, + type: TOAST_TYPE[n.severity] ?? 'info', + actionProps: n.link + ? { children: 'Open', onClick: () => { void openRef.current(n) } } + : undefined, + }) + }) + + // Anything pushed while the socket was down is only in the inbox — catch up. + connection.onreconnected(() => { void refresh() }) + + connection.start().catch(() => { + // Initial connect failed (offline / API down); the inbox endpoints still work. + }) + + return () => { void connection.stop() } + }, [user, refresh]) + + return ( + + {children} + + ) +} + +export function useNotifications(): NotificationContextValue { + const ctx = useContext(NotificationContext) + if (!ctx) throw new Error('useNotifications must be used inside NotificationProvider') + return ctx +} diff --git a/distribution_fe/lib/api/index.ts b/distribution_fe/lib/api/index.ts index 2b22eeb..feccf0c 100644 --- a/distribution_fe/lib/api/index.ts +++ b/distribution_fe/lib/api/index.ts @@ -16,6 +16,7 @@ export { bankAccounts } from './bank-accounts' export { chequeBooks, chequePages } from './cheques' export { receivedCheques } from './received-cheques' export { supplierPayments } from './supplier-payments' +export { notifications } from './notifications' export { getPaginatedItems } from './paginated' export type { PageParams } from './master' diff --git a/distribution_fe/lib/api/notifications.ts b/distribution_fe/lib/api/notifications.ts new file mode 100644 index 0000000..18d2f2a --- /dev/null +++ b/distribution_fe/lib/api/notifications.ts @@ -0,0 +1,24 @@ +import { api } from '@/lib/api' +import type { NotificationPage } from '@/types/notifications' + +export const notifications = { + list: (params?: { unreadOnly?: boolean; page?: number; pageSize?: number }) => + api.get('/notifications', { params }).then(r => r.data), + + unreadCount: () => + api.get<{ count: number }>('/notifications/unread-count').then(r => r.data.count), + + markRead: (id: string) => + api.patch(`/notifications/${id}/read`).then(() => undefined), + + markAllRead: () => + api.patch('/notifications/read-all').then(() => undefined), + + remove: (id: string) => + api.delete(`/notifications/${id}`).then(() => undefined), +} + +/** SignalR hub URL — the API base URL without its trailing /api. */ +export const NOTIFICATION_HUB_URL = + (process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:5089/api').replace(/\/api\/?$/, '') + + '/hubs/notifications' diff --git a/distribution_fe/package-lock.json b/distribution_fe/package-lock.json index 70cb4c4..3ff3ddf 100644 --- a/distribution_fe/package-lock.json +++ b/distribution_fe/package-lock.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^5.9.1", "@hugeicons/core-free-icons": "^4.3.0", "@hugeicons/react": "^1.1.10", + "@microsoft/signalr": "^10.0.11", "@types/leaflet": "^1.9.22", "axios": "^1.19.0", "class-variance-authority": "^0.7.1", @@ -1763,6 +1764,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/signalr": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-10.0.11.tgz", + "integrity": "sha512-FulOJ2EEtKvLQcswe/U7v8pzyXyk7Jua5xgWJPPwyQU/2Z9ORvKcVjyO75VvLsem+CJ1ORRexJ+7Bz1FCei2aw==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@microsoft/signalr/node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", @@ -3251,6 +3274,18 @@ "win32" ] }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -5683,6 +5718,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -5923,6 +5967,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "license": "Unlicense", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, "node_modules/fflate": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", @@ -8426,6 +8480,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -9272,11 +9346,22 @@ "node": ">=10" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9298,6 +9383,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9651,6 +9742,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/reselect": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", @@ -9971,6 +10068,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10860,6 +10963,36 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/traverse": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.10.tgz", @@ -11316,6 +11449,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -11391,6 +11534,22 @@ "d3-timer": "^3.0.1" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11507,6 +11666,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.0.tgz", diff --git a/distribution_fe/package.json b/distribution_fe/package.json index d40c022..a6d98c5 100644 --- a/distribution_fe/package.json +++ b/distribution_fe/package.json @@ -16,6 +16,7 @@ "@hookform/resolvers": "^5.9.1", "@hugeicons/core-free-icons": "^4.3.0", "@hugeicons/react": "^1.1.10", + "@microsoft/signalr": "^10.0.11", "@types/leaflet": "^1.9.22", "axios": "^1.19.0", "class-variance-authority": "^0.7.1", diff --git a/distribution_fe/types/notifications.ts b/distribution_fe/types/notifications.ts new file mode 100644 index 0000000..1bce432 --- /dev/null +++ b/distribution_fe/types/notifications.ts @@ -0,0 +1,23 @@ +export type NotificationSeverity = 'Info' | 'Success' | 'Warning' | 'Critical' + +export interface NotificationResponse { + /** The caller's own recipient row id — use it for read / delete. */ + id: string + type: string + severity: NotificationSeverity + title: string + message: string + entityType: string + entityId: string + link: string + createdAt: string + isRead: boolean + readAt: string | null +} + +export interface NotificationPage { + page: number + pageSize: number + total: number + items: NotificationResponse[] +}