using FrameworkDAL.CustomCode.SchedulerTaskGenerator;
using GB5Shared.DTO.Framework.Login;
using Quartz;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace FrameworkBLL.SchedulerTaskGenerator
{
///
/// Job admin operations (Pause / Resume / manual Trigger) — kept separate from
/// ISchedulerTaskServiceBLL, which owns the execution lifecycle, not job admin.
///
public interface IJobDefineOperationsBLL
{
Task PauseAsync(int jobId, LoginDTO login, CancellationToken ct = default);
Task ResumeAsync(int jobId, LoginDTO login, CancellationToken ct = default);
///
/// Fires the job immediately via Quartz. Throws InvalidOperationException if the
/// job has no registered Quartz trigger — deliberately does NOT fall back to an
/// anonymous one-shot trigger (that pattern loses TenantId scoping; see the
/// reference design doc's flagged pitfall).
///
Task TriggerAsync(int jobId, LoginDTO login, CancellationToken ct = default);
}
public sealed class JobDefineOperationsBLL : IJobDefineOperationsBLL
{
// CORRECTED (tracker §46 live-verification pass) — this file previously claimed
// MJOBDEFINE/TSCHEDULER use an inverted "0=active,1=inactive" convention, distinct
// from GB5's general entity status enum. That claim was wrong: the schema's own
// DFMJOBDEFINE_STATUS/DFTSCHEDULER_STATUS DEFAULT is 1, with the standard GB5 enum
// (0.Pending 1.Active 2.Deleted 3.Amended 4.Inactive 5.Archived) — confirmed live
// against a real tenant database (USCIMPSYS): 75/79 real MJOBDEFINE rows and 78/81
// real TSCHEDULER rows sit at STATUS=1, and a sibling query in this same file
// (MACTION.STATUS=1 in HasActiveSchedulerAction) already correctly treats 1 as active.
// The inverted belief was baked consistently into this class, QuartzSyncService's own
// sync-eligibility check, and LoadReadyJobs'/the dashboard query's WHERE clauses — all
// fixed together in this same pass, since a partial fix would have been worse than
// the original bug (a query that finds jobs and a check that then removes them again).
private const int StatusActive = 1;
private const int StatusInactive = 4; // matches the schema's own literal "4.Inactive"
private readonly ISchedulerTaskDAL _dal;
private readonly IQuartzSyncService _quartzSync;
private readonly ISchedulerFactory _schedulerFactory;
public JobDefineOperationsBLL(
ISchedulerTaskDAL dal,
IQuartzSyncService quartzSync,
ISchedulerFactory schedulerFactory)
{
_dal = dal ?? throw new ArgumentNullException(nameof(dal));
_quartzSync = quartzSync ?? throw new ArgumentNullException(nameof(quartzSync));
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
}
public async Task PauseAsync(int jobId, LoginDTO login, CancellationToken ct = default)
{
await _dal.SetJobStatusAsync(jobId, StatusInactive, login, ct).ConfigureAwait(false);
await _quartzSync.RemoveJobAsync(jobId, login, ct).ConfigureAwait(false);
}
public async Task ResumeAsync(int jobId, LoginDTO login, CancellationToken ct = default)
{
await _dal.SetJobStatusAsync(jobId, StatusActive, login, ct).ConfigureAwait(false);
await _quartzSync.SyncJobAsync(jobId, login, ct).ConfigureAwait(false);
}
public async Task TriggerAsync(int jobId, LoginDTO login, CancellationToken ct = default)
{
if (!await _quartzSync.IsJobRegisteredAsync(jobId, login.ClientId, ct).ConfigureAwait(false))
throw new InvalidOperationException(
$"JobId={jobId} has no registered Quartz trigger — resume/save it first before triggering manually.");
var scheduler = await _schedulerFactory.GetScheduler(ct).ConfigureAwait(false);
var key = new JobKey($"job-{jobId}", $"tenant-{login.ClientId}");
// Trigger-level JobDataMap override — merged with the job-level map at fire
// time, read by SchedulerJobExecutorQuartzJob via context.MergedJobDataMap so
// this run is recorded with TRIGGERTYPE='MANUAL' instead of 'SCHEDULED'.
var triggerData = new JobDataMap { ["TriggerType"] = "MANUAL" };
await scheduler.TriggerJob(key, triggerData, ct).ConfigureAwait(false);
}
}
}