< Summary

Line coverage
81%
Covered lines: 103
Uncovered lines: 24
Coverable lines: 127
Total lines: 1068
Line coverage: 81.1%
Branch coverage
60%
Covered branches: 65
Total branches: 108
Branch coverage: 60.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: AlertSplitRegex()100%11100%
File 1: TitleRegex()100%11100%
File 1: AnchorRegex()100%11100%
File 1: LineBreakRegex()100%11100%
File 1: ListItemRegex()100%11100%
File 1: TagRegex()100%11100%
File 1: SpacesRegex()100%11100%
File 1: BlankLinesRegex()100%11100%
File 2: .cctor()100%11100%
File 2: FromOzonShip()78.57%1414100%
File 2: OzonShipRecommendation(...)20%151063.63%
File 2: FromVerification()78.57%282891.66%
File 2: VerificationRecommendation(...)66.66%6681.81%
File 2: FromSupplierMessages()0%4260%
File 2: FromOrders()16.66%21625%
File 2: FromWarnings()100%11100%
File 2: HtmlToText(...)50%22100%
File 2: FromHtml()61.76%423480.76%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/obj/Release/net8.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/TransactionReport.cs

#LineLine coverage
 1using System.Net;
 2using System.Text;
 3using System.Text.RegularExpressions;
 4using OzonOrdersWeb.TransactionServices;
 5using OzonOrdersWeb.WebServices.OrderHistory;
 6using Services.LabelServices;
 7using Services.SupplierStatusServices;
 8
 9namespace OzonOrdersWeb.WebServices.Transactions;
 10
 11public static class TransactionReportLevel
 12{
 13    public const string Danger = "danger";
 14    public const string Warning = "warning";
 15    public const string Info = "info";
 16    public const string Success = "success";
 17
 18    public static int Rank(string level) => level switch
 19    {
 20        Danger => 0,
 21        Warning => 1,
 22        Info => 2,
 23        _ => 3
 24    };
 25}
 26
 27public sealed record TransactionReportLink(string Text, string Url);
 28
 29/// <summary>Одна строка результата проведения: что произошло, почему и что делать (рисуется AG Grid в модалке результат
 30public sealed record TransactionReportRow(
 31    string Level,
 32    string Section,
 33    int? OrderId,
 34    string? ShipmentNumber,
 35    string? Subject,
 36    string? LogoUrl,
 37    string Title,
 38    string? Details,
 39    string? Recommendation,
 40    string? Technical,
 41    IReadOnlyList<TransactionReportLink> Links);
 42
 43public sealed record TransactionReportSummary(
 44    int ChangedCount,
 45    int CancelledCount,
 46    string StatusName,
 47    string DateTime,
 48    string UserName,
 49    string? Comment);
 50
 51public sealed record TransactionReportDto(
 52    TransactionReportSummary Summary,
 53    IReadOnlyList<TransactionReportRow> Rows,
 54    int Errors,
 55    int Warnings,
 56    int Successes);
 57
 58public static class TransactionReportLinks
 59{
 60    public static TransactionReportLink OrderHistory(int orderId) =>
 61        new("История заказа", $"/Studio2/OrderHistory/GetOrderHistory?orderId={orderId}");
 62
 63    public static TransactionReportLink SupplierCart(string providerKey, string? text = null) =>
 64        new(text ?? "Корзина поставщика", $"/SupplierCart?provider={Uri.EscapeDataString(providerKey.ToLowerInvariant())
 65}
 66
 67/// <summary>Копит строки результата проведения; шаги добавляют их по мере работы.</summary>
 68public sealed class TransactionReportBuilder
 69{
 70    private readonly List<TransactionReportRow> _rows = new();
 71
 72    public IReadOnlyList<TransactionReportRow> Rows => _rows;
 73
 74    public void Add(TransactionReportRow row) => _rows.Add(row);
 75
 76    public void AddRange(IEnumerable<TransactionReportRow> rows) => _rows.AddRange(rows);
 77
 78    public TransactionReportDto Build(TransactionReportSummary summary)
 79    {
 80        var rows = _rows
 81            .Select((row, index) => (row, index))
 82            .OrderBy(x => TransactionReportLevel.Rank(x.row.Level))
 83            .ThenBy(x => x.index)
 84            .Select(x => x.row)
 85            .ToList();
 86        return new TransactionReportDto(
 87            summary, rows,
 88            rows.Count(x => x.Level == TransactionReportLevel.Danger),
 89            rows.Count(x => x.Level == TransactionReportLevel.Warning),
 90            rows.Count(x => x.Level == TransactionReportLevel.Success));
 91    }
 92}
 93
 94/// <summary>Преобразование результатов шагов (Ozon, поставщики, HTML-блоки) в строки отчёта.</summary>
 95public static partial class TransactionReportMapper
 96{
 97    public const string SectionOzonShip = "Отгрузка в Ozon";
 98    public const string SectionSupplierOrder = "Заказ у поставщика";
 99    public const string SectionSupplierCheck = "Проверка у поставщика";
 100    public const string SectionOrders = "Статус заказа";
 101    public const string SectionOther = "Прочее";
 102
 1103    private static readonly IReadOnlyList<TransactionReportLink> NoLinks = Array.Empty<TransactionReportLink>();
 104
 105    public static IEnumerable<TransactionReportRow> FromOzonShip(OrderToSupplierOzonShipResult? result)
 106    {
 2107        if (result is null) yield break;
 10108        foreach (var entry in result.Entries)
 109        {
 3110            var links = new List<TransactionReportLink> { TransactionReportLinks.OrderHistory(entry.OrderId) };
 3111            if (entry.IsSuccess)
 112            {
 1113                yield return new(TransactionReportLevel.Success, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 1114                    "Ozon", null, "Отправление отгружено в Ozon", entry.Message, null, null, links);
 115            }
 2116            else if (entry.IsSkipped)
 117            {
 1118                yield return new(TransactionReportLevel.Info, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 1119                    "Ozon", null, "Отгрузка в Ozon не потребовалась", entry.Message, null, null, links);
 120            }
 121            else
 122            {
 1123                var friendly = OzonLabelPrintErrorHelper.FormatShipmentError(entry.ShipmentNumber, entry.Message, entry.
 1124                var prefix = entry.ShipmentNumber + ": ";
 2125                if (friendly.StartsWith(prefix, StringComparison.Ordinal)) friendly = friendly[prefix.Length..];
 1126                var raw = OzonLabelPrintErrorHelper.TryGetOzonErrorCode(entry.Message, out _) || friendly != entry.Messa
 1127                yield return new(TransactionReportLevel.Warning, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 1128                    "Ozon", null, "Не удалось отгрузить отправление в Ozon", friendly,
 1129                    OzonShipRecommendation(entry.Message), raw, links);
 130            }
 131        }
 2132    }
 133
 134    private static string OzonShipRecommendation(string? raw)
 135    {
 1136        if (OzonLabelPrintErrorHelper.TryGetOzonErrorCode(raw, out var code))
 137        {
 1138            return code.ToUpperInvariant() switch
 1139            {
 1140                "HAS_INCORRECT_STATUS" =>
 1141                    "Если заказ уже отгружался раньше (например, перезаказ у другого поставщика), повторная отгрузка не 
 1142                    "Иначе проверьте статус отправления в личном кабинете Ozon.",
 0143                "HAS_INCORRECT_PRODUCT_QUANTITY" => "Сверьте количество в заказе с количеством в отправлении Ozon и повт
 0144                "POSTING_NOT_FOUND" or "POSTING_ALREADY_CANCELLED" => "Проверьте отправление в личном кабинете Ozon: оно
 0145                _ => "Проверьте отправление в личном кабинете Ozon и при необходимости отгрузите его вручную."
 1146            };
 147        }
 148
 0149        return "Проверьте отправление в личном кабинете Ozon и при необходимости отгрузите его вручную.";
 150    }
 151
 152    /// <summary>Строки проверки поставщиков. <paramref name="queueProviderByOrder"/> — поставщик из очереди для заказов
 153    public static IEnumerable<TransactionReportRow> FromVerification(
 154        SupplierVerificationDto? verification, IReadOnlyDictionary<int, string>? queueProviderByOrder = null)
 155    {
 2156        if (verification is null) yield break;
 8157        foreach (var row in verification.Rows)
 158        {
 2159            var providerKey = row.ProviderKey;
 2160            if (string.IsNullOrWhiteSpace(providerKey) && queueProviderByOrder?.TryGetValue(row.OrderId, out var fromQue
 1161                providerKey = fromQueue;
 162
 2163            var subject = string.IsNullOrWhiteSpace(providerKey) ? row.ProviderName : SupplierProviderNames.Describe(pro
 2164            var links = new List<TransactionReportLink>();
 4165            if (!string.IsNullOrWhiteSpace(providerKey)) links.Add(TransactionReportLinks.SupplierCart(providerKey));
 2166            links.Add(TransactionReportLinks.OrderHistory(row.OrderId));
 167
 2168            var level = row.Tone switch
 2169            {
 1170                "success" => TransactionReportLevel.Success,
 0171                "muted" => TransactionReportLevel.Info,
 0172                "warning" => TransactionReportLevel.Warning,
 1173                _ => TransactionReportLevel.Danger
 2174            };
 175
 2176            var details = new StringBuilder(row.Summary);
 3177            if (!string.IsNullOrWhiteSpace(row.SupplierOrderNumber)) details.Append($"\nНомер заказа у поставщика: {row.
 3178            if (!string.IsNullOrWhiteSpace(row.SupplierStatusName)) details.Append($"\nСтатус у поставщика: {row.Supplie
 4179            if (row.Checks.Count > 0) details.Append('\n').Append(string.Join('\n', row.Checks));
 180
 2181            yield return new(level, SectionSupplierCheck, row.OrderId, row.ShipmentNumber, subject,
 2182                string.IsNullOrWhiteSpace(providerKey) ? row.LogoUrl : OrderHistoryGridRows.SupplierProviderLogoUrl(prov
 2183                row.StatusTitle, details.ToString(), VerificationRecommendation(row.Status, subject), null, links);
 184        }
 2185    }
 186
 2187    private static string? VerificationRecommendation(string status, string supplier) => status switch
 2188    {
 2189        SupplierVerificationStatus.NotFound =>
 1190            $"Позиция не попала к поставщику «{supplier}». Откройте корзину поставщика и проверьте заказ; " +
 1191            "если позиции нет — оформите её вручную. Статус заказа в Studio2 уже изменён.",
 2192        SupplierVerificationStatus.Mismatch =>
 0193            $"Данные у поставщика «{supplier}» отличаются от Studio2 — сверьте заказ в кабинете поставщика.",
 2194        SupplierVerificationStatus.ApiError =>
 0195            $"Поставщик «{supplier}» не ответил или вернул ошибку API. Проверьте заказ в его кабинете вручную.",
 1196        _ => null
 2197    };
 198
 199    /// <summary>Строки сервисов оформления: по сообщению на строку.</summary>
 200    public static IEnumerable<TransactionReportRow> FromSupplierMessages(
 201        string providerKey, string supplierName, string section, string title, IEnumerable<string> messages)
 202    {
 0203        foreach (var message in messages.Where(m => !string.IsNullOrWhiteSpace(m)))
 204        {
 0205            var text = HtmlToText(message);
 0206            var warn = text.Contains("не проведён", StringComparison.OrdinalIgnoreCase)
 0207                       || text.Contains("не проведен", StringComparison.OrdinalIgnoreCase);
 0208            yield return new(warn ? TransactionReportLevel.Warning : TransactionReportLevel.Success, section, null, null
 0209                supplierName, OrderHistoryGridRows.SupplierProviderLogoUrl(providerKey), title, text, null, null,
 0210                new[] { TransactionReportLinks.SupplierCart(providerKey) });
 211        }
 0212    }
 213
 214    public static IEnumerable<TransactionReportRow> FromOrders(IEnumerable<OzonDomains.Models.Order> orders, string stat
 215    {
 26216        foreach (var order in orders.Where(o => o.Id > 0).Take(300))
 217        {
 0218            var status = order.AppStatus?.Name ?? statusName;
 0219            yield return new(TransactionReportLevel.Success, SectionOrders, order.Id, order.ShipmentNumber, null, null,
 0220                "Заказ проведён", $"Статус: {status}", null, null, new[] { TransactionReportLinks.OrderHistory(order.Id)
 221        }
 13222    }
 223
 224    public static IEnumerable<TransactionReportRow> FromWarnings(IEnumerable<string> warnings)
 225    {
 35226        foreach (var warning in warnings.Where(w => !string.IsNullOrWhiteSpace(w)
 17227                                                    && !w.StartsWith("Проверка поставщиков:", StringComparison.Ordinal))
 2228            yield return new(TransactionReportLevel.Warning, "Предупреждение", null, null, null, null,
 2229                "Проведено с предупреждением", warning, null, null, NoLinks);
 14230    }
 231
 232    // ---------- HTML-блоки шагов, у которых нет структурированного результата ----------
 233
 234    [GeneratedRegex(@"(?=<div[^>]*class=['""][^'""]*\balert\b)", RegexOptions.IgnoreCase)]
 235    private static partial Regex AlertSplitRegex();
 236
 237    [GeneratedRegex(@"<(?:strong|b)[^>]*>(.*?)</(?:strong|b)>|<div[^>]*fw-semibold[^>]*>(.*?)</div>", RegexOptions.Ignor
 238    private static partial Regex TitleRegex();
 239
 240    [GeneratedRegex(@"<a\s[^>]*href=['""]([^'""]+)['""][^>]*>(.*?)</a>", RegexOptions.IgnoreCase | RegexOptions.Singleli
 241    private static partial Regex AnchorRegex();
 242
 243    [GeneratedRegex(@"<(?:br\s*/?|/li|/p|/div|/tr|/h\d)>", RegexOptions.IgnoreCase)]
 244    private static partial Regex LineBreakRegex();
 245
 246    [GeneratedRegex(@"<li[^>]*>", RegexOptions.IgnoreCase)]
 247    private static partial Regex ListItemRegex();
 248
 249    [GeneratedRegex(@"<[^>]+>")]
 250    private static partial Regex TagRegex();
 251
 252    [GeneratedRegex(@"[ \t ]+")]
 253    private static partial Regex SpacesRegex();
 254
 255    [GeneratedRegex(@"\n{2,}")]
 256    private static partial Regex BlankLinesRegex();
 257
 258    public static string HtmlToText(string? html)
 259    {
 7260        if (string.IsNullOrWhiteSpace(html)) return string.Empty;
 7261        var text = ListItemRegex().Replace(html, "• ");
 7262        text = LineBreakRegex().Replace(text, "\n");
 7263        text = TagRegex().Replace(text, string.Empty);
 7264        text = WebUtility.HtmlDecode(text);
 7265        text = SpacesRegex().Replace(text, " ");
 21266        text = string.Join('\n', text.Split('\n').Select(x => x.Trim()));
 7267        return BlankLinesRegex().Replace(text, "\n").Trim();
 268    }
 269
 270    public static IEnumerable<TransactionReportRow> FromHtml(string? html, string section)
 271    {
 2272        if (string.IsNullOrWhiteSpace(html)) yield break;
 14273        foreach (var part in AlertSplitRegex().Split(html))
 274        {
 5275            if (string.IsNullOrWhiteSpace(part)) continue;
 3276            var text = HtmlToText(part);
 3277            if (text.Length == 0) continue;
 278
 3279            var level = Regex.IsMatch(part, @"alert-danger", RegexOptions.IgnoreCase) ? TransactionReportLevel.Danger
 3280                : Regex.IsMatch(part, @"alert-warning", RegexOptions.IgnoreCase) ? TransactionReportLevel.Warning
 3281                : Regex.IsMatch(part, @"alert-success", RegexOptions.IgnoreCase) ? TransactionReportLevel.Success
 3282                : TransactionReportLevel.Info;
 283
 3284            var titleMatch = TitleRegex().Match(part);
 3285            var title = titleMatch.Success ? HtmlToText(titleMatch.Groups[1].Success && titleMatch.Groups[1].Length > 0 
 3286            string? details = text;
 3287            if (title.Length > 0 && text.StartsWith(title, StringComparison.Ordinal))
 3288                details = text[title.Length..].Trim();
 3289            if (title.Length == 0)
 290            {
 0291                var lines = text.Split('\n', 2);
 0292                title = lines[0];
 0293                details = lines.Length > 1 ? lines[1].Trim() : null;
 294            }
 295
 3296            if (title.Length > 160)
 297            {
 0298                details = title + (string.IsNullOrEmpty(details) ? string.Empty : "\n" + details);
 0299                title = title[..157] + "…";
 300            }
 301
 3302            var links = AnchorRegex().Matches(part)
 1303                .Select(m => new TransactionReportLink(HtmlToText(m.Groups[2].Value), WebUtility.HtmlDecode(m.Groups[1].
 1304                .Where(l => l.Text.Length > 0 && (l.Url.StartsWith('/') || l.Url.StartsWith("http", StringComparison.Ord
 1305                .DistinctBy(l => l.Url)
 3306                .ToList();
 307
 3308            yield return new(level, section, null, null, null, null, title,
 3309                string.IsNullOrWhiteSpace(details) ? null : details, null, null, links);
 310        }
 2311    }
 312}