Compare commits

..

15 Commits

Author SHA1 Message Date
DeepnaPooja b7f58e6b73 Merge pull request 'Fix dashoard issues' (#85) from dev into production
Reviewed-on: #85
2026-10-22 10:55:41 +05:30
DeepnaPooja e13ec0f109 Merge pull request 'Fix dashboard stat card handling issue' (#84) from feat-notification into dev
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 12m59s
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 16m29s
Reviewed-on: #84
2026-10-22 10:54:52 +05:30
DeepnaPooja 13fdb28ec7 Merge pull request 'Dev' (#83) from dev into production
Reviewed-on: #83
2026-10-21 13:14:19 +05:30
DeepnaPooja 39ad424d21 Merge pull request 'Invoice flow' (#82) from feat-notification into dev
CI/CD - ERP Distribution / Build & Deploy Frontend (pull_request) Failing after 12m58s
CI/CD - ERP Distribution / Build & Deploy Backend (pull_request) Failing after 14m20s
Reviewed-on: #82
2026-10-21 13:13:21 +05:30
DeepnaPooja ee9a5385bf Merge pull request 'Dev' (#81) from dev into production
Reviewed-on: #81
2026-10-20 17:31:19 +05:30
kalana_Thilakarathna 138bf2f925 Merge pull request 'Add invoice creation mode support to Shop PO flow' (#79) from dev into production
Reviewed-on: #79
2026-10-20 14:02:31 +05:30
kalana_Thilakarathna 1b4461dc12 Merge pull request 'Add manual invoice creation mode with new API & UI flow' (#78) from dev into production
Reviewed-on: #78
2026-10-20 13:39:12 +05:30
kalana_Thilakarathna cc33ecdd54 Merge pull request 'Implement store credit tracking and enforcement' (#77) from dev into production
Reviewed-on: #77
2026-10-20 11:51:10 +05:30
kalana_Thilakarathna f77db46a02 Merge pull request 'Dev' (#76) from dev into production
Reviewed-on: #76
2026-10-19 23:38:07 +05:30
kalana_Thilakarathna 267144f7a1 Merge pull request 'feat: add EmptyState component for improved no-data display in supplier payments' (#75) from dev into production
Reviewed-on: #75
2026-10-19 22:35:48 +05:30
kalana_Thilakarathna efb5657b6b Merge pull request 'feat: update row actions to include payment functionality and add corresponding icon' (#74) from dev into production
Reviewed-on: #74
2026-10-19 22:13:07 +05:30
kalana_Thilakarathna 622b75e187 Merge pull request 'feat: integrate payment functionality with new payment sheet and enhance supplier payments UI' (#73) from dev into production
Reviewed-on: #73
2026-10-19 22:02:45 +05:30
kalana_Thilakarathna 293df41d4e Merge pull request 'Dev' (#72) from dev into production
Reviewed-on: #72
2026-10-19 21:41:42 +05:30
DeepnaPooja 5e81fd77b1 Merge pull request 'Dev' (#71) from dev into production
Reviewed-on: #71
2026-10-19 16:47:06 +05:30
kalana_Thilakarathna a1fd5f1a08 Merge pull request 'feat: add cheque books and received cheques management' (#69) from dev into production
Reviewed-on: #69
2026-10-19 16:12:32 +05:30
15 changed files with 60 additions and 6766 deletions
@@ -52,13 +52,6 @@ public class InvoiceController(InvoiceService service, ICurrentUserService curre
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("{id:guid}/preview-totals")]
public async Task<IActionResult> PreviewTotals(Guid id, [FromBody] PreviewInvoiceTotalsRequest request)
{
var result = await service.PreviewTotalsAsync(id, request);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/versions")]
public async Task<IActionResult> GetVersions(Guid id)
{
+4 -16
View File
@@ -1,5 +1,4 @@
using Distribution.Features.ShopPO;
using Distribution.Models;
using Distribution.Models;
namespace Distribution.Features.Invoice;
@@ -10,14 +9,7 @@ public record PrintInvoiceRequest(Guid? PrintedByUserId = null);
// InvoiceItemId set = edit/remove/restore an existing line; ItemId set (InvoiceItemId null) = add a new line.
public record InvoiceItemEdit(Guid? InvoiceItemId, Guid? ItemId, int Quantity, decimal? UnitPrice = null, bool IsRemoved = false);
// DiscountSchemeIds: the percentage-discount scheme chosen in the editor (at most one); null = leave as is.
public record UpdateInvoiceItemsRequest(List<InvoiceItemEdit> Items, Guid? EditedByUserId = null, List<Guid>? DiscountSchemeIds = null);
public record PreviewLine(Guid ItemId, int Quantity, decimal UnitPrice, bool IsFree = false, bool IsRemoved = false, Guid? SchemeId = null);
public record PreviewInvoiceTotalsRequest(List<PreviewLine> Items, List<Guid>? DiscountSchemeIds = null);
// Quantity = how many free units the scheme still entitles for the lines as typed.
public record FreeEntitlement(Guid SchemeId, int Quantity);
public record InvoiceTotalsPreview(decimal DiscountAmount, decimal TotalAmount, List<FreeEntitlement> FreeEntitlements);
public record UpdateInvoiceItemsRequest(List<InvoiceItemEdit> Items, Guid? EditedByUserId = null);
public record InvoiceVersionItemResponse(Guid? ShopPOItemId, Guid ItemId, string ItemName, int Quantity, decimal UnitPrice, bool IsRemoved);
@@ -38,10 +30,7 @@ public record InvoiceItemResponse(
int? OrderedQuantity,
int Quantity,
decimal UnitPrice,
bool IsRemoved,
bool IsFree = false,
Guid? SchemeId = null,
string? SchemeName = null);
bool IsRemoved);
public record InvoiceResponse(
Guid Id,
@@ -60,7 +49,6 @@ public record InvoiceResponse(
decimal PaidAmount,
bool IsActive,
DateTime CreatedAt,
List<InvoiceItemResponse> Items,
List<ShopPODiscountResponse>? Discounts = null);
List<InvoiceItemResponse> Items);
public record OutstandingPaymentsResponse(int InvoiceCount, decimal OutstandingAmount);
+11 -58
View File
@@ -1,5 +1,4 @@
using Distribution.Features.Scheme;
using Distribution.Features.ShopPO;
using Distribution.Features.ShopPO;
using Distribution.Features.Stock;
using Distribution.Infra.Authorization;
using Distribution.Infra.UoW;
@@ -10,7 +9,6 @@ namespace Distribution.Features.Invoice;
public class InvoiceService(
IInvoiceRepository repo,
IShopPORepository shopPoRepo,
SchemeService schemeService,
IUnitOfWork uow,
ICurrentUserService currentUser)
{
@@ -27,53 +25,20 @@ public class InvoiceService(
i.Items.Where(x => x.IsActive).Select(x =>
new InvoiceItemResponse(x.Id, x.FK_ShopPOItemId, x.FK_ItemId,
x.Item?.Name ?? string.Empty, x.ShopPOItem?.Quantity,
x.Quantity, x.UnitPrice, x.IsRemoved, x.IsFree, x.FK_SchemeId, x.SchemeName)).ToList());
x.Quantity, x.UnitPrice, x.IsRemoved)).ToList());
private static decimal BillableTotal(IEnumerable<InvoiceItem> items) =>
items.Where(i => i.IsActive && !i.IsRemoved).Sum(i => i.Quantity * i.UnitPrice);
// The lines that count toward a percentage discount: active, not struck out. Free issues carry the flag so the
// scheme rules ignore them.
private static List<ShopPOItem> DiscountableLines(IEnumerable<InvoiceItem> items) =>
items.Where(i => i.IsActive && !i.IsRemoved)
.Select(i => new ShopPOItem { FK_ItemId = i.FK_ItemId, Quantity = i.Quantity, UnitPrice = i.UnitPrice, IsFree = i.IsFree })
.ToList();
// TotalAmount = billable lines - scheme discount. The discount is re-evaluated against the current lines
// (so adding/removing/re-pricing items moves it, and a slab level can be gained or lost) whenever lines change
// and at print/finalize. The redemption amount is updated with it.
// TotalAmount = billable lines - scheme discount redeemed on the PO. Re-derived whenever lines change
// and at finalize, so a scheme applied after the invoice was drafted is still picked up before it locks.
private async Task ApplyTotalsAsync(Models.Invoice invoice, IEnumerable<InvoiceItem> items)
{
var lines = items.ToList();
// Lowering/removing the paid item that earned a free issue lowers the free quantity with it.
await schemeService.ReconcileInvoiceFreeLinesAsync(invoice.FK_ShopPOId, lines);
var billable = BillableTotal(lines);
var discount = await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, DiscountableLines(lines), persist: true);
invoice.DiscountAmount = Math.Min(discount, billable);
var billable = BillableTotal(items);
invoice.DiscountAmount = Math.Min(await repo.GetSchemeDiscountAsync(invoice.FK_ShopPOId), billable);
invoice.TotalAmount = billable - invoice.DiscountAmount;
}
// Live preview for the draft editor: what the discount and total would be for these unsaved lines. Changes nothing.
public async Task<InvoiceTotalsPreview?> PreviewTotalsAsync(Guid id, PreviewInvoiceTotalsRequest request)
{
var invoice = await repo.GetByIdAsync(id);
if (invoice is null) return null;
var lines = request.Items.Where(l => !l.IsRemoved)
.Select(l => new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice, IsFree = l.IsFree, FK_SchemeId = l.SchemeId })
.ToList();
var billable = lines.Sum(l => l.Quantity * l.UnitPrice);
var chosen = request.DiscountSchemeIds is null
? await schemeService.CalculateDiscountAsync(invoice.FK_ShopPOId, lines, persist: false)
: await schemeService.CalculateDiscountForSchemesAsync(request.DiscountSchemeIds, lines);
var discount = Math.Min(chosen, billable);
var schemeIds = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue).Select(l => l.FK_SchemeId!.Value).ToList();
var entitlements = (await schemeService.GetFreeEntitlementsAsync(schemeIds, lines))
.Select(e => new FreeEntitlement(e.Key, e.Value)).ToList();
return new InvoiceTotalsPreview(discount, billable - discount, entitlements);
}
// Immutable copy of the invoice lines as they stand right now.
public static InvoiceVersion BuildVersion(Models.Invoice invoice, IEnumerable<InvoiceItem> items, int versionNumber, InvoiceVersionStage stage, Guid userId) =>
new()
@@ -118,17 +83,13 @@ public class InvoiceService(
FK_ShopPOItemId = i.Id,
FK_ItemId = i.FK_ItemId,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice,
IsFree = i.IsFree,
FK_SchemeId = i.FK_SchemeId,
SchemeName = i.SchemeName
UnitPrice = i.UnitPrice
}).ToList();
public async Task<InvoiceResponse?> GetByIdAsync(Guid id)
{
var invoice = await repo.GetByIdAsync(id);
if (invoice is null) return null;
return ToResponse(invoice) with { Discounts = await schemeService.GetDiscountsForShopPOAsync(invoice.FK_ShopPOId) };
return invoice is null ? null : ToResponse(invoice);
}
public async Task<PagedResponse<InvoiceResponse>> GetAllAsync(Guid? agentId, Guid? routeId, Guid? shopId, InvoiceStatus? status, PaymentStatus? paymentStatus, int page, int pageSize)
@@ -211,12 +172,7 @@ public class InvoiceService(
ValidateQuantity(item, edit.Quantity);
item.Quantity = edit.Quantity;
}
if (edit.UnitPrice is decimal price)
{
if (item.IsFree && price != 0m)
throw new InvalidOperationException("A free-issue line must stay at zero price");
item.UnitPrice = price;
}
if (edit.UnitPrice is decimal price) item.UnitPrice = price;
}
else if (edit.ItemId is Guid itemId)
{
@@ -245,10 +201,7 @@ public class InvoiceService(
throw new InvalidOperationException("Each edit needs an invoiceItemId (existing line) or an itemId (new line)");
}
var allLines = invoice.Items.Concat(newLines).ToList();
await schemeService.ApplyInvoiceDiscountSelectionAsync(
invoice.FK_ShopPOId, invoice.ShopPO?.FK_StoreId ?? Guid.Empty, allLines, request.DiscountSchemeIds);
await ApplyTotalsAsync(invoice, allLines);
await ApplyTotalsAsync(invoice, invoice.Items.Concat(newLines));
await uow.BeginAsync(ct);
try
@@ -260,7 +213,7 @@ public class InvoiceService(
}
catch { await uow.RollbackAsync(ct); throw; }
return (await GetByIdAsync(id))!;
return ToResponse((await repo.GetByIdAsync(id))!);
}
private static void ValidateQuantity(InvoiceItem item, int quantity)
@@ -10,8 +10,6 @@ public interface ISchemeRepository
Task<List<Models.SchemeRedemption>> GetRedemptionsAsync(Guid schemeId);
Task AddRedemptionAsync(Models.SchemeRedemption entity);
Task<List<Models.SchemeRedemption>> GetRedemptionsByShopPOAsync(Guid shopPOId);
void RemoveRedemption(Models.SchemeRedemption entity);
Task<bool> HasRedemptionAsync(Guid schemeId, Guid orderId);
Task<bool> HasLockedInvoiceAsync(Guid shopPOId);
@@ -112,14 +112,6 @@ public class SchemeController(SchemeService service) : ControllerBase
return result is null ? NotFound() : Ok(result);
}
[HttpPost("suggest")]
[Authorize("scheme.view")]
public async Task<IActionResult> Suggest(SuggestSchemesRequest request)
{
var result = await service.SuggestAsync(request);
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/spend")]
[Authorize("scheme.view")]
public async Task<IActionResult> GetSpend(Guid id)
+1 -7
View File
@@ -94,13 +94,7 @@ public record SchemeEligibilityResponse(
string? Reason,
decimal? DiscountAmount,
Guid? FreeItemId,
decimal? FreeQty,
string? FreeItemName = null);
/// <summary>A cart line for scheme suggestion before the Shop PO exists. UnitPrice null = resolve from the price list.</summary>
public record SuggestLine(Guid ItemId, int Quantity, decimal? UnitPrice = null, bool IsFree = false);
public record SuggestSchemesRequest(Guid StoreId, List<SuggestLine> Items);
decimal? FreeQty);
public record SchemeRedemptionResponse(
Guid Id,
@@ -49,18 +49,6 @@ public class SchemeRepository(AppDBContext context) : ISchemeRepository
public async Task AddRedemptionAsync(Models.SchemeRedemption entity) =>
await context.SchemeRedemptions.AddAsync(entity);
public async Task<List<Models.SchemeRedemption>> GetRedemptionsByShopPOAsync(Guid shopPOId)
{
var list = await context.SchemeRedemptions.Include(r => r.Scheme).Where(r => r.FK_ShopPOId == shopPOId).ToListAsync();
// Rows added earlier in this unit of work are not in the database yet; rows removed are still there.
foreach (var added in context.SchemeRedemptions.Local.Where(r => r.FK_ShopPOId == shopPOId && !list.Contains(r)))
list.Add(added);
return list.Where(r => context.Entry(r).State != EntityState.Deleted).ToList();
}
public void RemoveRedemption(Models.SchemeRedemption entity) =>
context.SchemeRedemptions.Remove(entity);
public async Task<bool> HasRedemptionAsync(Guid schemeId, Guid shopPOId) =>
await context.SchemeRedemptions.AnyAsync(r => r.FK_SchemeId == schemeId && r.FK_ShopPOId == shopPOId);
+10 -360
View File
@@ -1,7 +1,5 @@
using System.Text.Json;
using Distribution.Features.Invoice;
using Distribution.Features.Item;
using Distribution.Features.PriceList;
using Distribution.Features.ShopPO;
using Distribution.Features.Stock;
using Distribution.Features.Store;
@@ -16,8 +14,6 @@ public class SchemeService(
IItemRepository itemRepo,
IStoreRepository storeRepo,
IStockRepository stockRepo,
IInvoiceRepository invoiceRepo,
PriceListService priceListService,
IUnitOfWork uow)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
@@ -265,7 +261,12 @@ public class SchemeService(
foreach (var scheme in candidates)
{
if (!IsInScope(scheme, store.Segment, regionId, shopPO.CreatedAt))
if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != regionId)
continue;
if (!string.IsNullOrEmpty(scheme.EligibleSegment) &&
!string.Equals(scheme.EligibleSegment, store.Segment, StringComparison.OrdinalIgnoreCase))
continue;
if (shopPO.CreatedAt < scheme.DateRangeFrom || shopPO.CreatedAt > scheme.DateRangeTo)
continue;
result.Add(EvaluateRule(scheme, items));
@@ -274,328 +275,6 @@ public class SchemeService(
return result;
}
private static bool IsInScope(Models.Scheme scheme, string? storeSegment, Guid? storeRegionId, DateTime asOf)
{
if (scheme.FK_RegionId.HasValue && scheme.FK_RegionId != storeRegionId) return false;
if (!string.IsNullOrEmpty(scheme.EligibleSegment) &&
!string.Equals(scheme.EligibleSegment, storeSegment, StringComparison.OrdinalIgnoreCase)) return false;
return asOf >= scheme.DateRangeFrom && asOf.Date <= scheme.DateRangeTo.Date;
}
/// <summary>
/// Suggestions for a Shop PO that is still being built (nothing saved yet). Returns eligible schemes,
/// plus BuyXGetY/FreeBundle schemes the cart is part-way to, so the UI can nudge ("add 4 more").
/// </summary>
public async Task<List<SchemeEligibilityResponse>?> SuggestAsync(SuggestSchemesRequest request)
{
var store = await storeRepo.GetByIdWithRegionAsync(request.StoreId);
if (store is null || !store.IsActive) return null;
var lines = new List<ShopPOItem>();
foreach (var l in request.Items.Where(i => !i.IsFree))
{
var price = l.UnitPrice
?? (await priceListService.ResolveByStoreAsync(l.ItemId, request.StoreId, DateTime.Now))?.Price
?? 0m;
lines.Add(new ShopPOItem { FK_ItemId = l.ItemId, Quantity = l.Quantity, UnitPrice = price });
}
var regionId = store.Route?.FK_RegionId;
var inCart = lines.Select(l => l.FK_ItemId).ToHashSet();
var result = new List<SchemeEligibilityResponse>();
foreach (var scheme in (await repo.GetAllAsync()).Where(s => s.IsActive))
{
if (!IsInScope(scheme, store.Segment, regionId, DateTime.Now)) continue;
var eval = EvaluateRule(scheme, lines);
var relevant = eval.Eligible || scheme.RuleType switch
{
SchemeRuleType.BuyXGetY => scheme.FK_TriggerItemId.HasValue && inCart.Contains(scheme.FK_TriggerItemId.Value),
SchemeRuleType.FreeBundle => scheme.BundleComponents.Any(c => inCart.Contains(c.FK_ItemId)),
_ => false
};
if (!relevant) continue;
result.Add(eval with { FreeItemName = scheme.FreeItem?.Name });
}
return result;
}
/// <summary>
/// Checks the free lines of a Shop PO that is about to be saved: each must reference an in-scope scheme the
/// paid lines still qualify for, be the scheme's free item, stay within the entitled qty, and be in stock.
/// Stamps the scheme-name snapshot and forces UnitPrice to 0. Read-only; throws InvalidOperationException.
/// </summary>
public async Task ValidateFreeLinesAsync(Guid storeId, DateTime asOf, List<ShopPOItem> lines)
{
var freeLines = lines.Where(l => l.IsFree).ToList();
if (freeLines.Count == 0) return;
if (freeLines.Any(l => l.FK_SchemeId is null))
throw new InvalidOperationException("A free-issue line must reference the scheme that granted it.");
if (freeLines.Any(l => l.Quantity <= 0))
throw new InvalidOperationException("Free-issue quantity must be greater than zero (remove the line instead).");
var store = await storeRepo.GetByIdWithRegionAsync(storeId)
?? throw new InvalidOperationException("Store not found.");
var regionId = store.Route?.FK_RegionId;
foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value))
{
var scheme = await repo.GetByIdAsync(group.Key);
if (scheme is null || !scheme.IsActive)
throw new InvalidOperationException("A free-issue line references a scheme that no longer exists.");
if (!IsInScope(scheme, store.Segment, regionId, asOf))
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date.");
var eval = EvaluateRule(scheme, lines);
if (!eval.Eligible || eval.FreeItemId is null || eval.FreeQty is null)
throw new InvalidOperationException(eval.Reason ?? $"Order no longer qualifies for scheme '{scheme.Name}'.");
if (group.Any(l => l.FK_ItemId != eval.FreeItemId))
throw new InvalidOperationException($"Scheme '{scheme.Name}' grants a different free item.");
var qty = group.Sum(l => l.Quantity);
if (qty > eval.FreeQty.Value)
throw new InvalidOperationException($"Free quantity {qty} exceeds the {eval.FreeQty.Value:0.##} entitled by scheme '{scheme.Name}'.");
var available = (await stockRepo.GetBalancesByItemAsync(eval.FreeItemId.Value)).Sum(b => b.Quantity);
if (available < qty)
throw new InvalidOperationException($"Insufficient stock to give {qty} free of scheme '{scheme.Name}'.");
foreach (var l in group)
{
l.UnitPrice = 0m;
l.SchemeName = scheme.Name;
}
}
}
/// <summary>
/// Checks the percentage-discount schemes (FlatPercent / SlabDiscount) the user accepted for a Shop PO that is
/// about to be saved: one at most, in scope, and the paid lines must still qualify. Read-only; throws.
/// </summary>
public async Task ValidateDiscountSchemesAsync(Guid storeId, DateTime asOf, List<ShopPOItem> lines, List<Guid>? schemeIds)
{
if (schemeIds is not { Count: > 0 }) return;
if (schemeIds.Distinct().Count() > 1)
throw new InvalidOperationException("Only one discount scheme can be applied to an order.");
var store = await storeRepo.GetByIdWithRegionAsync(storeId)
?? throw new InvalidOperationException("Store not found.");
var scheme = await repo.GetByIdAsync(schemeIds[0]);
if (scheme is null || !scheme.IsActive)
throw new InvalidOperationException("The selected discount scheme no longer exists.");
if (scheme.RuleType is not (SchemeRuleType.FlatPercent or SchemeRuleType.SlabDiscount))
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not a discount scheme.");
if (!IsInScope(scheme, store.Segment, store.Route?.FK_RegionId, asOf))
throw new InvalidOperationException($"Scheme '{scheme.Name}' is not available for this store or date.");
var eval = EvaluateRule(scheme, lines);
if (!eval.Eligible)
throw new InvalidOperationException(eval.Reason ?? $"Order does not qualify for scheme '{scheme.Name}'.");
}
/// <summary>
/// Re-evaluates the percentage discounts redeemed against a Shop PO for the given (invoice) lines and returns
/// their total. With persist = true the redemption amounts are updated too (the caller commits), so spend/ROI and
/// the invoice stay in step; a discount the lines no longer qualify for drops to 0 and comes back if they requalify.
/// </summary>
public async Task<decimal> CalculateDiscountAsync(Guid shopPOId, IEnumerable<ShopPOItem> lines, bool persist)
{
var priced = lines.ToList();
decimal total = 0m;
foreach (var r in (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue))
{
var scheme = await repo.GetByIdAsync(r.FK_SchemeId);
var eval = scheme is null ? null : EvaluateRule(scheme, priced);
var amount = eval is { Eligible: true, DiscountAmount: not null } ? eval.DiscountAmount.Value : 0m;
if (persist) r.DiscountAmount = amount;
total += amount;
}
return total;
}
// How many free units each scheme currently entitles for these paid lines (0 when no longer eligible).
private async Task<Dictionary<Guid, int>> FreeEntitlementsAsync(IEnumerable<Guid> schemeIds, List<ShopPOItem> paidLines)
{
var result = new Dictionary<Guid, int>();
foreach (var id in schemeIds.Distinct())
{
var scheme = await repo.GetByIdAsync(id);
var eval = scheme is null ? null : EvaluateRule(scheme, paidLines);
result[id] = eval is { Eligible: true, FreeQty: not null } ? (int)Math.Floor(eval.FreeQty.Value) : 0;
}
return result;
}
/// <summary>
/// Free-issue entitlement per scheme for unsaved invoice lines (editor preview). Lines carry IsFree / scheme id;
/// only the paid ones count toward the scheme rules.
/// </summary>
public async Task<Dictionary<Guid, int>> GetFreeEntitlementsAsync(IEnumerable<Guid> schemeIds, IEnumerable<ShopPOItem> lines) =>
await FreeEntitlementsAsync(schemeIds, lines.Where(l => !l.IsFree).ToList());
/// <summary>
/// Keeps an invoice's free-issue lines within what the remaining paid lines still earn: when the paid quantity of the
/// qualifying item is lowered (or removed), the free quantity is reduced to match (a line that earns nothing is struck
/// out) and the PO's redemption is resized. Stages changes only. Returns true if any free line was changed.
/// </summary>
public async Task<bool> ReconcileInvoiceFreeLinesAsync(Guid shopPOId, List<InvoiceItem> lines)
{
var freeLines = lines.Where(l => l.IsActive && l.IsFree && l.FK_SchemeId.HasValue).ToList();
if (freeLines.Count == 0) return false;
var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree)
.Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice })
.ToList();
var entitled = await FreeEntitlementsAsync(freeLines.Select(l => l.FK_SchemeId!.Value), paid);
var redemptions = await repo.GetRedemptionsByShopPOAsync(shopPOId);
var changed = false;
foreach (var group in freeLines.GroupBy(l => l.FK_SchemeId!.Value))
{
var remaining = entitled[group.Key];
foreach (var line in group.Where(l => !l.IsRemoved).OrderBy(l => l.CreatedAt))
{
if (line.Quantity <= remaining) { remaining -= line.Quantity; continue; }
if (remaining == 0) line.IsRemoved = true;
else line.Quantity = remaining;
remaining = 0;
changed = true;
}
var redemption = redemptions.FirstOrDefault(r => r.FK_SchemeId == group.Key && r.FK_FreeItemId.HasValue);
if (redemption is not null)
redemption.FreeQty = group.Where(l => !l.IsRemoved).Sum(l => l.Quantity);
}
return changed;
}
/// <summary>
/// Discount the given schemes would give for these lines, without touching any redemption (editor preview).
/// </summary>
public async Task<decimal> CalculateDiscountForSchemesAsync(IEnumerable<Guid> schemeIds, IEnumerable<ShopPOItem> lines)
{
var priced = lines.ToList();
decimal total = 0m;
foreach (var id in schemeIds.Distinct())
{
var scheme = await repo.GetByIdAsync(id);
var eval = scheme is null ? null : EvaluateRule(scheme, priced);
if (eval is { Eligible: true, DiscountAmount: not null }) total += eval.DiscountAmount.Value;
}
return total;
}
/// <summary>
/// Applies the discount choice made on a Draft invoice (the percentage scheme the user accepted, or none) to the
/// PO's redemptions: newly chosen schemes are validated and added, unchosen ones removed. The amount itself is
/// then set by CalculateDiscountAsync when the totals are re-derived. Stages changes only. Null = leave as is.
/// </summary>
public async Task ApplyInvoiceDiscountSelectionAsync(Guid shopPOId, Guid storeId, List<InvoiceItem> lines, List<Guid>? schemeIds)
{
if (schemeIds is null) return;
if (schemeIds.Distinct().Count() > 1)
throw new InvalidOperationException("Only one discount scheme can be applied to an order.");
var existing = (await repo.GetRedemptionsByShopPOAsync(shopPOId)).Where(r => r.DiscountAmount.HasValue).ToList();
foreach (var r in existing.Where(r => !schemeIds.Contains(r.FK_SchemeId)))
repo.RemoveRedemption(r);
var paid = lines.Where(l => l.IsActive && !l.IsRemoved && !l.IsFree)
.Select(l => new ShopPOItem { FK_ItemId = l.FK_ItemId, Quantity = l.Quantity, UnitPrice = l.UnitPrice })
.ToList();
var added = schemeIds.Where(id => existing.All(r => r.FK_SchemeId != id)).ToList();
await ValidateDiscountSchemesAsync(storeId, DateTime.Now, paid, added);
foreach (var id in added)
await repo.AddRedemptionAsync(new SchemeRedemption { FK_SchemeId = id, FK_ShopPOId = shopPOId, DiscountAmount = 0m });
}
/// <summary>Discounts already redeemed against a Shop PO, for display and for editing.</summary>
public async Task<List<ShopPODiscountResponse>> GetDiscountsForShopPOAsync(Guid shopPOId) =>
(await repo.GetRedemptionsByShopPOAsync(shopPOId))
.Where(r => r.DiscountAmount.HasValue)
.Select(r => new ShopPODiscountResponse(r.FK_SchemeId, r.Scheme?.Name ?? string.Empty, r.DiscountAmount!.Value))
.ToList();
/// <summary>
/// Brings the Shop PO's scheme redemptions in line with its saved lines: free-goods redemptions follow the
/// free lines (created, resized or removed), discount redemptions are recomputed or dropped if the order no
/// longer qualifies. Stages changes only — the caller owns the commit.
/// </summary>
public async Task SyncRedemptionsAsync(Guid shopPOId, List<ShopPOItem> lines, List<Guid>? discountSchemeIds = null)
{
var existing = await repo.GetRedemptionsByShopPOAsync(shopPOId);
var freeBySchemes = lines.Where(l => l.IsFree && l.FK_SchemeId.HasValue)
.GroupBy(l => l.FK_SchemeId!.Value).ToDictionary(g => g.Key, g => g.ToList());
foreach (var r in existing)
{
if (r.FK_FreeItemId.HasValue)
{
if (!freeBySchemes.ContainsKey(r.FK_SchemeId)) repo.RemoveRedemption(r);
}
else if (r.DiscountAmount.HasValue)
{
// A null list means "leave the accepted discounts alone" (just recompute them).
if (discountSchemeIds is not null && !discountSchemeIds.Contains(r.FK_SchemeId))
{
repo.RemoveRedemption(r);
continue;
}
var scheme = await repo.GetByIdAsync(r.FK_SchemeId);
var eval = scheme is null ? null : EvaluateRule(scheme, lines);
if (eval is { Eligible: true, DiscountAmount: not null }) r.DiscountAmount = eval.DiscountAmount;
else repo.RemoveRedemption(r);
}
}
foreach (var schemeId in discountSchemeIds ?? [])
{
if (existing.Any(r => r.FK_SchemeId == schemeId && r.DiscountAmount.HasValue)) continue;
var scheme = await repo.GetByIdAsync(schemeId);
var eval = scheme is null ? null : EvaluateRule(scheme, lines);
if (eval is { Eligible: true, DiscountAmount: not null })
await repo.AddRedemptionAsync(new SchemeRedemption
{
FK_SchemeId = schemeId,
FK_ShopPOId = shopPOId,
DiscountAmount = eval.DiscountAmount
});
}
foreach (var (schemeId, group) in freeBySchemes)
{
var freeItem = await itemRepo.GetByIdAsync(group[0].FK_ItemId)
?? throw new InvalidOperationException("Free item not found.");
var qty = group.Sum(l => l.Quantity);
var redemption = existing.FirstOrDefault(r => r.FK_SchemeId == schemeId && r.FK_FreeItemId.HasValue);
if (redemption is null)
await repo.AddRedemptionAsync(new SchemeRedemption
{
FK_SchemeId = schemeId,
FK_ShopPOId = shopPOId,
FK_FreeItemId = freeItem.Id,
FreeQty = qty,
FreeItemCostValue = freeItem.CostPrice ?? 0m
});
else
{
redemption.FK_FreeItemId = freeItem.Id;
redemption.FreeQty = qty;
redemption.FreeItemCostValue = freeItem.CostPrice ?? 0m;
}
}
}
/// <summary>
/// Scope-only check (region, segment, date range as of today) with no ShopPO to evaluate line
/// items against — used by Store Management's `checkStoreSchemeEligibility(storeId, schemeId)`
@@ -620,18 +299,14 @@ public class SchemeService(
return (false, $"Scheme is only available to the '{scheme.EligibleSegment}' segment.");
var today = DateTime.Now;
if (today < scheme.DateRangeFrom || today.Date > scheme.DateRangeTo.Date)
if (today < scheme.DateRangeFrom || today > scheme.DateRangeTo)
return (false, "Scheme is not currently active (outside its date range).");
return (true, null);
}
private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List<ShopPOItem> allItems)
private static SchemeEligibilityResponse EvaluateRule(Models.Scheme scheme, List<ShopPOItem> items)
{
// Free-issue lines (added by a previous redemption) must not count toward a trigger/bundle
// quantity, otherwise the free units inflate the multiplier.
var items = allItems.Where(i => !i.IsFree && i.UnitPrice > 0).ToList();
switch (scheme.RuleType)
{
case SchemeRuleType.BuyXGetY:
@@ -739,10 +414,7 @@ public class SchemeService(
FK_ShopPOId = shopPOId,
FK_ItemId = freeItem.Id,
Quantity = (int)eligibility.FreeQty.Value,
UnitPrice = 0m,
IsFree = true,
FK_SchemeId = scheme.Id,
SchemeName = scheme.Name
UnitPrice = 0m
});
redemption.FK_FreeItemId = freeItem.Id;
@@ -753,27 +425,7 @@ public class SchemeService(
await uow.BeginAsync(ct);
try
{
if (newItems.Count > 0)
{
await shopPORepo.AddItemsAsync(newItems);
// A still-draft invoice was copied from the PO before this line existed, so mirror it there.
if (shopPO.InvoiceId is Guid invoiceId &&
await invoiceRepo.GetByIdAsync(invoiceId) is { Status: InvoiceStatus.Draft })
{
await invoiceRepo.AddItemsAsync(newItems.Select(n => new InvoiceItem
{
FK_InvoiceId = invoiceId,
FK_ShopPOItemId = n.Id,
FK_ItemId = n.FK_ItemId,
Quantity = n.Quantity,
UnitPrice = 0m,
IsFree = true,
FK_SchemeId = n.FK_SchemeId,
SchemeName = n.SchemeName
}));
}
}
if (newItems.Count > 0) await shopPORepo.AddItemsAsync(newItems);
await repo.AddRedemptionAsync(redemption);
await uow.CommitAsync(ct);
}
@@ -835,8 +487,6 @@ public class SchemeService(
if (freeItemId is null || freeQty is null or <= 0)
throw new InvalidOperationException("FreeItemId and a positive FreeQty are required.");
if (freeQty.Value != decimal.Truncate(freeQty.Value))
throw new InvalidOperationException("FreeQty must be a whole number (order lines hold whole units).");
if (await itemRepo.GetByIdAsync(freeItemId.Value) is null)
throw new InvalidOperationException("Free item not found.");
}
+5 -11
View File
@@ -2,18 +2,13 @@
namespace Distribution.Features.ShopPO;
// DiscountSchemeIds: the FlatPercent/SlabDiscount scheme the user accepted (at most one). Null = leave as is.
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null, List<Guid>? 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<ShopPOItemRequest> Items, List<Guid>? DiscountSchemeIds = null);
public record CreateShopPORequest(Guid StoreId, List<ShopPOItemRequest> Items, Guid? RaisedByUserId = null);
public record ShopPOItemRequest(Guid ItemId, int Quantity, decimal? UnitPrice = null);
public record UpdateShopPOItemsRequest(List<ShopPOItemRequest> Items);
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,
bool IsFree = false, Guid? SchemeId = null, string? SchemeName = null);
public record ShopPODiscountResponse(Guid SchemeId, string SchemeName, decimal Amount);
public record ShopPOItemResponse(Guid Id, Guid ItemId, string ItemName, int Quantity, int QuantityFulfilled, decimal UnitPrice, bool IsActive);
public record ShopPOResponse(
Guid Id,
@@ -39,5 +34,4 @@ public record ShopPOResponse(
DateTime? CancelledAt = null,
string? CancelReason = null,
string? CancelledByName = null,
string? CancelledByRole = null,
List<ShopPODiscountResponse>? Discounts = null);
string? CancelledByRole = null);
+29 -46
View File
@@ -1,7 +1,6 @@
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;
@@ -12,30 +11,9 @@ 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<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.
private Task NotifyOrderAsync(string type, Models.ShopPO po, Guid actingUserId, string? reason, CancellationToken ct) =>
notifications.NotifyAsync(
@@ -44,15 +22,14 @@ 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<Guid, (string Name, string Role)>? users = null, List<ShopPODiscountResponse>? discounts = null) =>
private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary<Guid, (string Name, string Role)>? users = 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,
i.IsFree, i.FK_SchemeId, i.SchemeName)).ToList(),
i.Quantity, i.QuantityFulfilled, i.UnitPrice, i.IsActive)).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,
@@ -60,8 +37,7 @@ 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,
discounts ?? []);
users != null && p.CancelledBy is Guid xb2 && users.TryGetValue(xb2, out var x2) ? x2.Role : null);
private static IEnumerable<Guid> UserIds(Models.ShopPO p)
{
@@ -86,7 +62,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, await schemeService.GetDiscountsForShopPOAsync(po.Id));
return ToResponse(po, users);
}
public async Task<List<ShopPOResponse>> GetConfirmedWithoutInvoiceAsync(Guid? agentId, Guid? routeId, Guid? shopId)
@@ -106,13 +82,19 @@ public class ShopPOService(
foreach (var line in request.Items)
{
var item = await BuildLineAsync(line, request.StoreId);
items.Add(item);
total += item.Quantity * item.UnitPrice;
}
var unitPrice = line.UnitPrice
?? (await priceListService.ResolveByStoreAsync(line.ItemId, request.StoreId, DateTime.Now))?.Price
?? throw new InvalidOperationException($"No resolvable price for item {line.ItemId}");
await schemeService.ValidateFreeLinesAsync(request.StoreId, DateTime.Now, items);
await schemeService.ValidateDiscountSchemesAsync(request.StoreId, DateTime.Now, items, request.DiscountSchemeIds);
items.Add(new ShopPOItem
{
FK_ItemId = line.ItemId,
Quantity = line.Quantity,
QuantityFulfilled = 0,
UnitPrice = unitPrice
});
total += line.Quantity * unitPrice;
}
var po = new Models.ShopPO
{
@@ -128,7 +110,6 @@ 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; }
@@ -156,14 +137,20 @@ public class ShopPOService(
foreach (var line in request.Items)
{
var item = await BuildLineAsync(line, po.FK_StoreId);
item.FK_ShopPOId = id;
newItems.Add(item);
total += item.Quantity * item.UnitPrice;
}
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}");
await schemeService.ValidateFreeLinesAsync(po.FK_StoreId, po.CreatedAt, newItems);
await schemeService.ValidateDiscountSchemesAsync(po.FK_StoreId, po.CreatedAt, newItems, request.DiscountSchemeIds);
newItems.Add(new ShopPOItem
{
FK_ShopPOId = id,
FK_ItemId = line.ItemId,
Quantity = line.Quantity,
QuantityFulfilled = 0,
UnitPrice = unitPrice
});
total += line.Quantity * unitPrice;
}
po.TotalAmount = total;
@@ -171,7 +158,6 @@ public class ShopPOService(
try
{
await repo.ReplaceItemsAsync(id, newItems);
await schemeService.SyncRedemptionsAsync(id, newItems, request.DiscountSchemeIds);
repo.Update(po);
await uow.CommitAsync(ct);
}
@@ -226,9 +212,6 @@ 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
File diff suppressed because it is too large Load Diff
@@ -1,99 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Distribution.Migrations
{
/// <inheritdoc />
public partial class AddFreeIssueLines : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "FK_SchemeId",
table: "ShopPOItems",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<bool>(
name: "IsFree",
table: "ShopPOItems",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "SchemeName",
table: "ShopPOItems",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<Guid>(
name: "FK_SchemeId",
table: "InvoiceItems",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<bool>(
name: "IsFree",
table: "InvoiceItems",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "SchemeName",
table: "InvoiceItems",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
// Existing free-issue lines were plain zero-priced lines; flag them from their scheme redemption.
migrationBuilder.Sql(@"
UPDATE ShopPOItems i
JOIN SchemeRedemptions r ON r.FK_ShopPOId = i.FK_ShopPOId AND r.FK_FreeItemId = i.FK_ItemId
JOIN Schemes s ON s.Id = r.FK_SchemeId
SET i.IsFree = 1, i.FK_SchemeId = r.FK_SchemeId, i.SchemeName = s.Name
WHERE i.UnitPrice = 0 AND i.IsActive = 1;");
migrationBuilder.Sql(@"
UPDATE InvoiceItems ii
JOIN ShopPOItems pi ON ii.FK_ShopPOItemId = pi.Id
SET ii.IsFree = pi.IsFree, ii.FK_SchemeId = pi.FK_SchemeId, ii.SchemeName = pi.SchemeName
WHERE pi.IsFree = 1;");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FK_SchemeId",
table: "ShopPOItems");
migrationBuilder.DropColumn(
name: "IsFree",
table: "ShopPOItems");
migrationBuilder.DropColumn(
name: "SchemeName",
table: "ShopPOItems");
migrationBuilder.DropColumn(
name: "FK_SchemeId",
table: "InvoiceItems");
migrationBuilder.DropColumn(
name: "IsFree",
table: "InvoiceItems");
migrationBuilder.DropColumn(
name: "SchemeName",
table: "InvoiceItems");
}
}
}
@@ -932,27 +932,18 @@ namespace Distribution.Migrations
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid?>("FK_SchemeId")
.HasColumnType("char(36)");
b.Property<Guid?>("FK_ShopPOItemId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsFree")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsRemoved")
.HasColumnType("tinyint(1)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<string>("SchemeName")
.HasColumnType("longtext");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(65,30)");
@@ -3726,27 +3717,18 @@ namespace Distribution.Migrations
b.Property<Guid>("FK_ItemId")
.HasColumnType("char(36)");
b.Property<Guid?>("FK_SchemeId")
.HasColumnType("char(36)");
b.Property<Guid>("FK_ShopPOId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsFree")
.HasColumnType("tinyint(1)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("QuantityFulfilled")
.HasColumnType("int");
b.Property<string>("SchemeName")
.HasColumnType("longtext");
b.Property<decimal>("UnitPrice")
.HasColumnType("decimal(65,30)");
@@ -3766,7 +3748,6 @@ namespace Distribution.Migrations
FK_ItemId = new Guid("23000000-0000-0000-0000-000000000001"),
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"),
IsActive = true,
IsFree = false,
Quantity = 30,
QuantityFulfilled = 30,
UnitPrice = 60m
@@ -3778,7 +3759,6 @@ namespace Distribution.Migrations
FK_ItemId = new Guid("23000000-0000-0000-0000-000000000003"),
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000001"),
IsActive = true,
IsFree = false,
Quantity = 24,
QuantityFulfilled = 24,
UnitPrice = 50m
@@ -3790,7 +3770,6 @@ namespace Distribution.Migrations
FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"),
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000002"),
IsActive = true,
IsFree = false,
Quantity = 10,
QuantityFulfilled = 0,
UnitPrice = 120m
@@ -3802,7 +3781,6 @@ namespace Distribution.Migrations
FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"),
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"),
IsActive = true,
IsFree = false,
Quantity = 20,
QuantityFulfilled = 20,
UnitPrice = 120m
-5
View File
@@ -19,10 +19,5 @@ namespace Distribution.Models
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public bool IsRemoved { get; set; }
/// <summary>Free issue copied from the Shop PO line (UnitPrice 0).</summary>
public bool IsFree { get; set; }
public Guid? FK_SchemeId { get; set; }
public string? SchemeName { get; set; }
}
}
-6
View File
@@ -15,11 +15,5 @@ namespace Distribution.Models
public int Quantity { get; set; }
public int QuantityFulfilled { get; set; } = 0;
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; }
}
}