| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Net; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | using Microsoft.Extensions.Logging; |
| | | 5 | | using Microsoft.Extensions.Logging.Abstractions; |
| | | 6 | | using Newtonsoft.Json; |
| | | 7 | | using Newtonsoft.Json.Linq; |
| | | 8 | | using OzonDomains.Models; |
| | | 9 | | using OzonRepositories.Context; |
| | | 10 | | using Services.ApiServices.MotexApi; |
| | | 11 | | using Services.SupplierStatusServices; |
| | | 12 | | |
| | | 13 | | namespace OzonOrdersWeb.TransactionServices; |
| | | 14 | | |
| | | 15 | | public sealed record MotexCartQueueItem(int OrderId, string Code, string Brand, string? ProductName, string? StoreCode, |
| | | 16 | | string? StoreInfo, string? ShipmentNumber, decimal Quantity, decimal? SourcePrice, string? CurrencyCode, |
| | | 17 | | string? DeliveryAtUtc, int? DeliveryDaysMin, int? DeliveryDaysMax, string? Direction); |
| | | 18 | | |
| | | 19 | | public sealed record MotexCartResult(int Added, int Skipped, int Failed, IReadOnlyList<string> Messages) |
| | | 20 | | { |
| | | 21 | | public bool IsSuccess => Failed == 0; |
| | | 22 | | public string ToHtmlBlock() |
| | | 23 | | { |
| | | 24 | | var css = Failed > 0 ? "alert-warning" : "alert-success"; |
| | | 25 | | var list = string.Join(string.Empty, Messages.Select(x => $"<li>{WebUtility.HtmlEncode(x)}</li>")); |
| | | 26 | | return $"<div class='alert {css} mb-0'><strong>МоТехС: добавлено {Added}, уже было {Skipped}, ошибок {Failed}.</ |
| | | 27 | | } |
| | | 28 | | } |
| | | 29 | | |
| | | 30 | | public sealed class MotexCartService |
| | | 31 | | { |
| | | 32 | | private readonly ApplicationDbContext _db; |
| | | 33 | | private readonly MotexDataManager _api; |
| | | 34 | | private readonly IOrderSupplierStateService _supplierState; |
| | | 35 | | private readonly ILogger<MotexCartService> _logger; |
| | 38 | 36 | | public MotexCartService(ApplicationDbContext db, MotexDataManager api, IOrderSupplierStateService supplierState, |
| | 76 | 37 | | ILogger<MotexCartService>? logger = null) => (_db, _api, _supplierState, _logger) = |
| | 38 | 38 | | (db, api, supplierState, logger ?? NullLogger<MotexCartService>.Instance); |
| | | 39 | | |
| | 1 | 40 | | public Task<int> CountActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.CountAsync(x => MotexCartIte |
| | 1 | 41 | | public Task<List<MotexCartItem>> GetActiveAsync(CancellationToken ct = default) => _db.MotexCartItems.Include(x => x |
| | 1 | 42 | | public Task<List<MotexOrder>> GetOrdersAsync(CancellationToken ct = default) => _db.MotexOrders.Include(x => x.Items |
| | | 43 | | |
| | | 44 | | public async Task<MotexCartResult> QueueAsync(IReadOnlyCollection<MotexCartQueueItem> queue, string user, Cancellati |
| | | 45 | | { |
| | 60 | 46 | | var added = 0; var skipped = 0; var failed = 0; var messages = new List<string>(); |
| | 62 | 47 | | foreach (var item in queue) |
| | | 48 | | { |
| | 43 | 49 | | if (!IsValid(item)) { failed++; messages.Add("Некорректное предложение МоТехС."); continue; } |
| | 13 | 50 | | if (await _db.MotexCartItems.AnyAsync(x => x.ActiveStudioOrderId == item.OrderId, ct)) { skipped++; messages |
| | 5 | 51 | | var row = ToCartItem(item, user); |
| | 5 | 52 | | _db.MotexCartItems.Add(row); |
| | 5 | 53 | | await _db.SaveChangesAsync(ct); |
| | 10 | 54 | | await TrackAsync(() => _supplierState.MarkInCartAsync(row.StudioOrderId, ApiSupplierSettings.MotexProviderKe |
| | 10 | 55 | | added++; messages.Add($"{item.Code}: добавлен в корзину МоТехС."); |
| | 5 | 56 | | } |
| | 15 | 57 | | return new(added, skipped, failed, messages); |
| | 15 | 58 | | } |
| | | 59 | | |
| | | 60 | | public async Task<MotexCartResult> CheckoutDirectAsync(IReadOnlyCollection<MotexCartQueueItem> queue, string user, C |
| | | 61 | | { |
| | 23 | 62 | | if (queue.Count == 0 || queue.GroupBy(x => x.OrderId).Any(x => x.Key <= 0 || x.Count() > 1) || queue.Any(x => !I |
| | 3 | 63 | | return new(0, 0, 1, ["Для прямого оформления МоТехС требуется по одному корректному предложению на заказ."]) |
| | 5 | 64 | | var rows = queue.Select(x => ToCartItem(x, user)).ToList(); |
| | 2 | 65 | | return await CreateOrderAsync(rows, null, user, ct); |
| | 5 | 66 | | } |
| | | 67 | | |
| | | 68 | | public async Task<MotexCartResult> CheckoutAsync(IReadOnlyCollection<long>? ids, string user, CancellationToken ct = |
| | | 69 | | { |
| | 11 | 70 | | var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status)); |
| | 18 | 71 | | if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id)); |
| | 11 | 72 | | var rows = await query.ToListAsync(ct); |
| | 11 | 73 | | return rows.Count == 0 ? new(0, 0, 1, ["Корзина МоТехС пуста."]) : await CreateOrderAsync(rows, rows, user, ct); |
| | 11 | 74 | | } |
| | | 75 | | |
| | | 76 | | public async Task<MotexCartResult> RemoveAsync(IReadOnlyCollection<long>? ids, CancellationToken ct = default) |
| | | 77 | | { |
| | 4 | 78 | | var query = _db.MotexCartItems.Where(x => MotexCartItemStatus.Active.Contains(x.Status)); |
| | 7 | 79 | | if (ids is { Count: > 0 }) query = query.Where(x => ids.Contains(x.Id)); |
| | 4 | 80 | | var rows = await query.ToListAsync(ct); |
| | 5 | 81 | | if (rows.Count == 0) return new(0, 0, 1, ["Выберите позиции МоТехС."]); |
| | 34 | 82 | | foreach (var row in rows) { row.Status = MotexCartItemStatus.Removed; row.ActiveStudioOrderId = null; row.Update |
| | 3 | 83 | | await _db.SaveChangesAsync(ct); |
| | 3 | 84 | | return new(rows.Count, 0, 0, ["Позиции удалены из корзины МоТехС."]); |
| | 4 | 85 | | } |
| | | 86 | | |
| | | 87 | | private async Task<MotexCartResult> CreateOrderAsync(IReadOnlyList<MotexCartItem> rows, IReadOnlyList<MotexCartItem> |
| | | 88 | | { |
| | 12 | 89 | | var addresses = (await _api.GetAsync("/api/v1/Users/GetUserDeliveryAddresses", null, ct))["deliveryAddresses"] a |
| | 38 | 90 | | var codes = addresses?.OfType<JObject>().Select(x => x.Value<string>("code")).Where(x => !string.IsNullOrWhiteSp |
| | 14 | 91 | | if (codes.Count != 1) return new(0, 0, rows.Count, ["МоТехС: для оформления должен быть ровно один адрес доставк |
| | | 92 | | |
| | 50 | 93 | | foreach (var row in persistedRows ?? []) row.Status = MotexCartItemStatus.Processing; |
| | 18 | 94 | | if (persistedRows is not null) await _db.SaveChangesAsync(ct); |
| | 10 | 95 | | var order = new MotexOrder { DeliveryAddressCode = codes[0]!, CreatedBy = user }; |
| | 20 | 96 | | _db.MotexOrders.Add(order); await _db.SaveChangesAsync(ct); |
| | 10 | 97 | | order.ClientOrderNum = $"ST2-MTX-{order.Id}"; |
| | 10 | 98 | | var request = new JObject |
| | 10 | 99 | | { |
| | 10 | 100 | | ["clientOrderNum"] = order.ClientOrderNum, |
| | 10 | 101 | | ["deliveryAddressCode"] = order.DeliveryAddressCode, |
| | 10 | 102 | | ["comment"] = "Studio2", |
| | 10 | 103 | | ["orderForm"] = 1, |
| | 13 | 104 | | ["orderItems"] = new JArray(rows.Select(x => new JObject { ["code"] = x.Code, ["brand"] = x.Brand, ["quantit |
| | 10 | 105 | | }; |
| | 20 | 106 | | order.RequestJson = request.ToString(Formatting.None); await _db.SaveChangesAsync(ct); |
| | | 107 | | try |
| | | 108 | | { |
| | | 109 | | JToken response; |
| | 16 | 110 | | try { response = await _api.CreateOrderAsync(request, ct); } |
| | 4 | 111 | | catch (Exception ex) when (ex is MotexApiException or HttpRequestException) |
| | | 112 | | { |
| | | 113 | | // Восстанавливаем заказ по clientOrderNum на случай, если CreateOrder создал его, но ответ потерялся |
| | | 114 | | // (таймаут/обрыв сети). Если и это падает — значит заказа не было, и полезна исходная ошибка, а не 404 |
| | 5 | 115 | | try { response = await _api.GetOrderDetailsAsync(null, order.ClientOrderNum, ct); } |
| | 6 | 116 | | catch (Exception ex2) when (ex2 is MotexApiException or HttpRequestException) { throw ex; } |
| | | 117 | | } |
| | 7 | 118 | | var payload = response["order"] as JObject ?? response as JObject ?? new JObject(); |
| | 7 | 119 | | order.ExternalOrderId = payload.Value<string>("id"); |
| | 7 | 120 | | order.Status = payload.Value<string>("status") ?? "Создан"; |
| | 7 | 121 | | order.ResponseJson = payload.ToString(Formatting.None); |
| | 34 | 122 | | foreach (var row in rows) |
| | | 123 | | { |
| | 20 | 124 | | var local = persistedRows?.FirstOrDefault(x => x.Id == row.Id); |
| | 42 | 125 | | if (local is not null) { local.Status = MotexCartItemStatus.Ordered; local.ActiveStudioOrderId = null; l |
| | 10 | 126 | | order.Items.Add(new MotexOrderItem { MotexCartItemId = local?.Id, StudioOrderId = row.StudioOrderId, Cod |
| | 20 | 127 | | await TrackAsync(() => _supplierState.MarkOrderedAsync(row.StudioOrderId, ApiSupplierSettings.MotexProvi |
| | | 128 | | } |
| | 7 | 129 | | await _db.SaveChangesAsync(ct); |
| | 7 | 130 | | return new(rows.Count, 0, 0, [$"Создан заказ МоТехС №{order.ExternalOrderId ?? order.ClientOrderNum}."]); |
| | | 131 | | } |
| | 3 | 132 | | catch (Exception ex) when (ex is MotexApiException or HttpRequestException) |
| | | 133 | | { |
| | 6 | 134 | | order.Status = "Failed"; order.ResponseJson = ex.Message; |
| | 16 | 135 | | foreach (var row in persistedRows ?? []) { row.Status = MotexCartItemStatus.Failed; row.LastError = ex.Messa |
| | 3 | 136 | | await _db.SaveChangesAsync(ct); |
| | 3 | 137 | | return new(0, 0, rows.Count, [$"МоТехС: {ex.Message}"]); |
| | | 138 | | } |
| | 12 | 139 | | } |
| | | 140 | | |
| | 21 | 141 | | private static bool IsValid(MotexCartQueueItem item) => item.OrderId > 0 && !string.IsNullOrWhiteSpace(item.Code) && |
| | 8 | 142 | | private static MotexCartItem ToCartItem(MotexCartQueueItem item, string user) => new() { StudioOrderId = item.OrderI |
| | | 143 | | private async Task TrackAsync(Func<Task> action) |
| | | 144 | | { |
| | 36 | 145 | | try { await action(); } |
| | 6 | 146 | | catch (Exception exception) { _logger.LogWarning(exception, "Не удалось обновить состояние заказа у поставщика М |
| | 19 | 147 | | } |
| | | 148 | | } |