| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Xml.Linq; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | using OzonDomains.Models; |
| | | 5 | | using OzonDomains.Models.SupplierStatuses; |
| | | 6 | | using OzonRepositories.Context; |
| | | 7 | | using Services.ApiServices.MikadoApi; |
| | | 8 | | |
| | | 9 | | namespace Services.SupplierStatusServices.Providers; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Сверяет корзину Микадо и судьбу её позиций. У Микадо нет отдельной сущности заказа: |
| | | 13 | | /// позиция живёт по своему BasketID, поэтому Basket_List показывает, что ещё в буфере, |
| | | 14 | | /// а Zakaz_History — ушло ли оно в заказ (InZakaz) или было удалено. |
| | | 15 | | /// </summary> |
| | | 16 | | public sealed class MikadoSupplierStatusProvider : ISupplierStatusProvider |
| | | 17 | | { |
| | | 18 | | private readonly ApplicationDbContext _db; |
| | | 19 | | private readonly MikadoDataManager _api; |
| | 10 | 20 | | public MikadoSupplierStatusProvider(ApplicationDbContext db, MikadoDataManager api) => (_db, _api) = (db, api); |
| | 0 | 21 | | public string ProviderKey => ApiSupplierSettings.MikadoMskHod20ProviderKey; |
| | | 22 | | |
| | | 23 | | public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchCartAsync(IReadOnlyList<OrderSupplierStateRef> states, |
| | | 24 | | { |
| | | 25 | | Dictionary<long, XElement> basket; |
| | | 26 | | try |
| | | 27 | | { |
| | 4 | 28 | | basket = ParseBasket(await _api.GetBasketAsync(ct)); |
| | 3 | 29 | | } |
| | 1 | 30 | | catch (Exception ex) when (IsApiFailure(ex)) |
| | | 31 | | { |
| | 1 | 32 | | return Failures(states, SupplierStatusKind.Cart, ex); |
| | | 33 | | } |
| | | 34 | | |
| | | 35 | | var orderIds = states.Select(x => x.OrderId).ToList(); |
| | 3 | 36 | | var rows = await _db.MikadoCartItems |
| | 3 | 37 | | .Where(x => orderIds.Contains(x.StudioOrderId) && x.ActiveStudioOrderId != null) |
| | 3 | 38 | | .ToListAsync(ct); |
| | | 39 | | |
| | 3 | 40 | | var result = new List<SupplierStatusSnapshot>(); |
| | 12 | 41 | | foreach (var state in states) |
| | | 42 | | { |
| | 6 | 43 | | var row = rows.FirstOrDefault(x => x.StudioOrderId == state.OrderId); |
| | 3 | 44 | | if (row is null) |
| | | 45 | | { |
| | 0 | 46 | | result.Add(RemovedSnapshot(state.OrderId)); |
| | 0 | 47 | | continue; |
| | | 48 | | } |
| | | 49 | | |
| | 3 | 50 | | if (basket.TryGetValue(row.ApiBasketItemId, out var item)) |
| | | 51 | | { |
| | 1 | 52 | | result.Add(new SupplierStatusSnapshot |
| | 1 | 53 | | { |
| | 1 | 54 | | OrderId = state.OrderId, |
| | 1 | 55 | | Kind = SupplierStatusKind.Cart, |
| | 1 | 56 | | ExternalCode = row.Status, |
| | 1 | 57 | | StatusName = CartStatusName(row.Status), |
| | 1 | 58 | | ExpectedDeliveryAt = ParseDate(ReadString(item, "Srok")) |
| | 1 | 59 | | }); |
| | 1 | 60 | | continue; |
| | | 61 | | } |
| | | 62 | | |
| | | 63 | | // Позиции нет в буфере: либо её перевели в заказ, либо сняли. |
| | | 64 | | try |
| | | 65 | | { |
| | 2 | 66 | | var history = ParseHistory(await _api.GetOrderHistoryAsync(row.ApiBasketItemId, ct)); |
| | 2 | 67 | | var snapshot = FromHistory(state.OrderId, row.ApiBasketItemId, history); |
| | 3 | 68 | | if (IsClosed(snapshot)) CloseRow(row); |
| | 2 | 69 | | result.Add(snapshot); |
| | 2 | 70 | | } |
| | 0 | 71 | | catch (Exception ex) when (IsApiFailure(ex)) |
| | | 72 | | { |
| | 0 | 73 | | result.Add(Failure(state.OrderId, SupplierStatusKind.Cart, ex)); |
| | 0 | 74 | | } |
| | 2 | 75 | | } |
| | | 76 | | |
| | 3 | 77 | | await _db.SaveChangesAsync(ct); |
| | 3 | 78 | | return result; |
| | 4 | 79 | | } |
| | | 80 | | |
| | | 81 | | public async Task<IReadOnlyList<SupplierStatusSnapshot>> FetchOrderAsync(IReadOnlyList<OrderSupplierStateRef> states |
| | | 82 | | { |
| | 1 | 83 | | var result = new List<SupplierStatusSnapshot>(); |
| | 4 | 84 | | foreach (var state in states) |
| | | 85 | | { |
| | 1 | 86 | | var basketItemId = ReadBasketItemId(state); |
| | 1 | 87 | | if (basketItemId <= 0) |
| | | 88 | | { |
| | 0 | 89 | | result.Add(new SupplierStatusSnapshot |
| | 0 | 90 | | { |
| | 0 | 91 | | OrderId = state.OrderId, Kind = SupplierStatusKind.Order, StatusName = "Ожидает статуса" |
| | 0 | 92 | | }); |
| | 0 | 93 | | continue; |
| | | 94 | | } |
| | | 95 | | |
| | | 96 | | try |
| | | 97 | | { |
| | 1 | 98 | | var history = ParseHistory(await _api.GetOrderHistoryAsync(basketItemId, ct)); |
| | 1 | 99 | | result.Add(FromHistory(state.OrderId, basketItemId, history)); |
| | 1 | 100 | | } |
| | 0 | 101 | | catch (Exception ex) when (IsApiFailure(ex)) |
| | | 102 | | { |
| | 0 | 103 | | result.Add(Failure(state.OrderId, SupplierStatusKind.Order, ex)); |
| | 0 | 104 | | } |
| | 1 | 105 | | } |
| | | 106 | | |
| | 1 | 107 | | return result; |
| | 1 | 108 | | } |
| | | 109 | | |
| | | 110 | | /// <summary>Раскладывает Zakaz_History в снимок: InZakaz — заказ, остальное закрывает отслеживание.</summary> |
| | | 111 | | private static SupplierStatusSnapshot FromHistory(int orderId, long basketItemId, XElement? history) |
| | | 112 | | { |
| | 3 | 113 | | var status = ReadString(history, "ZakazStatus"); |
| | 3 | 114 | | if (!string.Equals(status, "InZakaz", StringComparison.OrdinalIgnoreCase)) |
| | | 115 | | { |
| | | 116 | | // InBuffer здесь означает, что позиции уже нет в Basket_List — доверяем корзине и закрываем. |
| | 1 | 117 | | return RemovedSnapshot(orderId); |
| | | 118 | | } |
| | | 119 | | |
| | 32 | 120 | | var lines = history?.Descendants().Where(x => x.Name.LocalName == "ZakazRow").ToList() ?? []; |
| | 8 | 121 | | var shipped = lines.Where(x => !ReadBool(x, "Otkaz")).Sum(x => ReadDecimal(x, "QTY")); |
| | 8 | 122 | | var rejected = lines.Where(x => ReadBool(x, "Otkaz")).Sum(x => ReadDecimal(x, "QTY")); |
| | 10 | 123 | | var expected = lines.Select(x => ParseDate(ReadString(x, "dDate"))).Where(x => x.HasValue).Max(); |
| | | 124 | | |
| | 2 | 125 | | return new SupplierStatusSnapshot |
| | 2 | 126 | | { |
| | 2 | 127 | | OrderId = orderId, |
| | 2 | 128 | | Kind = SupplierStatusKind.Order, |
| | 2 | 129 | | ExternalCode = "InZakaz", |
| | 2 | 130 | | StatusName = "В заказе у Микадо", |
| | 2 | 131 | | ExternalOrderId = basketItemId.ToString(CultureInfo.InvariantCulture), |
| | 2 | 132 | | ExternalOrderNumber = ReadString(history, "ZakazCode"), |
| | 2 | 133 | | QuantityConfirmed = ReadDecimal(history, "ZakazQty"), |
| | 2 | 134 | | QuantityShipped = shipped > 0 ? shipped : null, |
| | 2 | 135 | | QuantityRejected = rejected > 0 ? rejected : null, |
| | 2 | 136 | | ExpectedDeliveryAt = expected |
| | 2 | 137 | | }; |
| | | 138 | | } |
| | | 139 | | |
| | | 140 | | private static void CloseRow(MikadoCartItem row) |
| | | 141 | | { |
| | 1 | 142 | | row.Status = MikadoCartItemStatus.Removed; |
| | 1 | 143 | | row.ActiveStudioOrderId = null; |
| | 1 | 144 | | row.UpdatedAtUtc = DateTime.UtcNow; |
| | 1 | 145 | | } |
| | | 146 | | |
| | | 147 | | private static bool IsClosed(SupplierStatusSnapshot snapshot) => |
| | 2 | 148 | | snapshot.Kind == SupplierStatusKind.Cart && snapshot.ExternalCode == SupplierCartStatusCode.Removed; |
| | | 149 | | |
| | 1 | 150 | | private static SupplierStatusSnapshot RemovedSnapshot(int orderId) => new() |
| | 1 | 151 | | { |
| | 1 | 152 | | OrderId = orderId, Kind = SupplierStatusKind.Cart, |
| | 1 | 153 | | ExternalCode = SupplierCartStatusCode.Removed, StatusName = "Удалён из корзины" |
| | 1 | 154 | | }; |
| | | 155 | | |
| | | 156 | | private static long ReadBasketItemId(OrderSupplierStateRef state) => |
| | 1 | 157 | | long.TryParse(state.ExternalBasketItemId ?? state.ExternalOrderId, NumberStyles.Integer, |
| | 1 | 158 | | CultureInfo.InvariantCulture, out var id) ? id : 0; |
| | | 159 | | |
| | | 160 | | private static Dictionary<long, XElement> ParseBasket(string xml) => |
| | 26 | 161 | | XDocument.Parse(xml).Descendants().Where(x => x.Name.LocalName == "BasketItem") |
| | 1 | 162 | | .GroupBy(x => long.TryParse(ReadString(x, "ID"), out var id) ? id : 0) |
| | 1 | 163 | | .Where(x => x.Key > 0) |
| | 6 | 164 | | .ToDictionary(x => x.Key, x => x.First()); |
| | | 165 | | |
| | | 166 | | private static XElement? ParseHistory(string xml) => |
| | 15 | 167 | | XDocument.Parse(xml).Descendants().FirstOrDefault(x => x.Name.LocalName == "Zakaz_HistoryResult"); |
| | | 168 | | |
| | | 169 | | private static IReadOnlyList<SupplierStatusSnapshot> Failures( |
| | | 170 | | IReadOnlyList<OrderSupplierStateRef> states, string kind, Exception ex) => |
| | 2 | 171 | | states.Select(x => Failure(x.OrderId, kind, ex)).ToList(); |
| | | 172 | | |
| | 1 | 173 | | private static SupplierStatusSnapshot Failure(int orderId, string kind, Exception ex) => new() |
| | 1 | 174 | | { |
| | 1 | 175 | | OrderId = orderId, Kind = kind, StatusName = string.Empty, Error = $"Микадо: {ex.Message}" |
| | 1 | 176 | | }; |
| | | 177 | | |
| | | 178 | | private static bool IsApiFailure(Exception ex) => |
| | 1 | 179 | | ex is MikadoApiException or HttpRequestException or TaskCanceledException or System.Xml.XmlException; |
| | | 180 | | |
| | 1 | 181 | | private static string CartStatusName(string status) => status switch |
| | 1 | 182 | | { |
| | 1 | 183 | | MikadoCartItemStatus.Ready => "Проверен, готов к заказу", |
| | 0 | 184 | | MikadoCartItemStatus.Failed => "Ошибка проверки", |
| | 0 | 185 | | _ => "В корзине, нужна проверка" |
| | 1 | 186 | | }; |
| | | 187 | | |
| | | 188 | | private static string? ReadString(XElement? x, string name) => |
| | 90 | 189 | | x?.Descendants().FirstOrDefault(e => e.Name.LocalName == name)?.Value?.Trim(); |
| | | 190 | | |
| | | 191 | | private static decimal ReadDecimal(XElement? x, string name) => |
| | 6 | 192 | | decimal.TryParse(ReadString(x, name), NumberStyles.Number, CultureInfo.InvariantCulture, out var value) ? value |
| | | 193 | | |
| | | 194 | | private static bool ReadBool(XElement? x, string name) => |
| | 8 | 195 | | bool.TryParse(ReadString(x, name), out var value) && value; |
| | | 196 | | |
| | | 197 | | /// <summary>Микадо отдаёт даты как dd.MM.yyyy (Srok) и ISO (dDate).</summary> |
| | | 198 | | private static DateTime? ParseDate(string? value) |
| | | 199 | | { |
| | 5 | 200 | | if (string.IsNullOrWhiteSpace(value)) return null; |
| | 5 | 201 | | if (DateTime.TryParseExact(value, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact |
| | 1 | 202 | | return exact; |
| | 4 | 203 | | return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed) ? parsed : nu |
| | | 204 | | } |
| | | 205 | | } |