< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.ProductAnalogsDto
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridProductCardService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 6
Coverable lines: 6
Total lines: 273
Line coverage: 0%
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_Status()100%210%
get_Message()100%210%
get_Retry()100%210%
get_Analogs()100%210%
get_AnalogsBlocked()100%210%
get_Warnings()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridProductCardService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonRepositories.Context;
 4using Services.ApiServices.PartsnavigatorApi;
 5
 6namespace OzonOrdersWeb.WebServices.Orders.Grid;
 7
 8public interface IOrdersGridProductCardService
 9{
 10    /// <summary>Быстрая карточка товара заказа (без аналогов); null — заказ не найден или недоступен пользователю.</sum
 11    Task<ProductCardDto?> GetAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellationToken);
 12
 13    /// <summary>
 14    /// Аналоги артикула заказа (медленная страница сайта, грузится отдельно). <paramref name="brand"/> — бренд, под кот
 15    /// карточка; принимается только если совпадает с брендом заказа. null — заказ не найден или недоступен.
 16    /// </summary>
 17    Task<ProductAnalogsDto?> GetAnalogsAsync(int orderId, string? brand, ClaimsPrincipal principal, CancellationToken ca
 18}
 19
 20/// <summary>
 21/// Модалка «Карточка товара» (клик по названию в OrdersGrid и TransactionsGrid): проверяет доступ к заказу тем же
 22/// RLS-запросом, что и список, берёт бренд и артикул заказа и запрашивает карточку у парсера Partsnavigator.
 23/// Карточка и аналоги — два вызова: аналоги у сайта грузятся до 30+ с и не должны задерживать остальное.
 24/// </summary>
 25public sealed class OrdersGridProductCardService : IOrdersGridProductCardService
 26{
 27    private const string LegacyCardUrl = "http://studio-web-client.interparts.ru/info";
 28
 29    private readonly ApplicationDbContext _context;
 30    private readonly IOrdersGridScopedQuery _scoped;
 31    private readonly IPartsnavigatorService _partsnavigator;
 32
 33    public OrdersGridProductCardService(ApplicationDbContext context, IOrdersGridScopedQuery scoped, IPartsnavigatorServ
 34    {
 35        _context = context;
 36        _scoped = scoped;
 37        _partsnavigator = partsnavigator;
 38    }
 39
 40    private sealed record OrderInfo(
 41        string Article,
 42        string? ProductName,
 43        int? EtProducerId,
 44        string? ManufacturerName,
 45        string? EtProducerName,
 46        string? EtProducerExistName)
 47    {
 48        /// <summary>Сначала производитель заказа (на нём построен справочник), затем производитель из JCEtalon — назван
 49        public string?[] BrandCandidates => [ManufacturerName, EtProducerName, EtProducerExistName];
 50    }
 51
 52    private async Task<OrderInfo?> LoadOrderAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellation
 53    {
 54        var user = await _scoped.ResolveUserAsync(principal);
 55        if (user is null) return null;
 56        var accessible = await _scoped.FilterAccessibleAsync(user, [orderId], includeArchived: true, cancellationToken);
 57        if (accessible.Count == 0) return null;
 58
 59        var order = await _context.Orders.AsNoTracking()
 60            .Where(o => o.Id == orderId)
 61            .Select(o => new
 62            {
 63                o.Article,
 64                o.ProductName,
 65                o.EtProducerId,
 66                ManufacturerName = o.Manufacturer != null ? o.Manufacturer.Name : null,
 67                EtProducerName = o.EtProducer != null ? o.EtProducer.Name : null,
 68                EtProducerExistName = o.EtProducer != null ? o.EtProducer.ExistName : null
 69            })
 70            .FirstOrDefaultAsync(cancellationToken);
 71        return order is null
 72            ? null
 73            : new OrderInfo((order.Article ?? string.Empty).Trim(), order.ProductName, order.EtProducerId,
 74                order.ManufacturerName, order.EtProducerName, order.EtProducerExistName);
 75    }
 76
 77    public async Task<ProductCardDto?> GetAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellationTo
 78    {
 79        var order = await LoadOrderAsync(orderId, principal, cancellationToken);
 80        if (order is null) return null;
 81
 82        var dto = new ProductCardDto
 83        {
 84            Article = order.Article,
 85            Brand = order.ManufacturerName ?? order.EtProducerName,
 86            OrderProductName = order.ProductName,
 87            LegacyUrl = $"{LegacyCardUrl}?producerId={order.EtProducerId}&code={Uri.EscapeDataString(order.Article)}"
 88        };
 89
 90        if (order.Article.Length == 0)
 91        {
 92            dto.Status = ProductCardStatus.NotFound;
 93            dto.Message = "У заказа не указан артикул.";
 94            return dto;
 95        }
 96
 97        var result = await _partsnavigator.GetAsync(order.BrandCandidates, order.Article, withAnalogs: false, cancellati
 98        Fill(dto, result);
 99        return dto;
 100    }
 101
 102    public async Task<ProductAnalogsDto?> GetAnalogsAsync(int orderId, string? brand, ClaimsPrincipal principal, Cancell
 103    {
 104        var order = await LoadOrderAsync(orderId, principal, cancellationToken);
 105        if (order is null) return null;
 106
 107        var dto = new ProductAnalogsDto();
 108        if (order.Article.Length == 0)
 109        {
 110            dto.Status = ProductCardStatus.NotFound;
 111            return dto;
 112        }
 113
 114        // Бренд карточки ускоряет запрос (не перебираем остальные), но принимаем его только из известных брендов заказа
 115        var candidates = order.BrandCandidates;
 116        var matched = candidates.FirstOrDefault(c => !string.IsNullOrWhiteSpace(brand)
 117                                                     && string.Equals(c?.Trim(), brand!.Trim(), StringComparison.Ordinal
 118        var result = await _partsnavigator.GetAsync(matched is null ? candidates : [matched], order.Article, withAnalogs
 119
 120        switch (result.Status)
 121        {
 122            case PartsnavigatorStatus.Found:
 123                var lookup = result.Lookup!;
 124                dto.Status = ProductCardStatus.Found;
 125                dto.Analogs = MapAnalogs(lookup.Analogs);
 126                dto.AnalogsBlocked = lookup.AnalogsBlocked;
 127                dto.Warnings = lookup.Warnings;
 128                break;
 129            case PartsnavigatorStatus.NotFound:
 130                dto.Status = ProductCardStatus.NotFound;
 131                break;
 132            default:
 133                dto.Status = ProductCardStatus.Error;
 134                dto.Message = result.Message ?? "Не удалось получить аналоги.";
 135                dto.Retry = result.Retry;
 136                break;
 137        }
 138
 139        return dto;
 140    }
 141
 142    private static List<ProductCardAnalogDto> MapAnalogs(IEnumerable<PartsnavigatorAnalog>? analogs) =>
 143        (analogs ?? [])
 144            .Select(a => new ProductCardAnalogDto
 145            {
 146                Brand = a.Brand,
 147                Number = a.Number,
 148                Description = a.Description,
 149                Url = a.Url,
 150                ImageUrl = a.ImageUrl,
 151                Availability = a.Availability,
 152                Delivery = a.Delivery,
 153                Price = a.Price
 154            })
 155            .ToList();
 156
 157    private static void Fill(ProductCardDto dto, PartsnavigatorResult result)
 158    {
 159        switch (result.Status)
 160        {
 161            case PartsnavigatorStatus.NotFound:
 162                dto.Status = ProductCardStatus.NotFound;
 163                dto.Message = "Карточка товара не найдена на partsnavigator.com.au.";
 164                return;
 165            case PartsnavigatorStatus.Error:
 166                dto.Status = ProductCardStatus.Error;
 167                dto.Message = result.Message ?? "Не удалось получить карточку товара.";
 168                dto.Retry = result.Retry;
 169                return;
 170        }
 171
 172        var lookup = result.Lookup!;
 173        var part = lookup.Requested!;
 174        dto.Status = ProductCardStatus.Found;
 175        dto.LookupBrand = result.MatchedBrand;
 176        dto.Brand = string.IsNullOrWhiteSpace(part.Brand) ? dto.Brand : part.Brand;
 177        dto.Name = part.Name;
 178        dto.SourceUrl = part.Url;
 179        dto.Images = part.Images.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
 180        dto.CharacteristicsSource = part.CharacteristicsSource;
 181        dto.Rating = part.Rating;
 182        part.Characteristics.TryGetValue("EAN", out var ean);
 183        dto.Characteristics = part.Characteristics
 184            .Where(x => !string.IsNullOrWhiteSpace(x.Value))
 185            // «Extended description» на сайте часто дублирует EAN — шум в карточке.
 186            .Where(x => !(x.Key == "Extended description" && x.Value == ean))
 187            .Select(x => new ProductCardCharacteristicDto { Name = x.Key, Value = x.Value })
 188            .ToList();
 189        if (part.BrandInfo is { Found: true } info)
 190        {
 191            dto.BrandInfo = new ProductCardBrandDto
 192            {
 193                Name = info.Name,
 194                LogoUrl = info.LogoUrl,
 195                Description = info.Description,
 196                Website = info.Website,
 197                Country = info.Country
 198            };
 199        }
 200    }
 201}
 202
 203public enum ProductCardStatus
 204{
 205    Found,
 206    NotFound,
 207    Error
 208}
 209
 210/// <summary>Ответ <c>GET /Studio2/OrdersGrid/GetProductCard</c>; в JSON — camelCase, статус строкой. Аналогов здесь нет
 211public sealed class ProductCardDto
 212{
 213    [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
 214    public ProductCardStatus Status { get; set; } = ProductCardStatus.NotFound;
 215    public string? Message { get; set; }
 216    public bool Retry { get; set; }
 217
 218    public string Article { get; set; } = string.Empty;
 219    public string? Brand { get; set; }
 220    /// <summary>Бренд, под которым карточка нашлась в Partsnavigator; его же передаём в запрос аналогов.</summary>
 221    public string? LookupBrand { get; set; }
 222    /// <summary>Название товара из самого заказа (запасной заголовок, если на сайте нет названия).</summary>
 223    public string? OrderProductName { get; set; }
 224    public string? Name { get; set; }
 225    public string? SourceUrl { get; set; }
 226    public string LegacyUrl { get; set; } = string.Empty;
 227
 228    public List<string> Images { get; set; } = [];
 229    public string? CharacteristicsSource { get; set; }
 230    public string? Rating { get; set; }
 231    public List<ProductCardCharacteristicDto> Characteristics { get; set; } = [];
 232    public ProductCardBrandDto? BrandInfo { get; set; }
 233}
 234
 235/// <summary>Ответ <c>GET /Studio2/OrdersGrid/GetProductAnalogs</c>.</summary>
 236public sealed class ProductAnalogsDto
 237{
 238    [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
 0239    public ProductCardStatus Status { get; set; } = ProductCardStatus.NotFound;
 0240    public string? Message { get; set; }
 0241    public bool Retry { get; set; }
 242
 0243    public List<ProductCardAnalogDto> Analogs { get; set; } = [];
 0244    public bool AnalogsBlocked { get; set; }
 0245    public List<string> Warnings { get; set; } = [];
 246}
 247
 248public sealed class ProductCardCharacteristicDto
 249{
 250    public string Name { get; set; } = string.Empty;
 251    public string Value { get; set; } = string.Empty;
 252}
 253
 254public sealed class ProductCardBrandDto
 255{
 256    public string Name { get; set; } = string.Empty;
 257    public string? LogoUrl { get; set; }
 258    public string? Description { get; set; }
 259    public string? Website { get; set; }
 260    public string? Country { get; set; }
 261}
 262
 263public sealed class ProductCardAnalogDto
 264{
 265    public string Brand { get; set; } = string.Empty;
 266    public string Number { get; set; } = string.Empty;
 267    public string Description { get; set; } = string.Empty;
 268    public string Url { get; set; } = string.Empty;
 269    public string? ImageUrl { get; set; }
 270    public string? Availability { get; set; }
 271    public string? Delivery { get; set; }
 272    public string? Price { get; set; }
 273}