Log complete place_order requests
This commit is contained in:
1 parent
31daea55cf
commit
6cb57ad4d1
4 files changed
+78
No files matched your search
@@ -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)
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in new issue
Block a user