| | | 1 | | using System.Globalization; |
| | | 2 | | using Newtonsoft.Json.Linq; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.BergApi; |
| | | 5 | | |
| | | 6 | | /// <summary>Операции Berg API: чтение справочников и остатков, размещение заказа, статусы.</summary> |
| | | 7 | | public sealed class BergDataManager |
| | | 8 | | { |
| | | 9 | | private readonly BergHttpClient _client; |
| | 80 | 10 | | public BergDataManager(BergHttpClient client) => _client = client; |
| | | 11 | | |
| | 0 | 12 | | public Task<JToken> GetBrandsAsync(CancellationToken ct = default) => _client.GetAsync("references/brands.json", ct: |
| | 0 | 13 | | public Task<JToken> GetActiveShipmentAddressesAsync(CancellationToken ct = default) => _client.GetAsync("references/ |
| | | 14 | | |
| | | 15 | | /// <summary>Статусы по идентификаторам заказов. Параметр называется <c>orders[]</c>: |
| | | 16 | | /// с любым другим именем Berg отвечает ERR_STATES_ORDERS_IDS_INVALID.</summary> |
| | | 17 | | public Task<JToken> GetStatesAsync(IEnumerable<string>? orderIds = null, CancellationToken ct = default) |
| | | 18 | | { |
| | 22 | 19 | | var query = (orderIds ?? Enumerable.Empty<string>()) |
| | 23 | 20 | | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| | 23 | 21 | | .Select((x, index) => new KeyValuePair<string, string?>($"orders[{index}]", x.Trim())) |
| | 68 | 22 | | .ToDictionary(x => x.Key, x => x.Value); |
| | 22 | 23 | | return _client.GetAsync("ordering/states.json", query, ct); |
| | | 24 | | } |
| | | 25 | | |
| | 0 | 26 | | public Task<JToken> GetActiveOrderStatesAsync(CancellationToken ct = default) => _client.GetAsync("ordering/states/a |
| | | 27 | | |
| | | 28 | | /// <summary>Справочник статусов заказов. Это <c>references/states.json</c>, а не |
| | | 29 | | /// <c>ordering/states.json</c> — последний отдаёт заказы по идентификаторам.</summary> |
| | | 30 | | public async Task<IReadOnlyList<BergState>> GetStatesDictionaryAsync(CancellationToken ct = default) |
| | | 31 | | { |
| | 0 | 32 | | var response = await _client.GetAsync("references/states.json", ct: ct); |
| | 0 | 33 | | return (response["states"] as JArray ?? new JArray()) |
| | 0 | 34 | | .OfType<JObject>() |
| | | 35 | | .Select(x => new BergState(x.Value<int?>("id") ?? 0, x.Value<string>("name") ?? string.Empty, x.Value<int?>( |
| | | 36 | | .Where(x => x.Id > 0) |
| | 0 | 37 | | .ToList(); |
| | 0 | 38 | | } |
| | | 39 | | |
| | | 40 | | public Task<JToken> GetStockAsync(string article, string? brand, bool includeAnalogs, long? addressId = null, Cancel |
| | | 41 | | { |
| | 0 | 42 | | var query = new Dictionary<string, string?> |
| | 0 | 43 | | { |
| | 0 | 44 | | ["items[0][resource_article]"] = article, |
| | 0 | 45 | | ["items[0][brand_name]"] = brand, |
| | 0 | 46 | | ["analogs"] = includeAnalogs ? "1" : "0", |
| | 0 | 47 | | ["address_id"] = addressId?.ToString(CultureInfo.InvariantCulture) |
| | 0 | 48 | | }; |
| | 0 | 49 | | return _client.GetAsync("ordering/get_stock.json", query, ct); |
| | | 50 | | } |
| | | 51 | | |
| | | 52 | | /// <summary>Размещает заказ. У Berg нет корзины: заказ уходит одним вызовом целиком.</summary> |
| | | 53 | | public async Task<BergPlaceOrderResult> PlaceOrderAsync(BergOrderRequest request, CancellationToken ct = default) |
| | | 54 | | { |
| | 3 | 55 | | if (request.Items.Count == 0) |
| | 0 | 56 | | throw new BergApiException("Заказ Berg без позиций."); |
| | | 57 | | |
| | 3 | 58 | | var response = await _client.PostAsync("ordering/place_order.json", BuildOrderBody(request), ct); |
| | 3 | 59 | | return ParsePlaceOrderResponse(response); |
| | 3 | 60 | | } |
| | | 61 | | |
| | | 62 | | /// <summary>Тело запроса place_order. Публичный, потому что заказ сохраняется в BergOrders |
| | | 63 | | /// ровно в том виде, в каком ушёл поставщику.</summary> |
| | | 64 | | public static JObject BuildOrderBody(BergOrderRequest request) |
| | | 65 | | { |
| | 8 | 66 | | var order = new JObject |
| | 8 | 67 | | { |
| | 8 | 68 | | ["dispatch_type"] = request.DispatchType, |
| | 8 | 69 | | ["dispatch_at"] = request.DispatchAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), |
| | 8 | 70 | | ["dispatch_time"] = request.DispatchTime, |
| | 8 | 71 | | ["items"] = new JArray(request.Items.Select(BuildOrderItem)) |
| | 8 | 72 | | }; |
| | | 73 | | |
| | 11 | 74 | | if (request.IsTest) order["is_test"] = 1; |
| | 14 | 75 | | if (request.Reference.HasValue) order["reference"] = request.Reference.Value; |
| | 9 | 76 | | if (request.PaymentType.HasValue) order["payment_type"] = request.PaymentType.Value; |
| | | 77 | | // Адрес из справочника Berg принимает только вместе с доставкой: при самовывозе |
| | | 78 | | // он отвечает DELIVERY_ADDRESS_FOR_DISPATCH_SELF. |
| | 8 | 79 | | if (request.ShipmentAddressId.HasValue && request.DispatchType != BergDispatchType.SelfPickup) |
| | 7 | 80 | | order["shipment_address_id"] = request.ShipmentAddressId.Value; |
| | 8 | 81 | | if (!string.IsNullOrWhiteSpace(request.Person)) order["person"] = request.Person; |
| | 8 | 82 | | if (!string.IsNullOrWhiteSpace(request.Phone)) order["phone"] = request.Phone; |
| | 14 | 83 | | if (!string.IsNullOrWhiteSpace(request.Comment)) order["comment"] = request.Comment; |
| | | 84 | | |
| | 8 | 85 | | var body = new JObject { ["order"] = order }; |
| | 9 | 86 | | if (request.Force) body["force"] = 1; |
| | 8 | 87 | | return body; |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | private static JObject BuildOrderItem(BergOrderItemRequest item) |
| | | 91 | | { |
| | 8 | 92 | | var node = new JObject |
| | 8 | 93 | | { |
| | 8 | 94 | | ["resource_id"] = item.ResourceId, |
| | 8 | 95 | | ["warehouse_id"] = item.WarehouseId, |
| | 8 | 96 | | ["quantity"] = item.Quantity |
| | 8 | 97 | | }; |
| | 14 | 98 | | if (item.MaxPrice.HasValue) node["max_price"] = item.MaxPrice.Value; |
| | 9 | 99 | | if (item.DeliveryType.HasValue) node["delivery_type"] = item.DeliveryType.Value; |
| | 9 | 100 | | if (item.IsTransit == true) node["is_transit"] = 1; |
| | 14 | 101 | | if (!string.IsNullOrWhiteSpace(item.Comment)) node["comment"] = item.Comment; |
| | 8 | 102 | | return node; |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | internal static BergPlaceOrderResult ParsePlaceOrderResponse(JToken response) |
| | | 106 | | { |
| | 6 | 107 | | var order = response["order"] as JObject ?? response as JObject; |
| | 6 | 108 | | var parsed = order is null || order["id"] is null ? null : ParseOrder(order); |
| | 6 | 109 | | return new BergPlaceOrderResult(parsed, ParseMessages(response["errors"]), ParseMessages(response["warnings"])); |
| | | 110 | | } |
| | | 111 | | |
| | | 112 | | /// <summary>Разбирает <c>{"orders":[…]}</c> из ordering/states и ordering/states/active.</summary> |
| | | 113 | | public static IReadOnlyList<BergOrderState> ParseOrders(JToken response) => |
| | 18 | 114 | | (response["orders"] as JArray ?? new JArray()).OfType<JObject>().Select(ParseOrder).ToList(); |
| | | 115 | | |
| | | 116 | | private static IReadOnlyList<BergMessage> ParseMessages(JToken? node) => |
| | 12 | 117 | | (node as JArray ?? new JArray()).OfType<JObject>() |
| | 3 | 118 | | .Select(x => new BergMessage(x.Value<string>("code") ?? string.Empty, x.Value<string>("text") ?? string.Empt |
| | 12 | 119 | | .ToList(); |
| | | 120 | | |
| | 24 | 121 | | private static BergOrderState ParseOrder(JObject order) => new( |
| | 24 | 122 | | order.Value<long?>("id") ?? 0, |
| | 24 | 123 | | order.Value<long?>("reference"), |
| | 24 | 124 | | order.Value<bool?>("is_test") ?? false, |
| | 24 | 125 | | order.Value<int?>("dispatch_type"), |
| | 24 | 126 | | ParseDate(order["dispatch_at"]), |
| | 24 | 127 | | order.Value<int?>("dispatch_time"), |
| | 24 | 128 | | order.Value<int?>("payment_type"), |
| | 24 | 129 | | order.Value<string>("shipment_address"), |
| | 24 | 130 | | order.Value<long?>("shipment_address_id"), |
| | 24 | 131 | | order.Value<string>("person"), |
| | 24 | 132 | | order.Value<string>("phone"), |
| | 24 | 133 | | order.Value<string>("comment"), |
| | 24 | 134 | | ParseDate(order["created_at"]), |
| | 24 | 135 | | (order["items"] as JArray ?? new JArray()).OfType<JObject>().Select(ParseOrderItem).ToList()); |
| | | 136 | | |
| | 25 | 137 | | private static BergOrderItemState ParseOrderItem(JObject item) => new( |
| | 25 | 138 | | item.Value<int?>("sequence") ?? 0, |
| | 25 | 139 | | item.SelectToken("resource.id")?.Value<long?>(), |
| | 25 | 140 | | item.SelectToken("resource.article")?.Value<string>(), |
| | 25 | 141 | | item.SelectToken("resource.brand.name")?.Value<string>(), |
| | 25 | 142 | | item.SelectToken("resource.name")?.Value<string>(), |
| | 25 | 143 | | item.SelectToken("warehouse.id")?.Value<long?>(), |
| | 25 | 144 | | item.SelectToken("warehouse.name")?.Value<string>(), |
| | 25 | 145 | | item.Value<decimal?>("quantity"), |
| | 25 | 146 | | item.Value<decimal?>("price"), |
| | 25 | 147 | | item.SelectToken("state.id")?.Value<int?>(), |
| | 25 | 148 | | item.SelectToken("state.name")?.Value<string>(), |
| | 25 | 149 | | item.SelectToken("state.type")?.Value<int?>(), |
| | 25 | 150 | | ParseDate(item["average_time"]), |
| | 25 | 151 | | ParseDate(item["assured_time"]), |
| | 25 | 152 | | item.Value<string>("comment")); |
| | | 153 | | |
| | | 154 | | private static DateTime? ParseDate(JToken? token) |
| | | 155 | | { |
| | 98 | 156 | | var raw = token?.Value<string>(); |
| | 98 | 157 | | return DateTime.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustTo |
| | 98 | 158 | | ? parsed |
| | 98 | 159 | | : null; |
| | | 160 | | } |
| | | 161 | | } |