diff --git a/Distribution/Features/ShopPO/ShopPODTOs.cs b/Distribution/Features/ShopPO/ShopPODTOs.cs index 39bc7af..a242507 100644 --- a/Distribution/Features/ShopPO/ShopPODTOs.cs +++ b/Distribution/Features/ShopPO/ShopPODTOs.cs @@ -2,13 +2,18 @@ namespace Distribution.Features.ShopPO; -public record CreateShopPORequest(Guid StoreId, List Items, Guid? RaisedByUserId = null); -public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null); -public record UpdateShopPOItemsRequest(List Items); +// DiscountSchemeIds: the FlatPercent/SlabDiscount scheme the user accepted (at most one). Null = leave as is. +public record CreateShopPORequest(Guid StoreId, List Items, Guid? RaisedByUserId = null, List? DiscountSchemeIds = null); +// IsFree + SchemeId mark a free-issue line the user accepted from a scheme suggestion; its price is forced to 0. +public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false, Guid? SchemeId = null); +public record UpdateShopPOItemsRequest(List Items, List? DiscountSchemeIds = null); public record ConfirmShopPORequest(Guid? ConfirmedByUserId = null); public record CancelShopPORequest(string Reason, Guid? CancelledByUserId = null); -public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive); +public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive, + bool IsFree = false, Guid? SchemeId = null, string? SchemeName = null); + +public record ShopPODiscountResponse(Guid SchemeId, string SchemeName, decimal Amount); public record ShopPOResponse( Guid Id, @@ -34,4 +39,5 @@ public record ShopPOResponse( DateTime? CancelledAt = null, string? CancelReason = null, string? CancelledByName = null, - string? CancelledByRole = null); + string? CancelledByRole = null, + List? Discounts = null); diff --git a/Distribution/Features/ShopPO/ShopPOService.cs b/Distribution/Features/ShopPO/ShopPOService.cs index e427f9e..f21437c 100644 --- a/Distribution/Features/ShopPO/ShopPOService.cs +++ b/Distribution/Features/ShopPO/ShopPOService.cs @@ -1,6 +1,7 @@ using Distribution.Features.Invoice; using Distribution.Features.Notifications; using Distribution.Features.PriceList; +using Distribution.Features.Scheme; using Distribution.Features.Stock; using Distribution.Infra.UoW; using Distribution.Models; @@ -11,9 +12,30 @@ public class ShopPOService( IShopPORepository repo, IInvoiceRepository invoiceRepo, PriceListService priceListService, + SchemeService schemeService, INotificationService notifications, IUnitOfWork uow) { + // Paid lines are priced from the request or the price list; accepted free-issue lines are always priced 0. + private async Task BuildLineAsync(ShopPOItemRequest line, Guid storeId) + { + if (line.IsFree) + return new ShopPOItem + { + FK_ItemId = line.ItemId, + Quantity = line.Quantity, + UnitPrice = 0m, + IsFree = true, + FK_SchemeId = line.SchemeId + }; + + var unitPrice = line.UnitPrice + ?? (await priceListService.ResolveByStoreAsync(line.ItemId, storeId, DateTime.Now))?.Price + ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); + + return new ShopPOItem { FK_ItemId = line.ItemId, Quantity = line.Quantity, UnitPrice = unitPrice }; + } + // Tells the person who raised the order (and their supervisor) what happened to it, unless they did it themselves. private Task NotifyOrderAsync(string type, Models.ShopPO po, Guid actingUserId, string? reason, CancellationToken ct) => notifications.NotifyAsync( @@ -22,14 +44,15 @@ public class ShopPOService( new NotificationArgs(new { StoreName = po.Store?.StoreName, Reason = reason }, "ShopPO", po.Id, "/sales/orders", actingUserId), ct); - private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary? users = null) => + private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary? users = null, List? discounts = null) => new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty, p.Store?.StoreAddress, p.Store?.ContactNumber, p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status, p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt, p.Items.Where(i => i.IsActive).Select(i => new ShopPOItemResponse(i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty, - i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).ToList(), + i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive, + i.IsFree, i.FK_SchemeId, i.SchemeName)).ToList(), p.IsActive, p.CreatedAt, users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null, users != null && users.TryGetValue(p.RaisedById, out var r) ? r.Role : null, @@ -37,7 +60,8 @@ public class ShopPOService( users != null && p.ConfirmedBy is Guid cb2 && users.TryGetValue(cb2, out var c2) ? c2.Role : null, p.CancelledBy, p.CancelledAt, p.CancelReason, users != null && p.CancelledBy is Guid xb && users.TryGetValue(xb, out var x) ? x.Name : null, - users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null); + users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null, + discounts ?? []); private static IEnumerable UserIds(Models.ShopPO p) { @@ -62,7 +86,7 @@ public class ShopPOService( var po = await repo.GetByIdAsync(id); if (po is null) return null; var users = await repo.GetUserInfoAsync(UserIds(po)); - return ToResponse(po, users); + return ToResponse(po, users, await schemeService.GetDiscountsForShopPOAsync(po.Id)); } public async Task> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId) @@ -82,20 +106,14 @@ public class ShopPOService( foreach (var line in request.Items) { - var unitPrice = line.UnitPrice - ?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price - ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); - - items.Add(new ShopPOItem - { - FK_ItemId = line.ItemId, - Quantity = line.Quantity, - QuantityFulfilled = 0, - UnitPrice = unitPrice - }); - total += line.Quantity * unitPrice; + var item = await BuildLineAsync(line, request.StoreId); + items.Add(item); + total += item.Quantity * item.UnitPrice; } + await schemeService.ValidateFreeLinesAsync(request.StoreId, DateTime.Now, items); + await schemeService.ValidateDiscountSchemesAsync(request.StoreId, DateTime.Now, items, request.DiscountSchemeIds); + var po = new Models.ShopPO { FK_StoreId = request.StoreId, @@ -110,6 +128,7 @@ public class ShopPOService( await repo.AddAsync(po); foreach (var item in items) item.FK_ShopPOId = po.Id; await repo.AddItemsAsync(items); + await schemeService.SyncRedemptionsAsync(po.Id, items, request.DiscountSchemeIds); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } @@ -137,27 +156,22 @@ public class ShopPOService( foreach (var line in request.Items) { - var unitPrice = line.UnitPrice - ?? (await priceListService.ResolveByStoreAsync(line.ItemId, po.FK_StoreId, DateTime.Now))?.Price - ?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}"); - - newItems.Add(new ShopPOItem - { - FK_ShopPOId = id, - FK_ItemId = line.ItemId, - Quantity = line.Quantity, - QuantityFulfilled = 0, - UnitPrice = unitPrice - }); - total += line.Quantity * unitPrice; + var item = await BuildLineAsync(line, po.FK_StoreId); + item.FK_ShopPOId = id; + newItems.Add(item); + total += item.Quantity * item.UnitPrice; } + await schemeService.ValidateFreeLinesAsync(po.FK_StoreId, po.CreatedAt, newItems); + await schemeService.ValidateDiscountSchemesAsync(po.FK_StoreId, po.CreatedAt, newItems, request.DiscountSchemeIds); + po.TotalAmount = total; await uow.BeginAsync(ct); try { await repo.ReplaceItemsAsync(id, newItems); + await schemeService.SyncRedemptionsAsync(id, newItems, request.DiscountSchemeIds); repo.Update(po); await uow.CommitAsync(ct); } @@ -212,6 +226,9 @@ public class ShopPOService( CreatedBy = userId, TotalAmount = po.TotalAmount }; + // Start the invoice at the discounted total so it matches what print/finalize will recompute. + invoice.DiscountAmount = Math.Min(await invoiceRepo.GetSchemeDiscountAsync(po.Id), po.TotalAmount); + invoice.TotalAmount = po.TotalAmount - invoice.DiscountAmount; await uow.BeginAsync(ct); try diff --git a/Distribution/Models/ShopPOItem.cs b/Distribution/Models/ShopPOItem.cs index 0b7b7ec..4118879 100644 --- a/Distribution/Models/ShopPOItem.cs +++ b/Distribution/Models/ShopPOItem.cs @@ -15,5 +15,11 @@ namespace Distribution.Models public int Quantity { get; set; } public int QuantityFulfilled { get; set; } = 0; public decimal UnitPrice { get; set; } + + /// Free issue granted by a scheme (UnitPrice 0); shown in the Free Issue column. + public bool IsFree { get; set; } + public Guid? FK_SchemeId { get; set; } + /// Scheme name when it was applied, so later scheme edits don't rewrite history. + public string? SchemeName { get; set; } } }