feat: enhance Shop PO handling with discount scheme support and free issue tracking
This commit is contained in:
@@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
namespace Distribution.Features.ShopPO;
|
namespace Distribution.Features.ShopPO;
|
||||||
|
|
||||||
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null);
|
// DiscountSchemeIds: the FlatPercent/SlabDiscount scheme the user accepted (at most one). Null = leave as is.
|
||||||
public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null);
|
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null, List<Guid>? DiscountSchemeIds = null);
|
||||||
public record UpdateShopPOItemsRequest(List<ShopPOItemRequest> Items);
|
// 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<ShopPOItemRequest> Items, List<Guid>? DiscountSchemeIds = null);
|
||||||
public record ConfirmShopPORequest(Guid? ConfirmedByUserId = null);
|
public record ConfirmShopPORequest(Guid? ConfirmedByUserId = null);
|
||||||
public record CancelShopPORequest(string Reason, Guid? CancelledByUserId = 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(
|
public record ShopPOResponse(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
@@ -34,4 +39,5 @@ public record ShopPOResponse(
|
|||||||
DateTime? CancelledAt = null,
|
DateTime? CancelledAt = null,
|
||||||
string? CancelReason = null,
|
string? CancelReason = null,
|
||||||
string? CancelledByName = null,
|
string? CancelledByName = null,
|
||||||
string? CancelledByRole = null);
|
string? CancelledByRole = null,
|
||||||
|
List<ShopPODiscountResponse>? Discounts = null);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Distribution.Features.Invoice;
|
using Distribution.Features.Invoice;
|
||||||
using Distribution.Features.Notifications;
|
using Distribution.Features.Notifications;
|
||||||
using Distribution.Features.PriceList;
|
using Distribution.Features.PriceList;
|
||||||
|
using Distribution.Features.Scheme;
|
||||||
using Distribution.Features.Stock;
|
using Distribution.Features.Stock;
|
||||||
using Distribution.Infra.UoW;
|
using Distribution.Infra.UoW;
|
||||||
using Distribution.Models;
|
using Distribution.Models;
|
||||||
@@ -11,9 +12,30 @@ public class ShopPOService(
|
|||||||
IShopPORepository repo,
|
IShopPORepository repo,
|
||||||
IInvoiceRepository invoiceRepo,
|
IInvoiceRepository invoiceRepo,
|
||||||
PriceListService priceListService,
|
PriceListService priceListService,
|
||||||
|
SchemeService schemeService,
|
||||||
INotificationService notifications,
|
INotificationService notifications,
|
||||||
IUnitOfWork uow)
|
IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
|
// Paid lines are priced from the request or the price list; accepted free-issue lines are always priced 0.
|
||||||
|
private async Task<ShopPOItem> 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.
|
// 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) =>
|
private Task NotifyOrderAsync(string type, Models.ShopPO po, Guid actingUserId, string? reason, CancellationToken ct) =>
|
||||||
notifications.NotifyAsync(
|
notifications.NotifyAsync(
|
||||||
@@ -22,14 +44,15 @@ public class ShopPOService(
|
|||||||
new NotificationArgs(new { StoreName = po.Store?.StoreName, Reason = reason }, "ShopPO", po.Id, "/sales/orders", actingUserId),
|
new NotificationArgs(new { StoreName = po.Store?.StoreName, Reason = reason }, "ShopPO", po.Id, "/sales/orders", actingUserId),
|
||||||
ct);
|
ct);
|
||||||
|
|
||||||
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null) =>
|
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = null, List<ShopPODiscountResponse>? discounts = null) =>
|
||||||
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
|
new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty,
|
||||||
p.Store?.StoreAddress, p.Store?.ContactNumber,
|
p.Store?.StoreAddress, p.Store?.ContactNumber,
|
||||||
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
|
p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status,
|
||||||
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
|
p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt,
|
||||||
p.Items.Where(i => i.IsActive).Select(i =>
|
p.Items.Where(i => i.IsActive).Select(i =>
|
||||||
new ShopPOItemResponse(i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty,
|
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,
|
p.IsActive, p.CreatedAt,
|
||||||
users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null,
|
users != null && users.TryGetValue(p.RaisedById, out var u) ? u.Name : null,
|
||||||
users != null && users.TryGetValue(p.RaisedById, out var r) ? r.Role : 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,
|
users != null && p.ConfirmedBy is Guid cb2 && users.TryGetValue(cb2, out var c2) ? c2.Role : null,
|
||||||
p.CancelledBy, p.CancelledAt, p.CancelReason,
|
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 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<Guid> UserIds(Models.ShopPO p)
|
private static IEnumerable<Guid> UserIds(Models.ShopPO p)
|
||||||
{
|
{
|
||||||
@@ -62,7 +86,7 @@ public class ShopPOService(
|
|||||||
var po = await repo.GetByIdAsync(id);
|
var po = await repo.GetByIdAsync(id);
|
||||||
if (po is null) return null;
|
if (po is null) return null;
|
||||||
var users = await repo.GetUserInfoAsync(UserIds(po));
|
var users = await repo.GetUserInfoAsync(UserIds(po));
|
||||||
return ToResponse(po, users);
|
return ToResponse(po, users, await schemeService.GetDiscountsForShopPOAsync(po.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
|
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
|
||||||
@@ -82,20 +106,14 @@ public class ShopPOService(
|
|||||||
|
|
||||||
foreach (var line in request.Items)
|
foreach (var line in request.Items)
|
||||||
{
|
{
|
||||||
var unitPrice = line.UnitPrice
|
var item = await BuildLineAsync(line, request.StoreId);
|
||||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price
|
items.Add(item);
|
||||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
total += item.Quantity * item.UnitPrice;
|
||||||
|
|
||||||
items.Add(new ShopPOItem
|
|
||||||
{
|
|
||||||
FK_ItemId = line.ItemId,
|
|
||||||
Quantity = line.Quantity,
|
|
||||||
QuantityFulfilled = 0,
|
|
||||||
UnitPrice = unitPrice
|
|
||||||
});
|
|
||||||
total += line.Quantity * unitPrice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await schemeService.ValidateFreeLinesAsync(request.StoreId, DateTime.Now, items);
|
||||||
|
await schemeService.ValidateDiscountSchemesAsync(request.StoreId, DateTime.Now, items, request.DiscountSchemeIds);
|
||||||
|
|
||||||
var po = new Models.ShopPO
|
var po = new Models.ShopPO
|
||||||
{
|
{
|
||||||
FK_StoreId = request.StoreId,
|
FK_StoreId = request.StoreId,
|
||||||
@@ -110,6 +128,7 @@ public class ShopPOService(
|
|||||||
await repo.AddAsync(po);
|
await repo.AddAsync(po);
|
||||||
foreach (var item in items) item.FK_ShopPOId = po.Id;
|
foreach (var item in items) item.FK_ShopPOId = po.Id;
|
||||||
await repo.AddItemsAsync(items);
|
await repo.AddItemsAsync(items);
|
||||||
|
await schemeService.SyncRedemptionsAsync(po.Id, items, request.DiscountSchemeIds);
|
||||||
await uow.CommitAsync(ct);
|
await uow.CommitAsync(ct);
|
||||||
}
|
}
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
@@ -137,27 +156,22 @@ public class ShopPOService(
|
|||||||
|
|
||||||
foreach (var line in request.Items)
|
foreach (var line in request.Items)
|
||||||
{
|
{
|
||||||
var unitPrice = line.UnitPrice
|
var item = await BuildLineAsync(line, po.FK_StoreId);
|
||||||
?? (await priceListService.ResolveByStoreAsync(line.ItemId, po.FK_StoreId, DateTime.Now))?.Price
|
item.FK_ShopPOId = id;
|
||||||
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
|
newItems.Add(item);
|
||||||
|
total += item.Quantity * item.UnitPrice;
|
||||||
newItems.Add(new ShopPOItem
|
|
||||||
{
|
|
||||||
FK_ShopPOId = id,
|
|
||||||
FK_ItemId = line.ItemId,
|
|
||||||
Quantity = line.Quantity,
|
|
||||||
QuantityFulfilled = 0,
|
|
||||||
UnitPrice = unitPrice
|
|
||||||
});
|
|
||||||
total += line.Quantity * unitPrice;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await schemeService.ValidateFreeLinesAsync(po.FK_StoreId, po.CreatedAt, newItems);
|
||||||
|
await schemeService.ValidateDiscountSchemesAsync(po.FK_StoreId, po.CreatedAt, newItems, request.DiscountSchemeIds);
|
||||||
|
|
||||||
po.TotalAmount = total;
|
po.TotalAmount = total;
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await repo.ReplaceItemsAsync(id, newItems);
|
await repo.ReplaceItemsAsync(id, newItems);
|
||||||
|
await schemeService.SyncRedemptionsAsync(id, newItems, request.DiscountSchemeIds);
|
||||||
repo.Update(po);
|
repo.Update(po);
|
||||||
await uow.CommitAsync(ct);
|
await uow.CommitAsync(ct);
|
||||||
}
|
}
|
||||||
@@ -212,6 +226,9 @@ public class ShopPOService(
|
|||||||
CreatedBy = userId,
|
CreatedBy = userId,
|
||||||
TotalAmount = po.TotalAmount
|
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);
|
await uow.BeginAsync(ct);
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -15,5 +15,11 @@ namespace Distribution.Models
|
|||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public int QuantityFulfilled { get; set; } = 0;
|
public int QuantityFulfilled { get; set; } = 0;
|
||||||
public decimal UnitPrice { get; set; }
|
public decimal UnitPrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Free issue granted by a scheme (UnitPrice 0); shown in the Free Issue column.</summary>
|
||||||
|
public bool IsFree { get; set; }
|
||||||
|
public Guid? FK_SchemeId { get; set; }
|
||||||
|
/// <summary>Scheme name when it was applied, so later scheme edits don't rewrite history.</summary>
|
||||||
|
public string? SchemeName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user