| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonDomains.Models; |
| | | 3 | | using OzonRepositories.Context; |
| | | 4 | | using Services.BackgroundJobsServices; |
| | | 5 | | |
| | | 6 | | namespace Services.OrderDailySummaryServices; |
| | | 7 | | |
| | | 8 | | public sealed class OrderDailySummaryService : IOrderDailySummaryService |
| | | 9 | | { |
| | 1 | 10 | | private static readonly HashSet<string> CancelledClientStatuses = new(StringComparer.Ordinal) |
| | 1 | 11 | | { |
| | 1 | 12 | | "Отменён", |
| | 1 | 13 | | "Отменен во время доставки", |
| | 1 | 14 | | "Отменен во время обработки", |
| | 1 | 15 | | "Отменен до начала обработки", |
| | 1 | 16 | | "Отменен покупателем", |
| | 1 | 17 | | "Отменен при доставке", |
| | 1 | 18 | | "Отменен при обработке", |
| | 1 | 19 | | "Отменен продавцом", |
| | 1 | 20 | | "Отменен. Продавец отменил заказ после подтверждения.", |
| | 1 | 21 | | "Заказ отменен до обработки", |
| | 1 | 22 | | "Заказ отменен покупателем", |
| | 1 | 23 | | "Заказ отменен продавцом" |
| | 1 | 24 | | }; |
| | | 25 | | |
| | 1 | 26 | | private static readonly HashSet<string> DeliveredClientStatuses = new(StringComparer.Ordinal) |
| | 1 | 27 | | { |
| | 1 | 28 | | "Доставлен", |
| | 1 | 29 | | "Доставлен в пункт выдачи", |
| | 1 | 30 | | "Доставлен в пункт самовывоза", |
| | 1 | 31 | | "Доставлен на пункт выдачи", |
| | 1 | 32 | | "Доставлен покупателю", |
| | 1 | 33 | | "Доставлено", |
| | 1 | 34 | | "Заказ выполнен", |
| | 1 | 35 | | "Заказ получен", |
| | 1 | 36 | | "Получен покупателем", |
| | 1 | 37 | | "Получено", |
| | 1 | 38 | | "Условно доставлено" |
| | 1 | 39 | | }; |
| | | 40 | | |
| | | 41 | | private readonly ApplicationDbContext _db; |
| | | 42 | | private readonly TimeProvider _timeProvider; |
| | | 43 | | |
| | 3 | 44 | | public OrderDailySummaryService(ApplicationDbContext db, TimeProvider? timeProvider = null) |
| | | 45 | | { |
| | 3 | 46 | | _db = db; |
| | 3 | 47 | | _timeProvider = timeProvider ?? TimeProvider.System; |
| | 3 | 48 | | } |
| | | 49 | | |
| | | 50 | | public async Task<OrderDailySummary> GetYesterdayAsync(CancellationToken cancellationToken = default) |
| | | 51 | | { |
| | 3 | 52 | | var nowUtc = _timeProvider.GetUtcNow().UtcDateTime; |
| | 3 | 53 | | var localNow = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, RussianTimeZoneHelper.Zone); |
| | 3 | 54 | | var localStart = localNow.Date.AddDays(-1); |
| | 3 | 55 | | var localEnd = localStart.AddDays(1); |
| | | 56 | | |
| | 3 | 57 | | var acceptedForProcessingCount = await _db.Orders.AsNoTracking() |
| | 3 | 58 | | .Where(x => x.ProcessingDate >= localStart && x.ProcessingDate < localEnd) |
| | 3 | 59 | | .Select(x => x.Id) |
| | 3 | 60 | | .Distinct() |
| | 3 | 61 | | .CountAsync(cancellationToken); |
| | | 62 | | |
| | | 63 | | // Order.LastStatusChangeDate обновляется не во всех путях смены ClientStatusId |
| | | 64 | | // (например, SaveClientStatusUpdatesAsync делает ExecuteUpdateAsync только по ClientStatusId), |
| | | 65 | | // поэтому окно "за сутки" строим только по ProcessingDate — как и для acceptedForProcessingCount. |
| | 3 | 66 | | var processedYesterday = _db.Orders.AsNoTracking() |
| | 3 | 67 | | .Where(x => x.ClientStatus != null && x.ProcessingDate >= localStart && x.ProcessingDate < localEnd); |
| | 3 | 68 | | var cancelledCount = await processedYesterday |
| | 3 | 69 | | .Where(x => CancelledClientStatuses.Contains(x.ClientStatus!.Name)) |
| | 3 | 70 | | .Select(x => x.Id) |
| | 3 | 71 | | .Distinct() |
| | 3 | 72 | | .CountAsync(cancellationToken); |
| | 3 | 73 | | var deliveredCount = await processedYesterday |
| | 3 | 74 | | .Where(x => DeliveredClientStatuses.Contains(x.ClientStatus!.Name)) |
| | 3 | 75 | | .Select(x => x.Id) |
| | 3 | 76 | | .Distinct() |
| | 3 | 77 | | .CountAsync(cancellationToken); |
| | 3 | 78 | | var activeOrderControlTasksCount = await _db.OrderControlTasks.AsNoTracking() |
| | 3 | 79 | | .CountAsync(x => OrderControlTaskStatus.Active.Contains(x.Status), cancellationToken); |
| | 3 | 80 | | var overdueOrderControlTasksCount = await _db.OrderControlTasks.AsNoTracking() |
| | 3 | 81 | | .CountAsync(x => x.Status == OrderControlTaskStatus.Overdue, cancellationToken); |
| | 3 | 82 | | var missingSystemStatusCount = await MissingSystemStatusQuery(localEnd) |
| | 3 | 83 | | .CountAsync(cancellationToken); |
| | | 84 | | |
| | 3 | 85 | | return new OrderDailySummary( |
| | 3 | 86 | | DateOnly.FromDateTime(localStart), |
| | 3 | 87 | | acceptedForProcessingCount, |
| | 3 | 88 | | cancelledCount, |
| | 3 | 89 | | deliveredCount, |
| | 3 | 90 | | missingSystemStatusCount, |
| | 3 | 91 | | activeOrderControlTasksCount, |
| | 3 | 92 | | overdueOrderControlTasksCount); |
| | 3 | 93 | | } |
| | | 94 | | |
| | | 95 | | public async Task<IReadOnlyList<MissingSystemStatusOrder>> GetMissingSystemStatusOrdersAsync( |
| | | 96 | | int take = 200, |
| | | 97 | | CancellationToken cancellationToken = default) |
| | | 98 | | { |
| | 0 | 99 | | var localNow = TimeZoneInfo.ConvertTimeFromUtc(_timeProvider.GetUtcNow().UtcDateTime, RussianTimeZoneHelper.Zone |
| | 0 | 100 | | var endOfYesterday = localNow.Date; |
| | 0 | 101 | | take = Math.Clamp(take, 1, 500); |
| | | 102 | | |
| | 0 | 103 | | return await MissingSystemStatusQuery(endOfYesterday) |
| | 0 | 104 | | .OrderBy(x => x.ProcessingDate) |
| | 0 | 105 | | .Select(x => new MissingSystemStatusOrder( |
| | 0 | 106 | | x.Id, |
| | 0 | 107 | | x.ShipmentNumber ?? $"Заказ #{x.Id}", |
| | 0 | 108 | | x.ProductName, |
| | 0 | 109 | | x.Article, |
| | 0 | 110 | | x.EtProducer != null ? x.EtProducer.Name : x.Manufacturer != null ? x.Manufacturer.Name : null, |
| | 0 | 111 | | x.Supplier != null ? x.Supplier.Name : null, |
| | 0 | 112 | | x.ClientStatus != null ? x.ClientStatus.Name : null, |
| | 0 | 113 | | x.ClientStatus != null && x.ClientStatus.StatusColor != null ? x.ClientStatus.StatusColor.ColorCode : nu |
| | 0 | 114 | | x.ProcessingDate, |
| | 0 | 115 | | x.ProductInfo != null ? x.ProductInfo.ImageUrl : null, |
| | 0 | 116 | | x.OzonClient != null ? x.OzonClient.Name : null)) |
| | 0 | 117 | | .Take(take) |
| | 0 | 118 | | .ToListAsync(cancellationToken); |
| | 0 | 119 | | } |
| | | 120 | | |
| | 3 | 121 | | private IQueryable<Order> MissingSystemStatusQuery(DateTime beforeProcessingDate) => _db.Orders.AsNoTracking() |
| | 3 | 122 | | .Where(x => x.ProcessingDate < beforeProcessingDate && |
| | 3 | 123 | | (x.AppStatusId == null || (x.AppStatus != null && x.AppStatus.Name == "Не указан"))); |
| | | 124 | | } |