005 Example Code Transformations

Example Code Transformations for Lean Migration

This document shows exact code changes required for migrating from Coravel to Windows Task Scheduler, following the lean approach.


1. Job Class Transformation

1.1 BEFORE: IndentEmailScheduler.cs (Coravel Version)

using Coravel.Invocable;
using ErpCrystal_MFG.Models;
using ErpCrystal_MFG.Api.Repositories;
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Controllers;
using System.Text.RegularExpressions;

namespace ErpCrystal_MFG.Api.TaskScheduler;

public class IndentEmailScheduler(IEmailRepository iemailrepository,
    IUtilityMethodsRepository iutilitymethodsrepository, 
    IIndentRepository iindentrepository,
    IEnumerable<string> dbnamelist) : IInvocable
{
    private readonly IEmailRepository _IEmailRepository = iemailrepository;
    private readonly IUtilityMethodsRepository _IUtilityMethodsRepository = iutilitymethodsrepository;
    private readonly IIndentRepository _IIndentRepository = iindentrepository;

    public async Task Invoke()
    {
        foreach (var dbname in dbnamelist)
        {
            string pathDirectoryName = $"\\MFGReports\\Docs\\{dbname}\\Logs";
            if (!Directory.Exists(pathDirectoryName))
            {
                Directory.CreateDirectory(pathDirectoryName);
            }

            var pathName = $"{pathDirectoryName}\\{dbname}.csv";
            var logFilePath = Path.GetFullPath(pathName);
            if (!File.Exists(logFilePath))
            {
                LogMessage(logFilePath, $"Activity,DateTime,Status,Notes");
            }

            var stdInstructions = await _IUtilityMethodsRepository.GetStdInstructions(dbname, "5");

            if (string.IsNullOrEmpty(stdInstructions))
            {
                LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No Standard Instruction found");
                continue;
            }

            var indentList = await _IIndentRepository.GetScheduledIndents(dbname);
            if (indentList.Count == 0)
            {
                LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No records found");
            }
            else
            {
                // ... rest of the business logic
                foreach (var data in indentList)
                {
                    // Process each indent
                }
            }
        }
    }
    
    private void LogMessage(string path, string message)
    {
        // Existing logging implementation
        using StreamWriter sw = File.AppendText(path);
        sw.WriteLine(message);
        sw.Close();
    }
}

1.2 AFTER: IndentEmailJob.cs (Lean Version)

// REMOVED: using Coravel.Invocable;
// KEPT: All other using statements
using ErpCrystal_MFG.Models;
using ErpCrystal_MFG.Api.Repositories;
using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Controllers;
using System.Text.RegularExpressions;

namespace ErpCrystal_MFG.Api.Jobs;  // CHANGED NAMESPACE

public class IndentEmailJob  // REMOVED: : IInvocable
{
    private readonly IEmailRepository _IEmailRepository;
    private readonly IUtilityMethodsRepository _IUtilityMethodsRepository;
    private readonly IIndentRepository _IIndentRepository;
    private readonly ITaskSchedulerRepository _ITaskSchedulerRepository; // NEW: To fetch db list

    // CHANGED CONSTRUCTOR: Added ITaskSchedulerRepository parameter
    public IndentEmailJob(
        IEmailRepository iemailrepository,
        IUtilityMethodsRepository iutilitymethodsrepository, 
        IIndentRepository iindentrepository,
        ITaskSchedulerRepository itaskschedulerrepository)  // NEW parameter
    {
        _IEmailRepository = iemailrepository;
        _IUtilityMethodsRepository = iutilitymethodsrepository;
        _IIndentRepository = iindentrepository;
        _ITaskSchedulerRepository = itaskschedulerrepository;
    }

    // NEW METHOD: For single database execution (called by API endpoint with dbName parameter)
    public async Task Run(string dbname)
    {
        // SAME LOGIC from inside foreach loop (almost copy-paste)
        string pathDirectoryName = $"\\MFGReports\\Docs\\{dbname}\\Logs";
        if (!Directory.Exists(pathDirectoryName))
        {
            Directory.CreateDirectory(pathDirectoryName);
        }

        var pathName = $"{pathDirectoryName}\\{dbname}.csv";
        var logFilePath = Path.GetFullPath(pathName);
        if (!File.Exists(logFilePath))
        {
            LogMessage(logFilePath, $"Activity,DateTime,Status,Notes");
        }

        // OPTIONAL: Add Windows Scheduler trigger log
        LogMessage(logFilePath, $"Job triggered from Windows Scheduler at {DateTime.Now}");

        var stdInstructions = await _IUtilityMethodsRepository.GetStdInstructions(dbname, "5");

        if (string.IsNullOrEmpty(stdInstructions))
        {
            LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No Standard Instruction found");
            return;  // CHANGED: continue → return for single database
        }

        var indentList = await _IIndentRepository.GetScheduledIndents(dbname);
        if (indentList.Count == 0)
        {
            LogMessage(logFilePath, $"Indent Email, {DateTime.Now}, Failed, No records found");
            return;
        }
        
        // ... rest of the business logic (SAME AS BEFORE)
        foreach (var data in indentList)
        {
            // Process each indent (identical to original)
        }
    }

    // NEW METHOD: For all databases (replaces old Invoke() method)
    public async Task RunAll()
    {
        // Fetch database list dynamically from repository (like original Coravel did)
        var dbnamelist = await _ITaskSchedulerRepository.GetDbNameList("001");
        
        foreach (var dbname in dbnamelist)
        {
            await Run(dbname);
        }
    }
    
    // SAME LOGGING METHOD (unchanged)
    private void LogMessage(string path, string message)
    {
        using StreamWriter sw = File.AppendText(path);
        sw.WriteLine(message);
        sw.Close();
    }
}

2. Program.cs Transformations

2.1 BEFORE: Coravel Configuration in Program.cs

// Line 7: Coravel using statement
using Coravel;

// Lines 18-19: Coravel service registrations
builder.Services.AddScheduler();
builder.Services.AddQueue();

// Line 166: TaskSchedulerInitializer registration
builder.Services.AddScoped<TaskSchedulerInitializer>();

// Lines 210-215: Scheduler initialization
app.Services.UseScheduler(async scheduler =>
{
    using var scope = app.Services.CreateScope();
    var initializer = scope.ServiceProvider.GetRequiredService<TaskSchedulerInitializer>();
    await initializer.InitializeTasks(scheduler);
});

2.2 AFTER: Lean Configuration in Program.cs

// REMOVED: using Coravel;  (Delete this line)

// Lines 18-19: REMOVE Coravel service registrations
// builder.Services.AddScheduler();  // COMMENT OUT OR DELETE
// builder.Services.AddQueue();      // COMMENT OUT OR DELETE

// Line 166: REMOVE or comment out TaskSchedulerInitializer registration
// builder.Services.AddScoped<TaskSchedulerInitializer>();  // NO LONGER NEEDED

// NEW: Add job service registrations (add after other repository registrations)
builder.Services.AddScoped<IndentEmailJob>();
builder.Services.AddScoped<InvoiceEmailJob>();
builder.Services.AddScoped<SalesOrderEmailJob>();
builder.Services.AddScoped<ArEmailJob>();
builder.Services.AddScoped<VoucherPaymentEmailJob>();
builder.Services.AddScoped<VoucherReceiptEmailJob>();
builder.Services.AddScoped<AIInsightsJob>();

// Lines 210-215: REMOVE ENTIRE UseScheduler BLOCK
// app.Services.UseScheduler(async scheduler =>
// {
//     using var scope = app.Services.CreateScope();
//     var initializer = scope.ServiceProvider.GetRequiredService<TaskSchedulerInitializer>();
//     await initializer.InitializeTasks(scheduler);
// });

3. Controller Implementation

3.1 Complete ScheduledJobsController.cs

using Microsoft.AspNetCore.Mvc;
using ErpCrystal_MFG.Api.Jobs;
using ErpCrystal_MFG.Api.CustomAttributes;

namespace ErpCrystal_MFG.Api.Controllers;

[ApiController]
[Route("api/jobs")]
[ApiKeyAuth] // Existing authentication attribute
public class ScheduledJobsController : ControllerBase
{
    private readonly IndentEmailJob _indentEmailJob;
    private readonly InvoiceEmailJob _invoiceEmailJob;
    private readonly SalesOrderEmailJob _salesOrderEmailJob;
    private readonly ArEmailJob _arEmailJob;
    private readonly VoucherPaymentEmailJob _voucherPaymentEmailJob;
    private readonly VoucherReceiptEmailJob _voucherReceiptEmailJob;
    private readonly AIInsightsJob _aiInsightsJob;

    public ScheduledJobsController(
        IndentEmailJob indentEmailJob,
        InvoiceEmailJob invoiceEmailJob,
        SalesOrderEmailJob salesOrderEmailJob,
        ArEmailJob arEmailJob,
        VoucherPaymentEmailJob voucherPaymentEmailJob,
        VoucherReceiptEmailJob voucherReceiptEmailJob,
        AIInsightsJob aiInsightsJob)
    {
        _indentEmailJob = indentEmailJob;
        _invoiceEmailJob = invoiceEmailJob;
        _salesOrderEmailJob = salesOrderEmailJob;
        _arEmailJob = arEmailJob;
        _voucherPaymentEmailJob = voucherPaymentEmailJob;
        _voucherReceiptEmailJob = voucherReceiptEmailJob;
        _aiInsightsJob = aiInsightsJob;
    }

    // Indent Email Job Endpoints
    [HttpPost("indent-email")]
    public async Task<IActionResult> RunIndentEmails()
    {
        await _indentEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "Indent email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("indent-email/{dbName}")]
    public async Task<IActionResult> RunIndentEmailForDb(string dbName)
    {
        await _indentEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"Indent email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // Invoice Email Job Endpoints
    [HttpPost("invoice-email")]
    public async Task<IActionResult> RunInvoiceEmails()
    {
        await _invoiceEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "Invoice email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("invoice-email/{dbName}")]
    public async Task<IActionResult> RunInvoiceEmailForDb(string dbName)
    {
        await _invoiceEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"Invoice email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // Sales Order Email Job Endpoints
    [HttpPost("sales-order-email")]
    public async Task<IActionResult> RunSalesOrderEmails()
    {
        await _salesOrderEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "Sales order email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("sales-order-email/{dbName}")]
    public async Task<IActionResult> RunSalesOrderEmailForDb(string dbName)
    {
        await _salesOrderEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"Sales order email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // AR Email Job Endpoints
    [HttpPost("ar-email")]
    public async Task<IActionResult> RunArEmails()
    {
        await _arEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "AR email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("ar-email/{dbName}")]
    public async Task<IActionResult> RunArEmailForDb(string dbName)
    {
        await _arEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"AR email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // Voucher Payment Email Job Endpoints
    [HttpPost("voucher-payment-email")]
    public async Task<IActionResult> RunVoucherPaymentEmails()
    {
        await _voucherPaymentEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "Voucher payment email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("voucher-payment-email/{dbName}")]
    public async Task<IActionResult> RunVoucherPaymentEmailForDb(string dbName)
    {
        await _voucherPaymentEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"Voucher payment email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // Voucher Receipt Email Job Endpoints
    [HttpPost("voucher-receipt-email")]
    public async Task<IActionResult> RunVoucherReceiptEmails()
    {
        await _voucherReceiptEmailJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "Voucher receipt email job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("voucher-receipt-email/{dbName}")]
    public async Task<IActionResult> RunVoucherReceiptEmailForDb(string dbName)
    {
        await _voucherReceiptEmailJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"Voucher receipt email job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // AI Insights Job Endpoints
    [HttpPost("ai-insights")]
    public async Task<IActionResult> RunAIInsights()
    {
        await _aiInsightsJob.RunAll();
        return Ok(new { 
            Success = true, 
            Message = "AI insights job completed",
            Timestamp = DateTime.UtcNow 
        });
    }

    [HttpPost("ai-insights/{dbName}")]
    public async Task<IActionResult> RunAIInsightsForDb(string dbName)
    {
        await _aiInsightsJob.Run(dbName);
        return Ok(new { 
            Success = true, 
            Message = $"AI insights job completed for {dbName}",
            Timestamp = DateTime.UtcNow 
        });
    }

    // Health Check Endpoint (Optional)
    [HttpGet("health")]
    public IActionResult HealthCheck()
    {
        return Ok(new { 
            Status = "Healthy",
            Scheduler = "Windows Task Scheduler",
            Timestamp = DateTime.UtcNow 
        });
    }
}

4. Minimal API Key Authentication (Already Implemented)

The existing [ApiKeyAuth] attribute should already be implemented. Example of what it might look like:

// Existing in project: ErpCrystal_MFG.Api/CustomAttributes/ApiKeyAuthAttribute.cs
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ApiKeyAuthAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // Existing API key validation logic
        // This should already be working in your codebase
    }
}

5. Folder Structure Changes

Current Structure:

ErpCrystal_MFG.Api/
├─ TaskScheduler/
│   ├─ IndentEmailScheduler.cs
│   ├─ InvoiceEmailScheduler.cs
│   ├─ SalesOrderEmailScheduler.cs
│   ├─ ArEmailScheduler.cs
│   ├─ VoucherPaymentEmailScheduler.cs
│   ├─ VoucherReceiptEmailScheduler.cs
│   ├─ AIInsightsScheduler.cs
│   └─ TaskSchedulerInitializer.cs

New Structure (After Migration):

ErpCrystal_MFG.Api/
├─ Controllers/
│   ├─ Existing controllers...
│   └─ ScheduledJobsController.cs          ← NEW
├─ Jobs/                                   ← NEW FOLDER
│   ├─ IndentEmailJob.cs                   ← Renamed/Refactored
│   ├─ InvoiceEmailJob.cs                  ← Renamed/Refactored
│   ├─ SalesOrderEmailJob.cs               ← Renamed/Refactored
│   ├─ ArEmailJob.cs                       ← Renamed/Refactored
│   ├─ VoucherPaymentEmailJob.cs           ← Renamed/Refactored
│   ├─ VoucherReceiptEmailJob.cs           ← Renamed/Refactored
│   └─ AIInsightsJob.cs                    ← Renamed/Refactored
└─ TaskScheduler/                          ← DELETE after successful migration

6. Migration Notes

Key Differences to Remember:

  1. Constructor Changes: Job classes need ITaskSchedulerRepository parameter to fetch database lists dynamically
  2. Method Split: Invoke() becomes two methods: Run(string dbname) and RunAll()
  3. Return vs Continue: continue in loop becomes return in single database method
  4. Namespace: Change from ErpCrystal_MFG.Api.TaskScheduler to ErpCrystal_MFG.Api.Jobs
  5. Interface Removal: Remove : IInvocable from class definition

Copy-Paste Strategy:

  1. Copy entire Invoke() method body
  2. Paste into Run(string dbname) method
  3. Replace continue with return
  4. Create RunAll() method that calls Run() for each database
  5. Update constructor to include ITaskSchedulerRepository

Testing Strategy:

  1. Test Run(dbName) endpoint first with single database
  2. Test RunAll() endpoint next
  3. Compare logs with Coravel output
  4. Verify email delivery

7. Common Issues and Solutions

Issue 1: Database list not available in constructor

Solution: Inject ITaskSchedulerRepository and fetch in RunAll() method

Issue 2: Logging paths incorrect

Solution: Keep exact same logging code - it’s path-agnostic

Issue 3: Email attachments not working

Solution: Same business logic - if it worked with Coravel, it will work here

Issue 4: Dependency injection errors

Solution: Ensure all job classes are registered in Program.cs


These example transformations show the minimal changes required to migrate from Coravel to Windows Task Scheduler while keeping 95% of existing business logic unchanged.