Log complete place_order requests

This commit is contained in:
janusz committed 2026-09-18 13:44:58 +00:00
1 parent 31daea55cf
commit 6cb57ad4d1
4 files changed
+78

No files matched your search

+1
View File
@@ -111,6 +111,7 @@ Wycofanie polega na uruchomieniu `release-enova/daemon/uninstall.cmd` i ponownym
## Procedury WAMP ## 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`. - `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`. - `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`. - Diagnostyka: `eu.smartb2b.sync.get_version`, `get_info`, `get_log`, `get_error_log`.
@@ -20,6 +20,12 @@ public sealed class PlaceOrderHandler
public async Task<object?> HandleAsync(RpcInvocation invocation, CancellationToken cancellationToken) public async Task<object?> 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")) if (!invocation.Contains("currency_iso"))
{ {
throw WampErrorFactory.Create("eu.smartb2b.missing_currency", "Pole currency_iso jest wymagane."); throw WampErrorFactory.Create("eu.smartb2b.missing_currency", "Pole currency_iso jest wymagane.");
@@ -14,6 +14,8 @@ public sealed class RpcInvocation
public object?[] PositionalArguments { get; } public object?[] PositionalArguments { get; }
public IReadOnlyDictionary<string, object?> KeywordArguments => _keywordArguments;
public bool Contains(string name) => _keywordArguments.ContainsKey(name); public bool Contains(string name) => _keywordArguments.ContainsKey(name);
public T? Get<T>(string name, T? defaultValue = default) public T? Get<T>(string name, T? defaultValue = default)
+69
View File
@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Newtonsoft.Json.Linq;
using SmartB2B.Optima.Adapter; using SmartB2B.Optima.Adapter;
using SmartB2B.Sync.Contracts; using SmartB2B.Sync.Contracts;
using SmartB2B.Sync.Service.Orders; using SmartB2B.Sync.Service.Orders;
@@ -17,6 +18,8 @@ var tests = new (string Name, Func<Task> Run)[]
("Handler przekazuje pełny kontrakt do Optimy", HandlerMapsOptimaContract), ("Handler przekazuje pełny kontrakt do Optimy", HandlerMapsOptimaContract),
("Handler przyjmuje i pomija opcjonalne extraFields", HandlerMapsOptionalExtraFields), ("Handler przyjmuje i pomija opcjonalne extraFields", HandlerMapsOptionalExtraFields),
("Handler odrzuca błędne extraFields", HandlerRejectsInvalidExtraFields), ("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 przypisuje atrybuty nagłówka przed zapisem", FacadeAddsDocumentAttributes),
("Fasada odrzuca nieznany kod atrybutu", FacadeRejectsMissingAttribute), ("Fasada odrzuca nieznany kod atrybutu", FacadeRejectsMissingAttribute),
("Fasada zgłasza odrzuconą wartość atrybutu", FacadeRejectsAttributeValue), ("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<string, string> { ["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<string>(), "argument pozycyjny");
AssertEqual("eur", logged["kwargs"]?["currency_iso"]?.Value<string>(), "waluta bez normalizacji");
AssertEqual("123", logged["kwargs"]?["companyErpId"]?.Value<string>(), "kontrahent");
AssertEqual("PO/OPTIMA/1", logged["kwargs"]?["purchase_order_number"]?.Value<string>(), "referencja");
AssertEqual("Test", logged["kwargs"]?["notes"]?.Value<string>(), "uwagi");
AssertEqual("TOWAR", logged["kwargs"]?["lines"]?[0]?["symbol"]?.Value<string>(), "pozycja");
AssertEqual("B2B", logged["kwargs"]?["extraFields"]?["SOURCE"]?.Value<string>(), "atrybut");
}
static async Task HandlerLogsBeforeValidation()
{
using var adapter = new CapturingAdapter();
var handler = new PlaceOrderHandler(adapter, CreateConfiguration());
var invocation = new RpcInvocation([], new Dictionary<string, object?>
{
["extraFields"] = new Dictionary<string, string> { ["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<string>(),
"atrybut z odrzuconego wywołania");
}
static async Task<string> CaptureOutputAsync(Func<Task> 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() static Task FacadeAddsDocumentAttributes()
{ {
var session = new FakeAttributeSession(new Dictionary<string, int> { ["SOURCE"] = 7, ["NOTE"] = 8 }); var session = new FakeAttributeSession(new Dictionary<string, int> { ["SOURCE"] = 7, ["NOTE"] = 8 });