| | | 1 | | using System.Text.Encodings.Web; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using System.Text.Unicode; |
| | | 4 | | |
| | | 5 | | namespace Services.SupplierStatusServices; |
| | | 6 | | |
| | | 7 | | /// <summary>Итог независимой проверки заказа у API-поставщика после проведения транзакции.</summary> |
| | | 8 | | public static class SupplierVerificationStatus |
| | | 9 | | { |
| | | 10 | | /// <summary>Поставщик подтвердил позицию/заказ повторным запросом в реальный API.</summary> |
| | | 11 | | public const string Confirmed = "Confirmed"; |
| | | 12 | | |
| | | 13 | | /// <summary>Поставщик ответил, но данные не сошлись (нет номера, отказ по количеству, другой номер).</summary> |
| | | 14 | | public const string Mismatch = "Mismatch"; |
| | | 15 | | |
| | | 16 | | /// <summary>Поставщик не вернул позицию/заказ или позиции нет в его корзине.</summary> |
| | | 17 | | public const string NotFound = "NotFound"; |
| | | 18 | | |
| | | 19 | | /// <summary>API недоступен или вернул ошибку — проверить не удалось.</summary> |
| | | 20 | | public const string ApiError = "ApiError"; |
| | | 21 | | |
| | | 22 | | /// <summary>Проверить нельзя: у поставщика ещё ничего нет (локальная корзина) либо нет провайдера чтения.</summary> |
| | | 23 | | public const string NotVerifiable = "NotVerifiable"; |
| | | 24 | | |
| | | 25 | | public static bool IsProblem(string status) => status is Mismatch or NotFound or ApiError; |
| | | 26 | | |
| | | 27 | | public static string Title(string status) => status switch |
| | | 28 | | { |
| | | 29 | | Confirmed => "Подтверждено у поставщика", |
| | | 30 | | Mismatch => "Расхождение с поставщиком", |
| | | 31 | | NotFound => "Не найдено у поставщика", |
| | | 32 | | ApiError => "Поставщик не ответил", |
| | | 33 | | NotVerifiable => "Проверка невозможна", |
| | | 34 | | _ => status |
| | | 35 | | }; |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary>Результат проверки одного заказа Studio2 у одного поставщика.</summary> |
| | | 39 | | public sealed record SupplierVerificationRow( |
| | | 40 | | int OrderId, |
| | | 41 | | string? ShipmentNumber, |
| | | 42 | | string? Article, |
| | | 43 | | string? ProductName, |
| | | 44 | | string ProviderKey, |
| | | 45 | | string ProviderName, |
| | | 46 | | string Stage, |
| | | 47 | | string Status, |
| | | 48 | | string Summary, |
| | | 49 | | string? SupplierOrderNumber, |
| | | 50 | | string? SupplierStatusName, |
| | | 51 | | IReadOnlyList<string> Checks, |
| | | 52 | | DateTime VerifiedAt) |
| | | 53 | | { |
| | | 54 | | public bool IsProblem => SupplierVerificationStatus.IsProblem(Status); |
| | | 55 | | } |
| | | 56 | | |
| | | 57 | | /// <summary>Сводка проверки по всему проведению.</summary> |
| | | 58 | | public sealed record SupplierVerificationSummary(IReadOnlyList<SupplierVerificationRow> Rows) |
| | | 59 | | { |
| | | 60 | | public static readonly SupplierVerificationSummary Empty = new(Array.Empty<SupplierVerificationRow>()); |
| | | 61 | | |
| | | 62 | | public bool IsEmpty => Rows.Count == 0; |
| | | 63 | | |
| | | 64 | | public int ConfirmedCount => Rows.Count(x => x.Status == SupplierVerificationStatus.Confirmed); |
| | | 65 | | |
| | | 66 | | public int ProblemCount => Rows.Count(x => x.IsProblem); |
| | | 67 | | |
| | | 68 | | public int NotVerifiableCount => Rows.Count(x => x.Status == SupplierVerificationStatus.NotVerifiable); |
| | | 69 | | |
| | | 70 | | public bool HasProblems => ProblemCount > 0; |
| | | 71 | | |
| | | 72 | | /// <summary>Короткая строка для toast: «Поставщики: подтверждено 3, проблем 1».</summary> |
| | | 73 | | public string ToShortText() |
| | | 74 | | { |
| | | 75 | | if (IsEmpty) return string.Empty; |
| | | 76 | | var parts = new List<string> { $"подтверждено {ConfirmedCount}" }; |
| | | 77 | | if (ProblemCount > 0) parts.Add($"проблем {ProblemCount}"); |
| | | 78 | | if (NotVerifiableCount > 0) parts.Add($"не проверяется {NotVerifiableCount}"); |
| | | 79 | | return "Проверка у поставщиков: " + string.Join(", ", parts); |
| | | 80 | | } |
| | | 81 | | } |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Запись в orders_history о проверке заказа у поставщика. Пишется системой (ChangedBy = «System: …»), |
| | | 85 | | /// JSON лежит в NewValue. Поля после <see cref="Summary"/> обязаны оставаться nullable со значением по умолчанию: |
| | | 86 | | /// уже записанные строки не должны ломаться при расширении формата. |
| | | 87 | | /// </summary> |
| | 13 | 88 | | public sealed record SupplierVerificationHistoryEntry( |
| | 12 | 89 | | string ProviderKey, |
| | 12 | 90 | | string SupplierDisplayName, |
| | 12 | 91 | | string Stage, |
| | 13 | 92 | | string Status, |
| | 12 | 93 | | string Summary, |
| | 13 | 94 | | string? SupplierOrderNumber = null, |
| | 12 | 95 | | string? SupplierStatusName = null, |
| | 12 | 96 | | IReadOnlyList<string>? Checks = null, |
| | 12 | 97 | | string? ShipmentNumber = null, |
| | 25 | 98 | | DateTime? VerifiedAt = null) |
| | | 99 | | { |
| | | 100 | | /// <summary>Маркер в orders_history.ColumnName — по нему таймлайн истории рисует проверку поставщика. |
| | | 101 | | /// Rollback игнорирует нераспознанные ColumnName, поэтому откат такие записи не затрагивает.</summary> |
| | | 102 | | public const string HistoryColumnName = "SupplierOrderVerification"; |
| | | 103 | | |
| | | 104 | | /// <summary>Автор записи: содержит «system», поэтому история показывает её как системную.</summary> |
| | | 105 | | public const string SystemAuthor = "System: проверка поставщика"; |
| | | 106 | | |
| | | 107 | | // По умолчанию System.Text.Json экранирует кириллицу как \uXXXX — в БД должен лежать читаемый текст. |
| | 1 | 108 | | public static readonly JsonSerializerOptions JsonOptions = new() |
| | 1 | 109 | | { |
| | 1 | 110 | | Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) |
| | 1 | 111 | | }; |
| | | 112 | | |
| | 1 | 113 | | private static readonly JsonSerializerOptions ReadOptions = new() { PropertyNameCaseInsensitive = true }; |
| | | 114 | | |
| | 12 | 115 | | public static SupplierVerificationHistoryEntry FromRow(SupplierVerificationRow row) => new( |
| | 12 | 116 | | row.ProviderKey, row.ProviderName, row.Stage, row.Status, row.Summary, row.SupplierOrderNumber, |
| | 12 | 117 | | row.SupplierStatusName, row.Checks, row.ShipmentNumber, row.VerifiedAt); |
| | | 118 | | |
| | 12 | 119 | | public string ToJson() => JsonSerializer.Serialize(this, JsonOptions); |
| | | 120 | | |
| | | 121 | | public static SupplierVerificationHistoryEntry? TryParse(string? json) |
| | | 122 | | { |
| | 1 | 123 | | if (string.IsNullOrWhiteSpace(json)) return null; |
| | | 124 | | try |
| | | 125 | | { |
| | 1 | 126 | | return JsonSerializer.Deserialize<SupplierVerificationHistoryEntry>(json, ReadOptions); |
| | | 127 | | } |
| | 0 | 128 | | catch (JsonException) |
| | | 129 | | { |
| | 0 | 130 | | return null; |
| | | 131 | | } |
| | 1 | 132 | | } |
| | | 133 | | |
| | | 134 | | /// <summary>Плоский текст для поиска по истории заказа.</summary> |
| | 0 | 135 | | public string BuildFilterText() => string.Join(" ", new[] |
| | 0 | 136 | | { |
| | 0 | 137 | | SupplierDisplayName, SupplierVerificationStatus.Title(Status), Summary, SupplierOrderNumber, |
| | 0 | 138 | | SupplierStatusName, ShipmentNumber |
| | 0 | 139 | | } |
| | 0 | 140 | | .Where(x => !string.IsNullOrWhiteSpace(x))); |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | /// <summary>Русские названия провайдеров для сводки и истории.</summary> |
| | | 144 | | public static class SupplierProviderNames |
| | | 145 | | { |
| | | 146 | | public static string Describe(string providerKey) => providerKey switch |
| | | 147 | | { |
| | | 148 | | "Armtek" => "Армтек RU", |
| | | 149 | | "ArmtekBy" => "Армтек РБ", |
| | | 150 | | "Avd" => "АВД", |
| | | 151 | | "MlAuto" => "ML-Auto РБ", |
| | | 152 | | "MlAutoRu" => "ML-Auto RU", |
| | | 153 | | "ForumAuto" => "Форум-Авто (Союз)", |
| | | 154 | | "ForumAutoInterparts" => "Форум-Авто (Интерпартс)", |
| | | 155 | | "ForumAutoPiter" => "Форум-Авто (Питер)", |
| | | 156 | | "ForumAutoRostov" => "Форум-Авто (Ростов)", |
| | | 157 | | "ForumAutoIstra" => "Форум-Авто (Истра)", |
| | | 158 | | "FavoritParts" => "Фаворит Союз", |
| | | 159 | | "FavoritPartsIstra" => "Фаворит Истра", |
| | | 160 | | "FavoritPartsRostov" => "Фаворит Ростов", |
| | | 161 | | "ProfitLiga" => "Профит-Лига", |
| | | 162 | | "ZZapMoscow" => "ZZap Москва", |
| | | 163 | | "Berg" => "Берг", |
| | | 164 | | "Japarts" => "Japarts", |
| | | 165 | | "Motex" => "МоТехС", |
| | | 166 | | "MoskvorechieIstra" => "Москворечье Истра", |
| | | 167 | | "ShateM" => "Шате-М", |
| | | 168 | | "MikadoMskHod20" => "Микадо МскХод20", |
| | | 169 | | "Nikei" => "Nikei", |
| | | 170 | | _ => string.IsNullOrWhiteSpace(providerKey) ? "API-поставщик" : providerKey |
| | | 171 | | }; |
| | | 172 | | } |