< Summary

Information
Class: Services.SupplierStatusServices.SupplierStatusResolver
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/SupplierStatusResolver.cs
Line coverage
87%
Covered lines: 49
Uncovered lines: 7
Coverable lines: 56
Total lines: 121
Line coverage: 87.5%
Branch coverage
70%
Covered branches: 14
Total branches: 20
Branch coverage: 70%
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()64.28%151483.33%
InvalidateCache()100%210%
FindAsync()100%44100%
ResolveSupplierIdAsync()100%11100%
BuildCacheKey(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/SupplierStatusServices/SupplierStatusResolver.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Logging;
 3using OzonDomains.Models;
 4using OzonDomains.Models.SupplierStatuses;
 5using OzonRepositories.Context;
 6
 7namespace Services.SupplierStatusServices;
 8
 9public interface ISupplierStatusResolver
 10{
 11    /// <summary>
 12    /// Находит статус в справочнике по коду поставщика (или по имени, если кода нет)
 13    /// и создаёт его при первой встрече. Новый статус создаётся без цвета и без IsFinal —
 14    /// их задаёт админ на странице справочника.
 15    /// </summary>
 16    Task<SupplierStatus> ResolveAsync(
 17        string providerKey, string statusKind, string? externalCode, string name, CancellationToken ct = default);
 18
 19    /// <summary>Сбрасывает кэш справочника — после ручного редактирования статусов.</summary>
 20    void InvalidateCache();
 21}
 22
 23public sealed class SupplierStatusResolver : ISupplierStatusResolver
 24{
 25    private readonly ApplicationDbContext _db;
 26    private readonly ILogger<SupplierStatusResolver> _logger;
 27
 28    /// <summary>
 29    /// Кэш живёт в пределах одного scope (сервис scoped вместе с DbContext). Он нужен не как
 30    /// долговременный кэш, а чтобы один прогон джобы с сотнями строк не бил в БД на каждую.
 31    /// </summary>
 3932    private readonly Dictionary<string, SupplierStatus> _cache = new(StringComparer.Ordinal);
 33
 3934    public SupplierStatusResolver(ApplicationDbContext db, ILogger<SupplierStatusResolver> logger)
 35    {
 3936        _db = db;
 3937        _logger = logger;
 3938    }
 39
 40    public async Task<SupplierStatus> ResolveAsync(
 41        string providerKey, string statusKind, string? externalCode, string name, CancellationToken ct = default)
 42    {
 4643        var displayName = string.IsNullOrWhiteSpace(name)
 4644            ? (string.IsNullOrWhiteSpace(externalCode) ? "Без статуса" : $"Код {externalCode}")
 4645            : name.Trim();
 4646        if (displayName.Length > 200) displayName = displayName[..200];
 47
 4648        var cacheKey = BuildCacheKey(providerKey, statusKind, externalCode, displayName);
 4649        if (_cache.TryGetValue(cacheKey, out var cached))
 750            return cached;
 51
 3952        var existing = await FindAsync(providerKey, statusKind, externalCode, displayName, ct);
 3953        if (existing is not null)
 54        {
 155            _cache[cacheKey] = existing;
 156            return existing;
 57        }
 58
 3859        var created = new SupplierStatus
 3860        {
 3861            ProviderKey = providerKey,
 3862            StatusKind = statusKind,
 3863            ExternalCode = string.IsNullOrWhiteSpace(externalCode) ? null : externalCode.Trim(),
 3864            Name = displayName,
 3865            SupplierId = await ResolveSupplierIdAsync(providerKey, ct),
 3866            IsFinal = false,
 3867            CreatedAt = DateTime.UtcNow
 3868        };
 69
 3870        _db.SupplierStatuses.Add(created);
 71        try
 72        {
 3873            await _db.SaveChangesAsync(ct);
 3874            _logger.LogInformation(
 3875                "Создан статус поставщика '{Name}' ({ProviderKey}/{Kind}, код '{Code}') — задайте цвет в справочнике",
 3876                created.Name, providerKey, statusKind, created.ExternalCode);
 3877        }
 078        catch (DbUpdateException)
 79        {
 80            // Гонка с параллельным прогоном: уникальный индекс (ProviderKey, StatusKind, Name)
 81            // отверг вставку — статус уже создан кем-то другим, перечитываем его.
 082            _db.Entry(created).State = EntityState.Detached;
 083            existing = await FindAsync(providerKey, statusKind, externalCode, displayName, ct);
 084            if (existing is null) throw;
 085            _cache[cacheKey] = existing;
 086            return existing;
 87        }
 88
 3889        _cache[cacheKey] = created;
 3890        return created;
 4691    }
 92
 093    public void InvalidateCache() => _cache.Clear();
 94
 95    private async Task<SupplierStatus?> FindAsync(
 96        string providerKey, string statusKind, string? externalCode, string displayName, CancellationToken ct)
 97    {
 98        // Код поставщика надёжнее названия: у АВД и ML-Auto название статуса может измениться,
 99        // а код остаётся. Поэтому сначала ищем по коду и только затем по имени.
 39100        if (!string.IsNullOrWhiteSpace(externalCode))
 101        {
 39102            var byCode = await _db.SupplierStatuses.FirstOrDefaultAsync(
 39103                x => x.ProviderKey == providerKey && x.StatusKind == statusKind && x.ExternalCode == externalCode, ct);
 40104            if (byCode is not null) return byCode;
 105        }
 106
 38107        return await _db.SupplierStatuses.FirstOrDefaultAsync(
 38108            x => x.ProviderKey == providerKey && x.StatusKind == statusKind && x.Name == displayName, ct);
 39109    }
 110
 111    private async Task<int?> ResolveSupplierIdAsync(string providerKey, CancellationToken ct) =>
 38112        await _db.ApiSupplierSettings
 38113            .Where(x => x.ProviderKey == providerKey)
 38114            .Select(x => x.SupplierId)
 38115            .FirstOrDefaultAsync(ct);
 116
 117    private static string BuildCacheKey(string providerKey, string statusKind, string? externalCode, string name) =>
 46118        string.IsNullOrWhiteSpace(externalCode)
 46119            ? $"{providerKey}|{statusKind}|n:{name}"
 46120            : $"{providerKey}|{statusKind}|c:{externalCode}";
 121}