< Summary

Information
Class: Services.ApiServices.ArmtekApi.ArmtekBrandResolver
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ArmtekApi/ArmtekBrandResolver.cs
Line coverage
65%
Covered lines: 57
Uncovered lines: 30
Coverable lines: 87
Total lines: 155
Line coverage: 65.5%
Branch coverage
54%
Covered branches: 13
Total branches: 24
Branch coverage: 54.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ResolveAsync()33.33%7666.66%
SelectApiBrand(...)100%22100%
.cctor()100%11100%
GetArmtekBrandsAsync()57.14%751432.35%
NotifyDailyQuotaExceededAsync()50%2284.61%
Normalize(...)100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ApiServices/ArmtekApi/ArmtekBrandResolver.cs

#LineLine coverage
 1using Microsoft.Extensions.Caching.Memory;
 2using OzonRepositories.Data;
 3using Services.ApiServices.ArmtekApi.Models.Envelope;
 4using Services.SignalRServices;
 5
 6namespace Services.ApiServices.ArmtekApi;
 7
 8/// <summary>
 9/// Переводит бренд из Studio2/ET в точное написание, которое принимает Armtek.
 10/// Справочник Armtek кэшируется отдельно для каждого аккаунта, поскольку доступный набор брендов
 11/// может различаться между RU и BY.
 12/// </summary>
 13public sealed class ArmtekBrandResolver
 14{
 15    private readonly ArmtekHttpClient _client;
 16    private readonly EtProducerRepository _etProducers;
 17    private readonly IMemoryCache _cache;
 18    private readonly string _providerKey;
 19
 220    public ArmtekBrandResolver(ArmtekHttpClient client, EtProducerRepository etProducers, IMemoryCache cache,
 221        string providerKey)
 22    {
 223        _client = client;
 224        _etProducers = etProducers;
 225        _cache = cache;
 226        _providerKey = providerKey;
 227    }
 28
 29    public async Task<string> ResolveAsync(string brand, CancellationToken ct = default)
 30    {
 331        var requestedBrand = brand.Trim();
 332        if (requestedBrand.Length == 0)
 033            return requestedBrand;
 34
 335        var brands = await GetArmtekBrandsAsync(ct);
 236        if (brands.Count == 0)
 237            return requestedBrand;
 38
 039        var aliases = await _etProducers.GetAliasesByPrefixAsync(requestedBrand);
 040        return SelectApiBrand(requestedBrand, brands, aliases) ?? requestedBrand;
 241    }
 42
 43    internal static string? SelectApiBrand(string requestedBrand, IEnumerable<string> availableBrands,
 44        IEnumerable<string> etAliases)
 45    {
 246        var available = availableBrands
 347            .Where(brand => !string.IsNullOrWhiteSpace(brand))
 348            .Select(brand => brand.Trim())
 249            .Distinct(StringComparer.OrdinalIgnoreCase)
 250            .ToArray();
 251        var requestedNormalized = Normalize(requestedBrand);
 52
 253        var exact = available.FirstOrDefault(brand =>
 554            string.Equals(Normalize(brand), requestedNormalized, StringComparison.Ordinal));
 255        if (exact is not null)
 156            return exact;
 57
 158        var aliases = etAliases
 159            .Append(requestedBrand)
 160            .Select(Normalize)
 461            .Where(alias => alias.Length > 0)
 162            .ToHashSet(StringComparer.Ordinal);
 63
 264        return available.FirstOrDefault(brand => aliases.Contains(Normalize(brand)));
 65    }
 66
 167    private static readonly TimeSpan BrandListCacheTtl = TimeSpan.FromHours(6);
 168    private static readonly TimeSpan BrandListErrorCacheTtl = TimeSpan.FromMinutes(30);
 69
 70    private async Task<IReadOnlyList<string>> GetArmtekBrandsAsync(CancellationToken ct)
 71    {
 372        var cacheKey = $"api-supplier:{_providerKey}:brand-list";
 373        if (_cache.TryGetValue(cacheKey, out IReadOnlyList<string>? cached) && cached is not null)
 174            return cached;
 75
 76        ArmtekResponse<List<string>> response;
 77        try
 78        {
 279            response = await _client.GetAsync<List<string>>("ws_user/getBrandList", ct: ct);
 180        }
 081        catch (ArmtekApiException ex)
 82        {
 083            _cache.Set(cacheKey, Array.Empty<string>(), BrandListErrorCacheTtl);
 84
 085            var isDailyLimit = ex.Message.Contains("Превышено количество запросов", StringComparison.OrdinalIgnoreCase);
 086            var errorKey = $"api-supplier:{_providerKey}:brand-list-error-notified";
 087            if (!_cache.TryGetValue(errorKey, out _))
 88            {
 089                _cache.Set(errorKey, true, TimeSpan.FromHours(1));
 090                var toastMessage = isDailyLimit
 091                    ? "Превышено суточное количество запросов к API Armtek для данного логина. Сопоставление брендов вре
 092                    : $"Не удалось получить справочник брендов Armtek: {ex.Message}. Используется исходное написание.";
 93
 94                try
 95                {
 096                    await NotificationService.NotifyAsync(
 097                        "Armtek",
 098                        "#D97706",
 099                        toastMessage,
 0100                        "fa-triangle-exclamation");
 0101                }
 0102                catch
 103                {
 104                    // Игнорируем отсутствие активного NotificationService (например, в консоли/тестах)
 0105                }
 106            }
 107
 0108            return Array.Empty<string>();
 109        }
 110
 1111        if (response.HasErrors || response.Resp is null)
 112        {
 1113            _cache.Set(cacheKey, Array.Empty<string>(), BrandListErrorCacheTtl);
 114
 1115            if (ArmtekDailyQuotaGuard.IsExceeded(response.Messages))
 1116                await NotifyDailyQuotaExceededAsync();
 117
 1118            return Array.Empty<string>();
 119        }
 120
 0121        var brands = response.Resp
 0122            .Where(brand => !string.IsNullOrWhiteSpace(brand))
 0123            .Select(brand => brand.Trim())
 0124            .Distinct(StringComparer.OrdinalIgnoreCase)
 0125            .ToArray();
 0126        _cache.Set(cacheKey, brands, BrandListCacheTtl);
 0127        return brands;
 2128    }
 129
 130    private async Task NotifyDailyQuotaExceededAsync()
 131    {
 1132        var errorKey = $"api-supplier:{_providerKey}:brand-list-error-notified";
 1133        if (_cache.TryGetValue(errorKey, out _))
 0134            return;
 135
 1136        _cache.Set(errorKey, true, TimeSpan.FromHours(1));
 137        try
 138        {
 1139            await NotificationService.NotifyAsync(
 1140                "Armtek",
 1141                "#D97706",
 1142                "Превышено суточное количество запросов к API Armtek для данного логина. Сопоставление брендов временно 
 1143                "fa-triangle-exclamation");
 0144        }
 1145        catch
 146        {
 147            // Игнорируем отсутствие активного NotificationService (например, в консоли/тестах)
 1148        }
 1149    }
 150
 10151    private static string Normalize(string value) => new(value
 10152        .Where(char.IsLetterOrDigit)
 10153        .Select(char.ToUpperInvariant)
 10154        .ToArray());
 155}