elfisa-pharmacy/src/ElectronicPharmacy/Classes/DataSender.cs
Magomed 458b70e036 Add multi-pharmacy LocationId, batched price download, and SQL debug tools.
Persist last successful login only, show active pharmacy in the status bar, and keep settings compact at the bottom-right.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 10:09:04 +03:00

427 lines
16 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;
}
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;
}
if (!EnsureOrdersHaveLocationId(orders))
{
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;
}
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;
}
if (!EnsureOrdersHaveLocationId(orders))
{
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>();
var locationCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
string query = @"
SELECT
Orders.Id_Order,
Orders.ConsigneeName,
OrderItems.supplier_price_id,
OrderItems.DrugName,
OrderItems.es_code,
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 consigneeName = reader["ConsigneeName"] == DBNull.Value
? string.Empty
: reader["ConsigneeName"].ToString().Trim();
string supplierPriceId = reader["supplier_price_id"].ToString().Trim();
string drugName = reader["DrugName"] == DBNull.Value ? null : reader["DrugName"].ToString().Trim();
string esCode = reader["es_code"] == DBNull.Value ? null : reader["es_code"].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))
{
string locationId = ResolveLocationIdCached(consigneeName, locationCache);
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,
ItemName = string.IsNullOrWhiteSpace(drugName) ? null : drugName,
ItemCode = string.IsNullOrWhiteSpace(esCode) ? null : esCode
});
}
}
}
}
return orders.Values.ToList();
}
private static string ResolveLocationIdCached(string consigneeName, Dictionary<string, string> cache)
{
var key = consigneeName ?? string.Empty;
if (cache.TryGetValue(key, out var cached))
{
return cached;
}
var locationId = ConsigneeHelper.ResolveLocationId(consigneeName);
cache[key] = locationId ?? string.Empty;
return cache[key];
}
private bool EnsureOrdersHaveLocationId(List<BuyerOrderRequest> orders)
{
var missing = orders
.Where(o => string.IsNullOrWhiteSpace(o.LocationId))
.Select(o => o.LocalOrderId)
.Distinct()
.ToList();
if (missing.Count == 0)
{
return true;
}
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 false;
}
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
foreach (var order in orders.Where(o => string.IsNullOrWhiteSpace(o.LocationId)))
{
order.LocationId = AppConfig.LocationId;
}
if (orders.Any(o => string.IsNullOrWhiteSpace(o.LocationId)))
{
ToastNotification.ShowCustom(
"У части заказов нет Location ID. Укажите ID у грузополучателя.",
Color.DarkOrange,
Color.White);
return false;
}
return true;
}
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;
}
var createdOrder = JsonSerializer.Deserialize<BuyerOrderCreateResult>(responseString, options);
if (createdOrder == null || string.IsNullOrWhiteSpace(createdOrder.OrderID))
{
throw new InvalidOperationException("Сервер не вернул order_id созданного заказа.");
}
MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, createdOrder.Status);
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, string remoteOrderId, string remoteStatus)
{
if (string.IsNullOrWhiteSpace(localOrderId))
{
return;
}
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand(
@"UPDATE [Orders]
SET [OrderState] = @state,
[RemoteOrderId] = @remoteOrderId
WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
con))
{
cmd.Parameters.AddWithValue("@state", "ОТПРАВЛЕН");
cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId ?? string.Empty);
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; }
[JsonPropertyName("item_name")]
public string ItemName { get; set; }
[JsonPropertyName("item_code")]
public string ItemCode { get; set; }
}
public sealed class BuyerOrderCreateResult
{
[JsonPropertyName("order_id")]
public string OrderID { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
}
}