< Summary

Information
Class: Services.BitrixStockServices.BitrixStockApplyDisabledZeroResult
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncReportApplyService.DisabledZero.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 4
Coverable lines: 4
Total lines: 410
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Processed()100%210%
get_Total()100%210%
get_SuccessCount()100%210%
get_FailedCount()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/BitrixStockServices/BitrixStockSyncReportApplyService.DisabledZero.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OzonDomains;
 3using OzonDomains.Models;
 4using OzonRepositories.Context;
 5using Services.ApiServices.OzonApi;
 6using Services.ApiServices.OzonApi.Filters;
 7
 8namespace Services.BitrixStockServices;
 9
 10internal sealed class BitrixStockApplyDisabledZeroResult
 11{
 012    public int Processed { get; set; }
 013    public int Total { get; set; }
 014    public int SuccessCount { get; set; }
 015    public int FailedCount { get; set; }
 16}
 17
 18public partial class BitrixStockSyncReportApplyService
 19{
 20    private async Task<BitrixStockApplyDisabledZeroResult> ZeroDisabledWarehouseStocksAsync(
 21        ApplicationDbContext db,
 22        string reportId,
 23        ISet<int> disabledMappingIds,
 24        ReportMetaRow meta,
 25        IReadOnlyDictionary<int, OzonClient> clientMap,
 26        bool onlyPending,
 27        IReadOnlyList<string>? articleFilter,
 28        int processed,
 29        int total,
 30        int successCount,
 31        int failedCount,
 32        Func<string, Task> log,
 33        Func<int, int, string, Task> progress,
 34        Func<ApplyLineRow, int, string, bool, string?, string?, string?, Task> logLine,
 35        CancellationToken cancellationToken)
 36    {
 37        var result = new BitrixStockApplyDisabledZeroResult
 38        {
 39            Processed = processed,
 40            Total = total,
 41            SuccessCount = successCount,
 42            FailedCount = failedCount
 43        };
 44
 45        if (disabledMappingIds.Count == 0)
 46            return result;
 47
 48        var products = await LoadApplyDisabledReportProductsAsync(db, reportId, articleFilter, cancellationToken);
 49        if (products.Count == 0)
 50        {
 51            await log("Снятые связи: в отчёте нет товаров для live-обнуления.");
 52            return result;
 53        }
 54
 55        var lineStates = await LoadApplyDisabledLineStatesAsync(db, reportId, cancellationToken);
 56        var productsByClient = products
 57            .GroupBy(p => p.OzonClientId > 0 ? p.OzonClientId : meta.OzonClientId)
 58            .ToList();
 59
 60        var allTargets = new List<BitrixStockSyncApplyDisabledZeroTarget>();
 61        foreach (var clientGroup in productsByClient)
 62        {
 63            cancellationToken.ThrowIfCancellationRequested();
 64            var clientId = clientGroup.Key;
 65            if (!clientMap.TryGetValue(clientId, out var client))
 66            {
 67                await log($"Снятые связи: клиент Ozon ID {clientId} не найден — пропуск.");
 68                continue;
 69            }
 70
 71            var mappings = await db.WarehouseMappings
 72                .Where(m => m.OzonClientId == clientId && m.OzonWarehouseId != null && m.OzonWarehouseId != "")
 73                .OrderBy(m => m.Id)
 74                .ToListAsync(cancellationToken);
 75
 76            var warehouseSets = BitrixStockSyncApplyDisabledPlanner.BuildWarehouseSets(mappings, disabledMappingIds);
 77            if (warehouseSets.DisabledWarehouseIds.Count == 0)
 78            {
 79                await log($"[{client.Name}] Снятые связи: нет складов Ozon для обнуления (коллизия с включёнными связями
 80                continue;
 81            }
 82
 83            var whList = warehouseSets.DisabledWarehouseIds
 84                .Select(id => warehouseSets.WarehouseNameById.TryGetValue(id, out var name)
 85                    ? $"{name} ({id})"
 86                    : id.ToString())
 87                .ToList();
 88            await log($"[{client.Name}] Снятые связи: {whList.Count} склад(ов) Ozon — {string.Join(", ", whList)}");
 89
 90            var offerIds = clientGroup
 91                .Select(p => p.OfferId)
 92                .Where(id => !string.IsNullOrWhiteSpace(id))
 93                .Distinct(StringComparer.OrdinalIgnoreCase)
 94                .ToList();
 95
 96            await log($"[{client.Name}] Live FBS Ozon: {offerIds.Count} offer_id…");
 97            var apiManager = BitrixOzonApiFactory.Create(_httpClientFactory, _emailService, client);
 98            var fbsByOfferId = await BitrixStockSyncFbsWarehouseBatchLoader.LoadByOfferIdsAsync(
 99                apiManager,
 100                offerIds,
 101                async msg => await log($"[{client.Name}] {msg}"),
 102                cancellationToken);
 103
 104            var articleSet = articleFilter is { Count: > 0 }
 105                ? articleFilter.ToHashSet(StringComparer.OrdinalIgnoreCase)
 106                : null;
 107
 108            var targets = BitrixStockSyncApplyDisabledPlanner.SelectTargets(
 109                clientGroup.ToList(),
 110                fbsByOfferId,
 111                warehouseSets,
 112                lineStates,
 113                onlyPending,
 114                articleSet,
 115                out var selectionStats);
 116
 117            if (selectionStats.FallbackFromReportCount > 0)
 118                await log($"[{client.Name}] Снятые связи: {selectionStats.FallbackFromReportCount} поз. — live Ozon не в
 119            if (selectionStats.AlreadyZeroCount > 0)
 120                await log($"[{client.Name}] Снятые связи: уже 0 (обнулять нечего) — {selectionStats.AlreadyZeroCount} по
 121            if (selectionStats.AlreadyClearedSkipCount > 0)
 122                await log($"[{client.Name}] Снятые связи: пропущено — уже обнулено ранее (onlyPending) — {selectionStats
 123
 124            allTargets.AddRange(targets);
 125        }
 126
 127        if (allTargets.Count == 0)
 128        {
 129            await log("Снятые связи: live Ozon — нечего обнулять (остатки 0 или уже обработано).");
 130            return result;
 131        }
 132
 133        result.Total += allTargets.Count;
 134        await log($"Снятые связи: к обнулению {allTargets.Count} позиций (live Ozon)…");
 135
 136        foreach (var clientTargets in allTargets.GroupBy(t => t.OzonClientId))
 137        {
 138            cancellationToken.ThrowIfCancellationRequested();
 139            var clientId = clientTargets.Key;
 140            if (!clientMap.TryGetValue(clientId, out var client))
 141                continue;
 142
 143            var apiManager = BitrixOzonApiFactory.Create(_httpClientFactory, _emailService, client);
 144            var clientName = client.Name ?? meta.OzonClientName;
 145            var targets = clientTargets.ToList();
 146
 147            for (var offset = 0; offset < targets.Count; offset += BatchSize)
 148            {
 149                cancellationToken.ThrowIfCancellationRequested();
 150                var batch = targets.Skip(offset).Take(BatchSize).ToList();
 151                var lookup = batch.ToDictionary(t => $"{t.OzonProductId}_{t.WarehouseId}");
 152
 153                var payload = batch.Select(t => new StockItemForUpdate
 154                {
 155                    OfferId = t.OfferId,
 156                    ProductId = t.OzonProductId,
 157                    Stock = 0,
 158                    WarehouseId = t.WarehouseId
 159                }).ToList();
 160
 161                var pushItems = batch.Select(t => new BitrixStockPushItemDto
 162                {
 163                    Article = t.Article,
 164                    OfferId = t.OfferId,
 165                    ProductId = t.OzonProductId,
 166                    WarehouseId = t.WarehouseId,
 167                    WarehouseName = t.WarehouseName,
 168                    OzonClient = clientName,
 169                    LinkedStoreName = t.OneCStore,
 170                    PreviousStock = t.Present,
 171                    TargetStock = 0
 172                }).ToList();
 173
 174                var handled = new HashSet<string>(StringComparer.Ordinal);
 175                var batchOk = await TryUpdateBatchAsync(apiManager, payload, pushItems,
 176                    async (item, error) =>
 177                    {
 178                        if (!lookup.TryGetValue($"{item.ProductId}_{item.WarehouseId}", out var target))
 179                            return;
 180                        handled.Add($"{target.OzonProductId}_{target.WarehouseId}");
 181                        result.Processed++;
 182                        result.FailedCount++;
 183                        await BitrixStockSyncApplyDisabledLineWriter.UpsertAsync(
 184                            db, reportId, clientId, target.Article, target.WarehouseId, target.WarehouseName,
 185                            target.OneCStore, false, error, target.Present, target.OfferId, target.OzonProductId,
 186                            cancellationToken);
 187                        await log($"✗ {target.Article} ({target.OneCStore}→{target.WarehouseName}): {error} (связь отклю
 188                        await LogDisabledTargetLine(target, clientId, clientName, false, error, logLine);
 189                        await progress(result.Processed, result.Total, target.Article);
 190                    },
 191                    async item =>
 192                    {
 193                        if (!lookup.TryGetValue($"{item.ProductId}_{item.WarehouseId}", out var target))
 194                            return;
 195                        handled.Add($"{target.OzonProductId}_{target.WarehouseId}");
 196                        result.Processed++;
 197                        result.SuccessCount++;
 198                        await BitrixStockSyncApplyDisabledLineWriter.UpsertAsync(
 199                            db, reportId, clientId, target.Article, target.WarehouseId, target.WarehouseName,
 200                            target.OneCStore, true, null, target.Present, target.OfferId, target.OzonProductId,
 201                            cancellationToken);
 202                        await log($"⓪ {target.Article} ({target.OneCStore}→{target.WarehouseName}): {target.Present} → 0
 203                        await LogDisabledTargetLine(target, clientId, clientName, true, null, logLine);
 204                        await progress(result.Processed, result.Total, target.Article);
 205                    },
 206                    cancellationToken);
 207
 208                if (!batchOk)
 209                {
 210                    foreach (var target in batch.Where(t => !handled.Contains($"{t.OzonProductId}_{t.WarehouseId}")))
 211                    {
 212                        cancellationToken.ThrowIfCancellationRequested();
 213                        var single = new List<StockItemForUpdate>
 214                        {
 215                            new()
 216                            {
 217                                OfferId = target.OfferId,
 218                                ProductId = target.OzonProductId,
 219                                Stock = 0,
 220                                WarehouseId = target.WarehouseId
 221                            }
 222                        };
 223                        try
 224                        {
 225                            var apiResult = await apiManager.UpdateProductStocks(single);
 226                            var row = apiResult?.Result?.FirstOrDefault();
 227                            if (row != null && row.Updated && (row.Errors == null || !row.Errors.Any()))
 228                            {
 229                                result.Processed++;
 230                                result.SuccessCount++;
 231                                await BitrixStockSyncApplyDisabledLineWriter.UpsertAsync(
 232                                    db, reportId, clientId, target.Article, target.WarehouseId, target.WarehouseName,
 233                                    target.OneCStore, true, null, target.Present, target.OfferId, target.OzonProductId,
 234                                    cancellationToken);
 235                                await log($"⓪ {target.Article}: повтор по одной (связь отключена, Ozon ID {target.Wareho
 236                                await LogDisabledTargetLine(target, clientId, clientName, true, null, logLine);
 237                            }
 238                            else
 239                            {
 240                                var err = FormatStockErrors(row);
 241                                result.Processed++;
 242                                result.FailedCount++;
 243                                await BitrixStockSyncApplyDisabledLineWriter.UpsertAsync(
 244                                    db, reportId, clientId, target.Article, target.WarehouseId, target.WarehouseName,
 245                                    target.OneCStore, false, err, target.Present, target.OfferId, target.OzonProductId,
 246                                    cancellationToken);
 247                                await log($"✗ {target.Article}: {err} (связь отключена){DisabledZeroFallbackSuffix(targe
 248                                await LogDisabledTargetLine(target, clientId, clientName, false, err, logLine);
 249                            }
 250                        }
 251                        catch (Exception ex)
 252                        {
 253                            result.Processed++;
 254                            result.FailedCount++;
 255                            await BitrixStockSyncApplyDisabledLineWriter.UpsertAsync(
 256                                db, reportId, clientId, target.Article, target.WarehouseId, target.WarehouseName,
 257                                target.OneCStore, false, ex.Message, target.Present, target.OfferId, target.OzonProductI
 258                                cancellationToken);
 259                            await log($"✗ {target.Article}: {ex.Message} (связь отключена){DisabledZeroFallbackSuffix(ta
 260                            await LogDisabledTargetLine(target, clientId, clientName, false, ex.Message, logLine);
 261                        }
 262
 263                        await progress(result.Processed, result.Total, target.Article);
 264                        await Task.Delay(200, cancellationToken);
 265                    }
 266                }
 267
 268                if (offset + BatchSize < targets.Count)
 269                    await Task.Delay(DelayBetweenBatchesMs, cancellationToken);
 270            }
 271        }
 272
 273        return result;
 274    }
 275
 276    private static string DisabledZeroFallbackSuffix(BitrixStockSyncApplyDisabledZeroTarget target) =>
 277        target.FromReportFallback ? " [остаток из отчёта, live Ozon склад не вернул]" : string.Empty;
 278
 279    private static async Task LogDisabledTargetLine(
 280        BitrixStockSyncApplyDisabledZeroTarget target,
 281        int clientId,
 282        string clientName,
 283        bool ok,
 284        string? error,
 285        Func<ApplyLineRow, int, string, bool, string?, string?, string?, Task> logLine)
 286    {
 287        var pseudoLine = new ApplyLineRow
 288        {
 289            Article = target.Article,
 290            OneCStore = target.OneCStore,
 291            OzonWarehouseName = target.WarehouseName,
 292            OzonQty = target.Present,
 293            OneCQty = 0
 294        };
 295        await logLine(
 296            pseudoLine,
 297            clientId,
 298            clientName,
 299            ok,
 300            error,
 301            ApplyArticleActionCodes.ClearDisabledLink,
 302            ok ? $"{target.Present} → 0 (связь отключена)" : error);
 303    }
 304
 305    private static async Task<List<BitrixStockSyncApplyDisabledReportProduct>> LoadApplyDisabledReportProductsAsync(
 306        ApplicationDbContext db,
 307        string reportId,
 308        IReadOnlyList<string>? articleFilter,
 309        CancellationToken cancellationToken)
 310    {
 311        return await WithOpenConnectionAsync(db, async (connection, ct) =>
 312        {
 313            // Построчно (не DISTINCT по продукту) — нужны OzonWarehouseId/OzonQty каждой строки отчёта
 314            // как fallback-остаток, когда live-запрос к Ozon на этапе 2 не вернёт разбивку по складу.
 315            var sql = @"
 316SELECT COALESCE(l.OzonClientId, 0), l.Article, l.OfferId, l.OzonProductId, l.OzonWarehouseId, l.OzonQty
 317FROM BitrixStockSyncReportLines l
 318WHERE l.ReportId = @reportId
 319  AND l.OfferId IS NOT NULL AND l.OfferId != ''
 320  AND l.OzonProductId IS NOT NULL AND l.OzonProductId > 0";
 321
 322            if (articleFilter is { Count: > 0 })
 323            {
 324                var artIn = string.Join(", ", articleFilter.Select((_, i) => $"@art{i}"));
 325                sql += $" AND l.Article IN ({artIn})";
 326            }
 327
 328            var cmd = connection.CreateCommand();
 329            cmd.CommandText = sql;
 330            AddParam(cmd, "@reportId", reportId);
 331            if (articleFilter is { Count: > 0 })
 332            {
 333                for (var i = 0; i < articleFilter.Count; i++)
 334                    AddParam(cmd, $"@art{i}", articleFilter[i]);
 335            }
 336
 337            var warehousesByKey =
 338                new Dictionary<(int ClientId, string Article, string OfferId, long ProductId),
 339                    List<BitrixStockSyncApplyDisabledReportWarehouseQty>>();
 340
 341            await using var reader = await cmd.ExecuteReaderAsync(ct);
 342            while (await reader.ReadAsync(ct))
 343            {
 344                var clientId = reader.IsDBNull(0) ? 0 : reader.GetInt32(0);
 345                var article = reader.GetString(1);
 346                var offerId = reader.IsDBNull(2) ? string.Empty : reader.GetString(2);
 347                var productId = reader.IsDBNull(3) ? 0L : reader.GetInt64(3);
 348                var warehouseId = reader.IsDBNull(4) ? 0L : reader.GetInt64(4);
 349                var ozonQty = reader.IsDBNull(5) ? 0 : reader.GetInt32(5);
 350
 351                var key = (clientId, article, offerId, productId);
 352                if (!warehousesByKey.TryGetValue(key, out var whList))
 353                {
 354                    whList = new List<BitrixStockSyncApplyDisabledReportWarehouseQty>();
 355                    warehousesByKey[key] = whList;
 356                }
 357
 358                if (warehouseId > 0)
 359                    whList.Add(new BitrixStockSyncApplyDisabledReportWarehouseQty { WarehouseId = warehouseId, OzonQty =
 360            }
 361
 362            return warehousesByKey.Select(kv => new BitrixStockSyncApplyDisabledReportProduct
 363            {
 364                OzonClientId = kv.Key.ClientId,
 365                Article = kv.Key.Article,
 366                OfferId = kv.Key.OfferId,
 367                OzonProductId = kv.Key.ProductId,
 368                ReportWarehouses = kv.Value
 369            }).ToList();
 370        }, cancellationToken);
 371    }
 372
 373    private static async Task<Dictionary<string, BitrixStockSyncApplyDisabledLineState>> LoadApplyDisabledLineStatesAsyn
 374        ApplicationDbContext db,
 375        string reportId,
 376        CancellationToken cancellationToken)
 377    {
 378        return await WithOpenConnectionAsync(db, async (connection, ct) =>
 379        {
 380            const string sql = @"
 381SELECT l.Article, COALESCE(l.OzonClientId, 0), l.OzonWarehouseId,
 382       l.OzonApplyStatus, l.OzonApplyError, l.Action
 383FROM BitrixStockSyncReportLines l
 384WHERE l.ReportId = @reportId
 385  AND l.OzonWarehouseId > 0";
 386
 387            var cmd = connection.CreateCommand();
 388            cmd.CommandText = sql;
 389            AddParam(cmd, "@reportId", reportId);
 390
 391            var dict = new Dictionary<string, BitrixStockSyncApplyDisabledLineState>(StringComparer.OrdinalIgnoreCase);
 392            await using var reader = await cmd.ExecuteReaderAsync(ct);
 393            while (await reader.ReadAsync(ct))
 394            {
 395                var article = reader.GetString(0);
 396                var clientId = reader.IsDBNull(1) ? 0 : reader.GetInt32(1);
 397                var warehouseId = reader.GetInt64(2);
 398                var key = BitrixStockSyncApplyDisabledPlanner.BuildLineStateKey(article, clientId, warehouseId);
 399                dict[key] = new BitrixStockSyncApplyDisabledLineState
 400                {
 401                    OzonApplyStatus = reader.IsDBNull(3) ? null : reader.GetString(3),
 402                    OzonApplyError = reader.IsDBNull(4) ? null : reader.GetString(4),
 403                    Action = reader.IsDBNull(5) ? null : reader.GetString(5)
 404                };
 405            }
 406
 407            return dict;
 408        }, cancellationToken);
 409    }
 410}