006 Detailed Migration Plan

Detailed Windows Task Scheduler Migration Plan

Date: February 2, 2026 Based on: docs/004-Lean-Windows-Task-Scheduler-Migration-Plan.md & docs/005-Example-Code-Transformations.md

This document details the step-by-step plan to migrate from the Coravel scheduler to Windows Task Scheduler. CRITICAL RULE: Do NOT remove any old code. All replaced code must be commented out to allow for easy rollback and reference.


1. Migration Overview

The goal is to replace the internal Coravel scheduler with external triggers from Windows Task Scheduler via HTTP API calls. This reduces memory usage and improves reliability.

Current Flow: Program.cs -> Coravel -> TaskSchedulerInitializer -> IInvocable Classes New Flow: Windows Task Scheduler -> API Endpoint -> Job Class -> Business Logic


2. Implementation Steps

Phase 1: Create Job Classes (The “Jobs” Folder)

We will create a new folder Jobs in ErpCrystal_MFG.Api and create new job classes based on the existing schedulers.

  1. Create Folder: ErpCrystal_MFG.Api/Jobs

  2. Duplicate Files: Copy the following files from TaskScheduler/ to Jobs/:

    • IndentEmailScheduler.cs -> IndentEmailJob.cs
    • InvoiceEmailScheduler.cs -> InvoiceEmailJob.cs
    • SalesOrderEmailScheduler.cs -> SalesOrderEmailJob.cs
    • ArEmailScheduler.cs -> ArEmailJob.cs
    • VoucherPaymentEmailScheduler.cs -> VoucherPaymentEmailJob.cs
    • VoucherReceiptEmailScheduler.cs -> VoucherReceiptEmailJob.cs
    • AIInsightsScheduler.cs -> AIInsightsJob.cs
  3. Refactor Each Job Class:

    • Namespace: Change to ErpCrystal_MFG.Api.Jobs.
    • Class Name: Rename to [Name]Job.
    • Interface: Comment out : IInvocable.
    • Constructor: Add ITaskSchedulerRepository parameter.
    • Methods:
      • Comment out public async Task Invoke() signature (keep the body).
      • Create public async Task Run(string dbname): Paste the body of Invoke here.
        • Change “foreach dbname” loop to run only for the passed dbname.
        • Change continue to return.
      • Create public async Task RunAll(): Implement loop to call Run(dbname) for all DBs fetched via dbrepo.
    • Logging: Keep LogMessage as is.

    Example Code Strategy:

    // public class IndentEmailScheduler : IInvocable  <-- OLD
    public class IndentEmailJob // : IInvocable        <-- NEW
    {
        // ... Dependencies ...
    
        // public async Task Invoke()  <-- OLD (Commented out)
        // {
        //    foreach (var dbname in dbnamelist) { ... }
        // }
    
        public async Task Run(string dbname) // <-- NEW
        {
            // Logic from inside the loop
        }
    }

Phase 2: Create Controller

Create ScheduledJobsController.cs in Controllers/ folder.

  • Attributes: [ApiController], [Route("api/jobs")], [ApiKeyAuth].
  • Dependencies: Inject all new *Job classes.
  • Endpoints: Create 2 endpoints per job type:
    1. POST /api/jobs/[job-name]: Calls job.RunAll().
    2. POST /api/jobs/[job-name]/{dbName}: Calls job.Run(dbName).

Phase 3: Configure Program.cs

Modify ErpCrystal_MFG.Api/Program.cs to disable Coravel and enable new Jobs.

  1. Register New Jobs:

    // Add these lines
    builder.Services.AddScoped<IndentEmailJob>();
    builder.Services.AddScoped<InvoiceEmailJob>();
    // ... repeat for all 7 jobs
  2. Disable Coravel (Comment Out):

    // builder.Services.AddScheduler();
    builder.Services.AddQueue(); // KEEP: User requested
    // builder.Services.AddScoped<TaskSchedulerInitializer>();
  3. Disable Scheduler Execution (Comment Out):

    // app.Services.UseScheduler(async scheduler =>
    // {
    //     ...
    // });

Phase 4: Windows Task Scheduler Setup

For each of the 7 tasks, create a Windows Task Scheduler entry (Script provided in docs/004 step 307).

  • Trigger: Match existing CRON schedule.

  • Action: Invoke-RestMethod to the new API endpoints.

  • Auth: Pass ApiKey header (Confirmed analysis of ApiKeyAuthAttribute.cs).

    PowerShell Script Update:

    $action = New-ScheduledTaskAction -Execute "powershell.exe" `
        -Argument "-Command `"Invoke-RestMethod -Uri 'https://localhost:port/api/jobs/indent-email' -Method POST -Headers @{'ApiKey' = 'YOUR_API_KEY'} -UseBasicParsing`""

3. Plan for “Comment Out” Strategy

We strictly follow the instruction to comment out rather than delete.

In Program.cs:

/* MIGRATION: Coravel Scheduler disabled
builder.Services.AddScheduler();
builder.Services.AddQueue();
*/

In Job Classes:

// using Coravel.Invocable; // MIGRATION: Commented out

public class IndentEmailJob // : IInvocable // MIGRATION: Interface removed
{
    // ...
    
    /* MIGRATION: Old Invoke method replaced by Run() and RunAll()
    public async Task Invoke()
    {
        ...
    }
    */
}

4. Resolved Doubts & User Feedback

  1. Task Codes:

    • Confirmed: Task codes will remain exactly the same as old ones (001, 002, etc.).
  2. API Key Analysis:

    • Question: Do we need any changes?
    • Analysis: Checked ErpCrystal_MFG.Api/CustomAttributes/ApiKeyAuthAttribute.cs.
    • Result: The attribute expects the header name ApiKey (Line 9: private const string apiKeyName = "ApiKey";).
    • Action: The PowerShell script in docs/004 incorrectly used X-API-Key. We MUST use ApiKey in the Windows Task Scheduler configuration. No code changes required in the API, only in the scheduler script/configuration.
  3. Testing Strategy:

    • Confirmed: Testing will be done locally first.
    • Plan:
      1. Run the API locally (IIS Express or Kestrel).
      2. Set up Windows Task Scheduler on the local developer machine.
      3. Point the Task Scheduler URL to https://localhost:[PORT]/api/jobs/....
      4. Trigger tasks manually via Scheduler and verify local logs.
  4. Existing Methods:

    • Confirmed: ITaskSchedulerRepository and other dependencies are already present and will be reused.
  5. Dynamic Scheduling (CRON from DB):

    • Question: Earlier CRON was dynamic and fetched from DB. Is it handled in WTS?
    • Answer: No. The “Lean” migration shifts the scheduling responsibility to Windows Task Scheduler.
    • Implication: The CRON expression in the database will effectively be ignored for triggering. All databases will run when the Windows Task Scheduler triggers the job (e.g., if WTS is set to 9 AM, all databases run at 9 AM).
    • Action: If distinct times are required (e.g., Client A at 9 AM, Client B at 10 AM), you must manually create multiple Triggers in Windows Task Scheduler, or accept that all clients will run on the standard schedule defined in WTS.
  6. Missing Features & Logic Gaps vs Old Docs:

    This section highlights logic present in Coravel/Old Code that is NOT automatically transferred to Windows Task Scheduler. You must handle these manually in WTS settings.

    • Dynamic CRON Schedules:

      • Old Logic: TaskSchedulerInitializer fetched CRON expressions from the DB at startup.
      • New Logic: WTS uses Static Triggers defined in the Task Scheduler UI.
      • GAP: Changes to CRON strings in the DB will NO LONGER change the schedule. You must update the WTS Trigger manually.
    • Prevent Overlapping:

      • Old Logic: .PreventOverlapping("Key") in code ensured only one instance ran.
      • New Logic: In WTS > Settings tab > “If the task is already running, then the following rule applies: Do not start a new instance”.
      • GAP: If you forget to check this box in WTS, multiple instances could run simultaneously.
    • Retry on Failure:

      • Old Logic: Handled by custom code or Coravel policies (if any).
      • New Logic: In WTS > Settings tab > “If the task fails, restart every: [1 minute]”.
      • GAP: This is a pure configuration setting now, not code.
    • TimeZone Handling:

      • Old Logic: .Zoned(TimeZoneInfo.Local).
      • New Logic: WTS runs on the Server’s System Time by default.
      • GAP: Ensure the Server’s time matches the expected business time (usually standard on single servers).