< Summary

Information
Class: OzonOrdersWeb.TransactionServices.BergCartService
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/BergCartService.cs
Line coverage
93%
Covered lines: 233
Uncovered lines: 17
Coverable lines: 250
Total lines: 463
Line coverage: 93.2%
Branch coverage
72%
Covered branches: 80
Total branches: 110
Branch coverage: 72.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
PlaceQueuedItemsAsync()83.33%1212100%
CheckoutDirectAsync()76.66%313090.74%
BuildOrderRequest(...)100%44100%
SaveOrderAsync()75%2020100%
MarkFailedAsync()100%66100%
BuildCartItem(...)50%44100%
MatchLine(...)50%22100%
Invalid(...)50%66100%
Label(...)50%44100%
Comment(...)50%22100%
Trim(...)75%44100%
.cctor()100%11100%
TryTrackCartAsync()100%1171.42%
TryTrackOrderedAsync()50%2275%
TryRecordHistoryAsync()50%2289.47%
CheckoutAsync()50%22100%
CountActiveAsync(...)100%210%
GetActiveAsync(...)100%210%
RemoveAsync()83.33%6691.66%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Services/Transactions/BergCartService.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.EntityFrameworkCore;
 3using OzonDomains.Models;
 4using OzonDomains.Models.SupplierStatuses;
 5using OzonRepositories.Context;
 6using Services.ApiServices.BergApi;
 7using Services.ApiSupplierSettingsServices;
 8using Services.DataServices;
 9using Services.SupplierStatusServices;
 10
 11namespace OzonOrdersWeb.TransactionServices;
 12
 13public sealed record BergCartQueueItem(
 14    int OrderId,
 15    long ResourceId,
 16    long WarehouseId,
 17    decimal Quantity,
 18    decimal? Price,
 19    string? Article,
 20    string? Brand,
 21    string? ProductName,
 22    string? ShipmentNumber,
 23    string? ClientName,
 24    int Multi = 1,
 25    int? DeliveryDaysMin = null,
 26    int? DeliveryDaysMax = null,
 27    string? Direction = null);
 28
 29public sealed record BergCartResult(bool IsSuccess, IReadOnlyList<string> Messages);
 30
 31/// <summary>
 32/// Корзина Berg со страницы проведения. У Berg нет API корзины: <c>ordering/place_order.json</c>
 33/// размещает заказ целиком одним вызовом, поэтому отобранные позиции копятся в локальной таблице
 34/// <c>BergCartItems</c>, а прямое оформление отправляет их поставщику за один запрос.
 35/// </summary>
 36public sealed class BergCartService
 37{
 38    private const string ProviderKey = ApiSupplierSettings.BergProviderKey;
 39    private const string SupplierDisplayName = "Берг";
 40    private const string LogoUrl = "/images/berg-logo-2026.png";
 41    private const string CurrencyCode = "RUB";
 42    private const string SupplierCartUrl = "/SupplierCart?provider=berg&supplierTab=basket";
 43
 44    /// <summary>Заказы, поданные после этого часа, Berg отгружает во второй половине дня.</summary>
 45    private const int MiddayHour = 15;
 46
 47    private readonly ApplicationDbContext _context;
 48    private readonly BergDataManager _berg;
 49    private readonly BergDeliveryAddressMappingService _addresses;
 50    private readonly IOrderHistoryDataService _orderHistoryService;
 51    private readonly IOrderSupplierStateService _supplierStateService;
 52
 853    public BergCartService(ApplicationDbContext context, BergDataManager berg,
 854        BergDeliveryAddressMappingService addresses, IOrderHistoryDataService orderHistoryService,
 855        IOrderSupplierStateService supplierStateService)
 56    {
 857        _context = context;
 858        _berg = berg;
 859        _addresses = addresses;
 860        _orderHistoryService = orderHistoryService;
 861        _supplierStateService = supplierStateService;
 862    }
 63
 64    /// <summary>Позиции кладутся только в локальную корзину: заказ у Berg оформляется отдельно.</summary>
 65    public async Task<BergCartResult> PlaceQueuedItemsAsync(IReadOnlyCollection<BergCartQueueItem> items,
 66        string changedBy, CancellationToken ct = default)
 67    {
 768        if (items.Count == 0) return new(true, []);
 769        if (Invalid(items)) return new(false, ["Для корзины Berg требуется ровно одно корректное предложение на заказ."]
 70
 771        var messages = new List<string>();
 772        var added = new List<BergCartQueueItem>();
 2873        foreach (var item in items)
 74        {
 775            var existing = await _context.BergCartItems
 776                .SingleOrDefaultAsync(x => x.ActiveStudioOrderId == item.OrderId, ct);
 777            if (existing is not null)
 78            {
 279                messages.Add($"{Label(item)}: уже в корзине Berg, повторно не добавляем.");
 280                continue;
 81            }
 82
 583            _context.BergCartItems.Add(BuildCartItem(item, changedBy, BergCartItemStatus.Pending));
 584            added.Add(item);
 585        }
 86
 787        if (added.Count == 0)
 288            return new(false, messages);
 89
 590        await _context.SaveChangesAsync(ct);
 91
 92        // Журнал и стадия — только по реально добавленным строкам: пропущенный дубль
 93        // не должен появляться в истории заказа.
 2094        foreach (var item in added)
 95        {
 596            await TryTrackCartAsync(item, changedBy);
 597            await TryRecordHistoryAsync(item, true, null, null, changedBy, ApiSupplierBasketCartStatus.Pending);
 598            messages.Add($"{Label(item)}: добавлено в корзину Berg.");
 599        }
 100
 5101        return new(added.Count == items.Count, messages);
 7102    }
 103
 104    /// <summary>
 105    /// Прямое оформление: позиции сохраняются в локальной корзине и сразу уходят в Berg одним
 106    /// заказом. Berg не умеет дозаказывать в существующий заказ, поэтому дробить вызов нельзя.
 107    /// </summary>
 108    public async Task<BergCartResult> CheckoutDirectAsync(IReadOnlyCollection<BergCartQueueItem> items,
 109        string changedBy, CancellationToken ct = default, bool isTest = false)
 110    {
 4111        if (items.Count == 0) return new(false, ["Не выбраны позиции Berg для оформления."]);
 4112        if (Invalid(items)) return new(false, ["Для прямого оформления Berg требуется ровно одно корректное предложение 
 113
 114        // Адрес отгрузки обязателен и определяется складом заказа. Заказы с разными адресами —
 115        // это разные заказы Berg, а одним вызовом place_order их не разделить.
 4116        var addresses = new Dictionary<int, BergDeliveryAddressSelection>();
 4117        var missing = new List<string>();
 16118        foreach (var item in items)
 119        {
 4120            var address = await _addresses.ResolveAsync(item.OrderId, cancellationToken: ct);
 5121            if (address is null) missing.Add(Label(item));
 3122            else addresses[item.OrderId] = address;
 4123        }
 124
 4125        if (missing.Count > 0)
 1126            return new(false, [$"Для складов этих позиций не задан адрес отгрузки Berg: {string.Join(", ", missing)}. " 
 1127                               "Свяжите склад с адресом в <a href=\"/Studio2/ApiSupplierSettings/Berg\" target=\"_blank\
 1128                               "(раздел Справочники → Настройки API-поставщиков → Berg)."]);
 129
 130        var distinctAddresses = addresses.Values.Select(x => x.BergShipmentAddressId).Distinct().Count();
 3131        if (distinctAddresses > 1)
 0132            return new(false, ["Позиции относятся к разным адресам отгрузки Berg. Проведите их отдельными документами."]
 133
 3134        var selection = addresses.Values.First();
 3135        var request = BuildOrderRequest(items, selection) with { IsTest = isTest };
 136
 3137        var cartItems = new List<BergCartItem>();
 12138        foreach (var item in items)
 139        {
 3140            var existing = await _context.BergCartItems
 3141                .SingleOrDefaultAsync(x => x.ActiveStudioOrderId == item.OrderId, ct);
 3142            if (existing is null)
 143            {
 2144                existing = BuildCartItem(item, changedBy, BergCartItemStatus.Processing);
 2145                _context.BergCartItems.Add(existing);
 146            }
 147            else
 148            {
 1149                existing.Status = BergCartItemStatus.Processing;
 1150                existing.UpdatedAtUtc = DateTime.UtcNow;
 151            }
 152
 3153            cartItems.Add(existing);
 3154        }
 155
 3156        await _context.SaveChangesAsync(ct);
 157
 158        BergPlaceOrderResult result;
 159        try
 160        {
 3161            result = await _berg.PlaceOrderAsync(request, ct);
 3162        }
 0163        catch (BergApiException ex)
 164        {
 0165            await MarkFailedAsync(cartItems, items, ex.Message, changedBy, ct);
 0166            return new(false, [$"Berg не оформил заказ: {ex.Message}. Позиции остались в корзине Berg."]);
 167        }
 168
 3169        if (!result.IsSuccess)
 170        {
 1171            var error = result.Errors.Count > 0
 172                ? string.Join("; ", result.Errors.Select(x => $"{x.Text} ({x.Code})"))
 1173                : "поставщик не вернул номер заказа";
 1174            await MarkFailedAsync(cartItems, items, error, changedBy, ct);
 1175            return new(false, [$"Berg не оформил заказ: {error}. Позиции остались в корзине Berg."]);
 176        }
 177
 2178        var order = await SaveOrderAsync(result.Order!, request, items, cartItems, changedBy, ct);
 179
 2180        var messages = new List<string>();
 8181        foreach (var item in items)
 182        {
 2183            var line = MatchLine(result.Order!, item);
 2184            await TryTrackOrderedAsync(item, order.ExternalOrderId, line?.Sequence, changedBy);
 2185            await TryRecordHistoryAsync(item, true, line?.Sequence.ToString(), order.ExternalOrderId.ToString(),
 2186                changedBy, ApiSupplierBasketCartStatus.Ordered);
 2187        }
 188
 2189        messages.Add(request.IsTest
 2190            ? $"Berg принял тестовый заказ №{order.ExternalOrderId} (is_test — заказ не обрабатывается)."
 2191            : $"Создан заказ Berg №{order.ExternalOrderId}.");
 4192        foreach (var warning in result.Warnings)
 0193            messages.Add($"Berg: {warning.Text} ({warning.Code}).");
 194
 2195        return new(true, messages);
 4196    }
 197
 198    internal static BergOrderRequest BuildOrderRequest(IReadOnlyCollection<BergCartQueueItem> items,
 199        BergDeliveryAddressSelection address, DateTime? now = null)
 200    {
 6201        var moment = now ?? DateTime.Now;
 202        // Дату отгрузки задаёт самая долгая позиция: раньше неё заказ целиком не уедет.
 14203        var days = items.Max(x => x.DeliveryDaysMin ?? 0);
 6204        return new BergOrderRequest
 6205        {
 8206            Items = items.Select(x => new BergOrderItemRequest(
 8207                x.ResourceId, x.WarehouseId, x.Quantity,
 8208                MaxPrice: x.Price,
 8209                Comment: Comment(x))).ToList(),
 6210            DispatchType = address.DispatchType,
 6211            DispatchAt = moment.Date.AddDays(days),
 6212            DispatchTime = moment.Hour < MiddayHour ? BergDispatchTime.BeforeMidday : BergDispatchTime.AfterMidday,
 6213            ShipmentAddressId = address.BergShipmentAddressId,
 6214            Comment = $"Studio2: {string.Join(", ", items.Select(Label))}",
 6215            // Числовой ключ идемпотентности: Berg отклонит повторную отправку той же формы.
 6216            Reference = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
 6217        };
 218    }
 219
 220    private async Task<BergOrder> SaveOrderAsync(BergOrderState state, BergOrderRequest request,
 221        IReadOnlyCollection<BergCartQueueItem> items, IReadOnlyList<BergCartItem> cartItems, string changedBy,
 222        CancellationToken ct)
 223    {
 2224        var order = new BergOrder
 2225        {
 2226            ExternalOrderId = state.Id,
 2227            Reference = request.Reference,
 2228            DispatchType = request.DispatchType,
 2229            DispatchAt = request.DispatchAt,
 2230            DispatchTime = request.DispatchTime,
 2231            ShipmentAddressId = request.ShipmentAddressId,
 2232            ShipmentAddress = state.ShipmentAddress,
 2233            PaymentType = state.PaymentType,
 2234            IsTest = state.IsTest || request.IsTest,
 2235            Status = request.IsTest ? BergOrderStatus.Test : BergOrderStatus.Placed,
 2236            RequestJson = BergDataManager.BuildOrderBody(request).ToString(Newtonsoft.Json.Formatting.None),
 2237            ResponseJson = JsonSerializer.Serialize(state, HistoryJsonOptions),
 2238            CreatedBy = changedBy,
 2239            CreatedAtUtc = DateTime.UtcNow
 2240        };
 241
 8242        foreach (var item in items)
 243        {
 2244            var line = MatchLine(state, item);
 4245            var cartItem = cartItems.FirstOrDefault(x => x.StudioOrderId == item.OrderId);
 2246            order.Items.Add(new BergOrderItem
 2247            {
 2248                BergCartItemId = cartItem?.Id,
 2249                StudioOrderId = item.OrderId,
 2250                Sequence = line?.Sequence ?? 0,
 2251                ResourceId = item.ResourceId,
 2252                WarehouseId = item.WarehouseId,
 2253                Article = Trim(item.Article, 60),
 2254                Brand = Trim(item.Brand, 100),
 2255                Quantity = item.Quantity,
 2256                Price = line?.Price ?? item.Price,
 2257                StateId = line?.StateId,
 2258                StateName = Trim(line?.StateName, 200)
 2259            });
 260
 2261            if (cartItem is null) continue;
 2262            cartItem.Status = BergCartItemStatus.Ordered;
 263            // Строка уходит из активной корзины, но остаётся в истории.
 2264            cartItem.ActiveStudioOrderId = null;
 2265            cartItem.LastError = null;
 2266            cartItem.UpdatedAtUtc = DateTime.UtcNow;
 267        }
 268
 2269        _context.BergOrders.Add(order);
 2270        await _context.SaveChangesAsync(ct);
 2271        return order;
 2272    }
 273
 274    private async Task MarkFailedAsync(IReadOnlyList<BergCartItem> cartItems,
 275        IReadOnlyCollection<BergCartQueueItem> items, string error, string changedBy, CancellationToken ct)
 276    {
 4277        foreach (var cartItem in cartItems)
 278        {
 1279            cartItem.Status = BergCartItemStatus.Failed;
 1280            cartItem.LastError = Trim(error, 1000);
 1281            cartItem.UpdatedAtUtc = DateTime.UtcNow;
 282        }
 283
 1284        await _context.SaveChangesAsync(ct);
 285
 4286        foreach (var item in items)
 287        {
 1288            await TryTrackCartAsync(item, changedBy, SupplierCartStatusCode.Failed);
 1289            await TryRecordHistoryAsync(item, false, null, null, changedBy, ApiSupplierBasketCartStatus.Failed, error);
 1290        }
 1291    }
 292
 7293    private BergCartItem BuildCartItem(BergCartQueueItem item, string changedBy, string status) => new()
 7294    {
 7295        StudioOrderId = item.OrderId,
 7296        ActiveStudioOrderId = item.OrderId,
 7297        ResourceId = item.ResourceId,
 7298        WarehouseId = item.WarehouseId,
 7299        Article = Trim(item.Article, 60),
 7300        Brand = Trim(item.Brand, 100),
 7301        ProductName = Trim(item.ProductName, 200),
 7302        WarehouseName = Trim(item.Direction, 160),
 7303        Quantity = item.Quantity,
 7304        Multi = item.Multi > 0 ? item.Multi : 1,
 7305        SourcePrice = item.Price,
 7306        MaxPrice = item.Price,
 7307        CurrencyCode = CurrencyCode,
 7308        DeliveryAtUtc = item.DeliveryDaysMin.HasValue ? DateTime.UtcNow.Date.AddDays(item.DeliveryDaysMin.Value) : null,
 7309        Status = status,
 7310        CreatedBy = changedBy,
 7311        CreatedAtUtc = DateTime.UtcNow,
 7312        UpdatedAtUtc = DateTime.UtcNow
 7313    };
 314
 315    /// <summary>Berg нумерует строки заказа сам, поэтому сопоставляем по паре resource + склад.</summary>
 316    private static BergOrderItemState? MatchLine(BergOrderState state, BergCartQueueItem item) =>
 8317        state.Items.FirstOrDefault(x => x.ResourceId == item.ResourceId && x.WarehouseId == item.WarehouseId);
 318
 319    private static bool Invalid(IReadOnlyCollection<BergCartQueueItem> items) =>
 22320        items.Any(x => x.OrderId <= 0 || x.ResourceId <= 0 || x.WarehouseId <= 0 || x.Quantity <= 0) ||
 33321        items.GroupBy(x => x.OrderId).Any(x => x.Count() > 1);
 322
 323    /// <summary>Подпись позиции в сообщениях: номер отгрузки Studio2 и артикул — по ним
 324    /// пользователь находит заказ, к которому относится строка.</summary>
 325    private static string Label(BergCartQueueItem item)
 326    {
 16327        var order = string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber!;
 16328        return string.IsNullOrWhiteSpace(item.Article) ? order : $"{order} ({item.Article})";
 329    }
 330
 331    private static string Comment(BergCartQueueItem item) =>
 8332        $"Studio2 {(string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber)} (ID {i
 333
 334    private static string Trim(string? value, int maxLength)
 335    {
 35336        var trimmed = (value ?? string.Empty).Trim();
 35337        return trimmed.Length <= maxLength ? trimmed : trimmed[..maxLength];
 338    }
 339
 1340    private static readonly JsonSerializerOptions HistoryJsonOptions = new()
 1341    {
 1342        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All)
 1343    };
 344
 345    /// <summary>Стадия корзины — вспомогательный журнал: её сбой не должен ломать уже выполненное размещение.</summary>
 346    private async Task TryTrackCartAsync(BergCartQueueItem item, string changedBy,
 347        string statusCode = SupplierCartStatusCode.Pending)
 348    {
 349        try
 350        {
 6351            await _supplierStateService.MarkInCartAsync(item.OrderId, ProviderKey, statusCode,
 6352                cartItemId: null, externalBasketItemId: null, user: changedBy,
 6353                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 6354        }
 0355        catch
 356        {
 357            // См. комментарий к методу.
 0358        }
 6359    }
 360
 361    private async Task TryTrackOrderedAsync(BergCartQueueItem item, long externalOrderId, int? sequence, string changedB
 362    {
 363        try
 364        {
 2365            var orderId = externalOrderId.ToString();
 2366            await _supplierStateService.MarkOrderedAsync(item.OrderId, ProviderKey, externalOrderNumber: orderId,
 2367                externalOrderId: orderId, externalLineId: sequence?.ToString(), user: changedBy,
 2368                deliveryDaysMin: item.DeliveryDaysMin, deliveryDaysMax: item.DeliveryDaysMax, direction: item.Direction)
 2369        }
 0370        catch
 371        {
 372            // См. TryTrackCartAsync.
 0373        }
 2374    }
 375
 376    private async Task TryRecordHistoryAsync(BergCartQueueItem item, bool success, string? apiItemId,
 377        string? supplierOrderNumber, string changedBy, string cartStatus, string? errorMessage = null)
 378    {
 379        try
 380        {
 8381            var entry = new ApiSupplierBasketHistoryEntry(ProviderKey, SupplierDisplayName, LogoUrl, success,
 8382                errorMessage, apiItemId, item.Article, item.ProductName, item.Price, CurrencyCode, item.Quantity,
 8383                string.IsNullOrWhiteSpace(item.ShipmentNumber) ? $"ID {item.OrderId}" : item.ShipmentNumber,
 8384                ClientName: item.ClientName, WarehouseName: item.Direction, CartStatus: cartStatus,
 8385                SupplierOrderNumber: supplierOrderNumber, SupplierCartUrl: SupplierCartUrl);
 386
 8387            await _orderHistoryService.AddOrderHistory(new OrderHistory
 8388            {
 8389                OrderId = item.OrderId,
 8390                ColumnName = ApiSupplierBasketHistoryEntry.HistoryColumnName,
 8391                ColumnDisplayName = $"Корзина {SupplierDisplayName}",
 8392                OldValue = null,
 8393                NewValue = JsonSerializer.Serialize(entry, ApiSupplierBasketHistoryEntry.JsonOptions),
 8394                ChangedAt = DateTime.Now,
 8395                ChangedBy = changedBy
 8396            });
 8397        }
 0398        catch
 399        {
 400            // Не ломаем размещение в корзине из-за журнала истории.
 0401        }
 8402    }
 403
 404    /// <summary>
 405    /// Оформление отобранных строк локальной корзины со страницы «Корзина поставщика».
 406    /// <paramref name="isTest"/> отправляет заказ с order[is_test]=1: Berg сохранит его в истории,
 407    /// но обрабатывать не станет — это штатная проверка перед боевым размещением.
 408    /// </summary>
 409    public async Task<BergCartResult> CheckoutAsync(IReadOnlyCollection<long> cartItemIds, string changedBy,
 410        bool isTest = false, CancellationToken ct = default)
 411    {
 1412        var rows = await _context.BergCartItems
 1413            .Where(x => cartItemIds.Contains(x.Id) && BergCartItemStatus.Active.Contains(x.Status))
 1414            .ToListAsync(ct);
 1415        if (rows.Count == 0) return new(false, ["Не выбраны позиции корзины Berg."]);
 416
 417        var queue = rows.Select(x => new BergCartQueueItem(x.StudioOrderId, x.ResourceId, x.WarehouseId, x.Quantity,
 418            x.MaxPrice ?? x.SourcePrice, x.Article, x.Brand, x.ProductName, null, null,
 419            x.Multi, null, null, x.WarehouseName)).ToList();
 420
 1421        var shipmentNumbers = await _context.Orders.AsNoTracking()
 1422            .Where(x => queue.Select(q => q.OrderId).Contains(x.Id))
 423            .ToDictionaryAsync(x => x.Id, x => x.ShipmentNumber, ct);
 2424        queue = queue.Select(x => shipmentNumbers.TryGetValue(x.OrderId, out var number) && !string.IsNullOrWhiteSpace(n
 2425            ? x with { ShipmentNumber = number }
 2426            : x).ToList();
 427
 1428        return await CheckoutDirectAsync(queue, changedBy, ct, isTest);
 1429    }
 430
 431    public Task<int> CountActiveAsync(CancellationToken ct = default) =>
 0432        _context.BergCartItems.CountAsync(x => BergCartItemStatus.Active.Contains(x.Status), ct);
 433
 434    public Task<List<BergCartItem>> GetActiveAsync(CancellationToken ct = default) =>
 0435        _context.BergCartItems.AsNoTracking()
 0436            .Where(x => BergCartItemStatus.Active.Contains(x.Status))
 0437            .OrderByDescending(x => x.CreatedAtUtc)
 0438            .ToListAsync(ct);
 439
 440    /// <summary>Снимает строки с активной корзины, сохраняя их в истории.</summary>
 441    public async Task<int> RemoveAsync(IReadOnlyCollection<long> cartItemIds, CancellationToken ct = default)
 442    {
 1443        if (cartItemIds.Count == 0) return 0;
 444
 1445        var rows = await _context.BergCartItems.Where(x => cartItemIds.Contains(x.Id)).ToListAsync(ct);
 4446        foreach (var row in rows)
 447        {
 1448            row.Status = BergCartItemStatus.Removed;
 1449            row.ActiveStudioOrderId = null;
 1450            row.UpdatedAtUtc = DateTime.UtcNow;
 451        }
 452
 1453        await _context.SaveChangesAsync(ct);
 454
 4455        foreach (var row in rows)
 456        {
 2457            try { await _supplierStateService.RemoveFromCartAsync(row.StudioOrderId, ProviderKey); }
 0458            catch { /* журнал стадии не должен ломать удаление */ }
 459        }
 460
 1461        return rows.Count;
 1462    }
 463}