< Summary

Information
Class: Services.FeedPriceServices.FidApiFeedExplanationReader
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FeedPriceServices/FeedExplanationService.cs
Line coverage
100%
Covered lines: 34
Uncovered lines: 0
Coverable lines: 34
Total lines: 127
Line coverage: 100%
Branch coverage
50%
Covered branches: 3
Total branches: 6
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetAsync()50%22100%
Map(...)50%22100%
AsUtc(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FeedPriceServices/FeedExplanationService.cs

#LineLine coverage
 1using Microsoft.Extensions.Caching.Memory;
 2using Microsoft.Extensions.Logging;
 3using Services.ApiServices.FidApi;
 4using Services.ApiServices.FidApi.Models;
 5
 6namespace Services.FeedPriceServices;
 7
 8public interface IFeedExplanationReader
 9{
 10    Task<FeedExplanationResult> GetAsync(string articleKey, long? runId, CancellationToken ct = default);
 11}
 12
 13/// <summary>Читает расшифровку цены через feed API (<c>/api/feed/articles/price-explanation</c>).</summary>
 14public sealed class FidApiFeedExplanationReader : IFeedExplanationReader
 15{
 16    private readonly FidApiDataManager _fidApi;
 17
 1018    public FidApiFeedExplanationReader(FidApiDataManager fidApi) => _fidApi = fidApi;
 19
 20    public async Task<FeedExplanationResult> GetAsync(string articleKey, long? runId, CancellationToken ct = default)
 21    {
 522        var response = await _fidApi.GetPriceExplanationAsync(articleKey, runId, ct)
 523            ?? throw new InvalidOperationException("Фид-сервис не вернул расшифровку цены.");
 24
 425        return new FeedExplanationResult
 426        {
 427            Key = response.Key,
 428            Found = response.Found,
 429            Explanations = response.Explanations.Select(Map).ToArray()
 430        };
 431    }
 32
 833    private static FeedExplanation Map(FidPriceExplanation x) => new()
 834    {
 835        Cabinet = x.CabinetKey,
 836        RunId = x.RunId,
 837        AssembledAt = AsUtc(x.AssembledAt),
 838        AssembledBy = x.AssembledBy ?? string.Empty,
 839        HasFullPlan = x.HasFullPlan,
 840        Found = x.Found,
 841        Outcome = x.Outcome,
 842        Decision = x.Decision,
 843        Reason = x.Reason,
 844        FinalPrice = x.FinalPrice,
 845        OldPrice = x.OldPrice,
 846        MinPrice = x.MinPrice,
 847        PriceWarehouse = x.PriceWarehouse,
 848        PriceSupplier = x.PriceSupplier,
 849        PurchasePrice = x.PurchasePrice,
 850        Multiplicity = x.Multiplicity,
 851        ZeroedWarehouses = x.ZeroedWarehouses,
 852        Formula = x.Formula,
 853        Ozon = x.Ozon,
 854        Correction = x.Correction
 855    };
 56
 57    private static DateTimeOffset AsUtc(DateTime value) =>
 858        new(value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(value, DateTimeKind.Utc) : value.ToUniversalTi
 59}
 60
 61public interface IFeedExplanationService
 62{
 63    /// <summary>Расшифровка по первому ключу, у которого план цен что-то нашёл (ключей до двух).</summary>
 64    Task<FeedExplanationResult> GetAsync(IReadOnlyCollection<string> articleKeys, long? runId, CancellationToken ct = de
 65}
 66
 67/// <summary>Расшифровка цены для окна заказа: кеш 90 с, недоступность feed API возвращается как <c>Error</c> и не кешир
 68public sealed class FeedExplanationService : IFeedExplanationService
 69{
 70    private const string CacheKeyPrefix = "feed_explanation_v1_";
 71    private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(90);
 72
 73    private readonly IFeedExplanationReader _reader;
 74    private readonly IMemoryCache _cache;
 75    private readonly ILogger<FeedExplanationService> _logger;
 76
 77    public FeedExplanationService(IFeedExplanationReader reader, IMemoryCache cache, ILogger<FeedExplanationService> log
 78    {
 79        _reader = reader;
 80        _cache = cache;
 81        _logger = logger;
 82    }
 83
 84    public async Task<FeedExplanationResult> GetAsync(IReadOnlyCollection<string> articleKeys, long? runId, Cancellation
 85    {
 86        var keys = articleKeys
 87            .Where(x => !string.IsNullOrWhiteSpace(x))
 88            .Select(x => x.Trim())
 89            .Distinct(StringComparer.OrdinalIgnoreCase)
 90            .Take(2)
 91            .ToArray();
 92        if (keys.Length == 0)
 93            return new FeedExplanationResult { Error = "Не удалось собрать ключ артикула: у заказа нет артикула или преф
 94
 95        FeedExplanationResult? last = null;
 96        foreach (var key in keys)
 97        {
 98            last = await GetOneAsync(key, runId, ct);
 99            if (last.Error is not null || last.Found) return last;
 100        }
 101
 102        return last!;
 103    }
 104
 105    private async Task<FeedExplanationResult> GetOneAsync(string key, long? runId, CancellationToken ct)
 106    {
 107        var cacheKey = $"{CacheKeyPrefix}{key.ToUpperInvariant()}|{runId?.ToString() ?? "last"}";
 108        if (_cache.TryGetValue<FeedExplanationResult>(cacheKey, out var cached) && cached is not null)
 109            return cached;
 110
 111        try
 112        {
 113            var result = await _reader.GetAsync(key, runId, ct);
 114            _cache.Set(cacheKey, result, CacheTtl);
 115            return result;
 116        }
 117        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 118        {
 119            throw;
 120        }
 121        catch (Exception ex)
 122        {
 123            _logger.LogWarning(ex, "Фид: не удалось прочитать расшифровку цены артикула {Key}", key);
 124            return new FeedExplanationResult { Key = key, Error = "Фид-сервис недоступен. Повторите позже." };
 125        }
 126    }
 127}