Add Optima document attributes to place_order
This commit is contained in:
@@ -11,7 +11,8 @@ if (!options.TryGetValue("config", out var configPath) || string.IsNullOrWhiteSp
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"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;
|
||||
}
|
||||
|
||||
@@ -78,9 +79,28 @@ if (saveMode == OrderSaveMode.Buffer && runtime.AdapterConfiguration.SaveMode !=
|
||||
}
|
||||
|
||||
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);
|
||||
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(JsonSerializer.Serialize(new
|
||||
{
|
||||
@@ -124,7 +144,8 @@ static OrderRequest CreateRequest(
|
||||
string productCode,
|
||||
string currency,
|
||||
string reference,
|
||||
OrderSaveMode saveMode = OrderSaveMode.Buffer) => new()
|
||||
OrderSaveMode saveMode = OrderSaveMode.Buffer,
|
||||
IReadOnlyDictionary<string, string>? extraFields = null) => new()
|
||||
{
|
||||
CurrencyIso = currency,
|
||||
CustomerCode = customerId,
|
||||
@@ -133,6 +154,7 @@ static OrderRequest CreateRequest(
|
||||
DocumentDefinition = configuration.DocumentDefinition,
|
||||
WarehouseCode = configuration.DefaultWarehouseCode,
|
||||
SaveMode = saveMode,
|
||||
ExtraFields = extraFields ?? new Dictionary<string, string>(),
|
||||
Items =
|
||||
[
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -3,6 +3,7 @@ using SmartB2B.Optima.Adapter;
|
||||
using SmartB2B.Sync.Contracts;
|
||||
using SmartB2B.Sync.Service.Orders;
|
||||
using SmartB2B.Sync.Service.Rpc;
|
||||
using WampSharp.V2.Core.Contracts;
|
||||
|
||||
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),
|
||||
("Brak ceny wymaganej przez definicję jest stabilnym błędem", RequiredPriceIsEnforced),
|
||||
("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),
|
||||
("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized),
|
||||
("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");
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
using var adapter = new CapturingAdapter();
|
||||
@@ -135,6 +240,7 @@ static Task FacadeRunsOnSingleStaThread()
|
||||
AssertEqual(1, facade.ThreadIds.Distinct().Count(), "liczba wątków COM");
|
||||
AssertEqual(ApartmentState.STA, facade.ApartmentStates.Distinct().Single(), "apartment COM");
|
||||
AssertEqual("Buffer", info.Details?["Save mode"], "tryb zapisu w diagnostyce");
|
||||
AssertEqual(true, info.Capabilities.Contains("extraFields"), "możliwość extraFields");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -152,9 +258,9 @@ static async Task AdapterSerializesComCalls()
|
||||
AssertEqual(1, facade.MaximumConcurrency, "maksymalna równoległość fasady COM");
|
||||
}
|
||||
|
||||
static RpcInvocation CreateInvocation() => new(
|
||||
[],
|
||||
new Dictionary<string, object?>
|
||||
static RpcInvocation CreateInvocation(object? extraFields = null, bool includeExtraFields = false)
|
||||
{
|
||||
var arguments = new Dictionary<string, object?>
|
||||
{
|
||||
["currency_iso"] = "eur",
|
||||
["companyErpId"] = "123",
|
||||
@@ -172,7 +278,10 @@ static RpcInvocation CreateInvocation() => new(
|
||||
Discount = 12.5m
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
if (includeExtraFields || extraFields is not null) arguments["extraFields"] = extraFields;
|
||||
return new RpcInvocation([], arguments);
|
||||
}
|
||||
|
||||
static ErpAdapterConfiguration CreateConfiguration() => new(
|
||||
new ErpConnectionOptions("Firma demo", "ADMIN", "", "Server=.;Database=demo"),
|
||||
@@ -317,3 +426,69 @@ file sealed class CapturingComFacade : IOptimaComFacade
|
||||
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