diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs
new file mode 100644
index 0000000..1cb4499
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/IValidationService.cs
@@ -0,0 +1,25 @@
+using JdeScoping.ConfigManager.Models;
+
+namespace JdeScoping.ConfigManager.Services;
+
+///
+/// Result of a validation operation.
+///
+public class ValidationResult
+{
+ public bool IsValid => Errors.Count == 0;
+ public List Errors { get; } = [];
+ public List Warnings { get; } = [];
+
+ public void AddError(string message) => Errors.Add(message);
+ public void AddWarning(string message) => Warnings.Add(message);
+}
+
+///
+/// Service for validating configuration files.
+///
+public interface IValidationService
+{
+ ValidationResult ValidateAppSettings(ConfigModel config);
+ ValidationResult ValidatePipelines(PipelinesConfigModel config);
+}
diff --git a/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs
new file mode 100644
index 0000000..753c6e9
--- /dev/null
+++ b/NEW/src/Utils/JdeScoping.ConfigManager/Services/ValidationService.cs
@@ -0,0 +1,109 @@
+using JdeScoping.ConfigManager.Models;
+
+namespace JdeScoping.ConfigManager.Services;
+
+///
+/// Service for validating configuration files.
+///
+public class ValidationService : IValidationService
+{
+ private static readonly string[] ValidConnections = ["jde", "cms", "giw", "lotfinderdb"];
+
+ public ValidationResult ValidateAppSettings(ConfigModel config)
+ {
+ var result = new ValidationResult();
+
+ // DataSync validation
+ if (config.DataSync.MaxDegreeOfParallelism < 1 || config.DataSync.MaxDegreeOfParallelism > 32)
+ result.AddError("DataSync.MaxDegreeOfParallelism must be between 1 and 32");
+
+ if (config.DataSync.BatchSize < 1000 || config.DataSync.BatchSize > 10_000_000)
+ result.AddError("DataSync.BatchSize must be between 1,000 and 10,000,000");
+
+ if (config.DataSync.BulkCopyBatchSize < 100 || config.DataSync.BulkCopyBatchSize > 100_000)
+ result.AddError("DataSync.BulkCopyBatchSize must be between 100 and 100,000");
+
+ if (config.DataSync.LookbackMultiplier < 1 || config.DataSync.LookbackMultiplier > 10)
+ result.AddError("DataSync.LookbackMultiplier must be between 1 and 10");
+
+ if (config.DataSync.PurgeRetentionDays < 1 || config.DataSync.PurgeRetentionDays > 365)
+ result.AddError("DataSync.PurgeRetentionDays must be between 1 and 365");
+
+ if (config.DataSync.SyncTimeoutSeconds < 60 || config.DataSync.SyncTimeoutSeconds > 86400)
+ result.AddError("DataSync.SyncTimeoutSeconds must be between 60 and 86,400");
+
+ // DataAccess validation
+ if (config.DataAccess.DefaultTimeoutSeconds < 1)
+ result.AddError("DataAccess.DefaultTimeoutSeconds must be at least 1");
+
+ // Ldap validation
+ if (config.Ldap.ConnectionTimeoutSeconds < 1 || config.Ldap.ConnectionTimeoutSeconds > 300)
+ result.AddError("Ldap.ConnectionTimeoutSeconds must be between 1 and 300");
+
+ // Search validation
+ if (config.Search.MaxResultRows < 1)
+ result.AddError("Search.MaxResultRows must be at least 1");
+
+ if (config.Search.MaxConcurrentSearches < 1)
+ result.AddError("Search.MaxConcurrentSearches must be at least 1");
+
+ return result;
+ }
+
+ public ValidationResult ValidatePipelines(PipelinesConfigModel config)
+ {
+ var result = new ValidationResult();
+
+ foreach (var (name, pipeline) in config.Pipelines)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ result.AddError("Pipeline name cannot be empty");
+ continue;
+ }
+
+ // Source validation
+ if (string.IsNullOrWhiteSpace(pipeline.Source.Connection))
+ {
+ result.AddError($"Pipeline '{name}': Source.Connection is required");
+ }
+ else if (!ValidConnections.Contains(pipeline.Source.Connection.ToLowerInvariant()))
+ {
+ result.AddError($"Pipeline '{name}': Source.Connection '{pipeline.Source.Connection}' is not valid. Must be one of: {string.Join(", ", ValidConnections)}");
+ }
+
+ if (string.IsNullOrWhiteSpace(pipeline.Source.Query))
+ {
+ result.AddError($"Pipeline '{name}': Source.Query is required");
+ }
+
+ // Destination validation
+ if (string.IsNullOrWhiteSpace(pipeline.Destination.Table))
+ {
+ result.AddError($"Pipeline '{name}': Destination.Table is required");
+ }
+
+ if (pipeline.Destination.MatchColumns.Length == 0)
+ {
+ result.AddWarning($"Pipeline '{name}': No MatchColumns specified - all rows will be inserted");
+ }
+
+ // Schedule validation
+ ValidateSchedule(result, name, "Mass", pipeline.Schedules.Mass, 60);
+ ValidateSchedule(result, name, "Daily", pipeline.Schedules.Daily, 60);
+ ValidateSchedule(result, name, "Hourly", pipeline.Schedules.Hourly, 15);
+ }
+
+ return result;
+ }
+
+ private void ValidateSchedule(ValidationResult result, string pipelineName, string scheduleName, ScheduleModel? schedule, int minInterval)
+ {
+ if (schedule == null) return;
+
+ if (schedule.Enabled && schedule.IntervalMinutes < minInterval)
+ {
+ result.AddError($"Pipeline '{pipelineName}': {scheduleName} schedule interval must be at least {minInterval} minutes");
+ }
+ }
+}
diff --git a/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs
new file mode 100644
index 0000000..0fddef1
--- /dev/null
+++ b/NEW/tests/JdeScoping.ConfigManager.Tests/Services/ValidationServiceTests.cs
@@ -0,0 +1,90 @@
+using JdeScoping.ConfigManager.Models;
+using JdeScoping.ConfigManager.Services;
+
+namespace JdeScoping.ConfigManager.Tests.Services;
+
+public class ValidationServiceTests
+{
+ private readonly ValidationService _sut;
+
+ public ValidationServiceTests()
+ {
+ _sut = new ValidationService();
+ }
+
+ [Fact]
+ public void ValidateAppSettings_WithValidConfig_ReturnsNoErrors()
+ {
+ // Arrange
+ var config = new ConfigModel
+ {
+ DataSync = new DataSyncSection { MaxDegreeOfParallelism = 4 }
+ };
+
+ // Act
+ var result = _sut.ValidateAppSettings(config);
+
+ // Assert
+ result.IsValid.ShouldBeTrue();
+ result.Errors.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void ValidateAppSettings_WithInvalidParallelism_ReturnsError()
+ {
+ // Arrange
+ var config = new ConfigModel
+ {
+ DataSync = new DataSyncSection { MaxDegreeOfParallelism = 0 }
+ };
+
+ // Act
+ var result = _sut.ValidateAppSettings(config);
+
+ // Assert
+ result.IsValid.ShouldBeFalse();
+ result.Errors.ShouldContain(e => e.Contains("MaxDegreeOfParallelism"));
+ }
+
+ [Fact]
+ public void ValidatePipelines_WithDuplicateNames_ReturnsError()
+ {
+ // Arrange - duplicate keys not possible in dictionary, but empty names are invalid
+ var config = new PipelinesConfigModel
+ {
+ Pipelines = new Dictionary
+ {
+ [""] = new PipelineModel()
+ }
+ };
+
+ // Act
+ var result = _sut.ValidatePipelines(config);
+
+ // Assert
+ result.IsValid.ShouldBeFalse();
+ }
+
+ [Fact]
+ public void ValidatePipelines_WithInvalidConnection_ReturnsError()
+ {
+ // Arrange
+ var config = new PipelinesConfigModel
+ {
+ Pipelines = new Dictionary
+ {
+ ["Test"] = new PipelineModel
+ {
+ Source = new PipelineSource { Connection = "invalid" }
+ }
+ }
+ };
+
+ // Act
+ var result = _sut.ValidatePipelines(config);
+
+ // Assert
+ result.IsValid.ShouldBeFalse();
+ result.Errors.ShouldContain(e => e.Contains("Connection"));
+ }
+}