< Summary

Information
Class: Services.DailyWorkSummaryServices.DailyTransactionRow
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DailyWorkSummaryServices/DailyWorkSummaryService.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 223
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Type()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DailyWorkSummaryServices/DailyWorkSummaryService.cs

#LineLine coverage
 1using System.Data.Common;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains;
 4using OzonDomains.Models;
 5using OzonDomains.Models.SupplierStatuses;
 6using OzonRepositories.Context;
 7using Services.ApiServices.TelegramBotApi;
 8using Services.BackgroundJobsServices;
 9using Services.BitrixStockServices;
 10using Services.OrderDailySummaryServices;
 11
 12namespace Services.DailyWorkSummaryServices;
 13
 14public sealed record DailyWorkCount(string Name, int Count);
 15
 1916public sealed record DailyTransactionRow(string Type, int Transactions, int Orders);
 17
 18public sealed record DailyEmployeeRow(string User, int Transactions, int Orders);
 19
 20public sealed record DailyStockRolloutStats(int Reports, int Applied, int Failed);
 21
 22public sealed record DailyDisk(double FreeGb, double TotalGb);
 23
 24/// <summary>Итог рабочего дня по данным БД: что сделано за сутки и состояние на момент отправки.</summary>
 25public sealed record DailyWorkSummary(
 26    DateTime FromLocal,
 27    DateTime ToLocal,
 28    int NewOrders,
 29    IReadOnlyList<DailyWorkCount> NewOrdersByClient,
 30    IReadOnlyList<DailyTransactionRow> Transactions,
 31    IReadOnlyList<DailyEmployeeRow> Employees,
 32    int SupplierOrdersPlaced,
 33    IReadOnlyList<DailyWorkCount> SupplierOrdersByProvider,
 34    int OneCPostedOk,
 35    int OneCPostedFailed,
 36    int ControlTasksClosed,
 37    int ControlTasksOverdue,
 38    DailyStockRolloutStats Rollout,
 39    int AlertsCritical,
 40    int AlertsWarning,
 41    int MissingSystemStatus,
 42    DailyDisk? Disk,
 43    IReadOnlyList<TelegramOzonStockRow>? OzonStock = null);
 44
 45public interface IDailyWorkSummaryService
 46{
 47    Task<DailyWorkSummary> BuildAsync(CancellationToken cancellationToken = default);
 48}
 49
 50/// <summary>
 51/// Собирает сводку «что сделано сегодня» (с 00:00 по московскому времени до момента запуска).
 52/// Время в таблицах хранится по-разному: транзакции, история заказов и дата обработки — по Москве,
 53/// проводки 1С, история поставщика, задачи контроля и отчёты Bitrix — в UTC; окно переводится для каждого источника.
 54/// </summary>
 55public sealed class DailyWorkSummaryService : IDailyWorkSummaryService
 56{
 57    private const int TopClients = 6;
 58    private const int TopEmployees = 8;
 59
 60    private readonly ApplicationDbContext _db;
 61    private readonly IOrderDailySummaryService _orderSummary;
 62    private readonly TimeProvider _timeProvider;
 63
 64    public DailyWorkSummaryService(ApplicationDbContext db, IOrderDailySummaryService orderSummary, TimeProvider? timePr
 65    {
 66        _db = db;
 67        _orderSummary = orderSummary;
 68        _timeProvider = timeProvider ?? TimeProvider.System;
 69    }
 70
 71    public async Task<DailyWorkSummary> BuildAsync(CancellationToken ct = default)
 72    {
 73        var nowUtc = _timeProvider.GetUtcNow().UtcDateTime;
 74        var toLocal = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, RussianTimeZoneHelper.Zone);
 75        var fromLocal = toLocal.Date;
 76        var fromUtc = TimeZoneInfo.ConvertTimeToUtc(fromLocal, RussianTimeZoneHelper.Zone);
 77
 78        var newOrders = await _db.Orders.AsNoTracking()
 79            .Where(o => o.ProcessingDate >= fromLocal && o.ProcessingDate <= toLocal)
 80            .Select(o => o.OzonClient != null ? o.OzonClient.Name : null)
 81            .ToListAsync(ct);
 82        var byClient = newOrders
 83            .GroupBy(n => string.IsNullOrWhiteSpace(n) ? "Без клиента" : n!.Trim())
 84            .Select(g => new DailyWorkCount(g.Key, g.Count()))
 85            .OrderByDescending(x => x.Count).ThenBy(x => x.Name).Take(TopClients).ToList();
 86
 87        var transactions = await _db.Transactions.AsNoTracking()
 88            .Where(t => t.CreatedDateTime >= fromLocal && t.CreatedDateTime <= toLocal)
 89            .Select(t => new { t.Type, t.CreateBy, Orders = t.Orders.Count })
 90            .ToListAsync(ct);
 91        var byType = transactions
 92            .GroupBy(t => t.Type is { } type ? type.GetDisplayName() : "Без типа")
 93            .Select(g => new DailyTransactionRow(g.Key, g.Count(), g.Sum(x => x.Orders)))
 94            .OrderByDescending(x => x.Orders).ThenBy(x => x.Type).ToList();
 95        var employees = transactions
 96            .GroupBy(t => string.IsNullOrWhiteSpace(t.CreateBy) ? "—" : t.CreateBy!.Trim())
 97            .Select(g => new DailyEmployeeRow(g.Key, g.Count(), g.Sum(x => x.Orders)))
 98            .OrderByDescending(x => x.Orders).ThenBy(x => x.User).Take(TopEmployees).ToList();
 99
 100        var placed = await _db.OrderSupplierStateHistory.AsNoTracking()
 101            .Where(h => h.ToStage == OrderSupplierStage.Ordered && h.FromStage != OrderSupplierStage.Ordered && h.Perfor
 102            .Select(h => new { h.OrderId, Provider = h.OrderSupplierState != null ? h.OrderSupplierState.ProviderKey : "
 103            .ToListAsync(ct);
 104        var distinctPlaced = placed.DistinctBy(x => x.OrderId).ToList();
 105        var byProvider = distinctPlaced
 106            .GroupBy(x => x.Provider)
 107            .Select(g => new DailyWorkCount(g.Key, g.Count()))
 108            .OrderByDescending(x => x.Count).ThenBy(x => x.Name).ToList();
 109
 110        var postings = await _db.OrderHistoryOneCPostings.AsNoTracking()
 111            .Where(p => p.PostedAt >= fromUtc && p.PostedAt <= nowUtc)
 112            .GroupBy(p => p.Success)
 113            .Select(g => new { Success = g.Key, Count = g.Count() })
 114            .ToListAsync(ct);
 115
 116        var controlClosed = await _db.OrderControlTasks.AsNoTracking()
 117            .CountAsync(x => x.ClosedAtUtc >= fromUtc && x.ClosedAtUtc <= nowUtc, ct);
 118        var controlOverdue = await _db.OrderControlTasks.AsNoTracking()
 119            .CountAsync(x => x.Status == OrderControlTaskStatus.Overdue, ct);
 120
 121        var alerts = await _db.SystemAlertIncidents.AsNoTracking()
 122            .Where(x => x.IsActive && x.ConfirmedAtUtc != null && x.AcknowledgedAtUtc == null
 123                        && (x.MutedUntilUtc == null || x.MutedUntilUtc <= nowUtc))
 124            .GroupBy(x => x.Severity)
 125            .Select(g => new { Severity = g.Key, Count = g.Count() })
 126            .ToListAsync(ct);
 127
 128        var rollout = await LoadRolloutAsync(fromUtc, nowUtc, ct);
 129        var ozonStock = await LoadOzonStockAsync(nowUtc, ct);
 130        var missing = (await _orderSummary.GetYesterdayAsync(ct)).MissingSystemStatusCount;
 131
 132        return new DailyWorkSummary(
 133            fromLocal, toLocal, newOrders.Count, byClient, byType, employees,
 134            distinctPlaced.Count(), byProvider,
 135            postings.Where(x => x.Success).Sum(x => x.Count), postings.Where(x => !x.Success).Sum(x => x.Count),
 136            controlClosed, controlOverdue, rollout,
 137            alerts.Where(x => x.Severity == "danger").Sum(x => x.Count),
 138            alerts.Where(x => x.Severity == "warning").Sum(x => x.Count),
 139            missing, ReadDisk(), ozonStock);
 140    }
 141
 142    /// <summary>Отчёты наката остатков Bitrix за окно (таблица без DbSet — сырой запрос). Для не-реляционных провайдеро
 143    private async Task<DailyStockRolloutStats> LoadRolloutAsync(DateTime fromUtc, DateTime toUtc, CancellationToken ct)
 144    {
 145        if (!_db.Database.IsRelational()) return new DailyStockRolloutStats(0, 0, 0);
 146
 147        await _db.Database.OpenConnectionAsync(ct);
 148        try
 149        {
 150            await using var cmd = _db.Database.GetDbConnection().CreateCommand();
 151            cmd.CommandText = @"SELECT COUNT(*), COALESCE(SUM(OzonAppliedSuccessCount), 0), COALESCE(SUM(OzonApplyFailed
 152FROM BitrixStockSyncReports
 153WHERE Status = 'Completed' AND GeneratedAtUtc >= @from AND GeneratedAtUtc <= @to";
 154            AddParam(cmd, "@from", fromUtc);
 155            AddParam(cmd, "@to", toUtc);
 156            await using var reader = await cmd.ExecuteReaderAsync(ct);
 157            return await reader.ReadAsync(ct)
 158                ? new DailyStockRolloutStats(Convert.ToInt32(reader.GetValue(0)), Convert.ToInt32(reader.GetValue(1)), C
 159                : new DailyStockRolloutStats(0, 0, 0);
 160        }
 161        finally
 162        {
 163            await _db.Database.CloseConnectionAsync();
 164        }
 165    }
 166
 167    /// <summary>Актуальные остатки по складам Ozon: из последнего завершённого отчёта наката каждого клиента.</summary>
 168    private async Task<IReadOnlyList<TelegramOzonStockRow>> LoadOzonStockAsync(DateTime toUtc, CancellationToken ct)
 169    {
 170        if (!_db.Database.IsRelational()) return [];
 171
 172        await _db.Database.OpenConnectionAsync(ct);
 173        try
 174        {
 175            var conn = _db.Database.GetDbConnection();
 176            var reportIds = new List<string>();
 177            await using (var cmd = conn.CreateCommand())
 178            {
 179                cmd.CommandText = @"SELECT r.ReportId
 180FROM BitrixStockSyncReports r
 181JOIN (SELECT OzonClientId, MAX(GeneratedAtUtc) AS g FROM BitrixStockSyncReports
 182      WHERE Status = 'Completed' AND GeneratedAtUtc <= @to AND ProductsInReport > 0 GROUP BY OzonClientId) m
 183  ON m.OzonClientId = r.OzonClientId AND m.g = r.GeneratedAtUtc";
 184                AddParam(cmd, "@to", toUtc);
 185                await using var reader = await cmd.ExecuteReaderAsync(ct);
 186                while (await reader.ReadAsync(ct)) reportIds.Add(reader.GetString(0));
 187            }
 188
 189            var rows = new List<TelegramOzonStockRow>();
 190            foreach (var id in reportIds)
 191                rows.AddRange(await OzonWarehouseStockQuery.LoadAsync(conn, id, ct));
 192            return rows;
 193        }
 194        finally
 195        {
 196            await _db.Database.CloseConnectionAsync();
 197        }
 198    }
 199
 200    private static void AddParam(DbCommand command, string name, object value)
 201    {
 202        var parameter = command.CreateParameter();
 203        parameter.ParameterName = name;
 204        parameter.Value = value;
 205        command.Parameters.Add(parameter);
 206    }
 207
 208    private static DailyDisk? ReadDisk()
 209    {
 210        try
 211        {
 212            var root = Path.GetPathRoot(AppContext.BaseDirectory);
 213            if (string.IsNullOrEmpty(root)) return null;
 214            var drive = new DriveInfo(root);
 215            const double gb = 1024d * 1024 * 1024;
 216            return new DailyDisk(Math.Round(drive.AvailableFreeSpace / gb, 1), Math.Round(drive.TotalSize / gb, 1));
 217        }
 218        catch
 219        {
 220            return null;
 221        }
 222    }
 223}

Methods/Properties

get_Type()