< Summary

Information
Class: Services.FeedPriceServices.FeedExplanationService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FeedPriceServices/FeedExplanationService.cs
Line coverage
93%
Covered lines: 30
Uncovered lines: 2
Coverable lines: 32
Total lines: 127
Line coverage: 93.7%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GetAsync()100%88100%
GetOneAsync()100%8883.33%

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
 18    public FidApiFeedExplanationReader(FidApiDataManager fidApi) => _fidApi = fidApi;
 19
 20    public async Task<FeedExplanationResult> GetAsync(string articleKey, long? runId, CancellationToken ct = default)
 21    {
 22        var response = await _fidApi.GetPriceExplanationAsync(articleKey, runId, ct)
 23            ?? throw new InvalidOperationException("Фид-сервис не вернул расшифровку цены.");
 24
 25        return new FeedExplanationResult
 26        {
 27            Key = response.Key,
 28            Found = response.Found,
 29            Explanations = response.Explanations.Select(Map).ToArray()
 30        };
 31    }
 32
 33    private static FeedExplanation Map(FidPriceExplanation x) => new()
 34    {
 35        Cabinet = x.CabinetKey,
 36        RunId = x.RunId,
 37        AssembledAt = AsUtc(x.AssembledAt),
 38        AssembledBy = x.AssembledBy ?? string.Empty,
 39        HasFullPlan = x.HasFullPlan,
 40        Found = x.Found,
 41        Outcome = x.Outcome,
 42        Decision = x.Decision,
 43        Reason = x.Reason,
 44        FinalPrice = x.FinalPrice,
 45        OldPrice = x.OldPrice,
 46        MinPrice = x.MinPrice,
 47        PriceWarehouse = x.PriceWarehouse,
 48        PriceSupplier = x.PriceSupplier,
 49        PurchasePrice = x.PurchasePrice,
 50        Multiplicity = x.Multiplicity,
 51        ZeroedWarehouses = x.ZeroedWarehouses,
 52        Formula = x.Formula,
 53        Ozon = x.Ozon,
 54        Correction = x.Correction
 55    };
 56
 57    private static DateTimeOffset AsUtc(DateTime value) =>
 58        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_";
 171    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
 577    public FeedExplanationService(IFeedExplanationReader reader, IMemoryCache cache, ILogger<FeedExplanationService> log
 78    {
 579        _reader = reader;
 580        _cache = cache;
 581        _logger = logger;
 582    }
 83
 84    public async Task<FeedExplanationResult> GetAsync(IReadOnlyCollection<string> articleKeys, long? runId, Cancellation
 85    {
 886        var keys = articleKeys
 1087            .Where(x => !string.IsNullOrWhiteSpace(x))
 988            .Select(x => x.Trim())
 889            .Distinct(StringComparer.OrdinalIgnoreCase)
 890            .Take(2)
 891            .ToArray();
 892        if (keys.Length == 0)
 193            return new FeedExplanationResult { Error = "Не удалось собрать ключ артикула: у заказа нет артикула или преф
 94
 795        FeedExplanationResult? last = null;
 2496        foreach (var key in keys)
 97        {
 898            last = await GetOneAsync(key, runId, ct);
 1499            if (last.Error is not null || last.Found) return last;
 100        }
 101
 1102        return last!;
 8103    }
 104
 105    private async Task<FeedExplanationResult> GetOneAsync(string key, long? runId, CancellationToken ct)
 106    {
 8107        var cacheKey = $"{CacheKeyPrefix}{key.ToUpperInvariant()}|{runId?.ToString() ?? "last"}";
 8108        if (_cache.TryGetValue<FeedExplanationResult>(cacheKey, out var cached) && cached is not null)
 1109            return cached;
 110
 111        try
 112        {
 7113            var result = await _reader.GetAsync(key, runId, ct);
 6114            _cache.Set(cacheKey, result, CacheTtl);
 6115            return result;
 116        }
 0117        catch (OperationCanceledException) when (ct.IsCancellationRequested)
 118        {
 0119            throw;
 120        }
 1121        catch (Exception ex)
 122        {
 1123            _logger.LogWarning(ex, "Фид: не удалось прочитать расшифровку цены артикула {Key}", key);
 1124            return new FeedExplanationResult { Key = key, Error = "Фид-сервис недоступен. Повторите позже." };
 125        }
 8126    }
 127}