using GB5Shared.DTO.Ice; using IceImportDAL.DTO.IceImportPipeline; namespace IceImportBLL.HierarchyMapper; public class HierarchyMapper : IHierarchyMapper { // MENTITY.ENTITYID sentinel meaning "Addon entity" — MICEMAPDETAILS rows whose // DestinationMemberEntityId is this value carry an IceMapDetailsAddonFields (physical column // name) instead of a normal DBFIELDID/DataType=5 mapping. Mirrors IceImportPipeline's own // identical constant (duplicated per class, same convention as EntityLookupService.GcmEntityId). private const int AddonEntityId = -1399999424; public List BuildHierarchy( Dictionary> resolvedRowsByEntity, IceMapDTO mapDefinition) { // IceMapEntityId is the POCO-side MICEMAP.ENTITYID; IceMapDetails' DestinationMemberEntityId // is always the DTO-side entity (joined via MENTITYMEMBERS). Must compare via the resolved // link (IceMapRootEntityId), not IceMapEntityId directly — see IceMapDTO's doc comment. var rootEntityId = mapDefinition.IceMapRootEntityId; if (!resolvedRowsByEntity.TryGetValue(rootEntityId, out var rootCandidates) || rootCandidates.Count == 0) return new List(); var roots = rootCandidates.Select(c => c.Row).ToList(); // Last-write-wins is fine here — CorrelationIndex values are expected to be unique per // root row (either distinct positional indices, or distinct JSON __ROWINDEX__ values). var rootByIndex = new Dictionary(); foreach (var candidate in rootCandidates) rootByIndex[candidate.CorrelationIndex] = candidate.Row; foreach (var (childEntityId, childCandidates) in resolvedRowsByEntity) { if (childEntityId == rootEntityId) continue; foreach (var candidate in childCandidates) { if (!rootByIndex.TryGetValue(candidate.CorrelationIndex, out var parentRow)) continue; // no matching parent row at this correlation index — drop silently is // not acceptable; caller surfaces this via RequiredFieldValidator/row // count mismatches instead of HierarchyMapper inventing a parent. if (!parentRow.Children.TryGetValue(childEntityId, out var list)) { list = new List(); parentRow.Children[childEntityId] = list; } list.Add(candidate.Row); } } return roots; } public string? GetParentIdFieldName(IceMapDTO mapDefinition, int childEntityId) => mapDefinition.IceMapDetailsArray .FirstOrDefault(d => d.DestinationMemberEntityId == childEntityId && d.IceMapDetailsValueType == 6) ?.DestinationMemberMemberName; public string? GetFirstParentIdFieldName(IceMapDTO mapDefinition, int childEntityId) => mapDefinition.IceMapDetailsArray .FirstOrDefault(d => d.DestinationMemberEntityId == childEntityId && d.IceMapDetailsValueType == 11) ?.DestinationMemberMemberName; public List GetObjectListRelationships(IceMapDTO mapDefinition) { var result = new List(); // Addon: check for the presence of any field row hardcoded to the AddonEntityId sentinel // FIRST (e.g. BONUSAPPL/INCAPPL's own DestinationMemberEntityId=-1399999424) — the DataType=5 // marker representing the Addon object as a whole (e.g. EmployeeAddOn) has its OWN, // different, real EntityId/EntityPocoToDtoLinkId, so it can never be found by resolving the // marker's link the way BOM-style relationships are. If no such field exists at all, this // map has no Addon and the BOM-style detection below runs unchanged. var hasAddonFields = mapDefinition.IceMapDetailsArray .Any(d => d.DestinationMemberEntityId == AddonEntityId); if (hasAddonFields) { // Find the DataType=5 marker (whatever it's named, e.g. EmployeeAddOn) and emit ONE // relationship for the whole Addon object, keyed by the AddonEntityId sentinel directly // — not the marker's own (unrelated) resolved link. Individual field names (BONUSAPPL, // INCAPPL, ...) come from each field row's own IceMapDetailsAddonFields elsewhere in the // pipeline (AddonFieldAccumulator) — this DTO only carries the ONE containing member name. var addonMarker = mapDefinition.IceMapDetailsArray .FirstOrDefault(d => d.DestinationMemberDataType == 5); if (addonMarker is not null && !string.IsNullOrWhiteSpace(addonMarker.DestinationMemberMemberName)) { result.Add(new ObjectListRelationshipDTO( addonMarker.DestinationMemberEntityId, AddonEntityId, addonMarker.DestinationMemberMemberName!, addonMarker.DestinationMemberMemberName!)); } return result; } var listMemberRows = mapDefinition.IceMapDetailsArray .Where(d => d.IceMapDetailsValueType == 0 && d.DestinationMemberDataType == 5) .ToList(); foreach (var row in listMemberRows) { var listMemberName = row.DestinationMemberMemberName; if (string.IsNullOrWhiteSpace(listMemberName)) continue; // row.EntityId is POCO-side (mirrors IceMapDTO.IceMapEntityId's own POCO/DTO split) — // the actual DTO-side child entity, matching every other DestinationMemberEntityId // value in this map, is EntityPocoToDtoLinkId. != 0, not > 0 — entity ids here are // routinely large negative numbers (see IceMapDTO.IceMapRootEntityId's identical // convention). var childEntityId = row.EntityPocoToDtoLinkId != 0 ? row.EntityPocoToDtoLinkId : row.EntityId; var groupingField = mapDefinition.IceMapDetailsArray .FirstOrDefault(d => d.DestinationMemberEntityId == childEntityId && d.IceMapDetailsIsFilterField == 0) ?.IceMapDetailsSourceFieldName; if (string.IsNullOrWhiteSpace(groupingField)) continue; result.Add(new ObjectListRelationshipDTO( row.DestinationMemberEntityId, childEntityId, listMemberName!, groupingField!)); } return result; } }