< Summary

Information
Class: Services.OzonHandoverServices.OzonHandoverImportService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonHandoverServices/OzonHandoverImportService.cs
Line coverage
99%
Covered lines: 194
Uncovered lines: 1
Coverable lines: 195
Total lines: 279
Line coverage: 99.4%
Branch coverage
55%
Covered branches: 51
Total branches: 92
Branch coverage: 55.4%
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%
ImportAsync()51.72%5858100%
StorePhotosAsync()75%88100%
IsFbo(...)100%11100%
JoinErrors(...)100%11100%
.cctor()100%11100%
get_SavedCount()100%11100%
get_FailedCount()100%11100%
get_Warnings()100%11100%
NotifyAsync()100%4490%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/OzonHandoverServices/OzonHandoverImportService.cs

#LineLine coverage
 1using System.Text.Json;
 2using OzonDomains.Models.OzonHandover;
 3using OzonRepositories.Data;
 4using Services.SignalRServices;
 5
 6namespace Services.OzonHandoverServices;
 7
 8/// <summary>
 9/// Обрабатывает JSON-документ выдачи/приёма-передачи Ozon: сопоставляет позиции с заказами
 10/// по item_no (== Order.ShipmentNumber) и сохраняет батч/акты/позиции.
 11/// </summary>
 12public class OzonHandoverImportService : IOzonHandoverImportService
 13{
 114    private static readonly JsonSerializerOptions RawJsonOptions = new() { WriteIndented = false };
 15
 16    private readonly OzonHandoverRepository _repository;
 17    private readonly OrderRepository _orderRepository;
 18    private readonly IOzonHandoverPhotoStorage _photoStorage;
 19
 520    public OzonHandoverImportService(OzonHandoverRepository repository, OrderRepository orderRepository,
 521        IOzonHandoverPhotoStorage photoStorage)
 22    {
 523        _repository = repository;
 524        _orderRepository = orderRepository;
 525        _photoStorage = photoStorage;
 526    }
 27
 28    public async Task<OzonHandoverImportResult> ImportAsync(OzonHandoverImportRequest request, CancellationToken ct = de
 29    {
 730        var itemNumbers = request.Groups
 731            .Where(g => !IsFbo(g.ReturnType ?? request.ReturnType ?? request.Closed.ReturnType))
 632            .SelectMany(g => g.Acts)
 633            .Where(a => !IsFbo(a.ReturnType ?? request.ReturnType ?? request.Closed.ReturnType))
 634            .SelectMany(a => a.Items)
 635            .Select(i => i.ItemNo)
 636            .Where(n => !string.IsNullOrEmpty(n))
 737            .Distinct()
 738            .ToList();
 39
 740        var orderIdsByItemNo = await _orderRepository.GetOrderIdsByShipmentNumbersAsync(itemNumbers);
 41
 742        var batch = new OzonHandoverImportBatch
 743        {
 744            DocumentType = request.DocumentType,
 745            GeneratedAt = request.GeneratedAt,
 746            ReturnType = request.ReturnType,
 747            ClosedKind = request.Closed.Kind,
 748            ClosedId = request.Closed.Id,
 749            SummaryActsCount = request.Summary?.ActsCount ?? 0,
 750            SummaryItemsCount = request.Summary?.ItemsCount ?? 0,
 751            SummaryTotalValue = request.Summary?.TotalValue ?? 0,
 752            SummaryAcceptedItemsCount = request.Summary?.Accepted?.ItemsCount ?? 0,
 753            SummaryAcceptedValue = request.Summary?.Accepted?.TotalValue ?? 0,
 754            SummaryRejectedItemsCount = request.Summary?.Rejected?.ItemsCount ?? 0,
 755            SummaryRejectedValue = request.Summary?.Rejected?.TotalValue ?? 0,
 756            SummaryPendingItemsCount = request.Summary?.Pending?.ItemsCount ?? 0,
 757            SummaryPendingValue = request.Summary?.Pending?.TotalValue ?? 0,
 758            SummaryDefectCount = request.Summary?.DefectCount ?? 0,
 759            SummaryMissortCount = request.Summary?.MissortCount ?? 0,
 760            RawJson = JsonSerializer.Serialize(request, RawJsonOptions)
 761        };
 62
 763        var unmatchedItemNumbers = new List<string>();
 764        var matchedItems = new List<(string ItemNo, int OrderId)>();
 765        var warnings = new List<string>();
 766        var itemsCount = 0;
 767        var savedPhotosCount = 0;
 768        var failedPhotosCount = 0;
 69
 2870        foreach (var group in request.Groups)
 71        {
 2872            foreach (var actDto in group.Acts)
 73            {
 774                var act = new OzonHandoverAct
 775                {
 776                    GroupKey = group.GroupKey,
 777                    GroupName = group.GroupName,
 778                    ActNo = actDto.ActNo,
 779                    ActDate = actDto.ActDate,
 780                    ActType = actDto.ActType,
 781                    Pvz = actDto.Pvz,
 782                    AccountLabel = actDto.Account,
 783                    Source = actDto.Source,
 784                    ReturnType = actDto.ReturnType ?? group.ReturnType ?? request.ReturnType ?? request.Closed.ReturnTyp
 785                    BoxBarcode = actDto.BoxBarcode,
 786                    ReturnId = actDto.ReturnId
 787                };
 88
 2889                foreach (var itemDto in actDto.Items)
 90                {
 791                    itemsCount++;
 92
 793                    var item = new OzonHandoverActItem
 794                    {
 795                        RowNo = itemDto.RowNo,
 796                        ItemNo = itemDto.ItemNo,
 797                        ProductName = itemDto.ProductName,
 798                        Barcode = itemDto.Barcode,
 799                        Value = itemDto.Value,
 7100                        ValueApi = itemDto.ValueApi,
 7101                        WeightG = itemDto.WeightG,
 7102                        Status = itemDto.Status,
 7103                        Defect = itemDto.Defect,
 7104                        Missort = itemDto.Missort,
 7105                        Comment = itemDto.Comment,
 7106                        MarkedAt = itemDto.MarkedAt,
 7107                        Sku = itemDto.Sku,
 7108                        OfferId = itemDto.OfferId,
 7109                        AccountLabel = itemDto.Account,
 7110                        ReturnId = itemDto.ReturnId,
 7111                        OzonStatus = itemDto.OzonStatus,
 7112                        OzonStatusName = itemDto.OzonStatusName,
 7113                        ReturnType = itemDto.ReturnType ?? act.ReturnType,
 7114                        BoxBarcode = itemDto.BoxBarcode,
 7115                        Reason = itemDto.Reason,
 7116                        ReasonIsDefect = itemDto.ReasonIsDefect,
 7117                        ClientComment = itemDto.ClientComment,
 7118                        ClientPhotosExpiresAt = itemDto.ClientPhotosExpiresAt,
 7119                        PhotosCount = itemDto.PhotosCount,
 7120                        PhotosError = itemDto.PhotosError,
 7121                        RejectionComment = itemDto.RejectionComment,
 7122                        RejectionReason = itemDto.RejectionReason,
 7123                        RuPostTracking = itemDto.RuPostTracking,
 7124                        ReturnNumber = itemDto.ReturnNumber,
 7125                        OrderNumber = itemDto.OrderNumber,
 7126                        ClientReturnMethod = itemDto.ClientReturnMethod,
 7127                        WarehouseId = itemDto.WarehouseId
 7128                    };
 129
 7130                    if (!IsFbo(item.ReturnType) && !string.IsNullOrEmpty(itemDto.ItemNo) &&
 7131                        orderIdsByItemNo.TryGetValue(itemDto.ItemNo, out var orderId))
 132                    {
 2133                        item.OrderId = orderId;
 2134                        matchedItems.Add((itemDto.ItemNo, orderId));
 135                    }
 136                    else
 137                    {
 5138                        unmatchedItemNumbers.Add(itemDto.ItemNo);
 139                    }
 140
 7141                    var photoResult = await StorePhotosAsync(itemDto, request.Closed.Id, item, ct).ConfigureAwait(false)
 7142                    savedPhotosCount += photoResult.SavedCount;
 7143                    failedPhotosCount += photoResult.FailedCount;
 7144                    if (photoResult.Warnings.Count > 0)
 145                    {
 1146                        item.PhotosError = JoinErrors(item.PhotosError, photoResult.Warnings);
 1147                        warnings.AddRange(photoResult.Warnings);
 148                    }
 149
 7150                    act.Items.Add(item);
 7151                }
 152
 7153                batch.Acts.Add(act);
 7154            }
 7155        }
 156
 7157        batch.MatchedItemsCount = matchedItems.Count;
 7158        batch.UnmatchedItemsCount = unmatchedItemNumbers.Count;
 159
 7160        var existing = !string.IsNullOrEmpty(request.Closed.Id)
 7161            ? await _repository.GetByClosedIdAsync(request.Closed.Id, ct)
 7162            : null;
 163
 7164        var saved = await _repository.UpsertBatchAsync(batch, ct);
 165
 7166        var orderInfoById = await _orderRepository.GetHandoverMatchInfoByIdsAsync(
 9167            matchedItems.Select(m => m.OrderId));
 168
 7169        var matchedOrders = matchedItems
 7170            .Select(m =>
 7171            {
 2172                orderInfoById.TryGetValue(m.OrderId, out var info);
 2173                return new OzonHandoverMatchedOrderDto
 2174                {
 2175                    ItemNo = m.ItemNo,
 2176                    OrderId = m.OrderId,
 2177                    OrderNumber = info?.ShipmentNumber,
 2178                    Article = info?.Article,
 2179                    ClientName = info?.ClientName,
 2180                    ClientStatus = info?.ClientStatusName,
 2181                    ClientStatusColor = info?.ClientStatusColor,
 2182                    AppStatus = info?.AppStatusName,
 2183                    AppStatusColor = info?.AppStatusColor
 2184                };
 7185            })
 7186            .ToList();
 187
 7188        var result = new OzonHandoverImportResult
 7189        {
 7190            BatchId = saved.Id,
 7191            WasUpdated = existing != null,
 7192            ActsCount = saved.Acts.Count,
 7193            ItemsCount = itemsCount,
 7194            MatchedItemsCount = matchedItems.Count,
 7195            UnmatchedItemsCount = unmatchedItemNumbers.Count,
 7196            UnmatchedItemNumbers = unmatchedItemNumbers,
 7197            MatchedOrders = matchedOrders,
 7198            SavedPhotosCount = savedPhotosCount,
 7199            FailedPhotosCount = failedPhotosCount,
 7200            Warnings = warnings
 7201        };
 202
 7203        await NotifyAsync(result);
 204
 7205        return result;
 7206    }
 207
 208    private async Task<PhotoStoreResult> StorePhotosAsync(OzonHandoverItemDto dto, string batchId,
 209        OzonHandoverActItem item, CancellationToken ct)
 210    {
 7211        if (dto.ClientPhotos.Count == 0)
 2212            return PhotoStoreResult.Empty;
 213
 5214        var result = new PhotoStoreResult();
 5215        var itemKey = dto.ReturnId?.ToString() ?? $"{dto.ItemNo}|{dto.RowNo}";
 20216        for (var index = 0; index < dto.ClientPhotos.Count; index++)
 217        {
 218            try
 219            {
 5220                var stored = await _photoStorage.DownloadAndStoreAsync(dto.ClientPhotos[index], batchId, itemKey, index 
 5221                    .ConfigureAwait(false);
 4222                item.Photos.Add(new OzonHandoverActItemPhoto
 4223                {
 4224                    Sequence = index + 1,
 4225                    StoragePath = stored.StoragePath,
 4226                    ContentType = stored.ContentType
 4227                });
 4228                result.SavedCount++;
 4229            }
 1230            catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException ||
 1231                                       (ex is TaskCanceledException && !ct.IsCancellationRequested))
 232            {
 1233                result.FailedCount++;
 1234                result.Warnings.Add($"Фото возврата {itemKey} №{index + 1} не сохранено: {ex.Message}");
 1235            }
 236        }
 237
 5238        return result;
 7239    }
 240
 20241    private static bool IsFbo(string? returnType) => string.Equals(returnType, "fbo", StringComparison.OrdinalIgnoreCase
 242
 243    private static string JoinErrors(string? existing, IEnumerable<string> errors)
 244    {
 3245        var all = new[] { existing }.Concat(errors).Where(x => !string.IsNullOrWhiteSpace(x));
 1246        return string.Join("\n", all);
 247    }
 248
 249    private sealed class PhotoStoreResult
 250    {
 1251        public static readonly PhotoStoreResult Empty = new();
 15252        public int SavedCount { get; set; }
 9253        public int FailedCount { get; set; }
 16254        public List<string> Warnings { get; } = new();
 255    }
 256
 257    /// <summary>
 258    /// Уведомление в чат-виджет (не блокирует импорт при сбое отправки) — срабатывает
 259    /// одинаково и для машинного POST /api/ozon-handover/import, и для ручной загрузки
 260    /// файла на /Studio2/Orders/Upload, поскольку обе точки входа вызывают этот сервис.
 261    /// </summary>
 262    private async Task NotifyAsync(OzonHandoverImportResult result)
 263    {
 264        try
 265        {
 7266            var action = result.WasUpdated ? "обновлён" : "создан";
 7267            var text = $"Импорт выдачи Ozon: батч #{result.BatchId} {action}, " +
 7268                       $"актов {result.ActsCount}, позиций {result.ItemsCount}, " +
 7269                       $"сопоставлено с заказами {result.MatchedItemsCount}" +
 7270                       (result.UnmatchedItemsCount > 0 ? $", без пары {result.UnmatchedItemsCount}" : "") + ".";
 271
 7272            await NotificationService.NotifyAsync("Ozon", "#2563EB", text, "fa-box-open");
 0273        }
 7274        catch
 275        {
 276            // Сбой отправки уведомления не должен ронять сам импорт.
 7277        }
 7278    }
 279}