Files
ERP_Distribution/Distribution/Features/Supplier/SupplierService.cs
2026-09-09 16:30:38 +05:30

141 lines
5.2 KiB
C#

using Distribution.Features.Grn;
using Distribution.Features.Po;
using Distribution.Infra.UoW;
namespace Distribution.Features.Supplier;
public class SupplierService(
ISupplierRepository repo,
IPoRepository purchaseOrderRepo,
IGrnRepository goodsReceiptRepo,
IUnitOfWork uow)
{
private static SupplierResponse Map(Models.Supplier s) =>
new(s.Id, s.Name, s.ContactInfo, s.PerformanceScore, s.IsActive, s.CreatedAt);
public async Task<List<SupplierResponse>> GetAllAsync() =>
(await repo.GetAllAsync()).Select(Map).ToList();
public async Task<SupplierResponse?> GetByIdAsync(Guid id)
{
var supplier = await repo.GetByIdAsync(id);
return supplier is null ? null : Map(supplier);
}
public async Task<SupplierResponse> CreateAsync(CreateSupplierRequest request, CancellationToken ct = default)
{
var supplier = new Models.Supplier
{
Name = request.Name,
ContactInfo = request.ContactInfo
};
await uow.BeginAsync(ct);
try { await repo.AddAsync(supplier); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map(supplier);
}
public async Task<SupplierResponse?> UpdateAsync(Guid id, UpdateSupplierRequest request, CancellationToken ct = default)
{
var supplier = await repo.GetByIdAsync(id);
if (supplier is null) return null;
supplier.Name = request.Name;
supplier.ContactInfo = request.ContactInfo;
supplier.IsActive = request.IsActive;
await uow.BeginAsync(ct);
try { repo.Update(supplier); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map(supplier);
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
var supplier = await repo.GetByIdAsync(id);
if (supplier is null || !supplier.IsActive) return false;
if (await purchaseOrderRepo.HasActiveForSupplierAsync(id))
throw new InvalidOperationException(
"Cannot delete a supplier that is referenced by an active purchase order.");
await uow.BeginAsync(ct);
try { repo.Remove(supplier); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return true;
}
/// <summary>
/// The only writer of Supplier.PerformanceScore. Computes a simple 0-100 score from this
/// supplier's own purchase-order history:
/// - lineFulfillmentRatio: received (non-cancelled, fully-received) Po lines / all non-active
/// Po lines seen (received + cancelled) - how reliably the supplier fulfils what it commits to.
/// - acceptanceRatio: AcceptedQuantity / (AcceptedQuantity + RejectedQuantity) across every
/// GoodsReceipt against this supplier's Pos - how much of what arrives is usable.
/// score = round(average(lineFulfillmentRatio, acceptanceRatio) * 100, 2).
/// Defaults to 50 (neutral) when there is no history yet, rather than hardcoding a fixed score.
/// </summary>
public async Task<SupplierResponse?> EvaluateAsync(Guid id, CancellationToken ct = default)
{
var supplier = await repo.GetByIdAsync(id);
if (supplier is null || !supplier.IsActive) return null;
var purchaseOrders = await purchaseOrderRepo.GetBySupplierAsync(id);
var receivedLines = 0;
var cancelledLines = 0;
foreach (var po in purchaseOrders)
{
foreach (var item in po.Items.Where(i => i.IsActive))
{
if (item.IsCancelled) cancelledLines++;
else if (item.ReceivedQuantity >= item.Quantity) receivedLines++;
}
}
var totalAccepted = 0;
var totalRejected = 0;
foreach (var po in purchaseOrders)
{
foreach (var receipt in await goodsReceiptRepo.GetByPurchaseOrderIdAsync(po.Id))
{
foreach (var item in receipt.Items.Where(i => i.IsActive))
{
totalAccepted += item.AcceptedQuantity;
totalRejected += item.RejectedQuantity;
}
}
}
var lineFulfillmentRatio = receivedLines + cancelledLines > 0
? (decimal)receivedLines / (receivedLines + cancelledLines)
: (decimal?)null;
var acceptanceRatio = totalAccepted + totalRejected > 0
? (decimal)totalAccepted / (totalAccepted + totalRejected)
: (decimal?)null;
decimal score;
if (lineFulfillmentRatio.HasValue && acceptanceRatio.HasValue)
score = Math.Round((lineFulfillmentRatio.Value + acceptanceRatio.Value) / 2m * 100m, 2);
else if (lineFulfillmentRatio.HasValue)
score = Math.Round(lineFulfillmentRatio.Value * 100m, 2);
else if (acceptanceRatio.HasValue)
score = Math.Round(acceptanceRatio.Value * 100m, 2);
else
score = 50m;
supplier.PerformanceScore = score;
await uow.BeginAsync(ct);
try { repo.Update(supplier); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return Map(supplier);
}
}