diff --git a/ECMJobRunner.Application/Common/Exceptions/InactiveProfileException.cs b/ECMJobRunner.Application/Common/Exceptions/InactiveProfileException.cs
new file mode 100644
index 0000000..a18ed13
--- /dev/null
+++ b/ECMJobRunner.Application/Common/Exceptions/InactiveProfileException.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace ECMJobRunner.Application.Common.Exceptions
+{
+ ///
+ /// Exception thrown when attempting to execute a job for an inactive profile
+ /// Extends JobException with profile-specific context
+ ///
+ public class InactiveProfileException : JobException
+ {
+ ///
+ /// Initializes a new instance of InactiveProfileException
+ ///
+ /// ID of the inactive profile
+ /// Name of the inactive profile (nullable)
+ /// Unique batch identifier for tracking
+ ///
+ /// Use this exception when:
+ /// - Attempting to trigger DEX job batch for an inactive profile
+ /// - Attempting to execute individual jobs from an inactive profile
+ /// - Profile is deactivated during execution
+ ///
+ public InactiveProfileException(long profileId, string? profileName, string batchId)
+ : base(
+ jobName: "Profile Execution",
+ processName: "Profile Active Status Validation",
+ batchId: batchId,
+ reason: "The profile is marked as inactive and cannot be executed",
+ innerException: null,
+ ("Profile ID", profileId.ToString(), false),
+ ("Profile Name", profileName, true))
+ {
+ ProfileId = profileId;
+ ProfileName = profileName;
+ }
+
+ ///
+ /// Gets the ID of the inactive profile
+ ///
+ public long ProfileId { get; }
+
+ ///
+ /// Gets the name of the inactive profile (nullable)
+ ///
+ public string? ProfileName { get; }
+ }
+}
diff --git a/ECMJobRunner.Application/DEXJob/TriggeringDEXJobBatchCommand.cs b/ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobBatchCommand.cs
similarity index 70%
rename from ECMJobRunner.Application/DEXJob/TriggeringDEXJobBatchCommand.cs
rename to ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobBatchCommand.cs
index 35c82c3..cf4c28f 100644
--- a/ECMJobRunner.Application/DEXJob/TriggeringDEXJobBatchCommand.cs
+++ b/ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobBatchCommand.cs
@@ -1,10 +1,12 @@
-using ECMJobRunner.Domain.Interfaces;
+using ECMJobRunner.Application.Common.Exceptions;
+using ECMJobRunner.Application.Common.Interfaces;
+using ECMJobRunner.Domain.Interfaces;
using MediatR;
using System;
using System.Threading;
using System.Threading.Tasks;
-namespace ECMJobRunner.Application.DEXJob
+namespace ECMJobRunner.Application.DEXJob.Commands
{
///
/// Command to trigger DEX job batch for a profile
@@ -21,8 +23,9 @@ namespace ECMJobRunner.Application.DEXJob
///
/// Handler for TriggeringDEXJobBatchCommand
/// Retrieves all SQL jobs for a profile and executes them sequentially
+ /// Validates that the profile is active before execution
///
- public class TriggeringDEXJobBatchCommandHandler(IProfileSqlJobRepository jobRepo, ISender sender)
+ public class TriggeringDEXJobBatchCommandHandler(ICfgProfileRepository profileRepo, IProfileSqlJobRepository jobRepo, ISender sender)
: IRequestHandler
{
///
@@ -32,12 +35,28 @@ namespace ECMJobRunner.Application.DEXJob
/// The command containing the profile ID
/// Cancellation token
/// Unit value indicating completion
+ /// Thrown when the profile is not active
public async Task Handle(TriggeringDEXJobBatchCommand request, CancellationToken cancellationToken)
{
- var jobs = await jobRepo.FindAsync(j => j.ProfileId == request.ProfileId, cancellationToken);
-
var batchId = CreateBatchId();
+ // Retrieve the profile to check if it's active
+ var profile = await profileRepo.GetByIdAsync(request.ProfileId, cancellationToken);
+
+ // Check if profile exists and is active
+ if (profile == null)
+ {
+ throw new InvalidOperationException($"Profile with ID {request.ProfileId} not found.");
+ }
+
+ if (!profile.Active)
+ {
+ throw new InactiveProfileException(profile.Id, profile.ProfileName, batchId);
+ }
+
+ // Retrieve all jobs for the profile
+ var jobs = await jobRepo.FindAsync(j => j.ProfileId == request.ProfileId, cancellationToken);
+
foreach (var job in jobs)
{
await sender.Send(new TriggeringDEXJobCommand
diff --git a/ECMJobRunner.Application/DEXJob/TriggeringDEXJobCommand.cs b/ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobCommand.cs
similarity index 97%
rename from ECMJobRunner.Application/DEXJob/TriggeringDEXJobCommand.cs
rename to ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobCommand.cs
index f28053c..218548f 100644
--- a/ECMJobRunner.Application/DEXJob/TriggeringDEXJobCommand.cs
+++ b/ECMJobRunner.Application/DEXJob/Commands/TriggeringDEXJobCommand.cs
@@ -3,7 +3,7 @@ using MediatR;
using System.Threading;
using System.Threading.Tasks;
-namespace ECMJobRunner.Application.DEXJob
+namespace ECMJobRunner.Application.DEXJob.Commands
{
///
/// Command to trigger a single DEX job execution
diff --git a/ECMJobRunner.Tests/Application/TriggeringDEXJobBatchCommandTests.cs b/ECMJobRunner.Tests/Application/TriggeringDEXJobBatchCommandTests.cs
index 90f1775..937ec4b 100644
--- a/ECMJobRunner.Tests/Application/TriggeringDEXJobBatchCommandTests.cs
+++ b/ECMJobRunner.Tests/Application/TriggeringDEXJobBatchCommandTests.cs
@@ -1,8 +1,9 @@
using ECMJobRunner.Application.Common.Dtos;
-using ECMJobRunner.Domain.Interfaces;
+using ECMJobRunner.Application.Common.Interfaces;
using ECMJobRunner.Application.Common.Options;
-using ECMJobRunner.Application.DEXJob;
+using ECMJobRunner.Application.DEXJob.Commands;
using ECMJobRunner.Domain.Entities;
+using ECMJobRunner.Domain.Interfaces;
using FluentAssertions;
using MediatR;
using Microsoft.Extensions.Options;
diff --git a/ECMJobRunner.Tests/Application/TriggeringDEXJobCommandExceptionTests.cs b/ECMJobRunner.Tests/Application/TriggeringDEXJobCommandExceptionTests.cs
new file mode 100644
index 0000000..8e88f16
--- /dev/null
+++ b/ECMJobRunner.Tests/Application/TriggeringDEXJobCommandExceptionTests.cs
@@ -0,0 +1,164 @@
+using ECMJobRunner.Application.Common.Exceptions;
+using FluentAssertions;
+using System;
+using Xunit;
+
+namespace ECMJobRunner.Tests.Application
+{
+ public class TriggeringDEXJobCommandExceptionTests
+ {
+ [Fact]
+ public void JobSqlException_HasCorrectMessage()
+ {
+ // Arrange
+ var jobName = "Main Query Execution";
+ var processName = "MainQueryExecution";
+ var batchId = "12345678901234567890";
+ var reason = "SQL syntax error";
+ var query = "SELECT * FROM NonExistentTable";
+
+ // Act
+ var exception = new JobSqlException(
+ jobName,
+ processName,
+ batchId,
+ reason,
+ query,
+ null);
+
+ // Assert
+ exception.JobName.Should().Be(jobName);
+ exception.ProcessName.Should().Be(processName);
+ exception.BatchId.Should().Be(batchId);
+ exception.Query.Should().Be(query);
+ exception.Message.Should().Contain(jobName);
+ exception.Message.Should().Contain(processName);
+ exception.Message.Should().Contain(batchId);
+ exception.Message.Should().Contain(reason);
+ exception.Message.Should().Contain(query);
+ }
+
+ [Fact]
+ public void JobSqlException_WithInnerException_PreservesInnerException()
+ {
+ // Arrange
+ var innerException = new InvalidOperationException("Database connection failed");
+ var exception = new JobSqlException(
+ "Main Query",
+ "Execution",
+ "12345678901234567890",
+ "Connection error",
+ "SELECT 1",
+ innerException);
+
+ // Assert
+ exception.InnerException.Should().Be(innerException);
+ exception.InnerException!.Message.Should().Be("Database connection failed");
+ }
+
+ [Fact]
+ public void JobHttpException_HasCorrectMessage()
+ {
+ // Arrange
+ var jobName = "ReC Request Execution";
+ var processName = "ReCRequestExecution";
+ var batchId = "12345678901234567890";
+ var reason = "HTTP 500 Internal Server Error";
+ var clientLibrary = "ReC.Client";
+ var clientMethod = "ExecuteAsync";
+
+ // Act
+ var exception = new JobHttpException(
+ jobName,
+ processName,
+ batchId,
+ reason,
+ clientLibrary,
+ clientMethod,
+ null);
+
+ // Assert
+ exception.JobName.Should().Be(jobName);
+ exception.ProcessName.Should().Be(processName);
+ exception.BatchId.Should().Be(batchId);
+ exception.ClientLibrary.Should().Be(clientLibrary);
+ exception.ClientMethod.Should().Be(clientMethod);
+ exception.Message.Should().Contain(jobName);
+ exception.Message.Should().Contain(processName);
+ exception.Message.Should().Contain(batchId);
+ exception.Message.Should().Contain(reason);
+ exception.Message.Should().Contain(clientLibrary);
+ exception.Message.Should().Contain(clientMethod);
+ }
+
+ [Fact]
+ public void JobHttpException_WithInnerException_PreservesInnerException()
+ {
+ // Arrange
+ var innerException = new TimeoutException("Request timed out");
+ var exception = new JobHttpException(
+ "ReC Request",
+ "Execution",
+ "12345678901234567890",
+ "Timeout error",
+ "ReC.Client",
+ "ExecuteAsync",
+ innerException);
+
+ // Assert
+ exception.InnerException.Should().Be(innerException);
+ exception.InnerException!.Message.Should().Be("Request timed out");
+ }
+
+ [Fact]
+ public void JobException_OmitsNullValues_WhenIgnoreIfNullIsTrue()
+ {
+ // Arrange & Act
+ var exception = new JobException(
+ "Test Job",
+ "Test Process",
+ "12345678901234567890",
+ null, // reason is null
+ null,
+ ("Custom Detail", "Value", false));
+
+ // Assert
+ exception.Message.Should().NotContain("Reason:"); // Should be omitted because it's null and IgnoreIfNull=true
+ exception.Message.Should().Contain("Custom Detail: Value");
+ }
+
+ [Fact]
+ public void JobException_IncludesNullValues_WhenIgnoreIfNullIsFalse()
+ {
+ // Arrange & Act
+ var exception = new JobException(
+ "Test Job",
+ "Test Process",
+ "12345678901234567890",
+ null,
+ null,
+ ("Custom Detail", null, false)); // IgnoreIfNull=false
+
+ // Assert
+ exception.Message.Should().Contain("Custom Detail:"); // Should be included even though value is null
+ }
+
+ [Fact]
+ public void JobException_FormatsMessageWithSeparators()
+ {
+ // Arrange & Act
+ var exception = new JobException(
+ "Test Job",
+ "Test Process",
+ "12345678901234567890",
+ "Test reason",
+ null);
+
+ // Assert
+ exception.Message.Should().Contain("─────────────────────────────────────────");
+ exception.Message.Should().Contain("Test Job could not be completed.");
+ exception.Message.Should().Contain("Process Name: Test Process");
+ exception.Message.Should().Contain("Batch Id: 12345678901234567890");
+ }
+ }
+}