Feat/procurement #8

Merged
DeepnaPooja merged 9 commits from feat/procurement into dev 2026-08-24 16:58:48 +05:30
2 changed files with 93 additions and 3 deletions
Showing only changes of commit 095d35e1b8 - Show all commits
@@ -36,7 +36,18 @@ public class SupplierController(SupplierService service) : ControllerBase
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
var deleted = await service.DeleteAsync(id, ct);
return deleted ? NoContent() : NotFound();
try
{
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;
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) =>
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);
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);
}
}