# Pipeline Schedule Alignment Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Align pipelines.json with legacy DataSyncReport.md by supporting three schedules (Mass/Daily/Hourly), adding 8 missing pipelines, and adding GIW connection for StatusCode. **Architecture:** Replace `SyncMode` enum with `UpdateTypes` throughout the ETL system. Add `scheduleDefaults` to pipelines.json for global defaults. Each pipeline defines `schedules` (mass/daily/hourly) that inherit from or override defaults. Add GIW connection type for StatusCode pipeline. **Tech Stack:** .NET 10, System.Text.Json, Oracle.ManagedDataAccess, Microsoft.Data.SqlClient, xUnit, NSubstitute, Shouldly --- ## Phase 1: Schema & Models ### Task 1: Add ScheduleConfig and ScheduleDefaults Models **Files:** - Create: `src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs` **Step 1: Write the failing test** Create test file `tests/JdeScoping.DataSync.Tests/Configuration/ScheduleConfigTests.cs`: ```csharp using JdeScoping.DataSync.Configuration; using Shouldly; namespace JdeScoping.DataSync.Tests.Configuration; public class ScheduleConfigTests { [Fact] public void ScheduleConfig_DefaultValues_AreCorrect() { var config = new ScheduleConfig(); config.Enabled.ShouldBeTrue(); config.IntervalMinutes.ShouldBe(0); config.PrePurge.ShouldBeFalse(); config.ReIndex.ShouldBeFalse(); config.UpdateWhen.ShouldBeNull(); } [Fact] public void ScheduleConfig_WithValues_StoresCorrectly() { var config = new ScheduleConfig { Enabled = false, IntervalMinutes = 60, PrePurge = true, ReIndex = true, UpdateWhen = "src.LastUpdateDt > tgt.LastUpdateDt" }; config.Enabled.ShouldBeFalse(); config.IntervalMinutes.ShouldBe(60); config.PrePurge.ShouldBeTrue(); config.ReIndex.ShouldBeTrue(); config.UpdateWhen.ShouldBe("src.LastUpdateDt > tgt.LastUpdateDt"); } [Fact] public void ScheduleDefaults_HasCorrectDefaultValues() { var defaults = new ScheduleDefaults(); defaults.Mass.ShouldNotBeNull(); defaults.Daily.ShouldNotBeNull(); defaults.Hourly.ShouldNotBeNull(); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~ScheduleConfigTests" --verbosity normal` Expected: FAIL with "type or namespace 'ScheduleConfig' could not be found" **Step 3: Write minimal implementation** Create `src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs`: ```csharp namespace JdeScoping.DataSync.Configuration; /// /// Configuration for a single schedule type (Mass/Daily/Hourly). /// public record ScheduleConfig { /// /// Whether this schedule is enabled. /// public bool Enabled { get; init; } = true; /// /// Interval in minutes between syncs. /// public int IntervalMinutes { get; init; } /// /// Whether to truncate the table before import (full reload). /// public bool PrePurge { get; init; } /// /// Whether to rebuild indexes after import. /// public bool ReIndex { get; init; } /// /// Condition for updating existing rows (e.g., "src.LastUpdateDt > tgt.LastUpdateDt"). /// public string? UpdateWhen { get; init; } /// /// Merges this config with defaults. Non-null/non-default values in this config override defaults. /// public ScheduleConfig MergeWith(ScheduleConfig defaults) { return new ScheduleConfig { Enabled = Enabled, IntervalMinutes = IntervalMinutes > 0 ? IntervalMinutes : defaults.IntervalMinutes, PrePurge = PrePurge || defaults.PrePurge, ReIndex = ReIndex || defaults.ReIndex, UpdateWhen = UpdateWhen ?? defaults.UpdateWhen }; } } /// /// Default schedule configurations for all pipelines. /// public record ScheduleDefaults { /// /// Default Mass schedule config (weekly, full reload). /// public ScheduleConfig Mass { get; init; } = new() { Enabled = true, IntervalMinutes = 10080, // Weekly PrePurge = true, ReIndex = true }; /// /// Default Daily schedule config (incremental merge). /// public ScheduleConfig Daily { get; init; } = new() { Enabled = true, IntervalMinutes = 1440, // Daily PrePurge = false, ReIndex = false, UpdateWhen = "src.LastUpdateDt > tgt.LastUpdateDt" }; /// /// Default Hourly schedule config (incremental merge). /// public ScheduleConfig Hourly { get; init; } = new() { Enabled = true, IntervalMinutes = 60, // Hourly PrePurge = false, ReIndex = false, UpdateWhen = "src.LastUpdateDt > tgt.LastUpdateDt" }; } /// /// Per-pipeline schedule overrides. /// public record PipelineSchedules { public ScheduleConfig? Mass { get; init; } public ScheduleConfig? Daily { get; init; } public ScheduleConfig? Hourly { get; init; } } ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~ScheduleConfigTests" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Configuration/ScheduleConfig.cs tests/JdeScoping.DataSync.Tests/Configuration/ScheduleConfigTests.cs git commit -m "feat(datasync): add ScheduleConfig and ScheduleDefaults models" ``` --- ### Task 2: Update PipelinesRoot to Include ScheduleDefaults **Files:** - Modify: `src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs` - Test: `tests/JdeScoping.DataSync.Tests/Configuration/PipelinesRootTests.cs` **Step 1: Write the failing test** Create `tests/JdeScoping.DataSync.Tests/Configuration/PipelinesRootTests.cs`: ```csharp using JdeScoping.DataSync.Configuration; using Shouldly; namespace JdeScoping.DataSync.Tests.Configuration; public class PipelinesRootTests { [Fact] public void EffectiveScheduleDefaults_WhenNull_ReturnsDefaults() { var root = new PipelinesRoot(null, null, new Dictionary()); var defaults = root.EffectiveScheduleDefaults; defaults.ShouldNotBeNull(); defaults.Mass.IntervalMinutes.ShouldBe(10080); defaults.Daily.IntervalMinutes.ShouldBe(1440); defaults.Hourly.IntervalMinutes.ShouldBe(60); } [Fact] public void EffectiveScheduleDefaults_WhenProvided_ReturnsProvided() { var customDefaults = new ScheduleDefaults { Mass = new ScheduleConfig { IntervalMinutes = 20000 } }; var root = new PipelinesRoot(null, customDefaults, new Dictionary()); var defaults = root.EffectiveScheduleDefaults; defaults.Mass.IntervalMinutes.ShouldBe(20000); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~PipelinesRootTests" --verbosity normal` Expected: FAIL **Step 3: Write minimal implementation** Update `src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs`: ```csharp namespace JdeScoping.DataSync.Configuration; public record PipelinesRoot( PipelineSettings? Settings, ScheduleDefaults? ScheduleDefaults, Dictionary Pipelines) { public PipelineSettings EffectiveSettings => Settings ?? new PipelineSettings(); public ScheduleDefaults EffectiveScheduleDefaults => ScheduleDefaults ?? new ScheduleDefaults(); } public record PipelineSettings( string Timezone = "UTC"); ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~PipelinesRootTests" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Configuration/PipelinesRoot.cs tests/JdeScoping.DataSync.Tests/Configuration/PipelinesRootTests.cs git commit -m "feat(datasync): add ScheduleDefaults to PipelinesRoot" ``` --- ### Task 3: Update PipelineConfig to Include Schedules **Files:** - Modify: `src/JdeScoping.DataSync/Configuration/PipelineConfig.cs` **Step 1: Write the failing test** Add to `tests/JdeScoping.DataSync.Tests/Configuration/PipelinesRootTests.cs`: ```csharp [Fact] public void PipelineConfig_WithSchedules_ParsesCorrectly() { var config = new PipelineConfig( new SourceConfig("jde", "SELECT 1", null, null), null, // Old SyncModes - deprecated new PipelineSchedules { Mass = new ScheduleConfig { PrePurge = true, ReIndex = true }, Daily = new ScheduleConfig { Enabled = true }, Hourly = new ScheduleConfig { Enabled = false } }, null, new DestinationConfig("TestTable", ["Id"], null), null, null); config.Schedules.ShouldNotBeNull(); config.Schedules!.Mass!.PrePurge.ShouldBeTrue(); config.Schedules!.Hourly!.Enabled.ShouldBeFalse(); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~PipelineConfig_WithSchedules" --verbosity normal` Expected: FAIL **Step 3: Write minimal implementation** Update `src/JdeScoping.DataSync/Configuration/PipelineConfig.cs`: ```csharp namespace JdeScoping.DataSync.Configuration; public record PipelineConfig( SourceConfig Source, Dictionary? SyncModes, // Deprecated - kept for backward compatibility PipelineSchedules? Schedules, // New schedule-based config List? Transformers, DestinationConfig Destination, List? PreScripts, List? PostScripts); public record SourceConfig( string Connection, string Query, Dictionary? Parameters, string? MassQuery = null); public record ParameterConfig( string Name, string? Format, string Source = "offset", string? Value = null); public record SyncModeConfig( string? MinDtOffset, bool PrePurge = false, bool ReIndex = false, string? UpdateWhen = null, DestinationOverride? Destination = null); public record DestinationOverride( string? Type, List? MatchColumns, List? ExcludeFromUpdate); public record TransformerConfig( string Type, List? Columns, Dictionary? Mappings); public record DestinationConfig( string Table, List? MatchColumns, List? ExcludeFromUpdate); ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~PipelineConfig_WithSchedules" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Configuration/PipelineConfig.cs tests/JdeScoping.DataSync.Tests/Configuration/PipelinesRootTests.cs git commit -m "feat(datasync): add Schedules property to PipelineConfig" ``` --- ## Phase 2: Infrastructure Changes (GIW Connection) ### Task 4: Add GIW Connection to IDbConnectionFactory **Files:** - Modify: `src/JdeScoping.DataAccess/Interfaces/IDbConnectionFactory.cs` - Modify: `src/JdeScoping.DataAccess/DbConnectionFactory.cs` - Test: `tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryTests.cs` **Step 1: Write the failing test** Add to existing test file or create `tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs`: ```csharp using JdeScoping.DataAccess; using JdeScoping.DataAccess.Exceptions; using JdeScoping.DataAccess.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace JdeScoping.DataAccess.Tests; public class DbConnectionFactoryGiwTests { [Fact] public async Task CreateGiwConnectionAsync_WhenConnectionStringMissing_ThrowsConnectionException() { // Arrange var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary()) .Build(); var factory = new DbConnectionFactory(config, NullLogger.Instance); // Act & Assert var ex = await Should.ThrowAsync( () => factory.CreateGiwConnectionAsync()); ex.DataSource.ShouldBe("GIW"); } } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataAccess.Tests --filter "FullyQualifiedName~CreateGiwConnectionAsync" --verbosity normal` Expected: FAIL with "IDbConnectionFactory does not contain a definition for 'CreateGiwConnectionAsync'" **Step 3: Write minimal implementation** Update `src/JdeScoping.DataAccess/Interfaces/IDbConnectionFactory.cs`: ```csharp using Microsoft.Data.SqlClient; using Oracle.ManagedDataAccess.Client; namespace JdeScoping.DataAccess.Interfaces; /// /// Factory for creating database connections to all data sources. /// public interface IDbConnectionFactory { /// /// Creates and opens a connection to the LotFinderDB SQL Server cache database. /// Task CreateLotFinderConnectionAsync(CancellationToken ct = default); /// /// Creates and opens a connection to the JDE Oracle database (production schema). /// Task CreateJdeConnectionAsync(CancellationToken ct = default); /// /// Creates and opens a connection to the JDE Stage Oracle database. /// Task CreateJdeStageConnectionAsync(CancellationToken ct = default); /// /// Creates and opens a connection to the CMS Oracle database. /// Task CreateCmsConnectionAsync(CancellationToken ct = default); /// /// Creates and opens a connection to the GIW Oracle database (for StatusCode sync). /// Task CreateGiwConnectionAsync(CancellationToken ct = default); } ``` Update `src/JdeScoping.DataAccess/DbConnectionFactory.cs` - add method: ```csharp /// public async Task CreateGiwConnectionAsync(CancellationToken ct = default) { return await CreateOracleConnectionAsync("GIW", ct).ConfigureAwait(false); } ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataAccess.Tests --filter "FullyQualifiedName~CreateGiwConnectionAsync" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataAccess/Interfaces/IDbConnectionFactory.cs src/JdeScoping.DataAccess/DbConnectionFactory.cs tests/JdeScoping.DataAccess.Tests/DbConnectionFactoryGiwTests.cs git commit -m "feat(dataaccess): add GIW connection factory method" ``` --- ### Task 5: Update DbQuerySource to Support GIW Connection **Files:** - Modify: `src/JdeScoping.DataSync/Etl/Sources/DbQuerySource.cs` - Test: `tests/JdeScoping.DataSync.Tests/Etl/Sources/DbQuerySourceTests.cs` **Step 1: Write the failing test** Add test to existing `DbQuerySourceTests.cs`: ```csharp [Fact] public void Constructor_WithGiwConnectionType_DoesNotThrow() { // Arrange var connectionFactory = Substitute.For(); // Act & Assert - should not throw var source = new DbQuerySource(connectionFactory, "giw", "SELECT 1", null); source.SourceName.ShouldBe("DbQuery:giw"); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~Constructor_WithGiwConnectionType" --verbosity normal` Expected: FAIL with "Unknown connection type: giw" **Step 3: Write minimal implementation** Update `src/JdeScoping.DataSync/Etl/Sources/DbQuerySource.cs`: ```csharp private static readonly HashSet ValidConnectionTypes = new(StringComparer.OrdinalIgnoreCase) { "jde", "cms", "lotfinder", "giw" }; ``` And update `CreateConnectionAsync`: ```csharp private async Task CreateConnectionAsync(CancellationToken cancellationToken) { return _connectionType switch { "jde" => await _connectionFactory.CreateJdeConnectionAsync(cancellationToken), "cms" => await _connectionFactory.CreateCmsConnectionAsync(cancellationToken), "giw" => await _connectionFactory.CreateGiwConnectionAsync(cancellationToken), "lotfinder" => await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken), _ => throw new InvalidOperationException($"Unknown connection type: {_connectionType}") }; } ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~Constructor_WithGiwConnectionType" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Etl/Sources/DbQuerySource.cs tests/JdeScoping.DataSync.Tests/Etl/Sources/DbQuerySourceTests.cs git commit -m "feat(datasync): add GIW connection type to DbQuerySource" ``` --- ## Phase 3: Core ETL Changes ### Task 6: Update IEtlPipelineBuilder to Accept UpdateTypes **Files:** - Modify: `src/JdeScoping.DataSync/Contracts/IEtlPipelineFactory.cs` - Modify: `src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs` **Step 1: Write the failing test** Add to `tests/JdeScoping.DataSync.Tests/Services/EtlPipelineFactoryTests.cs`: ```csharp [Fact] public void Builder_WithUpdateTypesMass_BuildsPipeline() { // Arrange var config = CreateValidConfigWithSchedules(); var factory = CreateFactory(config); // Act var pipeline = factory.ForTable("TestTable") .WithUpdateType(UpdateTypes.Mass) .Build(); // Assert pipeline.ShouldNotBeNull(); } [Fact] public void Builder_WithUpdateTypesDaily_BuildsPipeline() { // Arrange var config = CreateValidConfigWithSchedules(); var factory = CreateFactory(config); // Act var pipeline = factory.ForTable("TestTable") .WithUpdateType(UpdateTypes.Daily) .Build(); // Assert pipeline.ShouldNotBeNull(); } [Fact] public void Builder_WithUpdateTypesHourly_BuildsPipeline() { // Arrange var config = CreateValidConfigWithSchedules(); var factory = CreateFactory(config); // Act var pipeline = factory.ForTable("TestTable") .WithUpdateType(UpdateTypes.Hourly) .Build(); // Assert pipeline.ShouldNotBeNull(); } private PipelinesRoot CreateValidConfigWithSchedules() { return new PipelinesRoot( new PipelineSettings("UTC"), new ScheduleDefaults(), new Dictionary { ["TestTable"] = new PipelineConfig( new SourceConfig("lotfinder", "SELECT * FROM Test WHERE UpdateDt >= @MinDt", new Dictionary { ["minDt"] = new ParameterConfig("@MinDt", null, "offset", null) }, "SELECT * FROM Test"), null, // No old SyncModes new PipelineSchedules { Mass = new ScheduleConfig { PrePurge = true, ReIndex = true }, Daily = new ScheduleConfig(), Hourly = new ScheduleConfig() }, null, new DestinationConfig("TestTable", ["Id"], null), null, null) }); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~Builder_WithUpdateTypes" --verbosity normal` Expected: FAIL with "'IEtlPipelineBuilder' does not contain a definition for 'WithUpdateType'" **Step 3: Write minimal implementation** Update `src/JdeScoping.DataSync/Contracts/IEtlPipelineFactory.cs`: ```csharp using JdeScoping.Core.Models.Enums; using JdeScoping.DataSync.Etl.Pipeline; namespace JdeScoping.DataSync.Contracts; public interface IEtlPipelineFactory { IEtlPipelineBuilder ForTable(string tableName); } public interface IEtlPipelineBuilder { [Obsolete("Use WithUpdateType instead")] IEtlPipelineBuilder WithMode(SyncMode mode); IEtlPipelineBuilder WithUpdateType(UpdateTypes updateType); IEtlPipelineBuilder WithMinimumDate(DateTime? minDt); EtlPipeline Build(); } ``` Update `src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs` - PipelineBuilder class: ```csharp private sealed class PipelineBuilder : IEtlPipelineBuilder { private readonly IDbConnectionFactory _connectionFactory; private readonly string _tableName; private readonly PipelineConfig _config; private readonly PipelineSettings _settings; private readonly ScheduleDefaults _scheduleDefaults; private readonly ILogger _logger; private UpdateTypes _updateType = UpdateTypes.Hourly; private DateTime? _minDtOverride; public PipelineBuilder( IDbConnectionFactory connectionFactory, string tableName, PipelineConfig config, PipelineSettings settings, ScheduleDefaults scheduleDefaults, ILogger logger) { _connectionFactory = connectionFactory; _tableName = tableName; _config = config; _settings = settings; _scheduleDefaults = scheduleDefaults; _logger = logger; } [Obsolete("Use WithUpdateType instead")] public IEtlPipelineBuilder WithMode(SyncMode mode) { _updateType = mode == SyncMode.Mass ? UpdateTypes.Mass : UpdateTypes.Hourly; return this; } public IEtlPipelineBuilder WithUpdateType(UpdateTypes updateType) { _updateType = updateType; return this; } public IEtlPipelineBuilder WithMinimumDate(DateTime? minDt) { _minDtOverride = minDt; return this; } public EtlPipeline Build() { var scheduleConfig = GetEffectiveScheduleConfig(_updateType); // Compute MinDt from schedule config var minDt = _minDtOverride; // Use massQuery for Mass, regular query for Daily/Hourly var useMassQuery = _updateType == UpdateTypes.Mass && !string.IsNullOrEmpty(_config.Source.MassQuery); // Create source with parameter substitution var source = CreateSource(_config.Source, minDt, useMassQuery); // Determine destination type (Mass = bulkImport, Daily/Hourly = bulkMerge unless prePurge) var destType = scheduleConfig.PrePurge ? "bulkImport" : "bulkMerge"; var destination = CreateDestination(destType, _config.Destination, scheduleConfig); // Build pipeline with scripts var builder = new EtlPipelineBuilder() .WithName(_tableName) .WithSource(source) .WithDestination(destination) .WithLogger(_logger); // Add pre-scripts: config scripts first, then prePurge foreach (var script in _config.PreScripts ?? []) { builder.WithPreScript(new SqlScriptRunner(_connectionFactory, script, $"PreScript:{script.Substring(0, Math.Min(30, script.Length))}")); } if (scheduleConfig.PrePurge) { var truncateSql = $"TRUNCATE TABLE [{_config.Destination.Table}]"; builder.WithPreScript(new SqlScriptRunner(_connectionFactory, truncateSql, "PrePurge")); } // Add post-scripts: reIndex first, then config scripts if (scheduleConfig.ReIndex) { var reindexSql = $"ALTER INDEX ALL ON [{_config.Destination.Table}] REBUILD"; builder.WithPostScript(new SqlScriptRunner(_connectionFactory, reindexSql, "ReIndex")); } foreach (var script in _config.PostScripts ?? []) { builder.WithPostScript(new SqlScriptRunner(_connectionFactory, script, $"PostScript:{script.Substring(0, Math.Min(30, script.Length))}")); } return builder.Build(); } private ScheduleConfig GetEffectiveScheduleConfig(UpdateTypes updateType) { // Get default for this update type var defaultConfig = updateType switch { UpdateTypes.Mass => _scheduleDefaults.Mass, UpdateTypes.Daily => _scheduleDefaults.Daily, UpdateTypes.Hourly => _scheduleDefaults.Hourly, _ => _scheduleDefaults.Hourly }; // Get pipeline-specific override if exists var pipelineConfig = updateType switch { UpdateTypes.Mass => _config.Schedules?.Mass, UpdateTypes.Daily => _config.Schedules?.Daily, UpdateTypes.Hourly => _config.Schedules?.Hourly, _ => null }; // Merge: pipeline config overrides defaults return pipelineConfig?.MergeWith(defaultConfig) ?? defaultConfig; } // ... rest of methods updated similarly } ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~Builder_WithUpdateTypes" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Contracts/IEtlPipelineFactory.cs src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs tests/JdeScoping.DataSync.Tests/Services/EtlPipelineFactoryTests.cs git commit -m "feat(datasync): add WithUpdateType to IEtlPipelineBuilder" ``` --- ### Task 7: Update TableSyncOperation to Use UpdateTypes **Files:** - Modify: `src/JdeScoping.DataSync/Services/TableSyncOperation.cs` **Step 1: Write the failing test** The existing tests should still pass with the refactored code. Add a new test: ```csharp [Fact] public async Task ExecuteAsync_WithDailyUpdateType_UsesDailyConfig() { // Arrange var task = new DataUpdateTask { TableName = "TestTable", SourceSystem = "JDE", SourceData = "TEST", UpdateType = UpdateTypes.Daily, MinimumDt = DateTime.UtcNow.AddDays(-1) }; // ... setup mocks // Act await _operation.ExecuteAsync(task); // Assert _mockFactory.Received(1).ForTable("TestTable"); // Verify WithUpdateType(UpdateTypes.Daily) was called } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~ExecuteAsync_WithDailyUpdateType" --verbosity normal` Expected: FAIL **Step 3: Write minimal implementation** Update `src/JdeScoping.DataSync/Services/TableSyncOperation.cs`: ```csharp private async Task ExecuteSyncCoreAsync(DataUpdateTask task, CancellationToken cancellationToken) { _logger.LogDebug("Building pipeline for {Table} with {UpdateType}", task.TableName, task.UpdateType); // Build and execute the pipeline using UpdateTypes directly var pipeline = _pipelineFactory .ForTable(task.TableName) .WithUpdateType(task.UpdateType) .WithMinimumDate(task.MinimumDt) .Build(); var result = await pipeline.ExecuteAsync(cancellationToken); if (!result.Success) { throw new InvalidOperationException( $"Pipeline failed for {task.TableName}: {result.Error?.Message ?? "Unknown error"}", result.Error); } return result.TotalRows; } ``` **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~TableSyncOperation" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Services/TableSyncOperation.cs git commit -m "refactor(datasync): use UpdateTypes in TableSyncOperation" ``` --- ### Task 8: Update DataUpdateRepository for Per-Pipeline Intervals **Files:** - Modify: `src/JdeScoping.DataSync/Services/DataUpdateRepository.cs` - Modify: `src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs` **Step 1: Write the failing test** ```csharp [Fact] public async Task GetSyncStatusAsync_WithCustomInterval_UsesProvidedInterval() { // Arrange - setup mock to return data // ... // Act var customIntervals = new Dictionary { ["MisData_0"] = 100800 // Mass interval for MisData }; var status = await _repository.GetSyncStatusAsync(customIntervals); // Assert var misDataStatus = status.FirstOrDefault(s => s.TableName == "MisData"); misDataStatus.ShouldNotBeNull(); misDataStatus.ExpectedIntervalMinutes.ShouldBe(100800); } ``` **Step 2: Run test to verify it fails** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~GetSyncStatusAsync_WithCustomInterval" --verbosity normal` Expected: FAIL **Step 3: Write minimal implementation** Update `IDataUpdateRepository` to accept optional interval overrides, then update `DataUpdateRepository.GetSyncStatusAsync` to use them. **Step 4: Run test to verify it passes** Run: `dotnet test tests/JdeScoping.DataSync.Tests --filter "FullyQualifiedName~GetSyncStatusAsync_WithCustomInterval" --verbosity normal` Expected: PASS **Step 5: Commit** ```bash git add src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs src/JdeScoping.DataSync/Services/DataUpdateRepository.cs git commit -m "feat(datasync): support per-pipeline intervals in DataUpdateRepository" ``` --- ## Phase 4: Pipeline Configurations ### Task 9: Migrate Existing Pipelines to New Schema **Files:** - Modify: `src/JdeScoping.DataSync/Pipelines/pipelines.json` **Step 1: Backup current config** ```bash cp src/JdeScoping.DataSync/Pipelines/pipelines.json src/JdeScoping.DataSync/Pipelines/pipelines.json.bak ``` **Step 2: Update pipelines.json** Add `scheduleDefaults` and convert each pipeline from `syncModes` to `schedules`. Keep `syncModes` for backward compatibility during transition. Example structure: ```json { "settings": { "timezone": "UTC" }, "scheduleDefaults": { "mass": { "enabled": true, "intervalMinutes": 10080, "prePurge": true, "reIndex": true }, "daily": { "enabled": true, "intervalMinutes": 1440, "prePurge": false, "reIndex": false, "updateWhen": "src.LastUpdateDt > tgt.LastUpdateDt" }, "hourly": { "enabled": true, "intervalMinutes": 60, "prePurge": false, "reIndex": false, "updateWhen": "src.LastUpdateDt > tgt.LastUpdateDt" } }, "pipelines": { "WorkOrder_Curr": { "source": { "connection": "jde", "massQuery": "SELECT ... FROM {ProductionSchema}.F4801 wo", "query": "SELECT ... FROM {ProductionSchema}.F4801 wo WHERE (...date filter...)", "parameters": { ... } }, "schedules": { "mass": {}, "daily": {}, "hourly": {} }, "destination": { ... } } } } ``` **Step 3: Run all tests** Run: `dotnet test tests/JdeScoping.DataSync.Tests --verbosity normal` Expected: All PASS **Step 4: Commit** ```bash git add src/JdeScoping.DataSync/Pipelines/pipelines.json git commit -m "refactor(datasync): migrate existing pipelines to new schedule schema" ``` --- ### Task 10: Add 8 Missing Pipeline Definitions **Files:** - Modify: `src/JdeScoping.DataSync/Pipelines/pipelines.json` Add these pipelines using SQL from `DATA_SYNC/JDE/*.sql`: 1. **WorkOrderTime_Curr** (F31122_VIEW) 2. **WorkOrderComponent_Curr** (F3111_VIEW) 3. **WorkOrderStep_Curr** (F3112_VIEW) 4. **WorkOrderRouting** (F3112Z1_VIEW) 5. **StatusCode** (F0005_VIEW via GIW connection) 6. **OrgHierarchy** (F30006_VIEW) 7. **RouteMaster** (F3003_VIEW) 8. **FunctionCode** (PRODDTA.F00192 - always full reload) Each pipeline follows this pattern: ```json "WorkOrderTime_Curr": { "source": { "connection": "jde", "massQuery": "SELECT wot.UNIQUEKEYIDINTERNAL_WTUKID AS UniqueID, TRIM(wot.COSTCENTERALT_WTMMCU) AS BranchCode, wot.DOCUMENTORDERINVOICEE_WTDOCO AS WorkOrderNumber, wot.SEQUENCENOOPERATIONS_WTOPSQ AS StepNumber, wot.ADDRESSNUMBER_WTAN8 AS AddressNumber, wot.DTFORGLANDVOUCH1_WTDGL AS GlDate, wot.DATEUPDATED_WTUPMJ AS DateUpdated, wot.TIMEOFDAY_WTTDAY AS TimeUpdated FROM JDESTAGE.F31122_VIEW wot", "query": "SELECT wot.UNIQUEKEYIDINTERNAL_WTUKID AS UniqueID, TRIM(wot.COSTCENTERALT_WTMMCU) AS BranchCode, wot.DOCUMENTORDERINVOICEE_WTDOCO AS WorkOrderNumber, wot.SEQUENCENOOPERATIONS_WTOPSQ AS StepNumber, wot.ADDRESSNUMBER_WTAN8 AS AddressNumber, wot.DTFORGLANDVOUCH1_WTDGL AS GlDate, wot.DATEUPDATED_WTUPMJ AS DateUpdated, wot.TIMEOFDAY_WTTDAY AS TimeUpdated FROM JDESTAGE.F31122_VIEW wot WHERE (wot.DATEUPDATED_WTUPMJ > :dateUpdated OR (wot.DATEUPDATED_WTUPMJ = :dateUpdated AND wot.TIMEOFDAY_WTTDAY >= :timeUpdated))", "parameters": { "dateUpdated": { "name": ":dateUpdated", "format": "jdeJulian", "source": "offset" }, "timeUpdated": { "name": ":timeUpdated", "format": "jdeTime", "source": "offset" } } }, "schedules": { "mass": {}, "daily": {}, "hourly": {} }, "destination": { "table": "WorkOrderTime_Curr", "matchColumns": ["UniqueID"], "excludeFromUpdate": ["UniqueID", "LastUpdateDt"] } } ``` Special cases: **StatusCode** (uses GIW connection): ```json "StatusCode": { "source": { "connection": "giw", ... } } ``` **FunctionCode** (always full reload): ```json "FunctionCode": { "schedules": { "mass": { "prePurge": true, "reIndex": true }, "daily": { "prePurge": true, "reIndex": true }, "hourly": { "prePurge": true, "reIndex": true } } } ``` **MisData** (hourly disabled, custom mass interval): ```json "MisData": { "schedules": { "mass": { "intervalMinutes": 100800 }, "daily": {}, "hourly": { "enabled": false } } } ``` **Step 3: Run tests** Run: `dotnet test tests/JdeScoping.DataSync.Tests --verbosity normal` Expected: PASS **Step 4: Commit** ```bash git add src/JdeScoping.DataSync/Pipelines/pipelines.json git commit -m "feat(datasync): add 8 missing pipeline definitions" ``` --- ### Task 11: Add GIW Connection String to appsettings **Files:** - Modify: `src/JdeScoping.Host/appsettings.json` - Modify: `src/JdeScoping.Host/appsettings.Development.json` **Step 1: Update appsettings.json** Add placeholder for GIW connection: ```json { "ConnectionStrings": { "LotFinderDB": "...", "JDE": "...", "CMS": "...", "GIW": "" } } ``` **Step 2: Commit** ```bash git add src/JdeScoping.Host/appsettings.json src/JdeScoping.Host/appsettings.Development.json git commit -m "config: add GIW connection string placeholder" ``` --- ## Phase 5: Validation & Testing ### Task 12: Update EtlPipelineFactory Validation **Files:** - Modify: `src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs` Update validation to support both old `SyncModes` and new `Schedules` format: ```csharp private static void ValidateConfig(PipelinesRoot root) { foreach (var (name, config) in root.Pipelines) { // Accept either old SyncModes or new Schedules format var hasOldConfig = config.SyncModes != null && config.SyncModes.ContainsKey("mass") && config.SyncModes.ContainsKey("incremental"); var hasNewConfig = config.Schedules != null; if (!hasOldConfig && !hasNewConfig) { throw new InvalidOperationException( $"Pipeline '{name}' must define either 'syncModes' (mass+incremental) or 'schedules'."); } // Validate no runtime parameters if (config.Source.Parameters != null) { foreach (var (paramName, paramConfig) in config.Source.Parameters) { if (paramConfig.Source.Equals("runtime", StringComparison.OrdinalIgnoreCase)) { throw new NotSupportedException( $"Pipeline '{name}' parameter '{paramName}': runtime parameter source is not yet supported."); } } } } } ``` **Commit:** ```bash git add src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs git commit -m "refactor(datasync): update validation to support both config formats" ``` --- ### Task 13: Update Existing Unit Tests **Files:** - Modify: `tests/JdeScoping.DataSync.Tests/Services/EtlPipelineFactoryTests.cs` Update `CreateValidConfig()` helper to use new schema format. Ensure all existing tests still pass. **Step 1: Run all tests** Run: `dotnet test tests/JdeScoping.DataSync.Tests --verbosity normal` Expected: All PASS **Step 2: Commit** ```bash git add tests/JdeScoping.DataSync.Tests/ git commit -m "test(datasync): update tests for new schedule config format" ``` --- ### Task 14: Run Full Test Suite **Step 1: Build entire solution** Run: `dotnet build` Expected: Build succeeded with 0 errors **Step 2: Run all tests** Run: `dotnet test --verbosity normal` Expected: All tests pass **Step 3: Commit any final fixes** ```bash git add . git commit -m "fix: address test failures from schedule alignment" ``` --- ### Task 15: Final Cleanup - Remove Deprecated SyncMode **Note:** This task should be done after confirming all tests pass and the new system works correctly. It's optional and can be deferred. **Files:** - Modify: `src/JdeScoping.DataSync/Contracts/SyncMode.cs` - Mark as obsolete - Modify: `src/JdeScoping.DataSync/Configuration/PipelineConfig.cs` - Mark SyncModes as obsolete ```csharp [Obsolete("Use Schedules property instead")] public Dictionary? SyncModes { get; init; } ``` **Commit:** ```bash git add . git commit -m "chore(datasync): mark SyncMode as obsolete" ``` --- ## Summary **Total Tasks:** 15 **Estimated Files Modified:** ~18 **New Files Created:** ~3 **Key Changes:** 1. ScheduleConfig/ScheduleDefaults models added 2. PipelinesRoot supports scheduleDefaults 3. PipelineConfig supports schedules (mass/daily/hourly) 4. GIW connection added for StatusCode 5. DbQuerySource supports "giw" connection type 6. IEtlPipelineBuilder.WithUpdateType() added 7. TableSyncOperation uses UpdateTypes directly 8. 8 missing pipelines added 9. Backward compatible with existing syncModes format **Verification:** - All existing tests continue to pass - New tests cover schedule config behavior - pipelines.json validates with new schema