< Summary

Information
Class: OzonOrdersWeb.WebServices.Transactions.TransactionReportLink
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Transactions/TransactionReport.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 312
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_Text()100%11100%

File(s)

/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
 1427public 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
 103    private static readonly IReadOnlyList<TransactionReportLink> NoLinks = Array.Empty<TransactionReportLink>();
 104
 105    public static IEnumerable<TransactionReportRow> FromOzonShip(OrderToSupplierOzonShipResult? result)
 106    {
 107        if (result is null) yield break;
 108        foreach (var entry in result.Entries)
 109        {
 110            var links = new List<TransactionReportLink> { TransactionReportLinks.OrderHistory(entry.OrderId) };
 111            if (entry.IsSuccess)
 112            {
 113                yield return new(TransactionReportLevel.Success, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 114                    "Ozon", null, "Отправление отгружено в Ozon", entry.Message, null, null, links);
 115            }
 116            else if (entry.IsSkipped)
 117            {
 118                yield return new(TransactionReportLevel.Info, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 119                    "Ozon", null, "Отгрузка в Ozon не потребовалась", entry.Message, null, null, links);
 120            }
 121            else
 122            {
 123                var friendly = OzonLabelPrintErrorHelper.FormatShipmentError(entry.ShipmentNumber, entry.Message, entry.
 124                var prefix = entry.ShipmentNumber + ": ";
 125                if (friendly.StartsWith(prefix, StringComparison.Ordinal)) friendly = friendly[prefix.Length..];
 126                var raw = OzonLabelPrintErrorHelper.TryGetOzonErrorCode(entry.Message, out _) || friendly != entry.Messa
 127                yield return new(TransactionReportLevel.Warning, SectionOzonShip, entry.OrderId, entry.ShipmentNumber,
 128                    "Ozon", null, "Не удалось отгрузить отправление в Ozon", friendly,
 129                    OzonShipRecommendation(entry.Message), raw, links);
 130            }
 131        }
 132    }
 133
 134    private static string OzonShipRecommendation(string? raw)
 135    {
 136        if (OzonLabelPrintErrorHelper.TryGetOzonErrorCode(raw, out var code))
 137        {
 138            return code.ToUpperInvariant() switch
 139            {
 140                "HAS_INCORRECT_STATUS" =>
 141                    "Если заказ уже отгружался раньше (например, перезаказ у другого поставщика), повторная отгрузка не 
 142                    "Иначе проверьте статус отправления в личном кабинете Ozon.",
 143                "HAS_INCORRECT_PRODUCT_QUANTITY" => "Сверьте количество в заказе с количеством в отправлении Ozon и повт
 144                "POSTING_NOT_FOUND" or "POSTING_ALREADY_CANCELLED" => "Проверьте отправление в личном кабинете Ozon: оно
 145                _ => "Проверьте отправление в личном кабинете Ozon и при необходимости отгрузите его вручную."
 146            };
 147        }
 148
 149        return "Проверьте отправление в личном кабинете Ozon и при необходимости отгрузите его вручную.";
 150    }
 151
 152    /// <summary>Строки проверки поставщиков. <paramref name="queueProviderByOrder"/> — поставщик из очереди для заказов
 153    public static IEnumerable<TransactionReportRow> FromVerification(
 154        SupplierVerificationDto? verification, IReadOnlyDictionary<int, string>? queueProviderByOrder = null)
 155    {
 156        if (verification is null) yield break;
 157        foreach (var row in verification.Rows)
 158        {
 159            var providerKey = row.ProviderKey;
 160            if (string.IsNullOrWhiteSpace(providerKey) && queueProviderByOrder?.TryGetValue(row.OrderId, out var fromQue
 161                providerKey = fromQueue;
 162
 163            var subject = string.IsNullOrWhiteSpace(providerKey) ? row.ProviderName : SupplierProviderNames.Describe(pro
 164            var links = new List<TransactionReportLink>();
 165            if (!string.IsNullOrWhiteSpace(providerKey)) links.Add(TransactionReportLinks.SupplierCart(providerKey));
 166            links.Add(TransactionReportLinks.OrderHistory(row.OrderId));
 167
 168            var level = row.Tone switch
 169            {
 170                "success" => TransactionReportLevel.Success,
 171                "muted" => TransactionReportLevel.Info,
 172                "warning" => TransactionReportLevel.Warning,
 173                _ => TransactionReportLevel.Danger
 174            };
 175
 176            var details = new StringBuilder(row.Summary);
 177            if (!string.IsNullOrWhiteSpace(row.SupplierOrderNumber)) details.Append($"\nНомер заказа у поставщика: {row.
 178            if (!string.IsNullOrWhiteSpace(row.SupplierStatusName)) details.Append($"\nСтатус у поставщика: {row.Supplie
 179            if (row.Checks.Count > 0) details.Append('\n').Append(string.Join('\n', row.Checks));
 180
 181            yield return new(level, SectionSupplierCheck, row.OrderId, row.ShipmentNumber, subject,
 182                string.IsNullOrWhiteSpace(providerKey) ? row.LogoUrl : OrderHistoryGridRows.SupplierProviderLogoUrl(prov
 183                row.StatusTitle, details.ToString(), VerificationRecommendation(row.Status, subject), null, links);
 184        }
 185    }
 186
 187    private static string? VerificationRecommendation(string status, string supplier) => status switch
 188    {
 189        SupplierVerificationStatus.NotFound =>
 190            $"Позиция не попала к поставщику «{supplier}». Откройте корзину поставщика и проверьте заказ; " +
 191            "если позиции нет — оформите её вручную. Статус заказа в Studio2 уже изменён.",
 192        SupplierVerificationStatus.Mismatch =>
 193            $"Данные у поставщика «{supplier}» отличаются от Studio2 — сверьте заказ в кабинете поставщика.",
 194        SupplierVerificationStatus.ApiError =>
 195            $"Поставщик «{supplier}» не ответил или вернул ошибку API. Проверьте заказ в его кабинете вручную.",
 196        _ => null
 197    };
 198
 199    /// <summary>Строки сервисов оформления: по сообщению на строку.</summary>
 200    public static IEnumerable<TransactionReportRow> FromSupplierMessages(
 201        string providerKey, string supplierName, string section, string title, IEnumerable<string> messages)
 202    {
 203        foreach (var message in messages.Where(m => !string.IsNullOrWhiteSpace(m)))
 204        {
 205            var text = HtmlToText(message);
 206            var warn = text.Contains("не проведён", StringComparison.OrdinalIgnoreCase)
 207                       || text.Contains("не проведен", StringComparison.OrdinalIgnoreCase);
 208            yield return new(warn ? TransactionReportLevel.Warning : TransactionReportLevel.Success, section, null, null
 209                supplierName, OrderHistoryGridRows.SupplierProviderLogoUrl(providerKey), title, text, null, null,
 210                new[] { TransactionReportLinks.SupplierCart(providerKey) });
 211        }
 212    }
 213
 214    public static IEnumerable<TransactionReportRow> FromOrders(IEnumerable<OzonDomains.Models.Order> orders, string stat
 215    {
 216        foreach (var order in orders.Where(o => o.Id > 0).Take(300))
 217        {
 218            var status = order.AppStatus?.Name ?? statusName;
 219            yield return new(TransactionReportLevel.Success, SectionOrders, order.Id, order.ShipmentNumber, null, null,
 220                "Заказ проведён", $"Статус: {status}", null, null, new[] { TransactionReportLinks.OrderHistory(order.Id)
 221        }
 222    }
 223
 224    public static IEnumerable<TransactionReportRow> FromWarnings(IEnumerable<string> warnings)
 225    {
 226        foreach (var warning in warnings.Where(w => !string.IsNullOrWhiteSpace(w)
 227                                                    && !w.StartsWith("Проверка поставщиков:", StringComparison.Ordinal))
 228            yield return new(TransactionReportLevel.Warning, "Предупреждение", null, null, null, null,
 229                "Проведено с предупреждением", warning, null, null, NoLinks);
 230    }
 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    {
 260        if (string.IsNullOrWhiteSpace(html)) return string.Empty;
 261        var text = ListItemRegex().Replace(html, "• ");
 262        text = LineBreakRegex().Replace(text, "\n");
 263        text = TagRegex().Replace(text, string.Empty);
 264        text = WebUtility.HtmlDecode(text);
 265        text = SpacesRegex().Replace(text, " ");
 266        text = string.Join('\n', text.Split('\n').Select(x => x.Trim()));
 267        return BlankLinesRegex().Replace(text, "\n").Trim();
 268    }
 269
 270    public static IEnumerable<TransactionReportRow> FromHtml(string? html, string section)
 271    {
 272        if (string.IsNullOrWhiteSpace(html)) yield break;
 273        foreach (var part in AlertSplitRegex().Split(html))
 274        {
 275            if (string.IsNullOrWhiteSpace(part)) continue;
 276            var text = HtmlToText(part);
 277            if (text.Length == 0) continue;
 278
 279            var level = Regex.IsMatch(part, @"alert-danger", RegexOptions.IgnoreCase) ? TransactionReportLevel.Danger
 280                : Regex.IsMatch(part, @"alert-warning", RegexOptions.IgnoreCase) ? TransactionReportLevel.Warning
 281                : Regex.IsMatch(part, @"alert-success", RegexOptions.IgnoreCase) ? TransactionReportLevel.Success
 282                : TransactionReportLevel.Info;
 283
 284            var titleMatch = TitleRegex().Match(part);
 285            var title = titleMatch.Success ? HtmlToText(titleMatch.Groups[1].Success && titleMatch.Groups[1].Length > 0 
 286            string? details = text;
 287            if (title.Length > 0 && text.StartsWith(title, StringComparison.Ordinal))
 288                details = text[title.Length..].Trim();
 289            if (title.Length == 0)
 290            {
 291                var lines = text.Split('\n', 2);
 292                title = lines[0];
 293                details = lines.Length > 1 ? lines[1].Trim() : null;
 294            }
 295
 296            if (title.Length > 160)
 297            {
 298                details = title + (string.IsNullOrEmpty(details) ? string.Empty : "\n" + details);
 299                title = title[..157] + "…";
 300            }
 301
 302            var links = AnchorRegex().Matches(part)
 303                .Select(m => new TransactionReportLink(HtmlToText(m.Groups[2].Value), WebUtility.HtmlDecode(m.Groups[1].
 304                .Where(l => l.Text.Length > 0 && (l.Url.StartsWith('/') || l.Url.StartsWith("http", StringComparison.Ord
 305                .DistinctBy(l => l.Url)
 306                .ToList();
 307
 308            yield return new(level, section, null, null, null, null, title,
 309                string.IsNullOrWhiteSpace(details) ? null : details, null, null, links);
 310        }
 311    }
 312}

Methods/Properties

get_Text()