using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using Microsoft.Extensions.Logging; using MMDAL.CustomCode.VendorComparison; using MMDAL.DTO.VendorComparison; using Newtonsoft.Json; namespace MMBLL.VendorComparison { public class VendorComparisonBLL : IVendorComparisonBLL { private readonly IVendorComparisonDAL _VendorComparisonDAL; private readonly ILogger _logger; public VendorComparisonBLL(IVendorComparisonDAL vendorComparisonDAL, ILogger logger) { _VendorComparisonDAL = vendorComparisonDAL; _logger = logger; } public async Task GetVendorComparison( int Type, int MetricType, int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO, CancellationToken ct = default) { try { var filter = ParseFilter(CriteriaDTO); var detailRows = (await _VendorComparisonDAL.GetVendorComparisonDetail(filter, FirstNumber, MaxResult, LoginDTO, ct)).ToList(); if (Type == 1) { // Detail — attach the per-formula charges array to the first row, matching // GB4's VendorComparisonDetailDTOs[0].VendorComparisonChargesArray exactly. if (detailRows.Count > 0) { var charges = await _VendorComparisonDAL.GetVendorComparisonCharges(filter, LoginDTO, ct); detailRows[0].VendorComparisonChargesArray = charges; } return JsonConvert.SerializeObject(detailRows); } // Type == 0 — Summary: dedupe the same detail rows down to one per unique // {ItemId, SkuId, UomId, Quantity, Remarks}, mirroring GB4's in-memory grouping // (GB4 never ran a separate SQL query for this). Each group's full vendor rows // (not just the first) feed BuildVendorSlots for V1-V4/BestFit. var summaryRows = detailRows .GroupBy(r => (r.ItemId, r.SkuId, r.UomId, r.Quantity, r.Remarks)) .Select(g => { var first = g.First(); var summary = new VendorComparisonSummaryDTO { ItemSlNo = first.ItemSlNo, ItemId = first.ItemId, ItemCode = first.ItemCode, ItemName = first.ItemName, SkuId = first.SkuId, SkuCode = first.SkuCode, SkuName = first.SkuName, Quantity = first.Quantity, UomId = first.UomId, UomCode = first.UomCode, UomName = first.UomName, Remarks = first.Remarks }; ApplyVendorSlots(summary, g.ToList(), MetricType); return summary; }) .ToList(); return JsonConvert.SerializeObject(summaryRows); } catch (Exception ex) { _logger.LogError(ex, "GetVendorComparison failed for Type {Type}", Type); throw; } } // V1-V4 = top 4 vendors for this item/sku by OverallRank (1=best), fixed order regardless // of MetricType. Cell value follows MetricType; BestFit is whichever of V1-V4 has the best // value for that metric — lower wins across the board (Rate/Cost are cheaper-is-better, // the three rank columns are already lower-is-better by definition). // Confirmed by explicit design decision — GB4 never implemented any of this. private static void ApplyVendorSlots(VendorComparisonSummaryDTO summary, List vendors, int metricType) { // PartyId is a real, valid key even when negative (this codebase uses large negative // numbers as normal primary keys throughout — NOT a "no vendor" marker). The LEFT // JOIN's actual "no vendor configured" sentinel is exactly 0 (Dapper's default for a // non-nullable int column that came back NULL) — confirmed against live data where all // 4 real vendors for one item had negative PartyIds and would have been wrongly // excluded by a "> 0" filter. var configuredVendors = vendors.Where(v => v.PartyId != 0).ToList(); summary.VendorCount = configuredVendors.Count; var top4 = configuredVendors .OrderBy(v => v.OverallRank ?? int.MaxValue) .Take(4) .ToList(); decimal? MetricValue(VendorComparisonDetailDTO v) => metricType switch { 0 => v.Rate, 1 => v.NetCost, 2 => v.DeliveryRank, 3 => v.QualityRank, 4 => v.OverallRank, _ => null // 5 = Grade — unsupported, no confirmed per-vendor source }; var slotValues = new decimal?[4]; for (int i = 0; i < top4.Count && i < 4; i++) slotValues[i] = MetricValue(top4[i]); summary.V1 = slotValues[0]; summary.V2 = slotValues[1]; summary.V3 = slotValues[2]; summary.V4 = slotValues[3]; int bestSlot = -1; for (int i = 0; i < 4; i++) { if (slotValues[i] == null) continue; if (bestSlot == -1 || slotValues[i]!.Value < slotValues[bestSlot]!.Value) bestSlot = i; } summary.BestFit = bestSlot >= 0 ? $"V{bestSlot + 1}" : null; } public async Task UpdateVendorDetail(VendorComparisonUpdateDTO VendorComparisonUpdateDTO, LoginDTO LoginDTO, CancellationToken ct = default) { try { int rows = await _VendorComparisonDAL.UpdateVendorDetail(VendorComparisonUpdateDTO, LoginDTO, ct); _logger.LogInformation("Vendor detail updated for ItemPartyId {ItemPartyId} by user {UserId}", VendorComparisonUpdateDTO.ItemPartyId, LoginDTO.UserId); return rows > 0 ? "Data updated.." : "ItemParty not found."; } catch (Exception ex) { _logger.LogError(ex, "UpdateVendorDetail failed for ItemPartyId {ItemPartyId}", VendorComparisonUpdateDTO.ItemPartyId); throw; } } // Matches GB4 MMHeadBLL.GetVendorComparison's own for-loop extraction of // DocumentId/ItemId/SkuId/ItemCategoryId/ItemSubCategoryId/ItemGroupId/BuyerId out of // CriteriaDTO.SectionCriteriaList[*].AttributesCriteriaList — same -1 "ignore filter" default. private static VendorComparisonFilterDTO ParseFilter(CriteriaDTO criteriaDTO) { var filter = new VendorComparisonFilterDTO(); var attributes = criteriaDTO?.SectionCriteriaList? .Where(s => s?.AttributesCriteriaList != null) .SelectMany(s => s.AttributesCriteriaList) ?? Enumerable.Empty(); foreach (var attribute in attributes) { if (attribute?.FieldName == null) continue; switch (attribute.FieldName.ToLowerInvariant()) { case "documentid": filter.DocumentId = ToInt(attribute.FieldValue, -1); break; case "itemid": filter.ItemId = ToInt(attribute.FieldValue, -1); break; case "skuid": filter.SkuId = ToInt(attribute.FieldValue, -1); break; case "itemcategoryid": filter.ItemCategoryId = ToInt(attribute.FieldValue, -1); break; case "itemsubcategoryid": filter.ItemSubCategoryId = ToInt(attribute.FieldValue, -1); break; case "itemgroupid": filter.ItemGroupId = ToInt(attribute.FieldValue, -1); break; case "buyerid": filter.BuyerId = ToInt(attribute.FieldValue, -1); break; } } return filter; } private static int ToInt(object? fieldValue, int defaultValue) { if (fieldValue == null) return defaultValue; try { if (fieldValue is JsonElement je) { return je.ValueKind switch { JsonValueKind.Number when je.TryGetInt32(out int n) => n, JsonValueKind.String when int.TryParse(je.GetString(), out int s) => s, _ => defaultValue }; } return Convert.ToInt32(fieldValue); } catch { return defaultValue; } } } }