< Summary

Information
Class: Services.BitrixStockServices.BitrixOzonLinkedStockApiSyncService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixOzonLinkedStockApiSyncService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 146
Coverable lines: 146
Total lines: 279
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 94
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
SyncPendingLinkedStocksAsync()0%342180%
CollectPending(...)0%342180%
ProcessBatchAsync()0%702260%
BuildDetail(...)0%7280%
ParseProductId(...)0%2040%
FormatStockErrors(...)0%210140%
RemoveStore(...)0%2040%
.ctor(...)100%210%
get_Item()100%210%
get_Store()100%210%
get_ResolvedProductId()100%210%
get_TargetStock()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixOzonLinkedStockApiSyncService.cs

#LineLine coverage
 1using Microsoft.Extensions.Http;
 2using OzonDomains.Models;
 3using OzonDomains.Models.BitrixModels;
 4using Services.ApiServices.OzonApi;
 5using Services.ApiServices.OzonApi.Filters;
 6using SlqStudio.Application.Services.EmailService;
 7
 8namespace Services.BitrixStockServices;
 9
 10public class BitrixOzonLinkedStockApiSyncService : IBitrixOzonLinkedStockApiSyncService
 11{
 12    private const int MaxUpdatesPerLoad = 400;
 13    private const int BatchSize = 50;
 14
 15    private readonly IHttpClientFactory _httpClientFactory;
 16    private readonly IEmailService? _emailService;
 17
 018    public BitrixOzonLinkedStockApiSyncService(
 019        IHttpClientFactory httpClientFactory,
 020        IEmailService? emailService = null)
 21    {
 022        _httpClientFactory = httpClientFactory;
 023        _emailService = emailService;
 024    }
 25
 26    public async Task<BitrixOzonStockSyncSummary> SyncPendingLinkedStocksAsync(
 27        IReadOnlyList<RemainingStockBitrix> items,
 28        IReadOnlyList<OzonClient> ozonClients,
 29        CancellationToken cancellationToken = default)
 30    {
 031        var summary = new BitrixOzonStockSyncSummary();
 032        if (items.Count == 0 || ozonClients.Count == 0)
 33        {
 034            return summary;
 35        }
 36
 037        var clientByName = ozonClients
 38            .Where(c => !string.IsNullOrWhiteSpace(c.Name))
 39            .ToDictionary(c => c.Name.Trim(), c => c, StringComparer.OrdinalIgnoreCase);
 40
 041        var pending = CollectPending(items);
 042        if (pending.Count == 0)
 43        {
 044            return summary;
 45        }
 46
 047        if (pending.Count > MaxUpdatesPerLoad)
 48        {
 049            summary.LimitReached = true;
 050            pending = pending.Take(MaxUpdatesPerLoad).ToList();
 51        }
 52
 053        foreach (var clientGroup in pending.GroupBy(
 54                     p => p.Store.ClientName ?? string.Empty,
 055                     StringComparer.OrdinalIgnoreCase))
 56        {
 057            cancellationToken.ThrowIfCancellationRequested();
 058            if (!clientByName.TryGetValue(clientGroup.Key, out var client))
 59            {
 060                foreach (var entry in clientGroup)
 61                {
 062                    summary.Skipped++;
 063                    summary.AddEntry(BuildDetail(entry,
 064                        (int)Math.Max(0, Math.Round(entry.Store.Amount)),
 065                        success: false,
 066                        error: $"Клиент Ozon «{entry.Store.ClientName}» не найден в настройках"));
 67                }
 68
 69                continue;
 70            }
 71
 072            var apiManager = BitrixOzonApiFactory.Create(_httpClientFactory, _emailService, client);
 73
 074            var clientItems = clientGroup.ToList();
 075            for (var offset = 0; offset < clientItems.Count; offset += BatchSize)
 76            {
 077                cancellationToken.ThrowIfCancellationRequested();
 078                var batch = clientItems.Skip(offset).Take(BatchSize).ToList();
 079                await ProcessBatchAsync(apiManager, batch, summary, cancellationToken);
 80            }
 081        }
 82
 083        foreach (var item in items)
 84        {
 085            BitrixOzonStockLinkage.PruneUnlistedOzonProductStores(item);
 86        }
 87
 088        return summary;
 089    }
 90
 91    private static List<PendingOzonStockUpdate> CollectPending(IReadOnlyList<RemainingStockBitrix> items)
 92    {
 093        var list = new List<PendingOzonStockUpdate>();
 094        foreach (var item in items)
 95        {
 096            if (BitrixOzonStockLinkage.IsInactiveProduct(item))
 97            {
 98                continue;
 99            }
 100
 0101            lock (item.OzonStores)
 102            {
 0103                foreach (var store in item.OzonStores)
 104                {
 0105                    if (!store.PendingOzonApiSync ||
 0106                        store.StoreId <= 0 ||
 0107                        string.IsNullOrWhiteSpace(store.OfferId) ||
 0108                        string.IsNullOrWhiteSpace(store.ClientName) ||
 0109                        store.LinkStores == null ||
 0110                        store.LinkStores.Count == 0)
 111                    {
 112                        continue;
 113                    }
 114
 0115                    list.Add(new PendingOzonStockUpdate(item, store));
 116                }
 117            }
 118        }
 119
 0120        return list;
 121    }
 122
 123    private async Task ProcessBatchAsync(
 124        OzonApiDataManager apiManager,
 125        List<PendingOzonStockUpdate> batch,
 126        BitrixOzonStockSyncSummary summary,
 127        CancellationToken cancellationToken)
 128    {
 0129        var resolved = new List<(PendingOzonStockUpdate Pending, StockItemForUpdate Payload)>();
 0130        foreach (var entry in batch)
 131        {
 0132            cancellationToken.ThrowIfCancellationRequested();
 133
 0134            var store = entry.Store;
 0135            var productId = ParseProductId(store.ProductId);
 0136            if (productId <= 0)
 137            {
 0138                var articleBase = store.OfferId.Contains('=')
 0139                    ? store.OfferId.Split('=')[0]
 0140                    : store.OfferId;
 0141                productId = await apiManager.TryResolveProductIdByOfferIdAsync(store.OfferId, articleBase)
 0142                    .ConfigureAwait(false);
 143            }
 144
 0145            if (productId <= 0)
 146            {
 0147                RemoveStore(entry.Item, store);
 0148                continue;
 149            }
 150
 0151            summary.Attempted++;
 152
 0153            store.ProductId = productId.ToString();
 0154            var stock = (int)Math.Max(0, Math.Round(store.Amount));
 0155            entry.ResolvedProductId = productId;
 0156            entry.TargetStock = stock;
 0157            resolved.Add((entry, new StockItemForUpdate
 0158            {
 0159                OfferId = store.OfferId,
 0160                ProductId = productId,
 0161                Stock = stock,
 0162                WarehouseId = store.StoreId
 0163            }));
 0164        }
 165
 0166        if (resolved.Count == 0)
 167        {
 0168            return;
 169        }
 170
 171        try
 172        {
 0173            var result = await apiManager
 174                .UpdateProductStocks(resolved.Select(r => r.Payload).ToList())
 0175                .ConfigureAwait(false);
 176
 0177            var rows = result?.Result ?? new List<UpdateStockResult>();
 0178            foreach (var (pending, payload) in resolved)
 179            {
 0180                var store = pending.Store;
 0181                var row = rows.FirstOrDefault(r =>
 0182                    string.Equals(r.OfferId, payload.OfferId, StringComparison.OrdinalIgnoreCase) &&
 0183                    r.WarehouseId == payload.WarehouseId);
 0184                if (row != null && row.Updated && (row.Errors == null || row.Errors.Count == 0))
 185                {
 0186                    summary.Succeeded++;
 0187                    store.PendingOzonApiSync = false;
 0188                    store.OzonPresentOnOzon = store.Amount;
 0189                    store.LastOzonSyncError = null;
 0190                    summary.AddEntry(BuildDetail(pending, pending.TargetStock, true, null, pending.ResolvedProductId));
 0191                    continue;
 192                }
 193
 0194                summary.Failed++;
 0195                store.LastOzonSyncError = FormatStockErrors(row);
 0196                summary.AddEntry(BuildDetail(pending, pending.TargetStock, false, store.LastOzonSyncError,
 0197                    pending.ResolvedProductId));
 198            }
 0199        }
 0200        catch (Exception ex)
 201        {
 0202            foreach (var entry in resolved)
 203            {
 0204                summary.Failed++;
 0205                entry.Pending.Store.LastOzonSyncError = ex.Message;
 0206                summary.AddEntry(BuildDetail(entry.Pending, entry.Pending.TargetStock, false, ex.Message,
 0207                    entry.Pending.ResolvedProductId));
 208            }
 0209        }
 0210    }
 211
 212    private static BitrixOzonStockSyncDetailEntry BuildDetail(
 213        PendingOzonStockUpdate pending,
 214        int stock,
 215        bool success,
 216        string? error,
 217        long? productId = null)
 218    {
 0219        var store = pending.Store;
 0220        var item = pending.Item;
 0221        int? previous = store.OzonPresentOnOzon.HasValue
 0222            ? (int)Math.Round(store.OzonPresentOnOzon.Value)
 0223            : null;
 224
 0225        return new BitrixOzonStockSyncDetailEntry
 0226        {
 0227            Article = item.Article,
 0228            OfferId = store.OfferId,
 0229            OzonClient = store.ClientName ?? string.Empty,
 0230            WarehouseName = store.Title,
 0231            WarehouseId = store.StoreId,
 0232            BitrixStores = store.LinkStores != null ? string.Join(", ", store.LinkStores) : null,
 0233            PreviousOzonStock = previous,
 0234            NewStock = stock,
 0235            ProductId = productId ?? ParseProductId(store.ProductId),
 0236            Success = success,
 0237            ErrorMessage = error
 0238        };
 239    }
 240
 241    private static long ParseProductId(string? raw) =>
 0242        long.TryParse(raw?.Trim(), out var id) ? id : 0;
 243
 244    private static string FormatStockErrors(UpdateStockResult? row)
 245    {
 0246        if (row?.Errors == null || row.Errors.Count == 0)
 247        {
 0248            return row?.Updated == false ? "Ozon не обновил остаток" : "Неизвестная ошибка Ozon";
 249        }
 250
 0251        return string.Join("; ", row.Errors.Select(e =>
 0252            string.IsNullOrWhiteSpace(e.Message) ? e.Code ?? "ошибка" : e.Message));
 253    }
 254
 255    private static void RemoveStore(RemainingStockBitrix item, StockByStore store)
 256    {
 0257        lock (item.OzonStores)
 258        {
 0259            item.OzonStores.RemoveAll(s =>
 0260                ReferenceEquals(s, store) ||
 0261                (s.StoreId == store.StoreId &&
 0262                 string.Equals(s.ClientName, store.ClientName, StringComparison.OrdinalIgnoreCase)));
 0263        }
 0264    }
 265
 266    private sealed class PendingOzonStockUpdate
 267    {
 0268        public PendingOzonStockUpdate(RemainingStockBitrix item, StockByStore store)
 269        {
 0270            Item = item;
 0271            Store = store;
 0272        }
 273
 0274        public RemainingStockBitrix Item { get; }
 0275        public StockByStore Store { get; }
 0276        public long ResolvedProductId { get; set; }
 0277        public int TargetStock { get; set; }
 278    }
 279}