< Summary

Information
Class: Services.FidSupplierLinkServices.FidSupplierLinkService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FidSupplierLinkServices/FidSupplierLinkService.cs
Line coverage
100%
Covered lines: 114
Uncovered lines: 0
Coverable lines: 114
Total lines: 199
Line coverage: 100%
Branch coverage
97%
Covered branches: 86
Total branches: 88
Branch coverage: 97.7%
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%
GetOverviewAsync()100%22100%
RefreshAsync()100%22100%
GetGlobalAutoCartEnabledAsync()100%44100%
SetGlobalAutoCartAsync()100%22100%
SetRowAutoCartAsync()100%22100%
SaveAsync()96.15%2626100%
UnlinkAsync()100%22100%
SyncDiscoveredAsync()100%88100%
GetPendingNotificationsAsync(...)100%11100%
MarkNotifiedAsync()100%22100%
CountActionRequiredAsync(...)100%11100%
BuildOverview(...)93.75%1616100%
ToRow(...)100%2424100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FidSupplierLinkServices/FidSupplierLinkService.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using Microsoft.Extensions.Caching.Memory;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5using Services.ApiServices.ApiSupplierIntegration;
 6using Services.ApiServices.FidApi.Models;
 7
 8namespace Services.FidSupplierLinkServices;
 9
 10public sealed class FidSupplierLinkService
 11{
 12    // Глобальный рубильник автопроценки — одна строка-синглтон, меняется только из админки.
 13    // Читается на каждой странице транзакции, поэтому кэшируем с инвалидацией в SetGlobalAutoCartAsync.
 14    private const string GlobalAutoCartCacheKey = "FidSupplierLink:GlobalAutoCartEnabled";
 115    private static readonly TimeSpan GlobalAutoCartCacheTtl = TimeSpan.FromMinutes(10);
 16
 17    private readonly ApplicationDbContext _context;
 18    private readonly ApiSupplierConnectorRegistry _registry;
 19    private readonly FidSuppliersFeedCache _feedCache;
 20    private readonly IMemoryCache _cache;
 21
 3022    public FidSupplierLinkService(ApplicationDbContext context, ApiSupplierConnectorRegistry registry,
 3023        FidSuppliersFeedCache feedCache, IMemoryCache cache)
 15024    { _context = context; _registry = registry; _feedCache = feedCache; _cache = cache; }
 25
 26    public async Task<FidSupplierLinkOverview> GetOverviewAsync(bool forceRefresh = false, CancellationToken ct = defaul
 27    {
 228        var (feed, error) = await _feedCache.GetAsync(forceRefresh, ct);
 429        if (feed is not null) await SyncDiscoveredAsync(feed, ct);
 230        var links = await _context.FidSupplierLinks.AsNoTracking().ToListAsync(ct);
 231        return BuildOverview(feed, links, error, await GetGlobalAutoCartEnabledAsync(ct));
 232    }
 33
 34    /// <summary>
 35    /// Принудительно получает список из фид-API и возвращает имена добавленных при синхронизации поставщиков.
 36    /// Первый снимок не считается уведомляемым изменением, чтобы не рассылать накопленные записи.
 37    /// </summary>
 38    public async Task<FidSupplierLinkRefreshResult> RefreshAsync(CancellationToken ct = default)
 39    {
 140        var (feed, error) = await _feedCache.GetAsync(forceRefresh: true, ct);
 141        var isInitialSnapshot = false;
 142        IReadOnlyList<string> newSupplierNames = [];
 143        if (feed is not null)
 44        {
 145            isInitialSnapshot = !await _context.FidSupplierLinks.AnyAsync(ct);
 146            newSupplierNames = await SyncDiscoveredAsync(feed, ct);
 47        }
 48
 149        var links = await _context.FidSupplierLinks.AsNoTracking().ToListAsync(ct);
 150        var overview = BuildOverview(feed, links, error, await GetGlobalAutoCartEnabledAsync(ct));
 151        return new FidSupplierLinkRefreshResult(overview, newSupplierNames, isInitialSnapshot);
 152    }
 53
 54    /// <summary>Главный рубильник автопроценки. Строки настроек может не быть — тогда автоподстановка включена.</summar
 55    public async Task<bool> GetGlobalAutoCartEnabledAsync(CancellationToken ct = default)
 56    {
 657        if (_cache.TryGetValue(GlobalAutoCartCacheKey, out bool cached))
 258            return cached;
 59
 460        var settings = await _context.FidSupplierLinkSettings.AsNoTracking()
 461            .SingleOrDefaultAsync(x => x.Id == FidSupplierLinkSettings.SingletonId, ct);
 462        var enabled = settings?.IsAutoCartEnabled ?? true;
 463        _cache.Set(GlobalAutoCartCacheKey, enabled, GlobalAutoCartCacheTtl);
 464        return enabled;
 665    }
 66
 67    public async Task SetGlobalAutoCartAsync(bool enabled, string userName, CancellationToken ct = default)
 68    {
 269        var settings = await _context.FidSupplierLinkSettings
 270            .SingleOrDefaultAsync(x => x.Id == FidSupplierLinkSettings.SingletonId, ct);
 271        if (settings is null)
 72        {
 173            settings = new FidSupplierLinkSettings { Id = FidSupplierLinkSettings.SingletonId };
 174            _context.FidSupplierLinkSettings.Add(settings);
 75        }
 276        settings.IsAutoCartEnabled = enabled;
 277        settings.UpdatedAtUtc = DateTime.UtcNow;
 278        settings.UpdatedBy = userName;
 279        await _context.SaveChangesAsync(ct);
 280        _cache.Remove(GlobalAutoCartCacheKey);
 281    }
 82
 83    public async Task SetRowAutoCartAsync(string name, bool enabled, string userName, CancellationToken ct = default)
 84    {
 285        var link = await _context.FidSupplierLinks.SingleOrDefaultAsync(x => x.FidSupplierName == name.Trim(), ct);
 386        if (link is null) throw new InvalidOperationException("Связь поставщика фида не найдена.");
 187        link.IsAutoCartEnabled = enabled;
 188        link.UpdatedAtUtc = DateTime.UtcNow;
 189        link.UpdatedBy = userName;
 190        await _context.SaveChangesAsync(ct);
 191    }
 92
 93    public async Task SaveAsync(FidSupplierLinkInput input, string userName, CancellationToken ct = default)
 94    {
 995        var name = input.FidSupplierName?.Trim();
 1296        if (string.IsNullOrWhiteSpace(name)) throw new InvalidOperationException("Имя поставщика фида не задано.");
 697        var providerKey = string.IsNullOrWhiteSpace(input.ProviderKey) ? null : input.ProviderKey.Trim();
 698        if (providerKey is not null && !_registry.TryGet(providerKey, out _))
 199            throw new InvalidOperationException("Указан неизвестный API-поставщик.");
 5100        var now = DateTime.UtcNow;
 5101        var link = await _context.FidSupplierLinks.SingleOrDefaultAsync(x => x.FidSupplierName == name, ct);
 5102        if (link is null)
 103        {
 2104            link = new FidSupplierLink { FidSupplierName = name, FirstSeenAtUtc = now };
 2105            _context.FidSupplierLinks.Add(link);
 106        }
 5107        var providerChanged = !string.Equals(link.ProviderKey, providerKey, StringComparison.OrdinalIgnoreCase);
 5108        link.ProviderKey = providerKey;
 5109        link.Note = string.IsNullOrWhiteSpace(input.Note) ? null : input.Note.Trim();
 10110        link.UpdatedAtUtc = now; link.UpdatedBy = userName;
 5111        if (providerChanged)
 112        {
 4113            link.LinkedAtUtc = providerKey is null ? null : now;
 4114            link.LinkedBy = providerKey is null ? null : userName;
 4115            var (feed, _) = await _feedCache.GetAsync(false, ct);
 4116            link.FidRowsAtLink = providerKey is null ? null : feed?.Suppliers
 6117                .FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))?.Rows;
 118        }
 5119        await _context.SaveChangesAsync(ct);
 5120    }
 121
 122    public async Task UnlinkAsync(string name, string userName, CancellationToken ct = default)
 123    {
 2124        var link = await _context.FidSupplierLinks.SingleOrDefaultAsync(x => x.FidSupplierName == name.Trim(), ct);
 3125        if (link is null) throw new InvalidOperationException("Связь поставщика фида не найдена.");
 4126        link.ProviderKey = null; link.FidRowsAtLink = null; link.LinkedAtUtc = null; link.LinkedBy = null;
 2127        link.UpdatedAtUtc = DateTime.UtcNow; link.UpdatedBy = userName;
 1128        await _context.SaveChangesAsync(ct);
 1129    }
 130
 131    public async Task<IReadOnlyList<string>> SyncDiscoveredAsync(FidSuppliers feed, CancellationToken ct = default)
 132    {
 9133        var now = DateTime.UtcNow;
 9134        var existing = await _context.FidSupplierLinks.ToListAsync(ct);
 135        var byName = existing.ToDictionary(x => x.FidSupplierName, StringComparer.OrdinalIgnoreCase);
 9136        var initial = existing.Count == 0;
 9137        var discovered = new List<string>();
 138        foreach (var supplier in feed.Suppliers.Where(x => !string.IsNullOrWhiteSpace(x.Name)))
 139        {
 12140            if (!byName.TryGetValue(supplier.Name, out var link))
 141            {
 5142                link = new FidSupplierLink { FidSupplierName = supplier.Name.Trim(), FirstSeenAtUtc = now, UpdatedAtUtc 
 15143                _context.FidSupplierLinks.Add(link); byName.Add(link.FidSupplierName, link); discovered.Add(link.FidSupp
 144            }
 24145            link.LastSeenInFeedAtUtc = now; link.UpdatedAtUtc = now;
 146        }
 18147        if (_context.ChangeTracker.HasChanges()) await _context.SaveChangesAsync(ct);
 9148        return discovered;
 9149    }
 150
 151    public Task<List<FidSupplierLink>> GetPendingNotificationsAsync(TimeSpan renotifyAfter, CancellationToken ct = defau
 152    {
 4153        var cutoff = DateTime.UtcNow - renotifyAfter;
 4154        return _context.FidSupplierLinks.Where(x => x.ProviderKey == null && x.LastSeenInFeedAtUtc != null &&
 4155            (x.NotifiedAtUtc == null || x.NotifiedAtUtc < cutoff)).ToListAsync(ct);
 156    }
 157
 158    public async Task MarkNotifiedAsync(IEnumerable<string> names, CancellationToken ct = default)
 159    {
 2160        var set = names.ToHashSet(StringComparer.OrdinalIgnoreCase);
 2161        var links = await _context.FidSupplierLinks.Where(x => set.Contains(x.FidSupplierName)).ToListAsync(ct);
 15162        var now = DateTime.UtcNow; foreach (var link in links) link.NotifiedAtUtc = now;
 2163        await _context.SaveChangesAsync(ct);
 2164    }
 165
 166    public Task<int> CountActionRequiredAsync(CancellationToken ct = default) =>
 1167        _context.FidSupplierLinks.CountAsync(x => x.ProviderKey == null && x.LastSeenInFeedAtUtc != null, ct);
 168
 169    public static FidSupplierLinkOverview BuildOverview(FidSuppliers? feed, IReadOnlyCollection<FidSupplierLink> links, 
 170        bool globalAutoCartEnabled = true)
 171    {
 7172        if (feed is null)
 173        {
 5174            var unavailable = links.Select(x => ToRow(x, null, FidSupplierLinkState.FeedUnavailable));
 5175            return new FidSupplierLinkOverview(unavailable.OrderBy(x => x.FidSupplierName).ToList(), 0, 0, 0, null,
 2176                feedError ?? "Фид-сервис недоступен.", globalAutoCartEnabled);
 177        }
 14178        var linkByName = links.ToDictionary(x => x.FidSupplierName, StringComparer.OrdinalIgnoreCase);
 5179        var rows = new List<FidSupplierLinkRow>();
 28180        foreach (var supplier in feed.Suppliers)
 181        {
 9182            linkByName.TryGetValue(supplier.Name, out var link);
 9183            rows.Add(ToRow(link, supplier, link?.ProviderKey is null ? FidSupplierLinkState.Unlinked : FidSupplierLinkSt
 184        }
 14185        var names = feed.Suppliers.Select(x => x.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
 14186        rows.AddRange(links.Where(x => !string.IsNullOrWhiteSpace(x.ProviderKey) && !names.Contains(x.FidSupplierName))
 6187            .Select(x => ToRow(x, null, FidSupplierLinkState.Orphaned)));
 25188        var sorted = rows.OrderBy(x => x.State switch { FidSupplierLinkState.Unlinked => 0, FidSupplierLinkState.Orphane
 15189            .ThenBy(x => x.FidSupplierName, StringComparer.OrdinalIgnoreCase).ToList();
 15190        return new FidSupplierLinkOverview(sorted, sorted.Count(x => x.State == FidSupplierLinkState.Linked),
 20191            sorted.Count(x => x.State == FidSupplierLinkState.Unlinked), sorted.Count(x => x.State == FidSupplierLinkSta
 5192            globalAutoCartEnabled);
 193    }
 194
 13195    private static FidSupplierLinkRow ToRow(FidSupplierLink? link, FidSupplier? supplier, FidSupplierLinkState state) =>
 13196        supplier?.Name ?? link!.FidSupplierName, link?.ProviderKey, link?.Note, state, supplier?.Rows, link?.FidRowsAtLi
 4197        supplier?.Warehouses.Select(x => x.LastImportedAt).Where(x => x.HasValue).Max(), supplier?.Warehouses ?? [], lin
 13198        link?.IsAutoCartEnabled ?? true);
 199}