| | | 1 | | using Newtonsoft.Json.Linq; |
| | | 2 | | using Services.ApiServices.ApiSupplierRequestLog; |
| | | 3 | | |
| | | 4 | | namespace Services.ApiServices.BergApi; |
| | | 5 | | |
| | | 6 | | /// <summary>Клиент read-only API Berg v1.0.</summary> |
| | | 7 | | public sealed class BergHttpClient |
| | | 8 | | { |
| | | 9 | | private readonly IHttpClientFactory _factory; |
| | | 10 | | private readonly BergConfig _config; |
| | | 11 | | private readonly ApiSupplierRequestLogStore _logs; |
| | | 12 | | |
| | 42 | 13 | | public BergHttpClient(IHttpClientFactory factory, BergConfig config, ApiSupplierRequestLogStore logs) |
| | | 14 | | { |
| | 42 | 15 | | _factory = factory; |
| | 42 | 16 | | _config = config; |
| | 42 | 17 | | _logs = logs; |
| | 42 | 18 | | } |
| | | 19 | | |
| | | 20 | | public Task<JToken> GetAsync(string path, IReadOnlyDictionary<string, string?>? query = null, CancellationToken ct = |
| | 24 | 21 | | SendAsync(HttpMethod.Get, path, query, body: null, ct); |
| | | 22 | | |
| | | 23 | | /// <summary> |
| | | 24 | | /// Запись в Berg (сейчас это только <c>ordering/place_order.json</c>). Berg принимает строгий |
| | | 25 | | /// JSON: ключи в кавычках, числа без кавычек — поэтому тело сериализуется, а не собирается строкой. |
| | | 26 | | /// </summary> |
| | | 27 | | public Task<JToken> PostAsync(string path, JToken body, CancellationToken ct = default) => |
| | 3 | 28 | | SendAsync(HttpMethod.Post, path, query: null, body, ct); |
| | | 29 | | |
| | | 30 | | private async Task<JToken> SendAsync(HttpMethod method, string path, IReadOnlyDictionary<string, string?>? query, |
| | | 31 | | JToken? body, CancellationToken ct) |
| | | 32 | | { |
| | 27 | 33 | | if (string.IsNullOrWhiteSpace(_config.ApiKey)) |
| | 1 | 34 | | throw new BergApiException("Не настроен Berg:ApiKey."); |
| | | 35 | | |
| | | 36 | | // Ключ уходит в лог под именем заголовка: ApiSupplierRequestLogStore.Redact знает |
| | | 37 | | // x-berg-api-key и заменяет значение звёздочками. |
| | 26 | 38 | | var loggedRequest = "X-Berg-API-Key=" + _config.ApiKey; |
| | 26 | 39 | | var payload = body?.ToString(Newtonsoft.Json.Formatting.None); |
| | 26 | 40 | | if (payload != null) |
| | 3 | 41 | | loggedRequest += " " + payload; |
| | | 42 | | |
| | 26 | 43 | | var timer = System.Diagnostics.Stopwatch.StartNew(); |
| | | 44 | | try |
| | | 45 | | { |
| | 26 | 46 | | using var request = new HttpRequestMessage(method, BuildUri(path, query)); |
| | 26 | 47 | | request.Headers.TryAddWithoutValidation("X-Berg-API-Key", _config.ApiKey); |
| | 26 | 48 | | if (payload != null) |
| | 3 | 49 | | request.Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json"); |
| | | 50 | | |
| | 26 | 51 | | using var response = await _factory.CreateClient("BergApi").SendAsync(request, ct); |
| | 24 | 52 | | var text = await response.Content.ReadAsStringAsync(ct); |
| | 24 | 53 | | _logs.Add("Berg", method.Method, path, (int)response.StatusCode, timer, loggedRequest, text); |
| | | 54 | | |
| | | 55 | | // Ошибки размещения заказа Berg отдаёт телом {"errors":[…]} вместе с кодом 4xx. |
| | | 56 | | // Тело важнее статуса: без него пользователь не узнает, какая строка не прошла. |
| | 24 | 57 | | JToken? parsed = null; |
| | 47 | 58 | | try { parsed = JToken.Parse(text); } |
| | 1 | 59 | | catch (Exception ex) |
| | | 60 | | { |
| | 1 | 61 | | if (response.IsSuccessStatusCode) |
| | 0 | 62 | | throw new BergApiException("Berg вернул невалидный JSON.", requestPath: path, innerException: ex); |
| | 1 | 63 | | } |
| | | 64 | | |
| | 24 | 65 | | if (!response.IsSuccessStatusCode && parsed?["errors"] is null) |
| | 3 | 66 | | throw new BergApiException($"Berg вернул HTTP {(int)response.StatusCode}.", (int)response.StatusCode, pa |
| | | 67 | | |
| | 21 | 68 | | return parsed!; |
| | | 69 | | } |
| | 6 | 70 | | catch (BergApiException) { throw; } |
| | 2 | 71 | | catch (Exception ex) |
| | | 72 | | { |
| | 2 | 73 | | _logs.Add("Berg", method.Method, path, null, timer, loggedRequest, ex.Message); |
| | 2 | 74 | | throw new BergApiException("Не удалось выполнить запрос к Berg.", requestPath: path, innerException: ex); |
| | | 75 | | } |
| | 21 | 76 | | } |
| | | 77 | | |
| | | 78 | | private Uri BuildUri(string path, IReadOnlyDictionary<string, string?>? query) |
| | | 79 | | { |
| | 26 | 80 | | var builder = new UriBuilder(_config.BaseUrl.TrimEnd('/') + "/v1.0/" + path.TrimStart('/')); |
| | 26 | 81 | | if (query != null) |
| | 43 | 82 | | builder.Query = string.Join("&", query.Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
| | 43 | 83 | | .Select(x => $"{Uri.EscapeDataString(x.Key)}={Uri.EscapeDataString(x.Value!)}")); |
| | 26 | 84 | | return builder.Uri; |
| | | 85 | | } |
| | | 86 | | } |