From 6cb57ad4d1e0522820964abd508f75f7444fc6fe Mon Sep 17 00:00:00 2001 From: Janusz Szczypka Date: Fri, 18 Sep 2026 13:44:58 +0000 Subject: [PATCH] Log complete place_order requests --- README.md | 1 + .../Orders/PlaceOrderHandler.cs | 6 ++ .../Rpc/RpcInvocation.cs | 2 + tests/SmartB2B.Optima.Tests/Program.cs | 69 +++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/README.md b/README.md index 24ba3f5..39ef667 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Wycofanie polega na uruchomieniu `release-enova/daemon/uninstall.cmd` i ponownym ## Procedury WAMP - `eu.smartb2b.place_order` przyjmuje `currency_iso`, `companyErpId`, `warehouseErpId`, `purchase_order_number`, `notes` i `lines[]` z polami `symbol`, `quantity`, opcjonalnymi `price_netto` i `price_brutto` oraz `discount`. Dla Optimy opcjonalne `extraFields` jest mapą kodów atrybutów dokumentu (`DefAtrybuty.DeA_Kod`) na wartości tekstowe, np. `{"ŹRÓDŁO":"B2B"}`. Nieznany kod lub wartość odrzucona przez Optimę przerywa zapis zamówienia. Enova wymaga `price_netto`; Optima wybiera cenę zgodnie z `TypNB` definicji dokumentu. Wynik jest zwracany jako kwargs: `order_erp_id`, `order_erp_symbol`, `value_netto`, `value_brutto`, `stocks`. +- Każde wywołanie `place_order` jest logowane przed walidacją do standardowego logu usługi jako jedna linia JSON z pełnymi `args` i `kwargs`, w tym `extraFields`. Działa to także bez `/verbose`; log zawiera niezmienione wartości przesłane przez portal i można go odczytać przez `eu.smartb2b.sync.get_log`. - `eu.smartb2b.sql_raw` przyjmuje `query` i opcjonalne `params`; parametry odpowiadają kolejno `@1`, `@2`, itd. Wynik ma pola `recordsets`, `recordset`, `output`, `rowsAffected`. - Diagnostyka: `eu.smartb2b.sync.get_version`, `get_info`, `get_log`, `get_error_log`. diff --git a/src/SmartB2B.Sync.Service/Orders/PlaceOrderHandler.cs b/src/SmartB2B.Sync.Service/Orders/PlaceOrderHandler.cs index 669acaf..bc10404 100644 --- a/src/SmartB2B.Sync.Service/Orders/PlaceOrderHandler.cs +++ b/src/SmartB2B.Sync.Service/Orders/PlaceOrderHandler.cs @@ -20,6 +20,12 @@ public sealed class PlaceOrderHandler public async Task HandleAsync(RpcInvocation invocation, CancellationToken cancellationToken) { + Console.WriteLine("Wywołanie eu.smartb2b.place_order: " + JsonConvert.SerializeObject(new + { + args = invocation.PositionalArguments, + kwargs = invocation.KeywordArguments + })); + if (!invocation.Contains("currency_iso")) { throw WampErrorFactory.Create("eu.smartb2b.missing_currency", "Pole currency_iso jest wymagane."); diff --git a/src/SmartB2B.Sync.Service/Rpc/RpcInvocation.cs b/src/SmartB2B.Sync.Service/Rpc/RpcInvocation.cs index 902b276..fd02d74 100644 --- a/src/SmartB2B.Sync.Service/Rpc/RpcInvocation.cs +++ b/src/SmartB2B.Sync.Service/Rpc/RpcInvocation.cs @@ -14,6 +14,8 @@ public sealed class RpcInvocation public object?[] PositionalArguments { get; } + public IReadOnlyDictionary KeywordArguments => _keywordArguments; + public bool Contains(string name) => _keywordArguments.ContainsKey(name); public T? Get(string name, T? defaultValue = default) diff --git a/tests/SmartB2B.Optima.Tests/Program.cs b/tests/SmartB2B.Optima.Tests/Program.cs index 0c04fa3..f7024a5 100644 --- a/tests/SmartB2B.Optima.Tests/Program.cs +++ b/tests/SmartB2B.Optima.Tests/Program.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Newtonsoft.Json.Linq; using SmartB2B.Optima.Adapter; using SmartB2B.Sync.Contracts; using SmartB2B.Sync.Service.Orders; @@ -17,6 +18,8 @@ var tests = new (string Name, Func Run)[] ("Handler przekazuje pełny kontrakt do Optimy", HandlerMapsOptimaContract), ("Handler przyjmuje i pomija opcjonalne extraFields", HandlerMapsOptionalExtraFields), ("Handler odrzuca błędne extraFields", HandlerRejectsInvalidExtraFields), + ("Handler loguje pełne wywołanie place_order", HandlerLogsFullInvocation), + ("Handler loguje wywołanie przed walidacją", HandlerLogsBeforeValidation), ("Fasada przypisuje atrybuty nagłówka przed zapisem", FacadeAddsDocumentAttributes), ("Fasada odrzuca nieznany kod atrybutu", FacadeRejectsMissingAttribute), ("Fasada zgłasza odrzuconą wartość atrybutu", FacadeRejectsAttributeValue), @@ -152,6 +155,72 @@ static async Task HandlerRejectsInvalidExtraFields() } } +static async Task HandlerLogsFullInvocation() +{ + using var adapter = new CapturingAdapter(); + var handler = new PlaceOrderHandler(adapter, CreateConfiguration()); + var request = CreateInvocation(new Dictionary { ["SOURCE"] = "B2B" }); + var invocation = new RpcInvocation(["trace-id"], request.KeywordArguments); + var output = await CaptureOutputAsync(() => handler.HandleAsync(invocation, CancellationToken.None)); + var logged = ReadLoggedInvocation(output); + + AssertEqual("trace-id", logged["args"]?[0]?.Value(), "argument pozycyjny"); + AssertEqual("eur", logged["kwargs"]?["currency_iso"]?.Value(), "waluta bez normalizacji"); + AssertEqual("123", logged["kwargs"]?["companyErpId"]?.Value(), "kontrahent"); + AssertEqual("PO/OPTIMA/1", logged["kwargs"]?["purchase_order_number"]?.Value(), "referencja"); + AssertEqual("Test", logged["kwargs"]?["notes"]?.Value(), "uwagi"); + AssertEqual("TOWAR", logged["kwargs"]?["lines"]?[0]?["symbol"]?.Value(), "pozycja"); + AssertEqual("B2B", logged["kwargs"]?["extraFields"]?["SOURCE"]?.Value(), "atrybut"); +} + +static async Task HandlerLogsBeforeValidation() +{ + using var adapter = new CapturingAdapter(); + var handler = new PlaceOrderHandler(adapter, CreateConfiguration()); + var invocation = new RpcInvocation([], new Dictionary + { + ["extraFields"] = new Dictionary { ["SOURCE"] = "B2B" } + }); + var output = await CaptureOutputAsync(async () => + { + try + { + await handler.HandleAsync(invocation, CancellationToken.None); + throw new InvalidOperationException("Oczekiwano błędu brakującej waluty."); + } + catch (WampException exception) when (exception.ErrorUri == "eu.smartb2b.missing_currency") + { + } + }); + AssertEqual("B2B", ReadLoggedInvocation(output)["kwargs"]?["extraFields"]?["SOURCE"]?.Value(), + "atrybut z odrzuconego wywołania"); +} + +static async Task CaptureOutputAsync(Func action) +{ + var original = Console.Out; + using var writer = new StringWriter(); + try + { + Console.SetOut(writer); + await action(); + return writer.ToString(); + } + finally + { + Console.SetOut(original); + } +} + +static JObject ReadLoggedInvocation(string output) +{ + const string prefix = "Wywołanie eu.smartb2b.place_order: "; + var line = output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) + .SingleOrDefault(item => item.StartsWith(prefix, StringComparison.Ordinal)) + ?? throw new InvalidOperationException("Brak logu pełnego wywołania."); + return JObject.Parse(line[prefix.Length..]); +} + static Task FacadeAddsDocumentAttributes() { var session = new FakeAttributeSession(new Dictionary { ["SOURCE"] = 7, ["NOTE"] = 8 });