< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridProductCardService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridProductCardService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 123
Coverable lines: 123
Total lines: 273
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 46
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.ctor(...)100%210%
get_Article()100%210%
get_ProductName()100%210%
get_EtProducerId()100%210%
get_ManufacturerName()100%210%
get_EtProducerName()100%210%
get_EtProducerExistName()100%210%
get_BrandCandidates()100%210%
LoadOrderAsync()0%7280%
GetAsync()0%4260%
GetAnalogsAsync()0%156120%
MapAnalogs(...)0%620%
Fill(...)0%210140%

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
 033    public OrdersGridProductCardService(ApplicationDbContext context, IOrdersGridScopedQuery scoped, IPartsnavigatorServ
 34    {
 035        _context = context;
 036        _scoped = scoped;
 037        _partsnavigator = partsnavigator;
 038    }
 39
 040    private sealed record OrderInfo(
 041        string Article,
 042        string? ProductName,
 043        int? EtProducerId,
 044        string? ManufacturerName,
 045        string? EtProducerName,
 046        string? EtProducerExistName)
 47    {
 48        /// <summary>Сначала производитель заказа (на нём построен справочник), затем производитель из JCEtalon — назван
 049        public string?[] BrandCandidates => [ManufacturerName, EtProducerName, EtProducerExistName];
 50    }
 51
 52    private async Task<OrderInfo?> LoadOrderAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellation
 53    {
 054        var user = await _scoped.ResolveUserAsync(principal);
 055        if (user is null) return null;
 056        var accessible = await _scoped.FilterAccessibleAsync(user, [orderId], includeArchived: true, cancellationToken);
 057        if (accessible.Count == 0) return null;
 58
 059        var order = await _context.Orders.AsNoTracking()
 060            .Where(o => o.Id == orderId)
 061            .Select(o => new
 062            {
 063                o.Article,
 064                o.ProductName,
 065                o.EtProducerId,
 066                ManufacturerName = o.Manufacturer != null ? o.Manufacturer.Name : null,
 067                EtProducerName = o.EtProducer != null ? o.EtProducer.Name : null,
 068                EtProducerExistName = o.EtProducer != null ? o.EtProducer.ExistName : null
 069            })
 070            .FirstOrDefaultAsync(cancellationToken);
 071        return order is null
 072            ? null
 073            : new OrderInfo((order.Article ?? string.Empty).Trim(), order.ProductName, order.EtProducerId,
 074                order.ManufacturerName, order.EtProducerName, order.EtProducerExistName);
 075    }
 76
 77    public async Task<ProductCardDto?> GetAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellationTo
 78    {
 079        var order = await LoadOrderAsync(orderId, principal, cancellationToken);
 080        if (order is null) return null;
 81
 082        var dto = new ProductCardDto
 083        {
 084            Article = order.Article,
 085            Brand = order.ManufacturerName ?? order.EtProducerName,
 086            OrderProductName = order.ProductName,
 087            LegacyUrl = $"{LegacyCardUrl}?producerId={order.EtProducerId}&code={Uri.EscapeDataString(order.Article)}"
 088        };
 89
 090        if (order.Article.Length == 0)
 91        {
 092            dto.Status = ProductCardStatus.NotFound;
 093            dto.Message = "У заказа не указан артикул.";
 094            return dto;
 95        }
 96
 097        var result = await _partsnavigator.GetAsync(order.BrandCandidates, order.Article, withAnalogs: false, cancellati
 098        Fill(dto, result);
 099        return dto;
 0100    }
 101
 102    public async Task<ProductAnalogsDto?> GetAnalogsAsync(int orderId, string? brand, ClaimsPrincipal principal, Cancell
 103    {
 0104        var order = await LoadOrderAsync(orderId, principal, cancellationToken);
 0105        if (order is null) return null;
 106
 0107        var dto = new ProductAnalogsDto();
 0108        if (order.Article.Length == 0)
 109        {
 0110            dto.Status = ProductCardStatus.NotFound;
 0111            return dto;
 112        }
 113
 114        // Бренд карточки ускоряет запрос (не перебираем остальные), но принимаем его только из известных брендов заказа
 0115        var candidates = order.BrandCandidates;
 0116        var matched = candidates.FirstOrDefault(c => !string.IsNullOrWhiteSpace(brand)
 0117                                                     && string.Equals(c?.Trim(), brand!.Trim(), StringComparison.Ordinal
 0118        var result = await _partsnavigator.GetAsync(matched is null ? candidates : [matched], order.Article, withAnalogs
 119
 0120        switch (result.Status)
 121        {
 122            case PartsnavigatorStatus.Found:
 0123                var lookup = result.Lookup!;
 0124                dto.Status = ProductCardStatus.Found;
 0125                dto.Analogs = MapAnalogs(lookup.Analogs);
 0126                dto.AnalogsBlocked = lookup.AnalogsBlocked;
 0127                dto.Warnings = lookup.Warnings;
 0128                break;
 129            case PartsnavigatorStatus.NotFound:
 0130                dto.Status = ProductCardStatus.NotFound;
 0131                break;
 132            default:
 0133                dto.Status = ProductCardStatus.Error;
 0134                dto.Message = result.Message ?? "Не удалось получить аналоги.";
 0135                dto.Retry = result.Retry;
 136                break;
 137        }
 138
 0139        return dto;
 0140    }
 141
 142    private static List<ProductCardAnalogDto> MapAnalogs(IEnumerable<PartsnavigatorAnalog>? analogs) =>
 0143        (analogs ?? [])
 0144            .Select(a => new ProductCardAnalogDto
 0145            {
 0146                Brand = a.Brand,
 0147                Number = a.Number,
 0148                Description = a.Description,
 0149                Url = a.Url,
 0150                ImageUrl = a.ImageUrl,
 0151                Availability = a.Availability,
 0152                Delivery = a.Delivery,
 0153                Price = a.Price
 0154            })
 0155            .ToList();
 156
 157    private static void Fill(ProductCardDto dto, PartsnavigatorResult result)
 158    {
 0159        switch (result.Status)
 160        {
 161            case PartsnavigatorStatus.NotFound:
 0162                dto.Status = ProductCardStatus.NotFound;
 0163                dto.Message = "Карточка товара не найдена на partsnavigator.com.au.";
 0164                return;
 165            case PartsnavigatorStatus.Error:
 0166                dto.Status = ProductCardStatus.Error;
 0167                dto.Message = result.Message ?? "Не удалось получить карточку товара.";
 0168                dto.Retry = result.Retry;
 0169                return;
 170        }
 171
 0172        var lookup = result.Lookup!;
 0173        var part = lookup.Requested!;
 0174        dto.Status = ProductCardStatus.Found;
 0175        dto.LookupBrand = result.MatchedBrand;
 0176        dto.Brand = string.IsNullOrWhiteSpace(part.Brand) ? dto.Brand : part.Brand;
 0177        dto.Name = part.Name;
 0178        dto.SourceUrl = part.Url;
 0179        dto.Images = part.Images.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
 0180        dto.CharacteristicsSource = part.CharacteristicsSource;
 0181        dto.Rating = part.Rating;
 0182        part.Characteristics.TryGetValue("EAN", out var ean);
 0183        dto.Characteristics = part.Characteristics
 0184            .Where(x => !string.IsNullOrWhiteSpace(x.Value))
 0185            // «Extended description» на сайте часто дублирует EAN — шум в карточке.
 0186            .Where(x => !(x.Key == "Extended description" && x.Value == ean))
 0187            .Select(x => new ProductCardCharacteristicDto { Name = x.Key, Value = x.Value })
 0188            .ToList();
 0189        if (part.BrandInfo is { Found: true } info)
 190        {
 0191            dto.BrandInfo = new ProductCardBrandDto
 0192            {
 0193                Name = info.Name,
 0194                LogoUrl = info.LogoUrl,
 0195                Description = info.Description,
 0196                Website = info.Website,
 0197                Country = info.Country
 0198            };
 199        }
 0200    }
 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))]
 239    public ProductCardStatus Status { get; set; } = ProductCardStatus.NotFound;
 240    public string? Message { get; set; }
 241    public bool Retry { get; set; }
 242
 243    public List<ProductCardAnalogDto> Analogs { get; set; } = [];
 244    public bool AnalogsBlocked { get; set; }
 245    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}