diff --git a/src/ElectronicPharmacy/Classes/DataSender.cs b/src/ElectronicPharmacy/Classes/DataSender.cs index 0d16877..7a158ba 100644 --- a/src/ElectronicPharmacy/Classes/DataSender.cs +++ b/src/ElectronicPharmacy/Classes/DataSender.cs @@ -143,6 +143,7 @@ namespace Электронная_Фармация.Classes string query = @" SELECT Orders.Id_Order, + Orders.OrderNumber, Orders.ConsigneeName, OrderItems.supplier_price_id, OrderItems.DrugName, @@ -172,6 +173,9 @@ WHERE Orders.OrderState = 'НОВЫЙ' while (reader.Read()) { string orderId = reader["Id_Order"].ToString().Trim(); + string orderNumber = reader["OrderNumber"] == DBNull.Value + ? string.Empty + : reader["OrderNumber"].ToString().Trim(); string consigneeName = reader["ConsigneeName"] == DBNull.Value ? string.Empty : reader["ConsigneeName"].ToString().Trim(); @@ -196,6 +200,9 @@ WHERE Orders.OrderState = 'НОВЫЙ' order = new BuyerOrderRequest { LocalOrderId = orderId, + GlobalSign = OrderNumberHelper.IsValidEx(orderNumber) + ? OrderNumberHelper.Normalize(orderNumber) + : null, LocationId = locationId, Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString() }; @@ -324,10 +331,25 @@ WHERE Orders.OrderState = 'НОВЫЙ' throw new InvalidOperationException("Сервер не вернул order_id созданного заказа."); } - MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, createdOrder.Status); - AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен"); + // Старые серверы создавали Draft — дожимаем до Placed. + var remoteStatus = createdOrder.Status; + if (!string.Equals(remoteStatus, "Placed", StringComparison.OrdinalIgnoreCase)) + { + remoteStatus = await PlaceOrderAsync(client, createdOrder.OrderID) ?? remoteStatus; + } + + MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, remoteStatus, createdOrder.GlobalSign); + AppDebugLog.Info("DataSender", + $"Заказ {order.LocalOrderId} отправлен, remote={createdOrder.OrderID}, status={remoteStatus}, sign={createdOrder.GlobalSign}"); return true; } + catch (InvalidOperationException ex) + { + stopwatch.Stop(); + AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}", ex); + ToastNotification.ShowError(ex.Message); + return false; + } catch (Exception ex) { stopwatch.Stop(); @@ -338,7 +360,67 @@ WHERE Orders.OrderState = 'НОВЫЙ' } } - private static void MarkOrderSent(string localOrderId, string remoteOrderId, string remoteStatus) + private async Task PlaceOrderAsync(HttpClient client, string remoteOrderId) + { + var placePath = $"{BuyerOrdersEndpoint}/{Uri.EscapeDataString(remoteOrderId)}/place"; + var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), placePath.TrimStart('/')).ToString(); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + AppDebugLog.ApiRequest("POST", fullUrl, string.Empty, $"place={remoteOrderId}"); + + try + { + using (var content = new StringContent(string.Empty, Encoding.UTF8, "application/json")) + using (HttpResponseMessage response = await client.PostAsync(placePath, 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, + $"place={remoteOrderId}"); + throw new InvalidOperationException( + $"Не удалось оформить заказ на сервере (HTTP {(int)response.StatusCode}). " + + "Заказ остался черновиком (Draft)."); + } + + try + { + using (var doc = JsonDocument.Parse(responseString)) + { + if (doc.RootElement.TryGetProperty("status", out var statusEl)) + { + return statusEl.GetString(); + } + } + } + catch + { + // ignore parse — place succeeded + } + + return "Placed"; + } + } + catch (InvalidOperationException) + { + throw; + } + catch (Exception ex) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw new InvalidOperationException( + $"Не удалось оформить заказ на сервере: {ex.Message}", ex); + } + } + + private static void MarkOrderSent(string localOrderId, string remoteOrderId, string remoteStatus, string globalSign) { if (string.IsNullOrWhiteSpace(localOrderId)) { @@ -351,12 +433,18 @@ WHERE Orders.OrderState = 'НОВЫЙ' using (var cmd = new SQLiteCommand( @"UPDATE [Orders] SET [OrderState] = @state, - [RemoteOrderId] = @remoteOrderId + [RemoteOrderId] = @remoteOrderId, + [OrderNumber] = CASE + WHEN @globalSign IS NOT NULL AND length(trim(@globalSign)) > 0 THEN @globalSign + ELSE [OrderNumber] + END WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'", con)) { cmd.Parameters.AddWithValue("@state", "ОТПРАВЛЕН"); cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId ?? string.Empty); + cmd.Parameters.AddWithValue("@globalSign", + string.IsNullOrWhiteSpace(globalSign) ? (object)DBNull.Value : globalSign.Trim()); cmd.Parameters.AddWithValue("@id", localOrderId); cmd.ExecuteNonQuery(); } @@ -393,6 +481,9 @@ WHERE Orders.OrderState = 'НОВЫЙ' [JsonPropertyName("location_id")] public string LocationId { get; set; } + [JsonPropertyName("global_sign")] + public string GlobalSign { get; set; } + [JsonPropertyName("comment")] public string Comment { get; set; } @@ -422,5 +513,8 @@ WHERE Orders.OrderState = 'НОВЫЙ' [JsonPropertyName("status")] public string Status { get; set; } + + [JsonPropertyName("global_sign")] + public string GlobalSign { get; set; } } } diff --git a/src/ElectronicPharmacy/Classes/OrderNumberHelper.cs b/src/ElectronicPharmacy/Classes/OrderNumberHelper.cs new file mode 100644 index 0000000..977de38 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/OrderNumberHelper.cs @@ -0,0 +1,157 @@ +using System; +using System.Data.SQLite; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Электронная_Фармация.Properties; + +namespace Электронная_Фармация.Classes +{ + /// + /// Глобальные номера заказов вида EX-0000001. + /// Источник истины — сервер (никогда не повторяются между аптеками). + /// Offline fallback — локальный счётчик EX- (потом сервер может заменить при отправке). + /// + public static class OrderNumberHelper + { + private static readonly Regex ExPattern = new Regex(@"^EX-(\d{1,7})$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Выделяет номер для нового локального заказа. + /// Сначала пробует сервер /api/buyer/global-sign/next, иначе локальный EX-#######. + /// + public static string AllocateForNewOrder() + { + try + { + var fromServer = AllocateFromServerAsync().GetAwaiter().GetResult(); + if (!string.IsNullOrWhiteSpace(fromServer)) + { + return fromServer; + } + } + catch (Exception ex) + { + AppDebugLog.Info("OrderNumber", $"Сервер недоступен для GlobalSign: {ex.Message}"); + } + + return AllocateLocalFallback(); + } + + public static async Task AllocateFromServerAsync() + { + var token = Settings.Default.stringToken; + var baseUrl = AppConfig.ApiBaseUrl; + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl)) + { + return null; + } + + using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) }) + { + client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Trim()); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + using (var response = await client.PostAsync("api/buyer/global-sign/next", new StringContent(string.Empty)).ConfigureAwait(false)) + { + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + AppDebugLog.Info("OrderNumber", $"global-sign/next HTTP {(int)response.StatusCode}: {body}"); + return null; + } + + using (var doc = JsonDocument.Parse(body)) + { + if (doc.RootElement.TryGetProperty("global_sign", out var el)) + { + var sign = el.GetString(); + if (IsValidEx(sign)) + { + return Normalize(sign); + } + } + } + } + } + + return null; + } + + public static string AllocateLocalFallback() + { + long max = 0; + try + { + using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + con.Open(); + using (var cmd = new SQLiteCommand( + "SELECT OrderNumber FROM Orders WHERE OrderNumber LIKE 'EX-%'", con)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var n = ParseExNumber(reader[0]?.ToString()); + if (n > max) + { + max = n; + } + } + } + } + } + catch (Exception ex) + { + AppDebugLog.Info("OrderNumber", $"Локальный max EX-: {ex.Message}"); + } + + return FormatEx(max + 1); + } + + public static bool IsValidEx(string value) + { + return ParseExNumber(value) > 0; + } + + public static string Normalize(string value) + { + var n = ParseExNumber(value); + return n > 0 ? FormatEx(n) : value?.Trim(); + } + + public static string FormatEx(long n) + { + if (n < 1) + { + n = 1; + } + + if (n > 9999999) + { + return "EX-" + n.ToString(); + } + + return "EX-" + n.ToString("D7"); + } + + public static long ParseExNumber(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return 0; + } + + var m = ExPattern.Match(value.Trim()); + if (!m.Success) + { + return 0; + } + + return long.TryParse(m.Groups[1].Value, out var n) ? n : 0; + } + } +} diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index ca9ef57..a5da65e 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -113,6 +113,7 @@ + diff --git a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs index d183d3c..1702d4b 100644 --- a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs +++ b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ using System.Runtime.InteropServices; // пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.22.0")] -[assembly: AssemblyFileVersion("1.0.22.0")] +[assembly: AssemblyVersion("1.0.23.0")] +[assembly: AssemblyFileVersion("1.0.23.0")] diff --git a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs index 234af22..13ff426 100644 --- a/src/ElectronicPharmacy/UserControls/UCOrderItems.cs +++ b/src/ElectronicPharmacy/UserControls/UCOrderItems.cs @@ -554,7 +554,8 @@ group by SupplierName void createOrderForNextSuppliers(string SupName) { - string OrderNumber = $"EP-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString("N").Substring(0, 6)}"; + // Глобальный номер EX-0000001… (сервер / локальный fallback). + string OrderNumber = OrderNumberHelper.AllocateForNewOrder(); string OrderDate = DateTime.Now.ToString("yyyy-MM-dd"); _consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty; var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''"); diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 5f63e79..406869e 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -1521,7 +1521,7 @@ BestBefore = '{BestBefore}' { string connectionStringToLocalDB = AppConfig.SqliteConnectionString; - string OrderNumber = $"EP-{DateTime.Now.Millisecond.ToString()}{DateTime.Now.Date.ToString("mmdd")}"; + string OrderNumber = OrderNumberHelper.AllocateForNewOrder(); string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");//DateTime.Now.ToShortDateString();