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> GetAllAsync() => (await repo.GetAllAsync()).Select(Map).ToList(); public async Task GetByIdAsync(Guid id) { var supplier = await repo.GetByIdAsync(id); return supplier is null ? null : Map(supplier); } public async Task 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 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 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; } /// /// 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. /// public async Task 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); } }