< Summary

Information
Class: Services.BackgroundJobsServices.BackgroundJobScheduleService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BackgroundJobsServices/BackgroundJobScheduleService.cs
Line coverage
58%
Covered lines: 94
Uncovered lines: 67
Coverable lines: 161
Total lines: 309
Line coverage: 58.3%
Branch coverage
41%
Covered branches: 25
Total branches: 60
Branch coverage: 41.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BackgroundJobsServices/BackgroundJobScheduleService.cs

#LineLine coverage
 1using Hangfire;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5
 6namespace Services.BackgroundJobsServices;
 7
 8public interface IBackgroundJobScheduleService
 9{
 10    Task<List<BackgroundJobSchedule>> GetAllSchedulesAsync(CancellationToken cancellationToken = default);
 11    Task<List<BackgroundJobSchedule>> GetActiveSchedulesAsync(CancellationToken cancellationToken = default);
 12    Task<BackgroundJobSchedule?> GetScheduleByIdAsync(int id, CancellationToken cancellationToken = default);
 13    Task<int> CreateScheduleAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToken = default);
 14    Task UpdateScheduleAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToken = default);
 15    Task DeleteScheduleAsync(int id, CancellationToken cancellationToken = default);
 16    Task ActivateScheduleAsync(int id, CancellationToken cancellationToken = default);
 17    Task DeactivateScheduleAsync(int id, CancellationToken cancellationToken = default);
 18    Task RegisterAllActiveSchedulesAsync(CancellationToken cancellationToken = default);
 19    Task RegisterScheduleInHangfireAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToken = default);
 20    Task UnregisterScheduleFromHangfireAsync(int scheduleId, CancellationToken cancellationToken = default);
 21}
 22
 23public class BackgroundJobScheduleService : IBackgroundJobScheduleService
 24{
 25    private readonly ApplicationDbContext _context;
 26    private readonly IRecurringJobManager _recurringJobManager;
 27
 1428    public BackgroundJobScheduleService(
 1429        ApplicationDbContext context,
 1430        IRecurringJobManager recurringJobManager)
 31    {
 1432        _context = context;
 1433        _recurringJobManager = recurringJobManager;
 1434    }
 35
 36    public async Task<List<BackgroundJobSchedule>> GetAllSchedulesAsync(CancellationToken cancellationToken = default)
 37    {
 038        return await _context.BackgroundJobSchedules
 039            .Include(s => s.OzonClient)
 040            .OrderByDescending(s => s.CreatedAt)
 041            .ToListAsync(cancellationToken);
 042    }
 43
 44    public async Task<List<BackgroundJobSchedule>> GetActiveSchedulesAsync(CancellationToken cancellationToken = default
 45    {
 146        return await _context.BackgroundJobSchedules
 147            .Include(s => s.OzonClient)
 148            .Where(s => s.IsActive)
 149            .OrderByDescending(s => s.CreatedAt)
 150            .ToListAsync(cancellationToken);
 151    }
 52
 53    public async Task<BackgroundJobSchedule?> GetScheduleByIdAsync(int id, CancellationToken cancellationToken = default
 54    {
 055        return await _context.BackgroundJobSchedules
 056            .Include(s => s.OzonClient)
 057            .FirstOrDefaultAsync(s => s.Id == id, cancellationToken);
 058    }
 59
 60    public async Task<int> CreateScheduleAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToken = def
 61    {
 562        NormalizeTransactionJobDaysBack(schedule);
 63
 64        // Auto-generate name if empty
 565        if (string.IsNullOrWhiteSpace(schedule.Name))
 66        {
 167            schedule.Name = GenerateScheduleName(schedule);
 68        }
 69
 570        schedule.CreatedAt = DateTime.UtcNow;
 571        schedule.UpdatedAt = DateTime.UtcNow;
 72
 573        _context.BackgroundJobSchedules.Add(schedule);
 574        await _context.SaveChangesAsync(cancellationToken);
 75
 576        if (schedule.IsActive)
 77        {
 178            await RegisterScheduleInHangfireAsync(schedule, cancellationToken);
 79        }
 80
 581        return schedule.Id;
 582    }
 83
 84    public async Task UpdateScheduleAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToken = default)
 85    {
 086        var existing = await _context.BackgroundJobSchedules.FindAsync(new object[] { schedule.Id }, cancellationToken);
 087        if (existing == null)
 088            throw new InvalidOperationException($"Schedule with ID {schedule.Id} not found");
 89
 090        existing.Name = schedule.Name;
 091        existing.JobType = schedule.JobType;
 092        existing.CronExpression = schedule.CronExpression;
 093        existing.IsActive = schedule.IsActive;
 094        existing.OzonClientId = schedule.OzonClientId;
 095        NormalizeTransactionJobDaysBack(schedule);
 096        existing.DaysBack = schedule.DaysBack;
 097        existing.UpdatedAt = DateTime.UtcNow;
 98
 099        await _context.SaveChangesAsync(cancellationToken);
 100
 0101        if (existing.IsActive)
 102        {
 0103            await RegisterScheduleInHangfireAsync(existing, cancellationToken);
 104        }
 105        else
 106        {
 0107            await UnregisterScheduleFromHangfireAsync(existing.Id, cancellationToken);
 108        }
 0109    }
 110
 111    public async Task DeleteScheduleAsync(int id, CancellationToken cancellationToken = default)
 112    {
 0113        var schedule = await _context.BackgroundJobSchedules.FindAsync(new object[] { id }, cancellationToken);
 0114        if (schedule == null)
 0115            throw new InvalidOperationException($"Schedule with ID {id} not found");
 116
 0117        await UnregisterScheduleFromHangfireAsync(id, cancellationToken);
 118
 0119        _context.BackgroundJobSchedules.Remove(schedule);
 0120        await _context.SaveChangesAsync(cancellationToken);
 0121    }
 122
 123    public async Task ActivateScheduleAsync(int id, CancellationToken cancellationToken = default)
 124    {
 0125        var schedule = await _context.BackgroundJobSchedules.FindAsync(new object[] { id }, cancellationToken);
 0126        if (schedule == null)
 0127            throw new InvalidOperationException($"Schedule with ID {id} not found");
 128
 0129        schedule.IsActive = true;
 0130        schedule.UpdatedAt = DateTime.UtcNow;
 0131        await _context.SaveChangesAsync(cancellationToken);
 132
 0133        await RegisterScheduleInHangfireAsync(schedule, cancellationToken);
 0134    }
 135
 136    public async Task DeactivateScheduleAsync(int id, CancellationToken cancellationToken = default)
 137    {
 0138        var schedule = await _context.BackgroundJobSchedules.FindAsync(new object[] { id }, cancellationToken);
 0139        if (schedule == null)
 0140            throw new InvalidOperationException($"Schedule with ID {id} not found");
 141
 0142        schedule.IsActive = false;
 0143        schedule.UpdatedAt = DateTime.UtcNow;
 0144        await _context.SaveChangesAsync(cancellationToken);
 145
 0146        await UnregisterScheduleFromHangfireAsync(id, cancellationToken);
 0147    }
 148
 149    public async Task RegisterAllActiveSchedulesAsync(CancellationToken cancellationToken = default)
 150    {
 1151        var activeSchedules = await GetActiveSchedulesAsync(cancellationToken);
 4152        foreach (var schedule in activeSchedules)
 153        {
 1154            await RegisterScheduleInHangfireAsync(schedule, cancellationToken);
 155        }
 1156    }
 157
 158    public async Task RegisterScheduleInHangfireAsync(BackgroundJobSchedule schedule, CancellationToken cancellationToke
 159    {
 9160        var jobId = $"background-job-schedule-{schedule.Id}";
 161
 9162        _recurringJobManager.RemoveIfExists(jobId);
 9163        if (string.Equals(schedule.JobType, "CustomerReturn", StringComparison.OrdinalIgnoreCase))
 164        {
 1165            _recurringJobManager.AddOrUpdate<IAutomatedTransactionProcessingService>(
 1166                jobId,
 1167                service => service.ProcessCustomerReturnAsync(schedule.Id, CancellationToken.None),
 1168                schedule.CronExpression,
 1169                new RecurringJobOptions
 1170                {
 1171                    TimeZone = RussianTimeZoneHelper.Zone
 1172                });
 173        }
 174        else
 175        {
 8176            _recurringJobManager.AddOrUpdate<IAutomatedTransactionProcessingService>(
 8177                jobId,
 8178                service => service.ProcessShippedToCommissionerAsync(schedule.Id, CancellationToken.None),
 8179                schedule.CronExpression,
 8180                new RecurringJobOptions
 8181                {
 8182                    TimeZone = RussianTimeZoneHelper.Zone
 8183                });
 184        }
 185
 9186        var tracked = await _context.BackgroundJobSchedules.FindAsync(new object[] { schedule.Id }, cancellationToken);
 9187        if (tracked != null)
 188        {
 9189            tracked.NextRunTime = GetNextRunTime(schedule.CronExpression);
 9190            tracked.UpdatedAt = DateTime.UtcNow;
 9191            await _context.SaveChangesAsync(cancellationToken);
 192        }
 9193    }
 194
 195    public async Task UnregisterScheduleFromHangfireAsync(int scheduleId, CancellationToken cancellationToken = default)
 196    {
 2197        var jobId = $"background-job-schedule-{scheduleId}";
 2198        _recurringJobManager.RemoveIfExists(jobId);
 199
 2200        var schedule = await _context.BackgroundJobSchedules.FindAsync(new object[] { scheduleId }, cancellationToken);
 2201        if (schedule != null)
 202        {
 2203            schedule.NextRunTime = null;
 2204            await _context.SaveChangesAsync(cancellationToken);
 205        }
 2206    }
 207
 208    private DateTime? GetNextRunTime(string cronExpression)
 209    {
 210        try
 211        {
 9212            var schedule = Cronos.CronExpression.Parse(cronExpression);
 9213            var russianTz = RussianTimeZoneHelper.Zone;
 9214            var nowInRussia = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, russianTz);
 9215            var next = schedule.GetNextOccurrence(nowInRussia, russianTz);
 0216            return next.HasValue ? TimeZoneInfo.ConvertTimeToUtc(next.Value, russianTz) : null;
 217        }
 9218        catch
 219        {
 9220            return null;
 221        }
 9222    }
 223
 224    private string GenerateScheduleName(BackgroundJobSchedule schedule)
 225    {
 226        try
 227        {
 1228            var client = _context.OzonClients.Find(schedule.OzonClientId);
 1229            var clientName = client?.Name ?? $"Client#{schedule.OzonClientId}";
 230
 1231            var jobTypeName = schedule.JobType switch
 1232            {
 1233                "ShippedToCommissioner" => "Отгружен комиссионеру",
 0234                "CustomerReturn" => "Возврат покупателя",
 0235                _ => schedule.JobType
 1236            };
 237
 1238            var periodText = FormatDaysBack(schedule.DaysBack);
 1239            var cronDesc = FormatCronExpression(schedule.CronExpression);
 240
 1241            return $"{jobTypeName} {periodText} для {clientName} ({cronDesc})";
 242        }
 0243        catch
 244        {
 0245            return $"Расписание #{Guid.NewGuid().ToString().Substring(0, 8)}";
 246        }
 1247    }
 248
 249    private static void NormalizeTransactionJobDaysBack(BackgroundJobSchedule schedule)
 250    {
 5251        if (IsTransactionJobType(schedule.JobType))
 5252            schedule.DaysBack = ReportDeliveryDateFilter.TransactionJobDaysBack;
 5253    }
 254
 255    private static bool IsTransactionJobType(string jobType) =>
 5256        string.Equals(jobType, "ShippedToCommissioner", StringComparison.OrdinalIgnoreCase)
 5257        || string.Equals(jobType, "CustomerReturn", StringComparison.OrdinalIgnoreCase);
 258
 1259    private string FormatDaysBack(int days) => "за вчера";
 260
 261    private string FormatCronExpression(string cron)
 262    {
 263        try
 264        {
 1265            var parts = cron.Split(' ');
 1266            if (parts.Length < 5) return cron;
 267
 1268            var minute = parts[0];
 1269            var hour = parts[1];
 1270            var dayOfMonth = parts[2];
 1271            var month = parts[3];
 1272            var dayOfWeek = parts[4];
 273
 1274            if (dayOfMonth == "*" && month == "*" && dayOfWeek == "*")
 275            {
 1276                if (hour.StartsWith("*/"))
 277                {
 0278                    var interval = hour.Substring(2);
 0279                    return $"каждые {interval}ч";
 280                }
 1281                if (hour != "*")
 282                {
 1283                    return $"ежедневно в {hour.PadLeft(2, '0')}:{minute.PadLeft(2, '0')}";
 284                }
 0285                return "каждый час";
 286            }
 287
 0288            if (dayOfWeek != "*" && dayOfMonth == "*")
 289            {
 0290                var dayNames = new[] { "Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб" };
 0291                var dayName = int.TryParse(dayOfWeek, out var dayIndex) && dayIndex < 7
 0292                    ? dayNames[dayIndex]
 0293                    : dayOfWeek;
 0294                return $"{dayName} в {hour.PadLeft(2, '0')}:{minute.PadLeft(2, '0')}";
 295            }
 296
 0297            if (dayOfMonth != "*" && month == "*")
 298            {
 0299                return $"{dayOfMonth}-го числа в {hour.PadLeft(2, '0')}:{minute.PadLeft(2, '0')}";
 300            }
 301
 0302            return cron;
 303        }
 0304        catch
 305        {
 0306            return cron;
 307        }
 1308    }
 309}