elfisa-pharmacy/src/ElectronicPharmacy/Classes/DataSender.cs
Magomed a6601567f2 Improve order UX and harden API debugging.
Prompt for Location ID after login, move cart delete there, refine price-list Backspace navigation, and log every API failure with stack and response body.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 16:31:00 +03:00

349 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows.Forms;
using Электроннаяармация.HelpForms;
using Электроннаяармация.Properties;
namespace Электроннаяармация.Classes
{
public class DataSender
{
private const string BuyerOrdersEndpoint = "/api/buyer/orders";
private string _baseUrl;
private string _token;
public async Task Main()
{
_token = Settings.Default.stringToken;
_baseUrl = AppConfig.ApiBaseUrl;
if (string.IsNullOrWhiteSpace(_token))
{
AppDebugLog.Error("DataSender", "Отправка заказов: токен отсутствует");
ToastNotification.ShowError("Не найден токен авторизации.");
return;
}
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан");
ToastNotification.ShowCustom(
"Не указан location_id. Заполните его после авторизации.",
Color.DarkOrange,
Color.White);
return;
}
}
List<BuyerOrderRequest> orders;
try
{
orders = FetchDataFromSql(null);
}
catch (Exception ex)
{
AppDebugLog.Error("DataSender", "Ошибка подготовки заказов к отправке", ex);
ToastNotification.ShowError($"Ошибка подготовки заказов: {ex.Message}");
return;
}
if (orders.Count == 0)
{
ToastNotification.ShowCustom("Нет новых заказов для отправки.", Color.DarkOrange, Color.White);
return;
}
int successCount = 0;
int failCount = 0;
foreach (var order in orders)
{
bool ok = await SendOrderAsync(order);
if (ok)
{
successCount++;
}
else
{
failCount++;
}
}
if (successCount > 0 && failCount == 0)
{
ToastNotification.ShowSuccess($"Отправлено заказов: {successCount}");
}
else if (successCount > 0)
{
ToastNotification.ShowCustom(
$"Отправлено: {successCount}, с ошибкой: {failCount}",
Color.DarkOrange,
Color.White);
}
}
/// <summary>
/// Sends a single local order by Id_Order (must be in state НОВЫЙ).
/// </summary>
public async Task<bool> SendSingleOrderAsync(string localOrderId)
{
_token = Settings.Default.stringToken;
_baseUrl = AppConfig.ApiBaseUrl;
if (string.IsNullOrWhiteSpace(_token))
{
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: токен отсутствует");
ToastNotification.ShowError("Не найден токен авторизации.");
return false;
}
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: location_id не указан");
ToastNotification.ShowCustom(
"Не указан location_id. Заполните его после авторизации.",
Color.DarkOrange,
Color.White);
return false;
}
}
List<BuyerOrderRequest> orders;
try
{
orders = FetchDataFromSql(localOrderId);
}
catch (Exception ex)
{
AppDebugLog.Error("DataSender", $"Ошибка подготовки заказа {localOrderId}", ex);
ToastNotification.ShowError($"Ошибка подготовки заказа: {ex.Message}");
return false;
}
if (orders.Count == 0)
{
ToastNotification.ShowCustom("Заказ не найден или уже отправлен.", Color.DarkOrange, Color.White);
return false;
}
bool ok = await SendOrderAsync(orders[0]);
if (ok)
{
ToastNotification.ShowSuccess("Заказ отправлен");
}
return ok;
}
public List<BuyerOrderRequest> FetchDataFromSql(string onlyOrderId)
{
var orders = new Dictionary<string, BuyerOrderRequest>();
string locationId = AppConfig.LocationId;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
string query = @"
SELECT
Orders.Id_Order,
OrderItems.supplier_price_id,
OrderItems.Zakaz,
Orders.Comment
FROM Orders
INNER JOIN OrderItems ON OrderItems.Id_Order = Orders.Id_Order
WHERE Orders.OrderState = 'НОВЫЙ'
";
if (!string.IsNullOrWhiteSpace(onlyOrderId))
{
query += " AND Orders.Id_Order = @orderId";
}
query += " ORDER BY Orders.Id_Order";
using (var command = new SQLiteCommand(query, connection))
{
if (!string.IsNullOrWhiteSpace(onlyOrderId))
{
command.Parameters.AddWithValue("@orderId", onlyOrderId);
}
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string orderId = reader["Id_Order"].ToString().Trim();
string supplierPriceId = reader["supplier_price_id"].ToString().Trim();
decimal qty = ReadDecimal(reader["Zakaz"]);
if (string.IsNullOrWhiteSpace(supplierPriceId))
{
throw new InvalidOperationException($"В заказе {orderId} есть позиция без supplier_price_id.");
}
if (qty <= 0)
{
throw new InvalidOperationException($"В заказе {orderId} количество должно быть больше нуля.");
}
if (!orders.TryGetValue(orderId, out BuyerOrderRequest order))
{
order = new BuyerOrderRequest
{
LocalOrderId = orderId,
LocationId = locationId,
Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString()
};
orders.Add(orderId, order);
}
order.Items.Add(new BuyerOrderItem
{
SupplierPriceId = supplierPriceId,
Qty = qty
});
}
}
}
}
return orders.Values.ToList();
}
private async Task<bool> SendOrderAsync(BuyerOrderRequest order)
{
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string json = JsonSerializer.Serialize(order, options);
var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), BuyerOrdersEndpoint.TrimStart('/')).ToString();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
AppDebugLog.ApiRequest("POST", fullUrl, json, $"order={order.LocalOrderId}");
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_baseUrl);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
var content = new StringContent(json, Encoding.UTF8, "application/json");
try
{
HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content);
string responseString = await response.Content.ReadAsStringAsync();
stopwatch.Stop();
AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseString);
if (!response.IsSuccessStatusCode)
{
AppDebugLog.ApiHttpError(
"POST",
fullUrl,
(int)response.StatusCode,
responseString,
$"order={order.LocalOrderId}");
AppDebugLog.Error(
"DataSender",
$"Ошибка отправки заказа {order.LocalOrderId}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
ToastNotification.ShowError(
$"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
return false;
}
MarkOrderSent(order.LocalOrderId);
AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен");
return true;
}
catch (Exception ex)
{
stopwatch.Stop();
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
ToastNotification.ShowError($"Ошибка сети при отправке заказа {order.LocalOrderId}: {ex.Message}");
return false;
}
}
}
private static void MarkOrderSent(string localOrderId)
{
if (string.IsNullOrWhiteSpace(localOrderId))
{
return;
}
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand(
"UPDATE [Orders] SET [OrderState] = 'ОТПРАВЛЕН' WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
con))
{
cmd.Parameters.AddWithValue("@id", localOrderId);
cmd.ExecuteNonQuery();
}
}
}
private static decimal ReadDecimal(object value)
{
if (value == null || value == DBNull.Value)
{
return 0;
}
string text = value.ToString();
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal invariantValue))
{
return invariantValue;
}
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out decimal currentCultureValue))
{
return currentCultureValue;
}
throw new InvalidOperationException($"Некорректное количество: {text}");
}
}
public class BuyerOrderRequest
{
[JsonIgnore]
public string LocalOrderId { get; set; }
[JsonPropertyName("location_id")]
public string LocationId { get; set; }
[JsonPropertyName("comment")]
public string Comment { get; set; }
[JsonPropertyName("items")]
public List<BuyerOrderItem> Items { get; set; } = new List<BuyerOrderItem>();
}
public class BuyerOrderItem
{
[JsonPropertyName("supplier_price_id")]
public string SupplierPriceId { get; set; }
[JsonPropertyName("qty")]
public decimal Qty { get; set; }
}
}