| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Options; |
| | | 3 | | using OzonRepositories.Context; |
| | | 4 | | |
| | | 5 | | namespace OzonOrdersWeb.WebServices.SystemAlerts; |
| | | 6 | | |
| | | 7 | | /// <summary>Строка отчёта BitrixStock, которую Ozon отклонил при накате остатков (этап 2).</summary> |
| | | 8 | | public sealed class OzonApplyFailureRow |
| | | 9 | | { |
| | 25 | 10 | | public string ReportId { get; set; } = string.Empty; |
| | 16 | 11 | | public int OzonClientId { get; set; } |
| | 22 | 12 | | public string ClientName { get; set; } = string.Empty; |
| | 25 | 13 | | public string Offer { get; set; } = string.Empty; |
| | 16 | 14 | | public int OneCQty { get; set; } |
| | 18 | 15 | | public string? Error { get; set; } |
| | | 16 | | } |
| | | 17 | | |
| | | 18 | | public enum OzonApplyFailureKind |
| | | 19 | | { |
| | | 20 | | Other = 0, |
| | | 21 | | Archived, |
| | | 22 | | Attributes |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | /// <summary> |
| | | 26 | | /// Контроль наката остатков в Ozon (этап 2 BitrixStock): товары в архиве и товары с ошибкой атрибутов карточки |
| | | 27 | | /// не принимают остаток, пока их не поправят вручную. Лимит частоты обновления сюда не входит — он проходит на следующе |
| | | 28 | | /// Смотрим последний отчёт с накатом по каждому клиенту: когда ошибки исчезают из него, алерт закрывается. |
| | | 29 | | /// </summary> |
| | | 30 | | public sealed class OzonStockApplyAlertSource(ApplicationDbContext context, IOptions<SystemAlertsOptions> options) : ISy |
| | | 31 | | { |
| | | 32 | | public const string WindowKey = "ozon-apply-errors"; |
| | | 33 | | private const int MaxExamples = 8; |
| | | 34 | | |
| | | 35 | | public async Task<IReadOnlyList<SystemAlertFinding>> CollectAsync(CancellationToken cancellationToken = default) |
| | | 36 | | { |
| | | 37 | | var settings = options.Value; |
| | | 38 | | if (!settings.StockChecksEnabled) return []; |
| | | 39 | | |
| | | 40 | | var since = DateTime.UtcNow.AddHours(-settings.WindowHoursFor(WindowKey)); |
| | | 41 | | var rows = await context.Database.SqlQuery<OzonApplyFailureRow>($@" |
| | | 42 | | SELECT l.ReportId AS ReportId, COALESCE(l.OzonClientId, 0) AS OzonClientId, COALESCE(l.OzonClientName, '') AS ClientName |
| | | 43 | | COALESCE(NULLIF(l.OfferId, ''), l.Article) AS Offer, l.OneCQty AS OneCQty, l.OzonApplyError AS Error |
| | | 44 | | FROM BitrixStockSyncReportLines l |
| | | 45 | | JOIN BitrixStockSyncReports r ON r.ReportId = l.ReportId |
| | | 46 | | WHERE l.OzonApplyStatus = 'Failed' |
| | | 47 | | AND r.Status = 'Completed' |
| | | 48 | | AND r.GeneratedAtUtc >= {since} |
| | | 49 | | AND NOT EXISTS ( |
| | | 50 | | SELECT 1 FROM BitrixStockSyncReports r2 |
| | | 51 | | WHERE r2.OzonClientId = r.OzonClientId AND r2.Status = 'Completed' |
| | | 52 | | AND r2.OzonAppliedSuccessCount + r2.OzonApplyFailedCount > 0 |
| | | 53 | | AND r2.GeneratedAtUtc > r.GeneratedAtUtc)").ToListAsync(cancellationToken); |
| | | 54 | | return BuildFindings(rows); |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | public static OzonApplyFailureKind Classify(string? error) |
| | | 58 | | { |
| | | 59 | | if (string.IsNullOrWhiteSpace(error)) return OzonApplyFailureKind.Other; |
| | | 60 | | if (error.Contains("archived product", StringComparison.OrdinalIgnoreCase)) return OzonApplyFailureKind.Archived |
| | | 61 | | if (error.Contains("ошибка атрибутов", StringComparison.OrdinalIgnoreCase) || |
| | | 62 | | error.Contains("tags validation failed", StringComparison.OrdinalIgnoreCase)) return OzonApplyFailureKind.At |
| | | 63 | | return OzonApplyFailureKind.Other; |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <summary>Группирует отказы по клиенту и типу: на каждую пару — одно предупреждение со ссылкой на отчёт наката.</ |
| | | 67 | | public static IReadOnlyList<SystemAlertFinding> BuildFindings(IEnumerable<OzonApplyFailureRow> rows) |
| | | 68 | | { |
| | | 69 | | var findings = new List<SystemAlertFinding>(); |
| | | 70 | | var groups = rows |
| | | 71 | | .Select(x => (Row: x, Kind: Classify(x.Error))) |
| | | 72 | | .Where(x => x.Kind != OzonApplyFailureKind.Other) |
| | | 73 | | .GroupBy(x => (x.Row.OzonClientId, x.Kind)) |
| | | 74 | | .OrderBy(g => g.Key.OzonClientId).ThenBy(g => g.Key.Kind); |
| | | 75 | | |
| | | 76 | | foreach (var group in groups) |
| | | 77 | | { |
| | | 78 | | var failures = group.Select(x => x.Row).ToList(); |
| | | 79 | | var offers = failures.Select(x => x.Offer).Where(x => !string.IsNullOrWhiteSpace(x)) |
| | | 80 | | .Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); |
| | | 81 | | var qty = failures.Sum(x => x.OneCQty); |
| | | 82 | | var reportId = failures.Select(x => x.ReportId).OrderByDescending(x => x, StringComparer.Ordinal).First(); |
| | | 83 | | var client = failures.Select(x => x.ClientName).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)) ?? $"клие |
| | | 84 | | var archived = group.Key.Kind == OzonApplyFailureKind.Archived; |
| | | 85 | | |
| | | 86 | | var title = archived |
| | | 87 | | ? $"Накат остатков в Ozon: товары в архиве не принимают остаток — {offers.Count} ({client})" |
| | | 88 | | : $"Накат остатков в Ozon: «Не продаётся (ошибка атрибутов)» — {offers.Count} ({client})"; |
| | | 89 | | var lines = new List<string> |
| | | 90 | | { |
| | | 91 | | $"Артикулов: {offers.Count}; по 1С к накату: {qty} шт.", |
| | | 92 | | archived |
| | | 93 | | ? "Ozon: Can't set positive stock to archived product — верните товары из архива или исключите их из |
| | | 94 | | : "Ozon не принимает остаток из-за ошибки атрибутов карточки — исправьте карточки в кабинете Ozon.", |
| | | 95 | | "Примеры: " + string.Join(", ", offers.Take(MaxExamples)) + (offers.Count > MaxExamples ? "…" : string.E |
| | | 96 | | }; |
| | | 97 | | |
| | | 98 | | findings.Add(new SystemAlertFinding( |
| | | 99 | | $"ozon-apply-{(archived ? "archived" : "attributes")}-c{group.Key.OzonClientId}", "stock", "warning", |
| | | 100 | | title, lines, SystemAlertLinks.BitrixSyncReportFailures(reportId, archived ? "archived product" : "ошибк |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | return findings; |
| | | 104 | | } |
| | | 105 | | } |