using GB5Shared.DTO.Ice; using GB5Shared.DTO.Framework.Login; using IceImportDAL.DTO.IceImportPipeline; namespace IceImportBLL.Validators; // Checks that every mandatory (IsOptional=1) ValueType 0/3/4/5 (Source/Expression/CodeToId/ // NameToId — the only types that read a real source column) IceMapDetails.SourceFieldName // actually exists as a column somewhere in the parsed source data (legacy's CheckHeaderFieldsName). // IsOptional=0 fields may legitimately be absent — the corresponding resolver falls back to // IceMapDetailsOptionalFiledValue instead of erroring (see SourceValueResolver et al.). // This is a header-level check, not a per-row one — guarded to run only once (on RowNumber==1) even // though the pipeline invokes every IRowValidator per row; findings are tagged RowNumber=0 by // convention to mark them as header-level rather than tied to a specific data row. public class HeaderFieldExistenceValidator : IRowValidator { public string Key => "HEADER_FIELD_EXISTENCE"; private static readonly HashSet SourceColumnValueTypes = new() { 0, 3, 4, 5 }; public Task> ValidateAsync( MappedEntityRowDTO row, IceMapDTO mapDefinition, ParsedImportDataSetDTO parsedData, LoginDTO login, CancellationToken ct) { var errors = new List(); if (row.RowNumber != 1) return Task.FromResult(errors); foreach (var detail in mapDefinition.IceMapDetailsArray) { if (!SourceColumnValueTypes.Contains(detail.IceMapDetailsValueType)) continue; if (detail.IceMapDetailsIsOptional == 0) continue; // optional — falls back to OptionalFiledValue instead // Exclude ONLY the object/List container marker itself (e.g. the "EmployeeAddon" row) — // its SourceFieldName is a container label, never a real Excel column. Individual addon // fields (BONUSAPPL, INCAPPL, ...) commonly share ONE placeholder DestinationMemberId // whose MENTITYMEMBERS.DATATYPE is ALSO 5 (they don't map to a real typed POCO member), // so DestinationMemberDataType==5 alone cannot distinguish "the container" from "an addon // field routed through that same placeholder member" — IceMapDetailsAddonFields (only set // on real addon field rows, never on the container) is what actually distinguishes them. var isContainerMarker = detail.DestinationMemberDataType == 5 && string.IsNullOrWhiteSpace(detail.IceMapDetailsAddonFields); if (isContainerMarker) continue; // Applies uniformly regardless of which entity group a detail row belongs to — including // Addon-designated fields (non-empty IceMapDetailsAddonFields, e.g. BONUSAPPL/INCAPPL, // grouped under the AddonEntityId sentinel): this check is keyed off ValueType/IsOptional/ // SourceFieldName directly against the parsed file, never filtered by entity. var fieldName = detail.IceMapDetailsSourceFieldName?.Trim().ToUpperInvariant(); if (string.IsNullOrEmpty(fieldName)) continue; var found = parsedData.Tables.Values.Any(t => t.Columns.Contains(fieldName)); if (found) continue; errors.Add(new IceImportFieldErrorDTO { RowNumber = 0, EntityCode = detail.EntityCode ?? string.Empty, FieldName = detail.DestinationMemberMemberName ?? fieldName, Message = $"Required source column '{fieldName}' was not found in the uploaded file.", Severity = IceImportSeverity.Error }); } return Task.FromResult(errors); } }