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 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); } } /// /// Sends a single local order by Id_Order (must be in state НОВЫЙ). /// public async Task SendSingleOrderAsync(string localOrderId) { _token = Settings.Default.stringToken; _baseUrl = AppConfig.ApiBaseUrl; if (string.IsNullOrWhiteSpace(_token)) { AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: токен отсутствует"); ToastNotification.ShowError("Не найден токен авторизации."); return false; } List 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 FetchDataFromSql(string onlyOrderId) { var orders = new Dictionary(); var locationCache = new Dictionary(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 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 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 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(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 Items { get; set; } = new List(); } 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; } } }