Add Optima document attributes to place_order
This commit is contained in:
@@ -45,5 +45,14 @@ Po pomyślnym `--check-config` uruchom usługę z tym samym plikiem konfiguracji
|
|||||||
- błędy dla brakującego kontrahenta, produktu, magazynu, waluty i wymaganej ceny;
|
- błędy dla brakującego kontrahenta, produktu, magazynu, waluty i wymaganej ceny;
|
||||||
- parametryzowane `sql_raw`, wiele recordsetów, dane binarne oraz błąd SQL;
|
- parametryzowane `sql_raw`, wiele recordsetów, dane binarne oraz błąd SQL;
|
||||||
- dwa równoczesne wywołania `place_order` — dokumenty mają zostać utworzone kolejno na jednym wątku COM.
|
- dwa równoczesne wywołania `place_order` — dokumenty mają zostać utworzone kolejno na jednym wątku COM.
|
||||||
|
- `extraFields` z dwoma istniejącymi atrybutami dokumentu: wartości mają być widoczne na nagłówku RO; nieznany kod ma zwrócić błąd bez zapisu dokumentu.
|
||||||
|
|
||||||
|
Runner może sprawdzić ostatni scenariusz po podaniu dwóch kodów atrybutów tekstowych istniejących w firmie testowej:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project .\tests\SmartB2B.Optima.IntegrationTests -- --config .\src\SmartB2B.Optima.Service\config\optima.json --write-order --customer-id 2 --product-code SMARTB2B_TEST_TWR --attribute-code-1 SOURCE --attribute-value-1 B2B --attribute-code-2 NOTE --attribute-value-2 test
|
||||||
|
```
|
||||||
|
|
||||||
|
Test odczytuje zapisane wartości z `CDN.DokAtrybuty` i sprawdza, że próba użycia nieznanego kodu nie utworzyła RO.
|
||||||
|
|
||||||
Przed testami trybu `Approved` wykonaj kopię bezpieczeństwa bazy lub użyj firmy przeznaczonej wyłącznie do testów.
|
Przed testami trybu `Approved` wykonaj kopię bezpieczeństwa bazy lub użyj firmy przeznaczonej wyłącznie do testów.
|
||||||
|
|||||||
@@ -110,7 +110,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`. 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`.
|
||||||
- `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`.
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ internal sealed class LateBoundOptimaComFacade : IOptimaComFacade
|
|||||||
doc.Uwagi = request.Notes?.Trim() ?? string.Empty;
|
doc.Uwagi = request.Notes?.Trim() ?? string.Empty;
|
||||||
doc.WalutaSymbol = request.CurrencyIso;
|
doc.WalutaSymbol = request.CurrencyIso;
|
||||||
doc.Export = 0;
|
doc.Export = 0;
|
||||||
|
AddDocumentAttributes(comSession, doc, request.ExtraFields);
|
||||||
elements = doc.Elementy;
|
elements = doc.Elementy;
|
||||||
|
|
||||||
var calculationMode = Convert.ToInt32(doc.TypNB);
|
var calculationMode = Convert.ToInt32(doc.TypNB);
|
||||||
@@ -184,7 +185,19 @@ internal sealed class LateBoundOptimaComFacade : IOptimaComFacade
|
|||||||
}
|
}
|
||||||
|
|
||||||
Verbose(verboseLogging, "Optima: zapis dokumentu przez session.Save().");
|
Verbose(verboseLogging, "Optima: zapis dokumentu przez session.Save().");
|
||||||
comSession.Save();
|
try
|
||||||
|
{
|
||||||
|
comSession.Save();
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (request.ExtraFields.Count > 0)
|
||||||
|
{
|
||||||
|
var codes = request.ExtraFields.Keys.ToArray();
|
||||||
|
throw new ErpOperationException(
|
||||||
|
"eu.smartb2b.erp.invalid_document_attribute",
|
||||||
|
$"Nie udało się zapisać zamówienia z atrybutami dokumentu: {string.Join(", ", codes)}.",
|
||||||
|
new Dictionary<string, object?> { ["attributeCodes"] = codes },
|
||||||
|
exception);
|
||||||
|
}
|
||||||
var net = Convert.ToDecimal(doc.RazemNettoWal);
|
var net = Convert.ToDecimal(doc.RazemNettoWal);
|
||||||
var gross = Convert.ToDecimal(doc.RazemBruttoWal);
|
var gross = Convert.ToDecimal(doc.RazemBruttoWal);
|
||||||
return new OrderResult(
|
return new OrderResult(
|
||||||
@@ -212,6 +225,58 @@ internal sealed class LateBoundOptimaComFacade : IOptimaComFacade
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void AddDocumentAttributes(
|
||||||
|
dynamic session,
|
||||||
|
dynamic document,
|
||||||
|
IReadOnlyDictionary<string, string> extraFields)
|
||||||
|
{
|
||||||
|
if (extraFields.Count == 0) return;
|
||||||
|
|
||||||
|
object? attributes = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
attributes = document.Atrybuty;
|
||||||
|
foreach (var (code, value) in extraFields)
|
||||||
|
{
|
||||||
|
object? definition = null;
|
||||||
|
object? attribute = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
definition = FindRequired(
|
||||||
|
session,
|
||||||
|
"CDN.DefAtrybuty",
|
||||||
|
$"DeA_Typ=4 AND DeA_Kod='{EscapeFilter(code)}'",
|
||||||
|
"eu.smartb2b.erp.document_attribute_not_found",
|
||||||
|
$"Nie znaleziono atrybutu dokumentu o kodzie '{code}'.");
|
||||||
|
attribute = ((dynamic)attributes).AddNew();
|
||||||
|
((dynamic)attribute).DeAID = Convert.ToInt32(((dynamic)definition).ID);
|
||||||
|
((dynamic)attribute).Wartosc = value;
|
||||||
|
}
|
||||||
|
catch (ErpOperationException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
throw new ErpOperationException(
|
||||||
|
"eu.smartb2b.erp.invalid_document_attribute",
|
||||||
|
$"Nie można przypisać wartości atrybutu dokumentu '{code}'.",
|
||||||
|
new Dictionary<string, object?> { ["attributeCode"] = code },
|
||||||
|
exception);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(attribute);
|
||||||
|
ReleaseComObject(definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseComObject(attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static object FindRequired(
|
private static object FindRequired(
|
||||||
dynamic session,
|
dynamic session,
|
||||||
string collectionName,
|
string collectionName,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public sealed class OptimaOrderAdapter : IErpOrderAdapter
|
|||||||
"Comarch ERP Optima",
|
"Comarch ERP Optima",
|
||||||
runtime.ErpVersion,
|
runtime.ErpVersion,
|
||||||
configuration.Connection.DatabaseName,
|
configuration.Connection.DatabaseName,
|
||||||
["price_netto", "price_brutto", "multi_currency", "Buffer", "Approved", "COM STA"],
|
["price_netto", "price_brutto", "multi_currency", "extraFields", "Buffer", "Approved", "COM STA"],
|
||||||
new Dictionary<string, string>
|
new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["COM ProgID"] = runtime.ComProgId,
|
["COM ProgID"] = runtime.ComProgId,
|
||||||
|
|||||||
@@ -7,5 +7,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../SmartB2B.Sync.Contracts/SmartB2B.Sync.Contracts.csproj" />
|
<ProjectReference Include="../SmartB2B.Sync.Contracts/SmartB2B.Sync.Contracts.csproj" />
|
||||||
|
<InternalsVisibleTo Include="SmartB2B.Optima.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public sealed class OrderRequest
|
|||||||
public OrderSaveMode SaveMode { get; init; } = OrderSaveMode.Buffer;
|
public OrderSaveMode SaveMode { get; init; } = OrderSaveMode.Buffer;
|
||||||
|
|
||||||
public IReadOnlyList<OrderLineRequest> Items { get; init; } = [];
|
public IReadOnlyList<OrderLineRequest> Items { get; init; } = [];
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> ExtraFields { get; init; } = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OrderLineRequest
|
public sealed class OrderLineRequest
|
||||||
@@ -126,6 +128,12 @@ public static class OrderValidator
|
|||||||
errors.Add("Definicja dokumentu jest wymagana.");
|
errors.Add("Definicja dokumentu jest wymagana.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var field in request.ExtraFields)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(field.Key)) errors.Add("Kod w extraFields nie może być pusty.");
|
||||||
|
if (field.Value is null) errors.Add($"extraFields['{field.Key}'] musi być tekstem.");
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Items is null || request.Items.Count == 0)
|
if (request.Items is null || request.Items.Count == 0)
|
||||||
{
|
{
|
||||||
errors.Add("Zamówienie musi zawierać co najmniej jedną pozycję.");
|
errors.Add("Zamówienie musi zawierać co najmniej jedną pozycję.");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using SmartB2B.Sync.Contracts;
|
using SmartB2B.Sync.Contracts;
|
||||||
using SmartB2B.Sync.Service.Diagnostics;
|
using SmartB2B.Sync.Service.Diagnostics;
|
||||||
using SmartB2B.Sync.Service.Rpc;
|
using SmartB2B.Sync.Service.Rpc;
|
||||||
@@ -34,6 +35,7 @@ public sealed class PlaceOrderHandler
|
|||||||
DocumentDefinition = _configuration.DocumentDefinition,
|
DocumentDefinition = _configuration.DocumentDefinition,
|
||||||
WarehouseCode = invocation.Get<string>("warehouseErpId") ?? _configuration.DefaultWarehouseCode,
|
WarehouseCode = invocation.Get<string>("warehouseErpId") ?? _configuration.DefaultWarehouseCode,
|
||||||
SaveMode = _configuration.SaveMode,
|
SaveMode = _configuration.SaveMode,
|
||||||
|
ExtraFields = ReadExtraFields(invocation),
|
||||||
Items = inputLines.Select(line => new OrderLineRequest
|
Items = inputLines.Select(line => new OrderLineRequest
|
||||||
{
|
{
|
||||||
ProductCode = line.Symbol ?? string.Empty,
|
ProductCode = line.Symbol ?? string.Empty,
|
||||||
@@ -93,6 +95,37 @@ public sealed class PlaceOrderHandler
|
|||||||
_orderLock.Release();
|
_orderLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, string> ReadExtraFields(RpcInvocation invocation)
|
||||||
|
{
|
||||||
|
if (!invocation.Contains("extraFields")) return new Dictionary<string, string>();
|
||||||
|
|
||||||
|
JToken? token;
|
||||||
|
try { token = invocation.Get<JToken>("extraFields"); }
|
||||||
|
catch (Exception exception) when (exception is JsonException or ArgumentException or InvalidCastException or FormatException)
|
||||||
|
{
|
||||||
|
throw InvalidExtraFields("extraFields musi być mapą kodów atrybutów na wartości tekstowe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token is not JObject fields)
|
||||||
|
throw InvalidExtraFields("extraFields musi być mapą kodów atrybutów na wartości tekstowe.");
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
foreach (var field in fields.Properties())
|
||||||
|
{
|
||||||
|
var code = field.Name.Trim();
|
||||||
|
if (code.Length == 0) throw InvalidExtraFields("Kod w extraFields nie może być pusty.");
|
||||||
|
if (field.Value.Type != JTokenType.String)
|
||||||
|
throw InvalidExtraFields($"extraFields['{code}'] musi być tekstem.");
|
||||||
|
if (!result.TryAdd(code, field.Value.Value<string>()!))
|
||||||
|
throw InvalidExtraFields($"Kod '{code}' występuje w extraFields więcej niż raz.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception InvalidExtraFields(string message) =>
|
||||||
|
WampErrorFactory.Create("eu.smartb2b.erp.invalid_order_lines", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlaceOrderLine
|
public sealed class PlaceOrderLine
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ if (!options.TryGetValue("config", out var configPath) || string.IsNullOrWhiteSp
|
|||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
"Użycie: --config <optima.json> [--negative-tests] [--write-order [--approved]] " +
|
"Użycie: --config <optima.json> [--negative-tests] [--write-order [--approved]] " +
|
||||||
"[--customer-id <ID> --product-code <KOD>]");
|
"[--customer-id <ID> --product-code <KOD>] " +
|
||||||
|
"[--attribute-code-1 <KOD> --attribute-value-1 <TEKST> --attribute-code-2 <KOD> --attribute-value-2 <TEKST>]");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +79,28 @@ if (saveMode == OrderSaveMode.Buffer && runtime.AdapterConfiguration.SaveMode !=
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reference = $"SMARTB2B-IT-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
var reference = $"SMARTB2B-IT-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
||||||
var request = CreateRequest(runtime.AdapterConfiguration, customerId!, productCode!, "PLN", reference, saveMode);
|
var extraFields = ReadTestAttributes(options);
|
||||||
|
if (extraFields.Count > 0)
|
||||||
|
{
|
||||||
|
var missingReference = reference + "-MISSING";
|
||||||
|
var missingCode = "__SMARTB2B_MISSING_ATTR__";
|
||||||
|
ExpectError(
|
||||||
|
() => adapter.CreateOrder(CreateRequest(
|
||||||
|
runtime.AdapterConfiguration, customerId!, productCode!, "PLN", missingReference,
|
||||||
|
OrderSaveMode.Buffer, new Dictionary<string, string> { [missingCode] = "test" })),
|
||||||
|
"eu.smartb2b.erp.document_attribute_not_found");
|
||||||
|
await AssertNoOrderWithReferenceAsync(sql, missingReference);
|
||||||
|
Console.WriteLine("PASS extraFields: nieznany kod nie utworzył zamówienia.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = CreateRequest(runtime.AdapterConfiguration, customerId!, productCode!, "PLN", reference, saveMode, extraFields);
|
||||||
|
|
||||||
var result = adapter.CreateOrder(request);
|
var result = adapter.CreateOrder(request);
|
||||||
|
if (extraFields.Count > 0)
|
||||||
|
{
|
||||||
|
await AssertSavedAttributesAsync(sql, result.Id, extraFields);
|
||||||
|
Console.WriteLine("PASS extraFields: dwa atrybuty zapisano na nagłówku zamówienia.");
|
||||||
|
}
|
||||||
Console.WriteLine("PASS place_order:");
|
Console.WriteLine("PASS place_order:");
|
||||||
Console.WriteLine(JsonSerializer.Serialize(new
|
Console.WriteLine(JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
@@ -124,7 +144,8 @@ static OrderRequest CreateRequest(
|
|||||||
string productCode,
|
string productCode,
|
||||||
string currency,
|
string currency,
|
||||||
string reference,
|
string reference,
|
||||||
OrderSaveMode saveMode = OrderSaveMode.Buffer) => new()
|
OrderSaveMode saveMode = OrderSaveMode.Buffer,
|
||||||
|
IReadOnlyDictionary<string, string>? extraFields = null) => new()
|
||||||
{
|
{
|
||||||
CurrencyIso = currency,
|
CurrencyIso = currency,
|
||||||
CustomerCode = customerId,
|
CustomerCode = customerId,
|
||||||
@@ -133,6 +154,7 @@ static OrderRequest CreateRequest(
|
|||||||
DocumentDefinition = configuration.DocumentDefinition,
|
DocumentDefinition = configuration.DocumentDefinition,
|
||||||
WarehouseCode = configuration.DefaultWarehouseCode,
|
WarehouseCode = configuration.DefaultWarehouseCode,
|
||||||
SaveMode = saveMode,
|
SaveMode = saveMode,
|
||||||
|
ExtraFields = extraFields ?? new Dictionary<string, string>(),
|
||||||
Items =
|
Items =
|
||||||
[
|
[
|
||||||
new OrderLineRequest
|
new OrderLineRequest
|
||||||
@@ -146,6 +168,56 @@ static OrderRequest CreateRequest(
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static IReadOnlyDictionary<string, string> ReadTestAttributes(Dictionary<string, string?> options)
|
||||||
|
{
|
||||||
|
var names = new[] { "attribute-code-1", "attribute-value-1", "attribute-code-2", "attribute-value-2" };
|
||||||
|
if (!names.Any(options.ContainsKey)) return new Dictionary<string, string>();
|
||||||
|
if (names.Any(name => !options.ContainsKey(name)))
|
||||||
|
throw new ArgumentException("Test extraFields wymaga dwóch kodów i dwóch wartości atrybutów tekstowych.");
|
||||||
|
|
||||||
|
var first = options["attribute-code-1"]?.Trim();
|
||||||
|
var second = options["attribute-code-2"]?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(first) || string.IsNullOrWhiteSpace(second) || first == second)
|
||||||
|
throw new ArgumentException("Kody dwóch atrybutów muszą być różne i niepuste.");
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[first] = options["attribute-value-1"] ?? string.Empty,
|
||||||
|
[second] = options["attribute-value-2"] ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task AssertNoOrderWithReferenceAsync(SqlRawHandler sql, string reference)
|
||||||
|
{
|
||||||
|
var result = (SqlRawResult?)await sql.HandleAsync(
|
||||||
|
new RpcInvocation([], new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["query"] = "SELECT COUNT(*) AS order_count FROM CDN.TraNag WHERE TrN_NumerObcy=@1",
|
||||||
|
["params"] = new object?[] { reference }
|
||||||
|
}), CancellationToken.None);
|
||||||
|
if (Convert.ToInt32(result?.Recordsets[0][0]["order_count"]) != 0)
|
||||||
|
throw new InvalidOperationException("Nieznany atrybut pozostawił zapisane zamówienie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task AssertSavedAttributesAsync(
|
||||||
|
SqlRawHandler sql, int orderId, IReadOnlyDictionary<string, string> expected)
|
||||||
|
{
|
||||||
|
var result = (SqlRawResult?)await sql.HandleAsync(
|
||||||
|
new RpcInvocation([], new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["query"] = "SELECT DeA_Kod, DAt_WartoscTxt FROM CDN.DokAtrybuty JOIN CDN.DefAtrybuty ON DAt_DeAId=DeA_DeAId WHERE DAt_TrNId=@1",
|
||||||
|
["params"] = new object?[] { orderId }
|
||||||
|
}), CancellationToken.None);
|
||||||
|
var actual = result?.Recordsets[0].ToDictionary(
|
||||||
|
row => Convert.ToString(row["DeA_Kod"])!,
|
||||||
|
row => Convert.ToString(row["DAt_WartoscTxt"]) ?? string.Empty,
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
foreach (var (code, value) in expected)
|
||||||
|
{
|
||||||
|
if (actual is null || !actual.TryGetValue(code, out var saved) || saved != value)
|
||||||
|
throw new InvalidOperationException($"Atrybut '{code}' nie ma oczekiwanej wartości na dokumencie {orderId}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void ExpectError(Action action, string expectedUri)
|
static void ExpectError(Action action, string expectedUri)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using SmartB2B.Optima.Adapter;
|
|||||||
using SmartB2B.Sync.Contracts;
|
using SmartB2B.Sync.Contracts;
|
||||||
using SmartB2B.Sync.Service.Orders;
|
using SmartB2B.Sync.Service.Orders;
|
||||||
using SmartB2B.Sync.Service.Rpc;
|
using SmartB2B.Sync.Service.Rpc;
|
||||||
|
using WampSharp.V2.Core.Contracts;
|
||||||
|
|
||||||
var tests = new (string Name, Func<Task> Run)[]
|
var tests = new (string Name, Func<Task> Run)[]
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,11 @@ var tests = new (string Name, Func<Task> Run)[]
|
|||||||
("Tryb brutto wybiera price_brutto", GrossDocumentSelectsGrossPrice),
|
("Tryb brutto wybiera price_brutto", GrossDocumentSelectsGrossPrice),
|
||||||
("Brak ceny wymaganej przez definicję jest stabilnym błędem", RequiredPriceIsEnforced),
|
("Brak ceny wymaganej przez definicję jest stabilnym błędem", RequiredPriceIsEnforced),
|
||||||
("Handler przekazuje pełny kontrakt do Optimy", HandlerMapsOptimaContract),
|
("Handler przekazuje pełny kontrakt do Optimy", HandlerMapsOptimaContract),
|
||||||
|
("Handler przyjmuje i pomija opcjonalne extraFields", HandlerMapsOptionalExtraFields),
|
||||||
|
("Handler odrzuca błędne extraFields", HandlerRejectsInvalidExtraFields),
|
||||||
|
("Fasada przypisuje atrybuty nagłówka przed zapisem", FacadeAddsDocumentAttributes),
|
||||||
|
("Fasada odrzuca nieznany kod atrybutu", FacadeRejectsMissingAttribute),
|
||||||
|
("Fasada zgłasza odrzuconą wartość atrybutu", FacadeRejectsAttributeValue),
|
||||||
("Wynik Optimy zachowuje kwargs WAMP", HandlerReturnsKeywordResult),
|
("Wynik Optimy zachowuje kwargs WAMP", HandlerReturnsKeywordResult),
|
||||||
("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized),
|
("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized),
|
||||||
("Fasada COM działa na jednym wątku STA", FacadeRunsOnSingleStaThread),
|
("Fasada COM działa na jednym wątku STA", FacadeRunsOnSingleStaThread),
|
||||||
@@ -102,6 +108,105 @@ static async Task HandlerMapsOptimaContract()
|
|||||||
AssertEqual(123m, request.Items[0].UnitPriceGross, "brutto");
|
AssertEqual(123m, request.Items[0].UnitPriceGross, "brutto");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task HandlerMapsOptionalExtraFields()
|
||||||
|
{
|
||||||
|
using var adapter = new CapturingAdapter();
|
||||||
|
var handler = new PlaceOrderHandler(adapter, CreateConfiguration());
|
||||||
|
await handler.HandleAsync(CreateInvocation(new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["SOURCE"] = "B2B",
|
||||||
|
["NOTE"] = ""
|
||||||
|
}), CancellationToken.None);
|
||||||
|
AssertEqual("B2B", adapter.LastRequest?.ExtraFields["SOURCE"], "wartość SOURCE");
|
||||||
|
AssertEqual("", adapter.LastRequest?.ExtraFields["NOTE"], "pusta wartość");
|
||||||
|
|
||||||
|
await handler.HandleAsync(CreateInvocation(), CancellationToken.None);
|
||||||
|
AssertEqual(0, adapter.LastRequest?.ExtraFields.Count, "brak extraFields");
|
||||||
|
await handler.HandleAsync(CreateInvocation(new Dictionary<string, string>()), CancellationToken.None);
|
||||||
|
AssertEqual(0, adapter.LastRequest?.ExtraFields.Count, "puste extraFields");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task HandlerRejectsInvalidExtraFields()
|
||||||
|
{
|
||||||
|
foreach (var invalid in new object?[]
|
||||||
|
{
|
||||||
|
null,
|
||||||
|
"text",
|
||||||
|
new[] { "SOURCE" },
|
||||||
|
new Dictionary<string, object?> { ["SOURCE"] = 123 },
|
||||||
|
new Dictionary<string, object?> { [" "] = "value" },
|
||||||
|
new Dictionary<string, object?> { ["SOURCE"] = "one", [" SOURCE "] = "two" }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
using var adapter = new CapturingAdapter();
|
||||||
|
var handler = new PlaceOrderHandler(adapter, CreateConfiguration());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await handler.HandleAsync(CreateInvocation(invalid, includeExtraFields: true), CancellationToken.None);
|
||||||
|
throw new InvalidOperationException("Oczekiwano odrzucenia extraFields.");
|
||||||
|
}
|
||||||
|
catch (WampException exception) when (exception.ErrorUri == "eu.smartb2b.erp.invalid_order_lines")
|
||||||
|
{
|
||||||
|
AssertEqual(null, adapter.LastRequest, "brak zapisu zamówienia");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Task FacadeAddsDocumentAttributes()
|
||||||
|
{
|
||||||
|
var session = new FakeAttributeSession(new Dictionary<string, int> { ["SOURCE"] = 7, ["NOTE"] = 8 });
|
||||||
|
var document = new FakeAttributeDocument();
|
||||||
|
LateBoundOptimaComFacade.AddDocumentAttributes(session, document, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["SOURCE"] = "B2B",
|
||||||
|
["NOTE"] = ""
|
||||||
|
});
|
||||||
|
AssertEqual(2, document.Atrybuty.Items.Count, "liczba atrybutów");
|
||||||
|
AssertEqual(7, document.Atrybuty.Items[0].DeAID, "ID SOURCE");
|
||||||
|
AssertEqual("B2B", document.Atrybuty.Items[0].Wartosc, "wartość SOURCE");
|
||||||
|
AssertEqual(8, document.Atrybuty.Items[1].DeAID, "ID NOTE");
|
||||||
|
AssertEqual("", document.Atrybuty.Items[1].Wartosc, "pusta wartość NOTE");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Task FacadeRejectsMissingAttribute()
|
||||||
|
{
|
||||||
|
var session = new FakeAttributeSession(new Dictionary<string, int>());
|
||||||
|
var document = new FakeAttributeDocument();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LateBoundOptimaComFacade.AddDocumentAttributes(session, document, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["MISSING"] = "value"
|
||||||
|
});
|
||||||
|
throw new InvalidOperationException("Oczekiwano błędu nieznanego kodu.");
|
||||||
|
}
|
||||||
|
catch (ErpOperationException exception) when (exception.ErrorUri == "eu.smartb2b.erp.document_attribute_not_found")
|
||||||
|
{
|
||||||
|
AssertEqual(0, document.Atrybuty.Items.Count, "brak dodanego atrybutu");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Task FacadeRejectsAttributeValue()
|
||||||
|
{
|
||||||
|
var session = new FakeAttributeSession(new Dictionary<string, int> { ["SOURCE"] = 7 });
|
||||||
|
var document = new FakeAttributeDocument();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LateBoundOptimaComFacade.AddDocumentAttributes(session, document, new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["SOURCE"] = "REJECTED"
|
||||||
|
});
|
||||||
|
throw new InvalidOperationException("Oczekiwano błędu wartości atrybutu.");
|
||||||
|
}
|
||||||
|
catch (ErpOperationException exception) when (exception.ErrorUri == "eu.smartb2b.erp.invalid_document_attribute")
|
||||||
|
{
|
||||||
|
AssertEqual("SOURCE", exception.Details["attributeCode"], "kod w błędzie");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async Task HandlerReturnsKeywordResult()
|
static async Task HandlerReturnsKeywordResult()
|
||||||
{
|
{
|
||||||
using var adapter = new CapturingAdapter();
|
using var adapter = new CapturingAdapter();
|
||||||
@@ -135,6 +240,7 @@ static Task FacadeRunsOnSingleStaThread()
|
|||||||
AssertEqual(1, facade.ThreadIds.Distinct().Count(), "liczba wątków COM");
|
AssertEqual(1, facade.ThreadIds.Distinct().Count(), "liczba wątków COM");
|
||||||
AssertEqual(ApartmentState.STA, facade.ApartmentStates.Distinct().Single(), "apartment COM");
|
AssertEqual(ApartmentState.STA, facade.ApartmentStates.Distinct().Single(), "apartment COM");
|
||||||
AssertEqual("Buffer", info.Details?["Save mode"], "tryb zapisu w diagnostyce");
|
AssertEqual("Buffer", info.Details?["Save mode"], "tryb zapisu w diagnostyce");
|
||||||
|
AssertEqual(true, info.Capabilities.Contains("extraFields"), "możliwość extraFields");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +258,9 @@ static async Task AdapterSerializesComCalls()
|
|||||||
AssertEqual(1, facade.MaximumConcurrency, "maksymalna równoległość fasady COM");
|
AssertEqual(1, facade.MaximumConcurrency, "maksymalna równoległość fasady COM");
|
||||||
}
|
}
|
||||||
|
|
||||||
static RpcInvocation CreateInvocation() => new(
|
static RpcInvocation CreateInvocation(object? extraFields = null, bool includeExtraFields = false)
|
||||||
[],
|
{
|
||||||
new Dictionary<string, object?>
|
var arguments = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["currency_iso"] = "eur",
|
["currency_iso"] = "eur",
|
||||||
["companyErpId"] = "123",
|
["companyErpId"] = "123",
|
||||||
@@ -172,7 +278,10 @@ static RpcInvocation CreateInvocation() => new(
|
|||||||
Discount = 12.5m
|
Discount = 12.5m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (includeExtraFields || extraFields is not null) arguments["extraFields"] = extraFields;
|
||||||
|
return new RpcInvocation([], arguments);
|
||||||
|
}
|
||||||
|
|
||||||
static ErpAdapterConfiguration CreateConfiguration() => new(
|
static ErpAdapterConfiguration CreateConfiguration() => new(
|
||||||
new ErpConnectionOptions("Firma demo", "ADMIN", "", "Server=.;Database=demo"),
|
new ErpConnectionOptions("Firma demo", "ADMIN", "", "Server=.;Database=demo"),
|
||||||
@@ -317,3 +426,69 @@ file sealed class CapturingComFacade : IOptimaComFacade
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class FakeAttributeSession
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyDictionary<string, int> _definitions;
|
||||||
|
|
||||||
|
public FakeAttributeSession(IReadOnlyDictionary<string, int> definitions) => _definitions = definitions;
|
||||||
|
|
||||||
|
public FakeDefinitionCollection CreateObject(string name)
|
||||||
|
{
|
||||||
|
if (name != "CDN.DefAtrybuty") throw new InvalidOperationException($"Nieoczekiwana kolekcja: {name}");
|
||||||
|
return new FakeDefinitionCollection(_definitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FakeDefinitionCollection
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyDictionary<string, int> _definitions;
|
||||||
|
|
||||||
|
public FakeDefinitionCollection(IReadOnlyDictionary<string, int> definitions) => _definitions = definitions;
|
||||||
|
|
||||||
|
public FakeDefinition? Item(string filter)
|
||||||
|
{
|
||||||
|
if (!filter.StartsWith("DeA_Typ=4 AND DeA_Kod='", StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException($"Niepoprawny filtr: {filter}");
|
||||||
|
var code = filter["DeA_Typ=4 AND DeA_Kod='".Length..^1].Replace("''", "'", StringComparison.Ordinal);
|
||||||
|
return _definitions.TryGetValue(code, out var id) ? new FakeDefinition(id) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FakeDefinition
|
||||||
|
{
|
||||||
|
public FakeDefinition(int id) => ID = id;
|
||||||
|
public int ID { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FakeAttributeDocument
|
||||||
|
{
|
||||||
|
public FakeAttributeCollection Atrybuty { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FakeAttributeCollection
|
||||||
|
{
|
||||||
|
public List<FakeDocumentAttribute> Items { get; } = [];
|
||||||
|
|
||||||
|
public FakeDocumentAttribute AddNew()
|
||||||
|
{
|
||||||
|
var attribute = new FakeDocumentAttribute();
|
||||||
|
Items.Add(attribute);
|
||||||
|
return attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FakeDocumentAttribute
|
||||||
|
{
|
||||||
|
public int DeAID { get; set; }
|
||||||
|
private string? _value;
|
||||||
|
public string? Wartosc
|
||||||
|
{
|
||||||
|
get => _value;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == "REJECTED") throw new ArgumentException("Niepoprawna wartość.");
|
||||||
|
_value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user