using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Text.Json; using System.Text.Json.Serialization; namespace Электронная_Фармация.Classes { public class ApiClient { private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = null }; private readonly HttpClient _httpClient; private readonly string _baseUrl; private string _token; public ApiClient(string baseUrl = null) { _baseUrl = AppConfig.NormalizeApiBaseUrl(baseUrl ?? AppConfig.ApiBaseUrl); _httpClient = new HttpClient { // Крупный прайс тянем одним запросом — даём серверу время посчитать и отдать. Timeout = TimeSpan.FromSeconds(120) }; AppDebugLog.Info("ApiClient", $"Создан клиент. BaseUrl={_baseUrl}, Timeout={_httpClient.Timeout.TotalSeconds}s"); } public string BaseUrl => _baseUrl; public async Task LoginAsync(string username, string password) { var request = new LoginRequest { Username = username, Password = password }; var path = "/auth/login"; var fullUrl = BuildFullUrl(path); var requestJson = JsonSerializer.Serialize(request, JsonOptions); var stopwatch = Stopwatch.StartNew(); AppDebugLog.ApiRequest("POST", fullUrl, requestJson, $"username={username}"); try { using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl)) { httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(httpRequest); var responseBody = await response.Content.ReadAsStringAsync(); stopwatch.Stop(); AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); return ParseLoginResponse(response, responseBody); } } catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) { stopwatch.Stop(); AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, "авторизацию"); } catch (TaskCanceledException ex) { stopwatch.Stop(); AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, "авторизацию"); } catch (Exception ex) { stopwatch.Stop(); if (ex is UnauthorizedAccessException || ex is HttpRequestException) { AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw; } AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw new HttpRequestException($"Ошибка при авторизации ({_baseUrl}): {ex.Message}", ex); } } public void SetToken(string token) { _token = string.IsNullOrWhiteSpace(token) ? null : token; AppDebugLog.Info("ApiClient", $"Токен установлен: {AppDebugLog.MaskToken(_token)}"); } public string Token => _token; public bool IsAuthenticated => !string.IsNullOrEmpty(_token); public async Task GetPriceSummaryAsync( string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null, IProgress transferProgress = null) { EnsureAuthenticated(); var queryParams = new List(); if (!string.IsNullOrEmpty(supplierId)) { queryParams.Add($"supplier_id={Uri.EscapeDataString(supplierId)}"); } if (!string.IsNullOrEmpty(regionId)) { queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}"); } if (!string.IsNullOrWhiteSpace(q)) { queryParams.Add($"q={Uri.EscapeDataString(q)}"); } if (limit.HasValue) { queryParams.Add($"limit={limit.Value}"); } if (offset.HasValue) { queryParams.Add($"offset={offset.Value}"); } var path = "/api/supplier-prices/summary"; if (queryParams.Count > 0) { path += "?" + string.Join("&", queryParams); } var responseBody = transferProgress == null ? await SendAuthorizedGetAsync(path, "загрузку прайса") : await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress); try { var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); var count = summary?.Summary?.Count ?? 0; AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций"); return summary; } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ прайса", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайса: {ex.Message}", ex); } } public async Task> GetBuyersAsync() { EnsureAuthenticated(); var responseBody = await SendAuthorizedGetAsync("/api/buyers", "загрузку покупателей"); return DeserializeBuyers(responseBody); } public async Task GetBuyerByIdAsync(string buyerId) { EnsureAuthenticated(); if (string.IsNullOrWhiteSpace(buyerId)) { throw new ArgumentException("Не указан buyer_id.", nameof(buyerId)); } var responseBody = await SendAuthorizedGetAsync( $"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}", "загрузку покупателя"); try { return JsonSerializer.Deserialize(responseBody, JsonOptions); } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать покупателя", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателя: {ex.Message}", ex); } } public async Task> GetBuyerPriceListsAsync(string buyerId) { EnsureAuthenticated(); if (string.IsNullOrWhiteSpace(buyerId)) { throw new ArgumentException("Не указан buyer_id.", nameof(buyerId)); } var responseBody = await SendAuthorizedGetAsync( $"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}/price-lists", "загрузку назначений прайсов покупателя"); return DeserializeBuyerPriceLists(responseBody); } public async Task> GetPriceListsAsync() { EnsureAuthenticated(); var responseBody = await SendAuthorizedGetAsync("/api/price-lists", "загрузку прайс-листов"); return DeserializePriceLists(responseBody); } public async Task> GetPriceListRegionsAsync(string priceListId) { EnsureAuthenticated(); if (string.IsNullOrWhiteSpace(priceListId)) { throw new ArgumentException("Не указан price_list_id.", nameof(priceListId)); } var path = "/api/sc/price-list-regions?price_list_id=" + Uri.EscapeDataString(priceListId.Trim()); var responseBody = await SendAuthorizedGetAsync(path, "загрузку региональных наценок прайса"); return DeserializePriceListRegions(responseBody); } private static List DeserializeBuyers(string responseBody) { try { using (var doc = JsonDocument.Parse(responseBody)) { if (doc.RootElement.ValueKind == JsonValueKind.Array) { return JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); } var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); return wrapped?.Buyers ?? wrapped?.Items ?? new List(); } } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать список покупателей", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателей: {ex.Message}", ex); } } private static List DeserializeBuyerPriceLists(string responseBody) { try { using (var doc = JsonDocument.Parse(responseBody)) { if (doc.RootElement.ValueKind == JsonValueKind.Array) { return JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); } var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); return wrapped?.Items ?? wrapped?.PriceLists ?? new List(); } } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать назначения прайсов покупателя", ex); throw new HttpRequestException( $"Некорректный ответ сервера при загрузке назначений прайсов: {ex.Message}", ex); } } private static List DeserializePriceLists(string responseBody) { try { using (var doc = JsonDocument.Parse(responseBody)) { if (doc.RootElement.ValueKind == JsonValueKind.Array) { return JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); } var wrapped = JsonSerializer.Deserialize(responseBody, JsonOptions); return wrapped?.PriceLists ?? wrapped?.Items ?? new List(); } } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать прайс-листы", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайс-листов: {ex.Message}", ex); } } private static List DeserializePriceListRegions(string responseBody) { try { using (var doc = JsonDocument.Parse(responseBody)) { if (doc.RootElement.ValueKind == JsonValueKind.Array) { return JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); } } return new List(); } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать региональные наценки прайса", ex); throw new HttpRequestException( $"Некорректный ответ сервера при загрузке региональных наценок: {ex.Message}", ex); } } public async Task> GetInvoicesAsync() { var orders = await GetBuyerOrdersAsync(status: "Placed", limit: 200); var result = new List(); foreach (var order in orders) { var detail = await GetBuyerOrderByIdAsync(order.OrderID); if (detail == null) { continue; } result.Add(InvoiceApiItem.FromBuyerOrder(detail)); } AppDebugLog.Info("ApiClient", $"Накладные (через buyer/orders) получены: {result.Count} шт."); return result; } public async Task> GetBuyerOrdersAsync(string status = null, int limit = 200, int offset = 0) { EnsureAuthenticated(); var query = new List(); if (!string.IsNullOrWhiteSpace(status)) { query.Add($"status={Uri.EscapeDataString(status)}"); } if (limit > 0) { query.Add($"limit={limit}"); } if (offset > 0) { query.Add($"offset={offset}"); } var path = "/api/buyer/orders"; if (query.Count > 0) { path += "?" + string.Join("&", query); } var responseBody = await SendAuthorizedGetAsync(path, "загрузку заказов"); try { var response = JsonSerializer.Deserialize(responseBody, JsonOptions); return response?.Orders ?? new List(); } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать список заказов", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказов: {ex.Message}", ex); } } public async Task GetBuyerOrderByIdAsync(string orderId) { EnsureAuthenticated(); if (string.IsNullOrWhiteSpace(orderId)) { throw new ArgumentException("Не указан ID заказа.", nameof(orderId)); } var responseBody = await SendAuthorizedGetAsync($"/api/buyer/orders/{Uri.EscapeDataString(orderId)}", "загрузку заказа"); try { return JsonSerializer.Deserialize(responseBody, JsonOptions); } catch (Exception ex) { AppDebugLog.Error("ApiClient", $"Не удалось разобрать заказ {orderId}", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказа: {ex.Message}", ex); } } public async Task PlaceBuyerOrderAsync(string orderId) { EnsureAuthenticated(); if (string.IsNullOrWhiteSpace(orderId)) { throw new ArgumentException("Не указан ID заказа.", nameof(orderId)); } var responseBody = await SendAuthorizedPostAsync($"/api/buyer/orders/{Uri.EscapeDataString(orderId)}/place", string.Empty, "оформление заказа"); try { return JsonSerializer.Deserialize(responseBody, JsonOptions); } catch (Exception ex) { AppDebugLog.Error("ApiClient", $"Не удалось разобрать ответ оформления заказа {orderId}", ex); throw new HttpRequestException($"Некорректный ответ сервера при оформлении заказа: {ex.Message}", ex); } } private void EnsureAuthenticated() { if (!IsAuthenticated) { var ex = new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); AppDebugLog.Error("ApiClient", "Запрос без токена авторизации", ex); throw ex; } } private async Task SendAuthorizedGetAsync(string path, string operationName) { return await SendAuthorizedGetWithProgressAsync(path, operationName, null); } private async Task SendAuthorizedPostAsync(string path, string requestBody, string operationName) { var fullUrl = BuildFullUrl(path); var stopwatch = Stopwatch.StartNew(); AppDebugLog.ApiRequest("POST", fullUrl, requestBody, $"token={AppDebugLog.MaskToken(_token)}"); try { using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl)) { httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); httpRequest.Content = new StringContent(requestBody ?? string.Empty, Encoding.UTF8, "application/json"); var response = await _httpClient.SendAsync(httpRequest); var responseBody = await response.Content.ReadAsStringAsync(); stopwatch.Stop(); AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); if (response.IsSuccessStatusCode) { return responseBody; } if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { _token = null; var error = TryDeserializeError(responseBody); var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"; AppDebugLog.ApiHttpError("POST", fullUrl, (int)response.StatusCode, responseBody, operationName); throw new UnauthorizedAccessException(message); } var httpError = TryDeserializeError(responseBody); var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"; AppDebugLog.ApiHttpError("POST", fullUrl, (int)response.StatusCode, responseBody, operationName); throw new HttpRequestException(httpMessage); } } catch (UnauthorizedAccessException) { throw; } catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) { stopwatch.Stop(); AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, operationName); } catch (TaskCanceledException ex) { stopwatch.Stop(); AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, operationName); } } private async Task SendAuthorizedGetWithProgressAsync( string path, string operationName, IProgress transferProgress) { var fullUrl = BuildFullUrl(path); var stopwatch = Stopwatch.StartNew(); AppDebugLog.ApiRequest("GET", fullUrl, extra: $"token={AppDebugLog.MaskToken(_token)}"); try { using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl)) { httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead); var responseBody = await ReadContentWithProgressAsync(response, transferProgress); stopwatch.Stop(); AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); if (response.IsSuccessStatusCode) { return responseBody; } if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { _token = null; var error = TryDeserializeError(responseBody); var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"; AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName); var unauthorized = new UnauthorizedAccessException(message); AppDebugLog.Error("ApiClient", message, unauthorized); throw unauthorized; } var httpError = TryDeserializeError(responseBody); var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"; AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName); var httpEx = new HttpRequestException(httpMessage); AppDebugLog.Error("ApiClient", httpMessage, httpEx); throw httpEx; } } catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) { stopwatch.Stop(); AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, operationName); } catch (TaskCanceledException ex) { stopwatch.Stop(); AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw WrapNetworkError(ex, operationName); } catch (UnauthorizedAccessException) { throw; } catch (HttpRequestException) { throw; } catch (Exception ex) { stopwatch.Stop(); AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); throw; } } private static async Task ReadContentWithProgressAsync( HttpResponseMessage response, IProgress transferProgress) { var totalBytes = response.Content.Headers.ContentLength; using (var stream = await response.Content.ReadAsStreamAsync()) using (var memory = new System.IO.MemoryStream()) { var buffer = new byte[81920]; long received = 0; int read; while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { memory.Write(buffer, 0, read); received += read; transferProgress?.Report(new HttpTransferProgress(received, totalBytes)); } transferProgress?.Report(new HttpTransferProgress(received, totalBytes ?? received)); return Encoding.UTF8.GetString(memory.ToArray()); } } private string ParseLoginResponse(HttpResponseMessage response, string responseBody) { if (response.IsSuccessStatusCode) { try { var loginResponse = JsonSerializer.Deserialize(responseBody, JsonOptions); if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token)) { var emptyTokenEx = new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации"); AppDebugLog.Error("ApiClient", "Пустой токен в ответе логина", emptyTokenEx); throw emptyTokenEx; } _token = loginResponse.Token; AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}"); return _token; } catch (HttpRequestException) { throw; } catch (Exception ex) { AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ логина", ex); throw new HttpRequestException($"Некорректный ответ сервера при авторизации: {ex.Message}", ex); } } if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { var error = TryDeserializeError(responseBody); var message = $"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}"; AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login"); var unauthorized = new UnauthorizedAccessException(message); AppDebugLog.Error("ApiClient", message, unauthorized); throw unauthorized; } var httpError = TryDeserializeError(responseBody); var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"; AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login"); var httpEx = new HttpRequestException(httpMessage); AppDebugLog.Error("ApiClient", httpMessage, httpEx); throw httpEx; } private string BuildFullUrl(string path) { return new Uri(new Uri(_baseUrl + "/"), path.TrimStart('/')).ToString(); } private static ErrorResponse TryDeserializeError(string body) { if (string.IsNullOrWhiteSpace(body)) { return null; } try { return JsonSerializer.Deserialize(body, JsonOptions); } catch { return null; } } private HttpRequestException WrapNetworkError(Exception ex, string operation) { var details = ex.InnerException != null && ex.InnerException.Message != ex.Message ? $" ({ex.InnerException.Message})" : string.Empty; return new HttpRequestException( $"Не удалось выполнить {operation}. Сервер: {_baseUrl}{details}. {ex.Message}", ex); } } public sealed class InvoiceApiItem { public string InvoiceNumber { get; set; } public string InvoiceDate { get; set; } public string SupplierName { get; set; } public string ConsigneeName { get; set; } public string InvoiceSum { get; set; } public string RefuseSum { get; set; } public string SourceOrderId { get; set; } public List Items { get; set; } = new List(); public static InvoiceApiItem FromBuyerOrder(BuyerOrderDetailDto detail) { var supplierNames = detail.Items == null ? string.Empty : string.Join(", ", detail.Items .Select(i => i.Supplier) .Where(s => !string.IsNullOrWhiteSpace(s)) .Distinct()); var invoiceDate = detail.PlacedAt ?? detail.CreatedAt; var invoice = new InvoiceApiItem { InvoiceNumber = string.IsNullOrWhiteSpace(detail.LocalOrderNumber) ? detail.OrderID : detail.LocalOrderNumber, InvoiceDate = invoiceDate?.ToString("yyyy-MM-dd") ?? string.Empty, SupplierName = supplierNames, ConsigneeName = detail.LocationAddress ?? string.Empty, InvoiceSum = (detail.TotalAmount ?? 0m).ToString(System.Globalization.CultureInfo.InvariantCulture), RefuseSum = "0", SourceOrderId = detail.OrderID }; if (detail.Items != null) { foreach (var item in detail.Items) { invoice.Items.Add(new InvoiceApiOrderItem { GoodCode = item.ItemCode ?? string.Empty, GoodName = item.Name ?? string.Empty, SupplierName = item.Supplier ?? string.Empty, Qty = item.Qty, Price = item.UnitPrice, Total = item.Total }); } } return invoice; } } public sealed class InvoiceApiOrderItem { public string GoodCode { get; set; } public string GoodName { get; set; } public string SupplierName { get; set; } public decimal Qty { get; set; } public decimal Price { get; set; } public decimal Total { get; set; } } public sealed class BuyerOrdersResponse { [JsonPropertyName("orders")] public List Orders { get; set; } } public sealed class BuyerOrderListItemDto { [JsonPropertyName("order_id")] public string OrderID { get; set; } [JsonPropertyName("status")] public string Status { get; set; } } public sealed class BuyerOrderDetailDto { [JsonPropertyName("order_id")] public string OrderID { get; set; } [JsonPropertyName("status")] public string Status { get; set; } [JsonPropertyName("total_amount")] public decimal? TotalAmount { get; set; } [JsonPropertyName("created_at")] public DateTime? CreatedAt { get; set; } [JsonPropertyName("placed_at")] public DateTime? PlacedAt { get; set; } [JsonPropertyName("comment")] public string Comment { get; set; } [JsonPropertyName("location_address")] public string LocationAddress { get; set; } [JsonPropertyName("items")] public List Items { get; set; } [JsonIgnore] public string LocalOrderNumber { get; set; } } public sealed class BuyerOrderItemDetailDto { [JsonPropertyName("name")] public string Name { get; set; } [JsonPropertyName("supplier")] public string Supplier { get; set; } [JsonPropertyName("qty")] public decimal Qty { get; set; } [JsonPropertyName("unit_price")] public decimal UnitPrice { get; set; } [JsonPropertyName("total")] public decimal Total { get; set; } [JsonPropertyName("item_code")] public string ItemCode { get; set; } } public sealed class HttpTransferProgress { public HttpTransferProgress(long bytesReceived, long? totalBytes) { BytesReceived = bytesReceived; TotalBytes = totalBytes; } public long BytesReceived { get; } public long? TotalBytes { get; } public int Percent { get { if (!TotalBytes.HasValue || TotalBytes.Value <= 0) { return 0; } return (int)Math.Max(0, Math.Min(100, (100.0 * BytesReceived) / TotalBytes.Value)); } } } }