sales fixers

This commit is contained in:
2026-10-21 10:45:19 +05:30
parent 47479fb4f2
commit a27bdaea64
14 changed files with 12580 additions and 832 deletions
+37 -2
View File
@@ -67,7 +67,7 @@ public sealed class BundleSaleService : IBundleSaleService
var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(x => x.BundleSaleTemplateId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
return PagedResponse<BundleSaleTemplateSummaryDto>.Create(rows.Select(x => new BundleSaleTemplateSummaryDto(
x.BundleSaleTemplateId, x.TemplateCode, x.TemplateName, x.Description, x.Status, x.Lines.Count, x.CreatedAt, x.UpdatedAt)).ToList(), query.Page, query.PageSize, total);
x.BundleSaleTemplateId, x.TemplateCode, x.TemplateName, x.Description, x.BundlePrice, x.Status, x.Lines.Count, x.CreatedAt, x.UpdatedAt)).ToList(), query.Page, query.PageSize, total);
}
public async Task<BundleSaleTemplateDto?> GetTemplateAsync(int bundleSaleTemplateId, CancellationToken ct = default)
@@ -78,6 +78,7 @@ public sealed class BundleSaleService : IBundleSaleService
template.TemplateCode,
template.TemplateName,
template.Description,
template.BundlePrice,
template.Status,
template.CreatedAt,
template.UpdatedAt,
@@ -92,10 +93,18 @@ public sealed class BundleSaleService : IBundleSaleService
TemplateCode = await _numbers.NextAsync(DocumentTypes.BundleSaleTemplate, ct),
TemplateName = request.TemplateName,
Description = request.Description,
BundlePrice = request.BundlePrice ?? 0m,
Status = EntityStatus.Active,
CreatedAt = DateTime.UtcNow
};
if (request.BundlePrice is { } bundlePrice)
{
if (!request.Lines.Any(l => l.IncludeInBundle))
throw new DomainException(ErrorCodes.Validation, "A bundle needs at least one included line to carry its price.", 422);
AllocateBundlePrice(request.Lines, bundlePrice);
}
foreach (var line in request.Lines)
{
if (!await _items.Query().AsNoTracking().AnyAsync(x => x.ItemId == line.ItemId, ct))
@@ -118,12 +127,38 @@ public sealed class BundleSaleService : IBundleSaleService
await _uow.SaveChangesAsync(ct);
return new BundleSaleTemplateDto(
template.BundleSaleTemplateId, template.TemplateCode, template.TemplateName, template.Description,
template.BundleSaleTemplateId, template.TemplateCode, template.TemplateName, template.Description, template.BundlePrice,
template.Status, template.CreatedAt, template.UpdatedAt,
template.Lines.OrderBy(x => x.SortOrder).Select(x => new BundleSaleTemplateLineDto(
x.BundleSaleTemplateLineId, x.ItemId, x.WarehouseId, x.Qty, x.UnitPrice, x.IncludeInBundle, x.SortOrder)).ToList());
}
/// <summary>
/// Splits the bundle price over the included lines' unit prices, in proportion to each line's
/// UnitPrice × Qty as sent (the items' normal value), or to Qty when no line carries a price.
/// Line totals are rounded to 2 dp with the last included line taking the rounding remainder, so the
/// totals add up to the bundle price; excluded lines are priced 0.
/// </summary>
private static void AllocateBundlePrice(List<CreateBundleSaleTemplateLineRequest> lines, decimal bundlePrice)
{
var included = lines.Where(l => l.IncludeInBundle).ToList();
var weights = included.Select(l => l.UnitPrice * l.Qty).ToList();
if (weights.Sum() <= 0) weights = included.Select(l => l.Qty).ToList();
var totalWeight = weights.Sum();
var allocated = 0m;
for (var i = 0; i < included.Count; i++)
{
var lineTotal = i == included.Count - 1
? bundlePrice - allocated
: Math.Round(bundlePrice * weights[i] / totalWeight, 2, MidpointRounding.AwayFromZero);
allocated += lineTotal;
included[i].UnitPrice = Math.Round(lineTotal / included[i].Qty, 4, MidpointRounding.AwayFromZero);
}
foreach (var line in lines.Where(l => !l.IncludeInBundle)) line.UnitPrice = 0m;
}
public async Task<PagedResponse<BundleSaleSummaryDto>> ListAsync(PageQuery query, BundleSaleStatus? status, int? customerId, int? warehouseId, CancellationToken ct = default)
{
IQueryable<BundleSale> q = _bundles.Query().AsNoTracking().Include(x => x.Lines);