Apply available buyer/price-list/region discounts to summary.price on download, keep consignee carts separate, and harden SQLite startup/migrations. Co-authored-by: Cursor <cursoragent@cursor.com>
822 lines
33 KiB
C#
822 lines
33 KiB
C#
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<string> 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<PriceSummaryResponse> GetPriceSummaryAsync(
|
||
string supplierId = null,
|
||
string regionId = null,
|
||
string q = null,
|
||
int? limit = null,
|
||
int? offset = null,
|
||
IProgress<HttpTransferProgress> transferProgress = null)
|
||
{
|
||
EnsureAuthenticated();
|
||
|
||
var queryParams = new List<string>();
|
||
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<PriceSummaryResponse>(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<List<BuyerDto>> GetBuyersAsync()
|
||
{
|
||
EnsureAuthenticated();
|
||
var responseBody = await SendAuthorizedGetAsync("/api/buyers", "загрузку покупателей");
|
||
return DeserializeBuyers(responseBody);
|
||
}
|
||
|
||
public async Task<BuyerDto> 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<BuyerDto>(responseBody, JsonOptions);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать покупателя", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателя: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
public async Task<List<BuyerPriceListAssignmentDto>> 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<List<PriceListDto>> GetPriceListsAsync()
|
||
{
|
||
EnsureAuthenticated();
|
||
var responseBody = await SendAuthorizedGetAsync("/api/price-lists", "загрузку прайс-листов");
|
||
return DeserializePriceLists(responseBody);
|
||
}
|
||
|
||
public async Task<List<PriceListRegionDto>> 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<BuyerDto> DeserializeBuyers(string responseBody)
|
||
{
|
||
try
|
||
{
|
||
using (var doc = JsonDocument.Parse(responseBody))
|
||
{
|
||
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return JsonSerializer.Deserialize<List<BuyerDto>>(responseBody, JsonOptions)
|
||
?? new List<BuyerDto>();
|
||
}
|
||
|
||
var wrapped = JsonSerializer.Deserialize<BuyersResponse>(responseBody, JsonOptions);
|
||
return wrapped?.Buyers ?? wrapped?.Items ?? new List<BuyerDto>();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать список покупателей", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателей: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private static List<BuyerPriceListAssignmentDto> DeserializeBuyerPriceLists(string responseBody)
|
||
{
|
||
try
|
||
{
|
||
using (var doc = JsonDocument.Parse(responseBody))
|
||
{
|
||
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return JsonSerializer.Deserialize<List<BuyerPriceListAssignmentDto>>(responseBody, JsonOptions)
|
||
?? new List<BuyerPriceListAssignmentDto>();
|
||
}
|
||
|
||
var wrapped = JsonSerializer.Deserialize<BuyerPriceListsResponse>(responseBody, JsonOptions);
|
||
return wrapped?.Items ?? wrapped?.PriceLists ?? new List<BuyerPriceListAssignmentDto>();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать назначения прайсов покупателя", ex);
|
||
throw new HttpRequestException(
|
||
$"Некорректный ответ сервера при загрузке назначений прайсов: {ex.Message}",
|
||
ex);
|
||
}
|
||
}
|
||
|
||
private static List<PriceListDto> DeserializePriceLists(string responseBody)
|
||
{
|
||
try
|
||
{
|
||
using (var doc = JsonDocument.Parse(responseBody))
|
||
{
|
||
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return JsonSerializer.Deserialize<List<PriceListDto>>(responseBody, JsonOptions)
|
||
?? new List<PriceListDto>();
|
||
}
|
||
|
||
var wrapped = JsonSerializer.Deserialize<PriceListsResponse>(responseBody, JsonOptions);
|
||
return wrapped?.PriceLists ?? wrapped?.Items ?? new List<PriceListDto>();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать прайс-листы", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайс-листов: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
private static List<PriceListRegionDto> DeserializePriceListRegions(string responseBody)
|
||
{
|
||
try
|
||
{
|
||
using (var doc = JsonDocument.Parse(responseBody))
|
||
{
|
||
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
return JsonSerializer.Deserialize<List<PriceListRegionDto>>(responseBody, JsonOptions)
|
||
?? new List<PriceListRegionDto>();
|
||
}
|
||
}
|
||
|
||
return new List<PriceListRegionDto>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать региональные наценки прайса", ex);
|
||
throw new HttpRequestException(
|
||
$"Некорректный ответ сервера при загрузке региональных наценок: {ex.Message}",
|
||
ex);
|
||
}
|
||
}
|
||
|
||
public async Task<List<InvoiceApiItem>> GetInvoicesAsync()
|
||
{
|
||
var orders = await GetBuyerOrdersAsync(status: "Placed", limit: 200);
|
||
var result = new List<InvoiceApiItem>();
|
||
|
||
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<List<BuyerOrderListItemDto>> GetBuyerOrdersAsync(string status = null, int limit = 200, int offset = 0)
|
||
{
|
||
EnsureAuthenticated();
|
||
|
||
var query = new List<string>();
|
||
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<BuyerOrdersResponse>(responseBody, JsonOptions);
|
||
return response?.Orders ?? new List<BuyerOrderListItemDto>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", "Не удалось разобрать список заказов", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказов: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
public async Task<BuyerOrderDetailDto> 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<BuyerOrderDetailDto>(responseBody, JsonOptions);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
AppDebugLog.Error("ApiClient", $"Не удалось разобрать заказ {orderId}", ex);
|
||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказа: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
public async Task<BuyerOrderDetailDto> 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<BuyerOrderDetailDto>(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<string> SendAuthorizedGetAsync(string path, string operationName)
|
||
{
|
||
return await SendAuthorizedGetWithProgressAsync(path, operationName, null);
|
||
}
|
||
|
||
private async Task<string> 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<string> SendAuthorizedGetWithProgressAsync(
|
||
string path,
|
||
string operationName,
|
||
IProgress<HttpTransferProgress> 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<string> ReadContentWithProgressAsync(
|
||
HttpResponseMessage response,
|
||
IProgress<HttpTransferProgress> 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<LoginResponse>(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<ErrorResponse>(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<InvoiceApiOrderItem> Items { get; set; } = new List<InvoiceApiOrderItem>();
|
||
|
||
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<BuyerOrderListItemDto> 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<BuyerOrderItemDetailDto> 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));
|
||
}
|
||
}
|
||
}
|
||
}
|