141 lines
5.2 KiB
C#
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);
|
|
if (supplier is null || !supplier.IsActive) return null;
|
|
return 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 || !supplier.IsActive) return null;
|
|
|
|
supplier.Name = request.Name;
|
|
supplier.ContactInfo = request.ContactInfo;
|
|
|
|
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);
|
|
}
|
|
}
|