diff --git a/README.md b/README.md index a797352..b1f8633 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Dedykowana usługa Windows zastępująca proces Node na stanowiskach z enova365. Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora wraz z hasłem, connection string SQL, definicję dokumentu, magazyn domyślny i tryb zapisu. Repozytorium oraz paczka zawierają tylko `config/enova.json.sample`. Skrypt instalacyjny kopiuje wzorzec do `config/enova.json` wyłącznie wtedy, gdy lokalna konfiguracja jeszcze nie istnieje. -Uzupełnij pola `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą. +Uzupełnij pola `enova.operator`, `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Jeżeli operator lub hasło Enovy nie są podane, usługa nie loguje się do bazy Enovy i nie rejestruje endpointu `eu.smartb2b.place_order`; endpoint SQL oraz diagnostyka pozostają dostępne. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą. ## Budowanie i testy diff --git a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs index d0a8b32..9334774 100644 --- a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs +++ b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs @@ -48,19 +48,22 @@ public sealed class ServiceSettings public RuntimeSettings ResolveRuntimeSettings() { - var enovaPassword = Enova.Password - ?? throw new ConfigurationException("Parametr 'enova.password' jest wymagany."); - var installationPath = Path.GetFullPath( Environment.ExpandEnvironmentVariables(Enova.InstallationPath)); - return new RuntimeSettings( - installationPath, - new EnovaAdapterConfiguration( - new EnovaConnectionOptions(Enova.Database, Enova.Operator, enovaPassword), + EnovaAdapterConfiguration? enovaConfiguration = null; + if (!string.IsNullOrWhiteSpace(Enova.Operator) && Enova.Password is not null) + { + enovaConfiguration = new EnovaAdapterConfiguration( + new EnovaConnectionOptions(Enova.Database, Enova.Operator, Enova.Password), Enova.DocumentDefinition, Enova.DefaultWarehouseCode, - Enova.SaveMode), + Enova.SaveMode); + } + + return new RuntimeSettings( + installationPath, + enovaConfiguration, Sql.ConnectionString, Sql.CommandTimeoutSeconds, ResolvePath(Diagnostics.LogDirectory), @@ -78,8 +81,6 @@ public sealed class ServiceSettings } Require(Enova.InstallationPath, "enova.installationPath", errors); Require(Enova.Database, "enova.database", errors); - Require(Enova.Operator, "enova.operator", errors); - RequirePresent(Enova.Password, "enova.password", errors); Require(Enova.DocumentDefinition, "enova.documentDefinition", errors); Require(Sql.ConnectionString, "sql.connectionString", errors); if (Sql.CommandTimeoutSeconds <= 0) @@ -101,14 +102,6 @@ public sealed class ServiceSettings } } - private static void RequirePresent(string? value, string name, ICollection errors) - { - if (value is null) - { - errors.Add($"Parametr '{name}' jest wymagany."); - } - } - private static string ResolvePath(string path) => Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(path, AppContext.BaseDirectory); } @@ -126,7 +119,7 @@ public sealed class EnovaSettings public string Database { get; init; } = string.Empty; - public string Operator { get; init; } = "Administrator"; + public string? Operator { get; init; } public string? Password { get; init; } @@ -153,7 +146,7 @@ public sealed class DiagnosticsSettings public sealed record RuntimeSettings( string EnovaInstallationPath, - EnovaAdapterConfiguration EnovaConfiguration, + EnovaAdapterConfiguration? EnovaConfiguration, string SqlConnectionString, int SqlCommandTimeoutSeconds, string LogDirectory, diff --git a/src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs b/src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs index 937d47c..c067288 100644 --- a/src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs +++ b/src/SmartB2B.Enova.Service/Diagnostics/DiagnosticsHandler.cs @@ -7,13 +7,13 @@ namespace SmartB2B.Enova.Service.Diagnostics; public sealed partial class DiagnosticsHandler { - private readonly EnovaAdapterInfo _adapterInfo; + private readonly EnovaAdapterInfo? _adapterInfo; private readonly string _enovaPath; private readonly string _logDirectory; private readonly string _logPrefix; public DiagnosticsHandler( - EnovaAdapterInfo adapterInfo, + EnovaAdapterInfo? adapterInfo, string enovaPath, string logDirectory, string logPrefix) @@ -35,16 +35,25 @@ public sealed partial class DiagnosticsHandler { cancellationToken.ThrowIfCancellationRequested(); var serviceVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "unknown"; - var lines = new[] + var lines = new List { $"SmartB2B Enova Sync version: {serviceVersion}", $".NET version: {Environment.Version}", - $"Adapter version: {_adapterInfo.AdapterVersion}", - $"Soneta.Business version: {_adapterInfo.SonetaBusinessVersion}", - $"Soneta.Handel version: {_adapterInfo.SonetaHandelVersion}", - $"Enova path: {_enovaPath}", - $"Database: {_adapterInfo.DatabaseName}" + $"Enova path: {_enovaPath}" }; + + if (_adapterInfo is null) + { + lines.Add("Adapter Enovy: nie zainicjalizowano (place_order jest wyłączony)"); + } + else + { + lines.Add($"Adapter version: {_adapterInfo.AdapterVersion}"); + lines.Add($"Soneta.Business version: {_adapterInfo.SonetaBusinessVersion}"); + lines.Add($"Soneta.Handel version: {_adapterInfo.SonetaHandelVersion}"); + lines.Add($"Database: {_adapterInfo.DatabaseName}"); + } + return Task.FromResult(string.Join(Environment.NewLine, lines)); } diff --git a/src/SmartB2B.Enova.Service/Program.cs b/src/SmartB2B.Enova.Service/Program.cs index df511b8..1f97762 100644 --- a/src/SmartB2B.Enova.Service/Program.cs +++ b/src/SmartB2B.Enova.Service/Program.cs @@ -1,3 +1,4 @@ +using SmartB2B.Enova.Contracts; using SmartB2B.Enova.Service.Configuration; using SmartB2B.Enova.Service.Diagnostics; using SmartB2B.Enova.Service.Orders; @@ -28,7 +29,21 @@ internal static class Program using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath); var adapter = adapterLoader.Load(); - var adapterInfo = adapter.Initialize(runtime.EnovaConfiguration); + EnovaAdapterInfo? adapterInfo = null; + if (runtime.EnovaConfiguration is not null) + { + adapterInfo = adapter.Initialize(runtime.EnovaConfiguration); + + Console.WriteLine( + $"Załadowano Enovę {adapterInfo.SonetaBusinessVersion} z '{runtime.EnovaInstallationPath}'."); + Console.WriteLine($"Baza: {adapterInfo.DatabaseName}; portal WAMP: {settings.Portal}."); + } + else + { + Console.WriteLine( + "Nie podano enova.operator lub enova.password; endpoint eu.smartb2b.place_order jest wyłączony."); + Console.WriteLine($"Portal WAMP: {settings.Portal}."); + } _ = new System.Data.Common.DbConnectionStringBuilder { @@ -38,32 +53,32 @@ internal static class Program runtime.SqlConnectionString, runtime.SqlCommandTimeoutSeconds); - Console.WriteLine( - $"Załadowano Enovę {adapterInfo.SonetaBusinessVersion} z '{runtime.EnovaInstallationPath}'."); - Console.WriteLine($"Baza: {adapterInfo.DatabaseName}; portal WAMP: {settings.Portal}."); - if (commandLine.CheckConfigurationOnly) { - Console.WriteLine("Konfiguracja i adapter Enovy są poprawne."); + Console.WriteLine(runtime.EnovaConfiguration is null + ? "Konfiguracja jest poprawna; endpoint place_order pozostaje wyłączony." + : "Konfiguracja i adapter Enovy są poprawne."); return 0; } - var orders = new PlaceOrderHandler(adapter, runtime.EnovaConfiguration); var diagnostics = new DiagnosticsHandler( adapterInfo, runtime.EnovaInstallationPath, runtime.LogDirectory, runtime.LogPrefix); - IReadOnlyList operations = - [ - new DelegateRpcOperation("eu.smartb2b.place_order", orders.HandleAsync), - new DelegateRpcOperation("eu.smartb2b.sql_raw", sql.HandleAsync), - new DelegateRpcOperation("eu.smartb2b.sync.get_version", diagnostics.GetVersionAsync), - new DelegateRpcOperation("eu.smartb2b.sync.get_info", diagnostics.GetInfoAsync), - new DelegateRpcOperation("eu.smartb2b.sync.get_log", diagnostics.GetLogAsync), - new DelegateRpcOperation("eu.smartb2b.sync.get_error_log", diagnostics.GetErrorLogAsync) - ]; + var operations = new List(); + if (runtime.EnovaConfiguration is not null) + { + var orders = new PlaceOrderHandler(adapter, runtime.EnovaConfiguration); + operations.Add(new DelegateRpcOperation("eu.smartb2b.place_order", orders.HandleAsync)); + } + + operations.Add(new DelegateRpcOperation("eu.smartb2b.sql_raw", sql.HandleAsync)); + operations.Add(new DelegateRpcOperation("eu.smartb2b.sync.get_version", diagnostics.GetVersionAsync)); + operations.Add(new DelegateRpcOperation("eu.smartb2b.sync.get_info", diagnostics.GetInfoAsync)); + operations.Add(new DelegateRpcOperation("eu.smartb2b.sync.get_log", diagnostics.GetLogAsync)); + operations.Add(new DelegateRpcOperation("eu.smartb2b.sync.get_error_log", diagnostics.GetErrorLogAsync)); var wampClient = new WampServiceClient( settings.Wamp.ServerUrl, diff --git a/tests/SmartB2B.Enova.Tests/Program.cs b/tests/SmartB2B.Enova.Tests/Program.cs index 93b664d..91e303a 100644 --- a/tests/SmartB2B.Enova.Tests/Program.cs +++ b/tests/SmartB2B.Enova.Tests/Program.cs @@ -25,7 +25,9 @@ var tests = new (string Name, Func Run)[] ("Brak waluty zwraca stabilny błąd", MissingCurrency), ("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency), ("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory), - ("Sekrety są odczytywane z pliku konfiguracji", SecretsComeFromConfigurationFile) + ("Sekrety są odczytywane z pliku konfiguracji", SecretsComeFromConfigurationFile), + ("Brak danych operatora wyłącza place_order", MissingCredentialsDisablePlaceOrder), + ("Częściowe dane operatora wyłączają place_order", PartialCredentialsDisablePlaceOrder) }; var failures = 0; @@ -214,7 +216,10 @@ static Task SecretsComeFromConfigurationFile() var runtime = ServiceSettings.Load(path).ResolveRuntimeSettings(); - AssertEqual("haslo-z-pliku", runtime.EnovaConfiguration.Connection.Password, "hasło operatora"); + AssertEqual( + "haslo-z-pliku", + runtime.EnovaConfiguration?.Connection.Password, + "hasło operatora"); AssertEqual( "Server=db;Database=enova;User ID=user;Password=sql-z-pliku", runtime.SqlConnectionString, @@ -227,6 +232,47 @@ static Task SecretsComeFromConfigurationFile() } } +static Task MissingCredentialsDisablePlaceOrder() +{ + var runtime = LoadRuntimeSettings(string.Empty); + AssertEqual(null, runtime.EnovaConfiguration, "konfiguracja endpointu place_order"); + return Task.CompletedTask; +} + +static Task PartialCredentialsDisablePlaceOrder() +{ + var runtime = LoadRuntimeSettings("\"operator\": \"Administrator\","); + AssertEqual(null, runtime.EnovaConfiguration, "konfiguracja endpointu place_order"); + return Task.CompletedTask; +} + +static RuntimeSettings LoadRuntimeSettings(string credentialsJson) +{ + var path = Path.Combine(Path.GetTempPath(), $"enova-settings-{Guid.NewGuid():N}.json"); + try + { + File.WriteAllText(path, $$""" + { + "portal": "demo", + "wamp": { "serverUrl": "ws://localhost:8080/" }, + "enova": { + "installationPath": "C:/Enova", + "database": "Firma demo", + {{credentialsJson}} + "documentDefinition": "ZO" + }, + "sql": { "connectionString": "Server=db;Database=enova;User ID=user;Password=secret" } + } + """); + + return ServiceSettings.Load(path).ResolveRuntimeSettings(); + } + finally + { + File.Delete(path); + } +} + static RpcInvocation CreateInvocation() => new( [], new Dictionary