| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using Microsoft.Extensions.Logging; |
| | | 3 | | using OzonDomains.Models; |
| | | 4 | | using OzonDomains.Models.SupplierStatuses; |
| | | 5 | | using OzonRepositories.Context; |
| | | 6 | | |
| | | 7 | | namespace Services.SupplierStatusServices; |
| | | 8 | | |
| | | 9 | | public interface IOrderSupplierStateService |
| | | 10 | | { |
| | | 11 | | /// <summary>Заказ попал в корзину поставщика либо сменил статус внутри корзины.</summary> |
| | | 12 | | Task MarkInCartAsync( |
| | | 13 | | int orderId, string providerKey, string cartStatusCode, |
| | | 14 | | long? cartItemId = null, string? externalBasketItemId = null, |
| | | 15 | | string? user = null, |
| | | 16 | | int? deliveryDaysMin = null, int? deliveryDaysMax = null, string? direction = null, |
| | | 17 | | CancellationToken ct = default); |
| | | 18 | | |
| | | 19 | | /// <summary>Заказ оформлен у поставщика: стадия переходит в Ordered, фиксируется внешний номер.</summary> |
| | | 20 | | Task MarkOrderedAsync( |
| | | 21 | | int orderId, string providerKey, string? externalOrderNumber, |
| | | 22 | | string? externalOrderId = null, string? externalLineId = null, |
| | | 23 | | string? user = null, |
| | | 24 | | int? deliveryDaysMin = null, int? deliveryDaysMax = null, string? direction = null, |
| | | 25 | | CancellationToken ct = default); |
| | | 26 | | |
| | | 27 | | /// <summary>Позиция ушла из корзины до оформления — состояние закрывается.</summary> |
| | | 28 | | Task RemoveFromCartAsync(int orderId, string providerKey, string? user = null, CancellationToken ct = default); |
| | | 29 | | |
| | | 30 | | /// <summary>Применяет результаты опроса поставщика к строкам состояния.</summary> |
| | | 31 | | Task<int> ApplySnapshotsAsync( |
| | | 32 | | string providerKey, IReadOnlyList<SupplierStatusSnapshot> snapshots, CancellationToken ct = default); |
| | | 33 | | |
| | | 34 | | /// <summary>Помечает строки как опрошенные — чтобы они уступили очередь следующему батчу.</summary> |
| | | 35 | | Task MarkSyncedAsync(IReadOnlyCollection<long> stateIds, string? error, CancellationToken ct = default); |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Единственная точка записи в OrderSupplierStates и OrderSupplierStateHistory. |
| | | 40 | | /// Все изменения стадии и статуса проходят здесь, поэтому история всегда полная. |
| | | 41 | | /// </summary> |
| | | 42 | | public sealed class OrderSupplierStateService : IOrderSupplierStateService |
| | | 43 | | { |
| | | 44 | | /// <summary>Маркер записи в orders_history — по нему таймлайн заказа рисует статус поставщика.</summary> |
| | | 45 | | public const string HistoryColumnName = "SupplierOrderStatus"; |
| | | 46 | | |
| | | 47 | | /// <summary>Маркер записи в orders_history о перезаписи номера заказа у поставщика — признак возможного дубля заказ |
| | | 48 | | public const string OrderNumberChangedColumnName = "SupplierOrderNumberChanged"; |
| | | 49 | | |
| | | 50 | | private readonly ApplicationDbContext _db; |
| | | 51 | | private readonly ISupplierStatusResolver _resolver; |
| | | 52 | | private readonly ILogger<OrderSupplierStateService> _logger; |
| | | 53 | | |
| | 32 | 54 | | public OrderSupplierStateService( |
| | 32 | 55 | | ApplicationDbContext db, |
| | 32 | 56 | | ISupplierStatusResolver resolver, |
| | 32 | 57 | | ILogger<OrderSupplierStateService> logger) |
| | | 58 | | { |
| | 32 | 59 | | _db = db; |
| | 32 | 60 | | _resolver = resolver; |
| | 32 | 61 | | _logger = logger; |
| | 32 | 62 | | } |
| | | 63 | | |
| | | 64 | | public async Task MarkInCartAsync( |
| | | 65 | | int orderId, string providerKey, string cartStatusCode, |
| | | 66 | | long? cartItemId = null, string? externalBasketItemId = null, |
| | | 67 | | string? user = null, |
| | | 68 | | int? deliveryDaysMin = null, int? deliveryDaysMax = null, string? direction = null, |
| | | 69 | | CancellationToken ct = default) |
| | | 70 | | { |
| | | 71 | | // Removed — это выход из корзины, а не состояние внутри неё. |
| | 9 | 72 | | if (cartStatusCode == SupplierCartStatusCode.Removed) |
| | | 73 | | { |
| | 1 | 74 | | await RemoveFromCartAsync(orderId, providerKey, user, ct); |
| | 1 | 75 | | return; |
| | | 76 | | } |
| | | 77 | | |
| | 8 | 78 | | var state = await GetActiveAsync(orderId, providerKey, ct); |
| | 8 | 79 | | var status = await _resolver.ResolveAsync( |
| | 8 | 80 | | providerKey, SupplierStatusKind.Cart, cartStatusCode, CartStatusFallbackName(cartStatusCode), ct); |
| | | 81 | | |
| | 8 | 82 | | if (state is null) |
| | | 83 | | { |
| | 7 | 84 | | state = new OrderSupplierState |
| | 7 | 85 | | { |
| | 7 | 86 | | OrderId = orderId, |
| | 7 | 87 | | ActiveOrderId = orderId, |
| | 7 | 88 | | ProviderKey = providerKey, |
| | 7 | 89 | | SupplierId = status.SupplierId, |
| | 7 | 90 | | Stage = OrderSupplierStage.Cart, |
| | 7 | 91 | | CreatedBy = user, |
| | 7 | 92 | | CreatedAt = DateTime.UtcNow |
| | 7 | 93 | | }; |
| | 7 | 94 | | _db.OrderSupplierStates.Add(state); |
| | | 95 | | } |
| | | 96 | | |
| | 8 | 97 | | if (cartItemId.HasValue) state.CartItemId = cartItemId; |
| | 8 | 98 | | if (!string.IsNullOrWhiteSpace(externalBasketItemId)) state.ExternalBasketItemId = externalBasketItemId; |
| | 11 | 99 | | if (deliveryDaysMin.HasValue) state.DeliveryDaysMin = deliveryDaysMin; |
| | 11 | 100 | | if (deliveryDaysMax.HasValue) state.DeliveryDaysMax = deliveryDaysMax; |
| | 11 | 101 | | if (!string.IsNullOrWhiteSpace(direction)) state.Direction = Trim(direction, 255); |
| | | 102 | | |
| | 8 | 103 | | await ApplyStatusAsync(state, status, OrderSupplierStage.Cart, OrderSupplierStateSource.Checkout, user, null, ct |
| | 9 | 104 | | } |
| | | 105 | | |
| | | 106 | | public async Task MarkOrderedAsync( |
| | | 107 | | int orderId, string providerKey, string? externalOrderNumber, |
| | | 108 | | string? externalOrderId = null, string? externalLineId = null, |
| | | 109 | | string? user = null, |
| | | 110 | | int? deliveryDaysMin = null, int? deliveryDaysMax = null, string? direction = null, |
| | | 111 | | CancellationToken ct = default) |
| | | 112 | | { |
| | 20 | 113 | | var state = await GetActiveAsync(orderId, providerKey, ct); |
| | 20 | 114 | | var status = await _resolver.ResolveAsync( |
| | 20 | 115 | | providerKey, SupplierStatusKind.Cart, SupplierCartStatusCode.Ordered, |
| | 20 | 116 | | CartStatusFallbackName(SupplierCartStatusCode.Ordered), ct); |
| | | 117 | | |
| | 20 | 118 | | if (state is null) |
| | | 119 | | { |
| | | 120 | | // Заказ мог быть оформлен без прохождения через нашу корзину — например, |
| | | 121 | | // импортом ручного заказа ML-Auto. Строку создаём сразу в стадии Ordered. |
| | 13 | 122 | | state = new OrderSupplierState |
| | 13 | 123 | | { |
| | 13 | 124 | | OrderId = orderId, |
| | 13 | 125 | | ActiveOrderId = orderId, |
| | 13 | 126 | | ProviderKey = providerKey, |
| | 13 | 127 | | SupplierId = status.SupplierId, |
| | 13 | 128 | | Stage = OrderSupplierStage.Ordered, |
| | 13 | 129 | | CreatedBy = user, |
| | 13 | 130 | | CreatedAt = DateTime.UtcNow |
| | 13 | 131 | | }; |
| | 13 | 132 | | _db.OrderSupplierStates.Add(state); |
| | | 133 | | } |
| | | 134 | | |
| | 20 | 135 | | var previousOrderNumber = state.ExternalOrderNumber; |
| | 39 | 136 | | if (!string.IsNullOrWhiteSpace(externalOrderNumber)) state.ExternalOrderNumber = Trim(externalOrderNumber, 50); |
| | 21 | 137 | | if (!string.IsNullOrWhiteSpace(externalOrderId)) state.ExternalOrderId = Trim(externalOrderId, 50); |
| | 20 | 138 | | if (!string.IsNullOrWhiteSpace(externalLineId)) state.ExternalLineId = Trim(externalLineId, 50); |
| | 20 | 139 | | if (deliveryDaysMin.HasValue) state.DeliveryDaysMin = deliveryDaysMin; |
| | 20 | 140 | | if (deliveryDaysMax.HasValue) state.DeliveryDaysMax = deliveryDaysMax; |
| | 20 | 141 | | if (!string.IsNullOrWhiteSpace(direction)) state.Direction = Trim(direction, 255); |
| | | 142 | | // Фиксируется один раз, при первом переходе в Ordered — в отличие от LastStatusChangeAt |
| | | 143 | | // не смещается последующими сменами статуса от API, поэтому годится как база для прогноза даты доставки. |
| | 20 | 144 | | state.OrderedAt ??= DateTime.UtcNow; |
| | | 145 | | // Статус заказа ещё не известен — до первого опроса показываем «Заказан у поставщика». |
| | 20 | 146 | | state.LastSyncedAt = null; |
| | 20 | 147 | | state.SyncError = null; |
| | | 148 | | |
| | 20 | 149 | | var comment = string.IsNullOrWhiteSpace(externalOrderNumber) |
| | 20 | 150 | | ? null |
| | 20 | 151 | | : $"Заказ у поставщика: {externalOrderNumber}"; |
| | | 152 | | |
| | 20 | 153 | | await ApplyStatusAsync(state, status, OrderSupplierStage.Ordered, OrderSupplierStateSource.Checkout, user, comme |
| | | 154 | | |
| | | 155 | | // Повторное оформление молча заменило бы номер первого заказа у поставщика, и тот остался бы |
| | | 156 | | // «потерянным» — оставляем след в истории, чтобы дубль можно было заметить и отменить. |
| | 20 | 157 | | if (IsOrderNumberOverwritten(previousOrderNumber, state.ExternalOrderNumber)) |
| | | 158 | | { |
| | 1 | 159 | | await RecordOrderNumberOverwriteAsync(state, previousOrderNumber!, state.ExternalOrderNumber!, user, ct); |
| | | 160 | | } |
| | 20 | 161 | | } |
| | | 162 | | |
| | | 163 | | public async Task RemoveFromCartAsync(int orderId, string providerKey, string? user = null, CancellationToken ct = d |
| | | 164 | | { |
| | 2 | 165 | | var state = await GetActiveAsync(orderId, providerKey, ct); |
| | | 166 | | // Закрываем только то, что ещё не оформлено: оформленный заказ из корзины уже ушёл |
| | | 167 | | // и продолжает отслеживаться по API. |
| | 3 | 168 | | if (state is null || state.Stage != OrderSupplierStage.Cart) return; |
| | | 169 | | |
| | 1 | 170 | | var status = await _resolver.ResolveAsync( |
| | 1 | 171 | | providerKey, SupplierStatusKind.Cart, SupplierCartStatusCode.Removed, |
| | 1 | 172 | | CartStatusFallbackName(SupplierCartStatusCode.Removed), ct); |
| | | 173 | | |
| | 1 | 174 | | await ApplyStatusAsync(state, status, OrderSupplierStage.Closed, OrderSupplierStateSource.Checkout, user, null, |
| | 2 | 175 | | } |
| | | 176 | | |
| | | 177 | | public async Task<int> ApplySnapshotsAsync( |
| | | 178 | | string providerKey, IReadOnlyList<SupplierStatusSnapshot> snapshots, CancellationToken ct = default) |
| | | 179 | | { |
| | 7 | 180 | | if (snapshots.Count == 0) return 0; |
| | | 181 | | |
| | 14 | 182 | | var orderIds = snapshots.Select(x => x.OrderId).Distinct().ToList(); |
| | 7 | 183 | | var states = await _db.OrderSupplierStates |
| | 7 | 184 | | .Where(x => x.ProviderKey == providerKey && x.ActiveOrderId != null && orderIds.Contains(x.OrderId)) |
| | 7 | 185 | | .ToListAsync(ct); |
| | 14 | 186 | | var byOrderId = states.ToDictionary(x => x.OrderId); |
| | | 187 | | |
| | 7 | 188 | | var changed = 0; |
| | 28 | 189 | | foreach (var snapshot in snapshots) |
| | | 190 | | { |
| | 7 | 191 | | if (!byOrderId.TryGetValue(snapshot.OrderId, out var state)) continue; |
| | | 192 | | |
| | 7 | 193 | | if (!string.IsNullOrWhiteSpace(snapshot.Error)) |
| | | 194 | | { |
| | 1 | 195 | | state.SyncError = Trim(snapshot.Error, 1000); |
| | 1 | 196 | | state.LastSyncedAt = DateTime.UtcNow; |
| | 1 | 197 | | state.UpdatedAt = DateTime.UtcNow; |
| | 1 | 198 | | continue; |
| | | 199 | | } |
| | | 200 | | |
| | 6 | 201 | | var status = await _resolver.ResolveAsync( |
| | 6 | 202 | | providerKey, snapshot.Kind, snapshot.ExternalCode, snapshot.StatusName, ct); |
| | | 203 | | |
| | 6 | 204 | | if (!string.IsNullOrWhiteSpace(snapshot.ExternalOrderNumber)) |
| | 0 | 205 | | state.ExternalOrderNumber = Trim(snapshot.ExternalOrderNumber, 50); |
| | 6 | 206 | | if (!string.IsNullOrWhiteSpace(snapshot.ExternalOrderId)) |
| | 0 | 207 | | state.ExternalOrderId = Trim(snapshot.ExternalOrderId, 50); |
| | 6 | 208 | | if (!string.IsNullOrWhiteSpace(snapshot.ExternalLineId)) |
| | 0 | 209 | | state.ExternalLineId = Trim(snapshot.ExternalLineId, 50); |
| | 6 | 210 | | if (snapshot.QuantityConfirmed.HasValue) state.QuantityConfirmed = snapshot.QuantityConfirmed; |
| | 6 | 211 | | if (snapshot.QuantityShipped.HasValue) state.QuantityShipped = snapshot.QuantityShipped; |
| | 6 | 212 | | if (snapshot.QuantityRejected.HasValue) state.QuantityRejected = snapshot.QuantityRejected; |
| | 6 | 213 | | if (snapshot.ExpectedDeliveryAt.HasValue) state.ExpectedDeliveryAt = snapshot.ExpectedDeliveryAt; |
| | | 214 | | |
| | 6 | 215 | | state.SyncError = null; |
| | 6 | 216 | | state.LastSyncedAt = DateTime.UtcNow; |
| | | 217 | | |
| | | 218 | | // Конечный статус закрывает отслеживание: строка выходит из-под уникального индекса |
| | | 219 | | // и перестаёт попадать в выборку джобы. Removed в корзине закрываем всегда, как и в |
| | | 220 | | // MarkInCartAsync/RemoveFromCartAsync — это выход из корзины, а не состояние внутри неё, |
| | | 221 | | // справочник IsFinal тут не авторитетен (обычно не проставлен для карточных статусов). |
| | 6 | 222 | | var isCartRemoved = snapshot.Kind == SupplierStatusKind.Cart && snapshot.ExternalCode == SupplierCartStatusC |
| | 6 | 223 | | var targetStage = status.IsFinal || isCartRemoved |
| | 6 | 224 | | ? OrderSupplierStage.Closed |
| | 6 | 225 | | : snapshot.Kind == SupplierStatusKind.Cart ? OrderSupplierStage.Cart : OrderSupplierStage.Ordered; |
| | | 226 | | |
| | 6 | 227 | | if (await ApplyStatusAsync( |
| | 6 | 228 | | state, status, targetStage, OrderSupplierStateSource.Job, null, |
| | 6 | 229 | | snapshot.StatusChangedAt.HasValue ? $"Статус поставщика от {snapshot.StatusChangedAt:dd.MM.yyyy HH:m |
| | 6 | 230 | | ct, snapshot.StatusChangedAt)) |
| | | 231 | | { |
| | 5 | 232 | | changed++; |
| | | 233 | | } |
| | 6 | 234 | | } |
| | | 235 | | |
| | 7 | 236 | | await _db.SaveChangesAsync(ct); |
| | 7 | 237 | | return changed; |
| | 7 | 238 | | } |
| | | 239 | | |
| | | 240 | | public async Task MarkSyncedAsync(IReadOnlyCollection<long> stateIds, string? error, CancellationToken ct = default) |
| | | 241 | | { |
| | 0 | 242 | | if (stateIds.Count == 0) return; |
| | | 243 | | |
| | 0 | 244 | | var ids = stateIds.ToList(); |
| | 0 | 245 | | var now = DateTime.UtcNow; |
| | 0 | 246 | | var trimmedError = Trim(error, 1000); |
| | | 247 | | |
| | 0 | 248 | | await _db.OrderSupplierStates |
| | 0 | 249 | | .Where(x => ids.Contains(x.Id)) |
| | 0 | 250 | | .ExecuteUpdateAsync(s => s |
| | 0 | 251 | | .SetProperty(x => x.LastSyncedAt, now) |
| | 0 | 252 | | .SetProperty(x => x.SyncError, trimmedError), ct); |
| | 0 | 253 | | } |
| | | 254 | | |
| | | 255 | | /// <summary> |
| | | 256 | | /// Ставит статус и стадию, пишет историю и запись в таймлайн заказа. |
| | | 257 | | /// Возвращает true, если состояние действительно изменилось. |
| | | 258 | | /// </summary> |
| | | 259 | | private async Task<bool> ApplyStatusAsync( |
| | | 260 | | OrderSupplierState state, SupplierStatus status, string targetStage, |
| | | 261 | | string source, string? user, string? comment, CancellationToken ct, |
| | | 262 | | DateTime? statusChangedAt = null) |
| | | 263 | | { |
| | 35 | 264 | | var statusUnchanged = state.SupplierStatusId == status.Id; |
| | 35 | 265 | | var stageUnchanged = state.Stage == targetStage; |
| | 35 | 266 | | if (statusUnchanged && stageUnchanged && state.Id != 0) |
| | | 267 | | { |
| | 5 | 268 | | state.UpdatedAt = DateTime.UtcNow; |
| | 5 | 269 | | await _db.SaveChangesAsync(ct); |
| | 5 | 270 | | return false; |
| | | 271 | | } |
| | | 272 | | |
| | 30 | 273 | | var fromStatusId = state.SupplierStatusId; |
| | 30 | 274 | | var fromStage = state.Id == 0 ? null : state.Stage; |
| | 30 | 275 | | var fromStatusName = fromStatusId.HasValue |
| | 30 | 276 | | ? await _db.SupplierStatuses.Where(x => x.Id == fromStatusId).Select(x => x.Name).FirstOrDefaultAsync(ct) |
| | 30 | 277 | | : null; |
| | | 278 | | |
| | 30 | 279 | | state.SupplierStatusId = status.Id; |
| | 30 | 280 | | state.Stage = targetStage; |
| | 30 | 281 | | state.LastStatusChangeAt = statusChangedAt ?? DateTime.UtcNow; |
| | 30 | 282 | | state.UpdatedAt = DateTime.UtcNow; |
| | | 283 | | // Закрытая строка освобождает уникальный индекс, оставаясь в таблице для истории. |
| | 30 | 284 | | state.ActiveOrderId = targetStage == OrderSupplierStage.Closed ? null : state.OrderId; |
| | | 285 | | |
| | 30 | 286 | | await _db.SaveChangesAsync(ct); |
| | | 287 | | |
| | 30 | 288 | | var timelineEntry = new OrderHistory |
| | 30 | 289 | | { |
| | 30 | 290 | | OrderId = state.OrderId, |
| | 30 | 291 | | ColumnName = HistoryColumnName, |
| | 30 | 292 | | ColumnDisplayName = $"Статус поставщика · {DescribeProvider(state.ProviderKey)}", |
| | 30 | 293 | | OldValue = fromStatusName, |
| | 30 | 294 | | NewValue = status.Name, |
| | 30 | 295 | | ChangedAt = DateTime.Now, |
| | 30 | 296 | | ChangedBy = Trim(user ?? $"Система: {state.ProviderKey}", 255) |
| | 30 | 297 | | }; |
| | | 298 | | |
| | 30 | 299 | | _db.OrderHistories.Add(timelineEntry); |
| | 30 | 300 | | _db.OrderSupplierStateHistory.Add(new OrderSupplierStateHistory |
| | 30 | 301 | | { |
| | 30 | 302 | | OrderSupplierStateId = state.Id, |
| | 30 | 303 | | OrderId = state.OrderId, |
| | 30 | 304 | | OrderHistory = timelineEntry, |
| | 30 | 305 | | FromSupplierStatusId = fromStatusId, |
| | 30 | 306 | | ToSupplierStatusId = status.Id, |
| | 30 | 307 | | FromStage = fromStage, |
| | 30 | 308 | | ToStage = targetStage, |
| | 30 | 309 | | Comment = Trim(comment, 500), |
| | 30 | 310 | | Source = source, |
| | 30 | 311 | | PerformedAt = DateTime.UtcNow, |
| | 30 | 312 | | PerformedBy = Trim(user, 450) |
| | 30 | 313 | | }); |
| | | 314 | | |
| | 30 | 315 | | await _db.SaveChangesAsync(ct); |
| | | 316 | | |
| | 30 | 317 | | _logger.LogInformation( |
| | 30 | 318 | | "Заказ {OrderId}: статус у {ProviderKey} '{From}' -> '{To}' (стадия {Stage})", |
| | 30 | 319 | | state.OrderId, state.ProviderKey, fromStatusName ?? "—", status.Name, targetStage); |
| | | 320 | | |
| | 30 | 321 | | return true; |
| | 35 | 322 | | } |
| | | 323 | | |
| | | 324 | | /// <summary>Номера у Армтека приходят списком через запятую; перестановка и пробелы заменой не считаются.</summary> |
| | | 325 | | private static bool IsOrderNumberOverwritten(string? previous, string? current) |
| | | 326 | | { |
| | 36 | 327 | | if (string.IsNullOrWhiteSpace(previous) || string.IsNullOrWhiteSpace(current)) return false; |
| | | 328 | | |
| | 8 | 329 | | static HashSet<string> Split(string value) => value |
| | 8 | 330 | | .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) |
| | 8 | 331 | | .ToHashSet(StringComparer.OrdinalIgnoreCase); |
| | | 332 | | |
| | 4 | 333 | | return !Split(previous).SetEquals(Split(current)); |
| | | 334 | | } |
| | | 335 | | |
| | | 336 | | private async Task RecordOrderNumberOverwriteAsync( |
| | | 337 | | OrderSupplierState state, string previousNumber, string currentNumber, string? user, CancellationToken ct) |
| | | 338 | | { |
| | 1 | 339 | | var provider = DescribeProvider(state.ProviderKey); |
| | 1 | 340 | | var warning = |
| | 1 | 341 | | $"Внимание: номер заказа у поставщика перезаписан ({previousNumber} → {currentNumber}). " + |
| | 1 | 342 | | $"Возможно, заказ у {provider} оформлен повторно — проверьте и отмените лишний у поставщика."; |
| | | 343 | | |
| | 1 | 344 | | var timelineEntry = new OrderHistory |
| | 1 | 345 | | { |
| | 1 | 346 | | OrderId = state.OrderId, |
| | 1 | 347 | | ColumnName = OrderNumberChangedColumnName, |
| | 1 | 348 | | ColumnDisplayName = $"Номер заказа у поставщика · {provider}", |
| | 1 | 349 | | OldValue = previousNumber, |
| | 1 | 350 | | NewValue = currentNumber, |
| | 1 | 351 | | ChangedAt = DateTime.Now, |
| | 1 | 352 | | ChangedBy = Trim(user ?? $"Система: {state.ProviderKey}", 255) |
| | 1 | 353 | | }; |
| | | 354 | | |
| | 1 | 355 | | _db.OrderHistories.Add(timelineEntry); |
| | 1 | 356 | | _db.OrderSupplierStateHistory.Add(new OrderSupplierStateHistory |
| | 1 | 357 | | { |
| | 1 | 358 | | OrderSupplierStateId = state.Id, |
| | 1 | 359 | | OrderId = state.OrderId, |
| | 1 | 360 | | OrderHistory = timelineEntry, |
| | 1 | 361 | | FromSupplierStatusId = state.SupplierStatusId, |
| | 1 | 362 | | ToSupplierStatusId = state.SupplierStatusId, |
| | 1 | 363 | | FromStage = state.Stage, |
| | 1 | 364 | | ToStage = state.Stage, |
| | 1 | 365 | | Comment = Trim(warning, 500), |
| | 1 | 366 | | Source = OrderSupplierStateSource.Checkout, |
| | 1 | 367 | | PerformedAt = DateTime.UtcNow, |
| | 1 | 368 | | PerformedBy = Trim(user, 450) |
| | 1 | 369 | | }); |
| | | 370 | | |
| | 1 | 371 | | await _db.SaveChangesAsync(ct); |
| | | 372 | | |
| | 1 | 373 | | _logger.LogWarning( |
| | 1 | 374 | | "Заказ {OrderId}: номер заказа у {ProviderKey} перезаписан '{From}' -> '{To}', возможен дубль", |
| | 1 | 375 | | state.OrderId, state.ProviderKey, previousNumber, currentNumber); |
| | 1 | 376 | | } |
| | | 377 | | |
| | | 378 | | private Task<OrderSupplierState?> GetActiveAsync(int orderId, string providerKey, CancellationToken ct) => |
| | 30 | 379 | | _db.OrderSupplierStates.FirstOrDefaultAsync( |
| | 30 | 380 | | x => x.ProviderKey == providerKey && x.ActiveOrderId == orderId, ct); |
| | | 381 | | |
| | 134 | 382 | | public static string DescribeProvider(string providerKey) => providerKey switch |
| | 134 | 383 | | { |
| | 86 | 384 | | "Armtek" => "Армтек RU", |
| | 0 | 385 | | "ArmtekBy" => "Армтек РБ", |
| | 28 | 386 | | "Avd" => "АВД", |
| | 20 | 387 | | "MlAuto" => "ML-Auto РБ", |
| | 0 | 388 | | "MlAutoRu" => "ML-Auto RU", |
| | 0 | 389 | | "ForumAuto" => "Форум-Авто (Союз)", |
| | 0 | 390 | | "ForumAutoInterparts" => "Форум-Авто (Интерпартс)", |
| | 0 | 391 | | "ForumAutoPiter" => "Форум-Авто (Питер)", |
| | 0 | 392 | | "ForumAutoRostov" => "Форум-Авто (Ростов)", |
| | 0 | 393 | | "ForumAutoIstra" => "Форум-Авто (Истра)", |
| | 0 | 394 | | "ProfitLiga" => "Профит-Лига", |
| | 0 | 395 | | "ZZapMoscow" => "ZZap Москва", |
| | 0 | 396 | | "FavoritParts" => "FavoritParts", |
| | 0 | 397 | | "FavoritPartsIstra" => "Фаворит Истра", |
| | 0 | 398 | | "FavoritPartsRostov" => "Фаворит Ростов", |
| | 0 | 399 | | "MoskvorechieIstra" => "Москворечье (Истра)", |
| | 0 | 400 | | "Motex" => "МоТехС", |
| | 0 | 401 | | "Nikei" => "Nikei", |
| | 0 | 402 | | "MikadoMskHod20" => "Mikado", |
| | 0 | 403 | | "Japarts" => "Japarts", |
| | 0 | 404 | | "Berg" => "Berg", |
| | 0 | 405 | | "ShateM" => "Шате-М", |
| | 0 | 406 | | _ => providerKey |
| | 134 | 407 | | }; |
| | | 408 | | |
| | | 409 | | /// <summary> |
| | | 410 | | /// Имя для автосоздания статуса корзины, если сид не применялся. При применённом сиде |
| | | 411 | | /// резолвер найдёт запись по коду и это значение не используется. |
| | | 412 | | /// </summary> |
| | 29 | 413 | | private static string CartStatusFallbackName(string code) => code switch |
| | 29 | 414 | | { |
| | 7 | 415 | | SupplierCartStatusCode.Pending => "В корзине, нужна проверка", |
| | 1 | 416 | | SupplierCartStatusCode.Ready => "Проверен, готов к заказу", |
| | 0 | 417 | | SupplierCartStatusCode.Processing => "Оформляется", |
| | 20 | 418 | | SupplierCartStatusCode.Ordered => "Заказан у поставщика", |
| | 0 | 419 | | SupplierCartStatusCode.Failed => "Ошибка проверки", |
| | 1 | 420 | | SupplierCartStatusCode.Removed => "Удалён из корзины", |
| | 0 | 421 | | _ => code |
| | 29 | 422 | | }; |
| | | 423 | | |
| | | 424 | | private static string? Trim(string? value, int maxLength) => |
| | 117 | 425 | | string.IsNullOrWhiteSpace(value) ? null : value.Length <= maxLength ? value : value[..maxLength]; |
| | | 426 | | } |