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); } } }