< Summary

Information
Class: Services.PartnerServices.PartnerOrdersService
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/PartnerServices/PartnerOrdersService.cs
Line coverage
100%
Covered lines: 135
Uncovered lines: 0
Coverable lines: 135
Total lines: 251
Line coverage: 100%
Branch coverage
76%
Covered branches: 32
Total branches: 42
Branch coverage: 76.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ListAsync()100%11100%
GetAsync()100%22100%
GetFiltersAsync()100%11100%
SuggestAsync()83.33%66100%
GetExistingLabelAsync()66.66%66100%
PreviewAsync()100%11100%
VisibleOrdersAsync()100%11100%
ApplyFilters(...)77.77%1818100%
ApplySearch(...)100%11100%
.ctor(...)100%11100%
get_Id()100%11100%
get_ImageUrl()100%11100%
get_ClientId()100%11100%
get_TrackingNumber()100%11100%
Project(...)100%11100%
ToDto(...)50%22100%
WithLabelsAsync()100%22100%
ToOptions(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/PartnerServices/PartnerOrdersService.cs

#LineLine coverage
 1using Microsoft.EntityFrameworkCore;
 2using OzonDomains.Models;
 3using OzonDomains.Models.Partners;
 4using OzonRepositories.Context;
 5using Services.LabelServices;
 6
 7namespace Services.PartnerServices;
 8
 9public interface IPartnerOrdersService
 10{
 11    Task<PartnerPage<PartnerOrderListItemDto>> ListAsync(long partnerId, PartnerOrderQuery query, CancellationToken ct =
 12
 13    /// <summary>Заказ, если он попадает под правила партнёра. Иначе null — даже если заказ существует.</summary>
 14    Task<PartnerOrderDetailsDto?> GetAsync(long partnerId, int orderId, CancellationToken ct = default);
 15
 16    /// <summary>Значения для фильтров: только те статусы, склады и клиенты, что есть среди видимых партнёру заказов.</s
 17    Task<PartnerOrderFilters> GetFiltersAsync(long partnerId, CancellationToken ct = default);
 18
 19    /// <summary>Быстрые подсказки по номеру, товару, артикулу, трек-номеру и ScanIt в рамках scope партнёра.</summary>
 20    Task<IReadOnlyList<PartnerOrderSuggestion>> SuggestAsync(long partnerId, string? query, int limit = PartnerOrdersSer
 21
 22    /// <summary>Уже готовая этикетка из хранилища. Новую не создаёт и в Ozon не ходит.</summary>
 23    Task<PartnerLabelFile?> GetExistingLabelAsync(long partnerId, int orderId, CancellationToken ct = default);
 24
 25    /// <summary>
 26    /// Предпросмотр набора правил, ещё не сохранённых: сколько заказов подойдёт и id первых из них (новые сверху),
 27    /// чтобы владелец мог открыть их в таблице заказов.
 28    /// </summary>
 29    Task<PartnerRulesPreview> PreviewAsync(IReadOnlyList<PartnerRuleDto> rules, int idsLimit = PartnerOrdersService.Prev
 30}
 31
 2432public sealed class PartnerOrdersService(
 2433    ApplicationDbContext db,
 2434    ILabelExistenceService labelExistence,
 2435    ILabelFileStorage labelStorage) : IPartnerOrdersService
 36{
 37    internal const int MaxPageSize = 100;
 38    /// <summary>Больше подсказок в выпадающем списке не помогают выбрать заказ и только нагружают БД.</summary>
 39    public const int MaxSuggestions = 10;
 40
 41    /// <summary>Сколько id отдаём в ссылку на таблицу заказов: адрес с большим списком перестаёт проходить через сервер
 42    public const int PreviewIdsLimit = 500;
 43
 44    public async Task<PartnerPage<PartnerOrderListItemDto>> ListAsync(
 45        long partnerId, PartnerOrderQuery query, CancellationToken ct = default)
 46    {
 2647        var page = Math.Max(1, query.Page);
 2648        var pageSize = Math.Clamp(query.PageSize, 1, MaxPageSize);
 49
 2650        var visible = await VisibleOrdersAsync(partnerId, ct);
 2651        var filtered = ApplyFilters(visible, query);
 52
 2653        var total = await filtered.CountAsync(ct);
 54        // Порядок задаём ДО проекции в DTO: ссылка на свойство DTO в OrderBy на MariaDB не переводится в SQL.
 2655        var rows = (await Project(filtered
 2656                    .OrderByDescending(o => o.ProcessingDate)
 2657                    .ThenByDescending(o => o.Id)
 2658                    .Skip((page - 1) * pageSize)
 2659                    .Take(pageSize))
 2660                .ToListAsync(ct))
 2661            .Select(ToDto)
 2662            .ToList();
 63
 2664        var items = await WithLabelsAsync(rows);
 2665        return new PartnerPage<PartnerOrderListItemDto>(items, total, page, pageSize);
 2666    }
 67
 68    public async Task<PartnerOrderDetailsDto?> GetAsync(long partnerId, int orderId, CancellationToken ct = default)
 69    {
 570        var one = (await VisibleOrdersAsync(partnerId, ct)).Where(o => o.Id == orderId);
 71
 572        var row = await Project(one).FirstOrDefaultAsync(ct);
 773        if (row is null) return null;
 374        var item = ToDto(row);
 75
 376        var extra = await one
 377            .Select(o => new
 378            {
 379                o.DeliveryCity,
 380                Barcode = o.ProductInfo != null ? o.ProductInfo.Barcode : null,
 381                o.Comment,
 382                o.LastStatusChangeDate,
 383                o.Scanit
 384            })
 385            .FirstAsync(ct);
 86
 387        var withLabel = (await WithLabelsAsync([item]))[0];
 388        return new PartnerOrderDetailsDto(withLabel, extra.DeliveryCity, extra.Barcode, extra.Comment, extra.LastStatusC
 589    }
 90
 91    public async Task<PartnerOrderFilters> GetFiltersAsync(long partnerId, CancellationToken ct = default)
 92    {
 193        var visible = await VisibleOrdersAsync(partnerId, ct);
 94
 195        var statuses = await visible.Where(o => o.AppStatus != null)
 196            .Select(o => new { o.AppStatus!.Id, o.AppStatus.Name }).Distinct().ToListAsync(ct);
 197        var ozonStatuses = await visible.Where(o => o.ClientStatus != null && o.ClientStatus.Name != null)
 198            .Select(o => o.ClientStatus!.Name!).Distinct().OrderBy(x => x).ToListAsync(ct);
 199        var warehouses = await visible.Where(o => o.ShipmentWarehouse != null)
 1100            .Select(o => new { o.ShipmentWarehouse!.Id, o.ShipmentWarehouse.Name }).Distinct().ToListAsync(ct);
 1101        var clients = await visible.Where(o => o.OzonClient != null)
 1102            .Select(o => new { o.OzonClient!.Id, o.OzonClient.Name }).Distinct().ToListAsync(ct);
 103
 1104        return new PartnerOrderFilters(
 105            ToOptions(statuses.Select(x => (x.Id, (string?)x.Name))),
 106            ToOptions(warehouses.Select(x => (x.Id, (string?)x.Name))),
 107            ToOptions(clients.Select(x => (x.Id, (string?)x.Name))),
 1108            ozonStatuses);
 1109    }
 110
 111    public async Task<IReadOnlyList<PartnerOrderSuggestion>> SuggestAsync(
 112        long partnerId, string? query, int limit = MaxSuggestions, CancellationToken ct = default)
 113    {
 2114        var search = query?.Trim();
 115        // Один символ почти всегда даёт широкий скан и бесполезные подсказки. На фронте запрос начинается от 2 символов
 3116        if (string.IsNullOrWhiteSpace(search) || search.Length < 2) return [];
 117
 1118        var visible = await VisibleOrdersAsync(partnerId, ct);
 1119        var rows = await Project(ApplySearch(visible, search)
 1120                .OrderByDescending(o => o.ProcessingDate).ThenByDescending(o => o.Id)
 1121                .Take(Math.Clamp(limit, 1, MaxSuggestions)))
 1122            .ToListAsync(ct);
 123
 124        return rows.Select(r => new PartnerOrderSuggestion(
 125            r.Id, r.ShipmentNumber, r.ProductName, r.Article, r.TrackingNumber, r.Scanit, r.OzonStatus)).ToList();
 2126    }
 127
 128    public async Task<PartnerLabelFile?> GetExistingLabelAsync(long partnerId, int orderId, CancellationToken ct = defau
 129    {
 3130        var visible = await VisibleOrdersAsync(partnerId, ct);
 3131        var shipment = await visible.Where(o => o.Id == orderId).Select(o => o.ShipmentNumber).FirstOrDefaultAsync(ct);
 5132        if (string.IsNullOrWhiteSpace(shipment)) return null;
 133
 1134        var candidates = new[] { shipment, OzonPostingNumberHelper.GetOzonApiPostingNumber(shipment) }
 135            .Where(x => !string.IsNullOrWhiteSpace(x))
 136            .Select(x => $"{x}.pdf")
 1137            .Distinct(StringComparer.OrdinalIgnoreCase)
 1138            .ToList();
 139
 1140        var fileName = await labelStorage.FindExistingFileNameAsync(candidates, ct);
 1141        if (fileName is null) return null;
 142
 1143        var bytes = await labelStorage.TryDownloadAsync(fileName, ct);
 1144        return bytes is null ? null : new PartnerLabelFile(bytes, fileName);
 3145    }
 146
 147    public async Task<PartnerRulesPreview> PreviewAsync(
 148        IReadOnlyList<PartnerRuleDto> rules, int idsLimit = PreviewIdsLimit, CancellationToken ct = default)
 149    {
 150        var entities = rules.Select(r => new PartnerRule
 151        {
 152            Name = r.Name,
 153            IsActive = r.IsActive,
 154            Conditions = r.Conditions.Select(c => new PartnerRuleCondition
 155            {
 156                Field = c.Field,
 157                ValuesJson = System.Text.Json.JsonSerializer.Serialize(c.Values.Select(v => v.Trim()).ToList())
 158            }).ToList()
 159        }).ToList();
 160
 4161        var matched = PartnerOrderScope.Apply(db.Orders.AsNoTracking(), entities);
 4162        var total = await matched.CountAsync(ct);
 4163        var limit = Math.Clamp(idsLimit, 1, PreviewIdsLimit);
 4164        var ids = await matched
 4165            .OrderByDescending(o => o.ProcessingDate).ThenByDescending(o => o.Id)
 4166            .Select(o => o.Id).Take(limit).ToListAsync(ct);
 167
 4168        return new PartnerRulesPreview(total, ids, total > ids.Count, limit);
 4169    }
 170
 171    private async Task<IQueryable<Order>> VisibleOrdersAsync(long partnerId, CancellationToken ct)
 172    {
 36173        var rules = await db.PartnerRules.AsNoTracking()
 36174            .Where(r => r.PartnerId == partnerId && r.IsActive)
 36175            .Include(r => r.Conditions)
 36176            .ToListAsync(ct);
 36177        return PartnerOrderScope.Apply(db.Orders.AsNoTracking(), rules);
 36178    }
 179
 180    private static IQueryable<Order> ApplyFilters(IQueryable<Order> q, PartnerOrderQuery f)
 181    {
 26182        if (f.StatusId is { } s) q = q.Where(o => o.AppStatusId == s);
 27183        if (f.WarehouseId is { } w) q = q.Where(o => o.ShipmentWarehouseId == w);
 26184        if (f.ClientId is { } c) q = q.Where(o => o.OzonClientId == c);
 26185        var ozonStatus = f.OzonStatus?.Trim();
 28186        if (!string.IsNullOrEmpty(ozonStatus)) q = q.Where(o => o.ClientStatus != null && o.ClientStatus.Name == ozonSta
 26187        if (f.From is { } from) q = q.Where(o => o.ProcessingDate >= from.Date);
 26188        if (f.To is { } to) q = q.Where(o => o.ProcessingDate < to.Date.AddDays(1));
 189
 26190        return string.IsNullOrEmpty(f.Search?.Trim()) ? q : ApplySearch(q, f.Search.Trim());
 191    }
 192
 193    private static IQueryable<Order> ApplySearch(IQueryable<Order> q, string search) =>
 4194        q.Where(o =>
 4195            (o.ShipmentNumber != null && o.ShipmentNumber.Contains(search)) ||
 4196            (o.Article != null && o.Article.Contains(search)) ||
 4197            (o.ProductKey != null && o.ProductKey.Contains(search)) ||
 4198            (o.ProductName != null && o.ProductName.Contains(search)) ||
 4199            (o.TrackingNumber != null && o.TrackingNumber.Contains(search)) ||
 4200            (o.Scanit != null && o.Scanit.Contains(search)));
 201
 202    /// <summary>Строка выборки: enum маркетплейса переводим в текст уже после запроса (Enum.ToString в SQL не переводим
 43203    private sealed record OrderRow(
 295204        int Id, string? ShipmentNumber, string? ProductName, string? Article, string? Brand, int? Quantity, decimal? Pri
 248205        string? ImageUrl, int? StatusId, string? Status, string? OzonStatus, int? WarehouseId, string? Warehouse,
 205206        int? ClientId, string? Client, OzonDomains.ClientType? ClientType, DateTime? ProcessingDate, DateTime? ShippingD
 129207        string? TrackingNumber, string? Scanit);
 208
 209    private static IQueryable<OrderRow> Project(IQueryable<Order> q) =>
 32210        q.Select(o => new OrderRow(
 32211            o.Id,
 32212            o.ShipmentNumber,
 32213            o.ProductName,
 32214            // Артикул для партнёра — ключ товара «АРТИКУЛ=КОД» (как offer_id в Ozon, например 122RU19=HSH); нет ключа —
 32215            !string.IsNullOrEmpty(o.ProductKey) ? o.ProductKey : o.Article,
 32216            o.Manufacturer != null ? o.Manufacturer.Name : null,
 32217            o.Quantity,
 32218            o.Price,
 32219            o.ProductInfo != null ? o.ProductInfo.ImageUrl : null,
 32220            o.AppStatusId,
 32221            o.AppStatus != null ? o.AppStatus.Name : null,
 32222            o.ClientStatus != null ? o.ClientStatus.Name : null,
 32223            o.ShipmentWarehouseId,
 32224            o.ShipmentWarehouse != null ? o.ShipmentWarehouse.Name : null,
 32225            o.OzonClientId,
 32226            o.OzonClient != null ? o.OzonClient.Name : null,
 32227            o.OzonClient != null ? o.OzonClient.ClientType : (OzonDomains.ClientType?)null,
 32228            o.ProcessingDate,
 32229            o.ShippingDate,
 32230            o.TrackingNumber,
 32231            o.Scanit));
 232
 41233    private static PartnerOrderListItemDto ToDto(OrderRow r) => new(
 41234        r.Id, r.ShipmentNumber, r.ProductName, r.Article, r.Brand, r.Quantity, r.Price, r.ImageUrl,
 41235        r.StatusId, r.Status, r.OzonStatus, r.WarehouseId, r.Warehouse, r.ClientId, r.Client,
 41236        r.ClientType?.ToString(), r.ProcessingDate, r.ShippingDate, r.TrackingNumber, false, r.Scanit);
 237
 238    private async Task<IReadOnlyList<PartnerOrderListItemDto>> WithLabelsAsync(IReadOnlyList<PartnerOrderListItemDto> ro
 239    {
 240        var numbers = rows.Select(r => r.ShipmentNumber).Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!).ToLis
 35241        if (numbers.Count == 0) return rows;
 242
 23243        var exists = await labelExistence.CheckBatchAsync(numbers);
 23244        return rows
 41245            .Select(r => r.ShipmentNumber is { } n && exists.TryGetValue(n, out var has) && has ? r with { HasLabel = tr
 23246            .ToList();
 29247    }
 248
 249    private static IReadOnlyList<PartnerFilterOption> ToOptions(IEnumerable<(int Id, string? Name)> source) =>
 11250        source.OrderBy(x => x.Name).Select(x => new PartnerFilterOption(x.Id, x.Name ?? x.Id.ToString())).ToList();
 251}