using GB5Shared.DTO.Ice; using GB5Shared.DTO.Framework.Login; using IceImportDAL.DTO.IceImportPipeline; namespace IceImportBLL.Validators; // IceMapDetailsIsOptional == 1 (mandatory) fields must have a non-null/non-empty resolved value. // IsOptional == 0 is optional — its resolver already falls back to IceMapDetailsOptionalFiledValue // when the source column is absent, so an empty resolved value there is never an error. // // ValueType 0/3/4/5 (Source/Expression/CodeToId/NameToId — the types that read a real source // column) are deliberately EXCLUDED here: their "required and not provided" check is done in // IceImportPipeline.MapAndTransformAsync against the RAW Excel cell (IceMapDetailsSourceFieldName), // not against the resolved/coerced row.Fields value — a failed date coercion or an unresolved // Code/Name lookup must surface as its own distinct warning, never get mislabeled as "missing" just // because resolution happened to produce null for some other reason. // // ValueType 8/9 (Fixed/Global Value During Update) are ALSO excluded here — IceImportPipeline // already flags them with a distinct "not supported" Error for Insert-mode runs (isUpdateMode // false; CommitImport is the only Insert entry point), so checking them again here would just // double up on the same finding. ValueType 1/2 (their Insert-time counterparts) get the identical // "not supported" treatment from IceImportPipeline, but ONLY for Update-mode runs (isUpdateMode // true; IceImport/Update is the only Update entry point) — for Insert-mode runs 1/2 remain valid, // so THIS validator is still what catches a mandatory Fixed/Global field configured with nothing to // resolve (e.g. an empty IceMapDetailsSourceFieldName) during Insert. What's left in scope here — // 1 (Fixed), 2 (Global), 6 (ParentIdAssigning), 7 (AutoNumberAssigning) — never read Excel at all, // so checking their resolved value is the only meaningful "required" check available for them. // // The object/List CONTAINER marker row (e.g. "EmployeeAddon", DestinationMemberDataType==5 AND // IceMapDetailsAddonFields empty) is also excluded: IceImportPipeline only fills // row.Fields[ListMemberName] for it at Commit time, never during MapAndTransform/Validate, so an // empty resolved value there is structural, not a data problem. DestinationMemberDataType==5 alone // cannot distinguish "the container" from "an addon field routed through the same shared placeholder // member" (see IceImportPipeline's identical check), hence the IceMapDetailsAddonFields condition. public class RequiredFieldValidator : IRowValidator { public string Key => "REQUIRED_FIELD"; private static readonly HashSet PipelineHandledValueTypes = new() { 0, 3, 4, 5, 8, 9 }; public Task> ValidateAsync( MappedEntityRowDTO row, IceMapDTO mapDefinition, ParsedImportDataSetDTO parsedData, LoginDTO login, CancellationToken ct) { var errors = new List(); var requiredFields = mapDefinition.IceMapDetailsArray .Where(d => d.DestinationMemberEntityId == row.EntityId && d.IceMapDetailsIsOptional == 1 && !(d.DestinationMemberDataType == 5 && string.IsNullOrWhiteSpace(d.IceMapDetailsAddonFields)) && !PipelineHandledValueTypes.Contains(d.IceMapDetailsValueType)); foreach (var detail in requiredFields) { var fieldName = detail.DestinationMemberMemberName; if (string.IsNullOrEmpty(fieldName)) continue; var hasValue = row.Fields.TryGetValue(fieldName, out var value) && value is not null && !(value is string s && string.IsNullOrWhiteSpace(s)); if (!hasValue) { errors.Add(new IceImportFieldErrorDTO { RowNumber = row.RowNumber, EntityCode = row.EntityCode, FieldName = fieldName, Message = $"'{fieldName}' is required and was not provided.", Severity = IceImportSeverity.Error }); } } return Task.FromResult(errors); } }