Add supplier evaluation and block delete when referenced by a purchase order
This commit is contained in:
@@ -36,7 +36,18 @@ public class SupplierController(SupplierService service) : ControllerBase
|
|||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var deleted = await service.DeleteAsync(id, ct);
|
try
|
||||||
return deleted ? NoContent() : NotFound();
|
{
|
||||||
|
var deleted = await service.DeleteAsync(id, ct);
|
||||||
|
return deleted ? NoContent() : NotFound();
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/evaluate")]
|
||||||
|
public async Task<IActionResult> Evaluate(Guid id, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await service.EvaluateAsync(id, ct);
|
||||||
|
return result is null ? NotFound() : Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
using Distribution.Features.Grn;
|
||||||
|
using Distribution.Features.Po;
|
||||||
using Distribution.Infra.UoW;
|
using Distribution.Infra.UoW;
|
||||||
|
|
||||||
namespace Distribution.Features.Supplier;
|
namespace Distribution.Features.Supplier;
|
||||||
|
|
||||||
public class SupplierService(ISupplierRepository repo, IUnitOfWork uow)
|
public class SupplierService(
|
||||||
|
ISupplierRepository repo,
|
||||||
|
IPoRepository purchaseOrderRepo,
|
||||||
|
IGrnRepository goodsReceiptRepo,
|
||||||
|
IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
private static SupplierResponse Map(Models.Supplier s) =>
|
private static SupplierResponse Map(Models.Supplier s) =>
|
||||||
new(s.Id, s.Name, s.ContactInfo, s.PerformanceScore, s.IsActive, s.CreatedAt);
|
new(s.Id, s.Name, s.ContactInfo, s.PerformanceScore, s.IsActive, s.CreatedAt);
|
||||||
@@ -52,10 +58,83 @@ public class SupplierService(ISupplierRepository repo, IUnitOfWork uow)
|
|||||||
var supplier = await repo.GetByIdAsync(id);
|
var supplier = await repo.GetByIdAsync(id);
|
||||||
if (supplier is null || !supplier.IsActive) return false;
|
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);
|
await uow.BeginAsync(ct);
|
||||||
try { repo.Remove(supplier); await uow.CommitAsync(ct); }
|
try { repo.Remove(supplier); await uow.CommitAsync(ct); }
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user