Fix international order VAT and currencies in Enova and Optima

This commit is contained in:
janusz committed 2026-10-29 01:31:13 +01:00
1 parent 75ef1ed69c
commit 248d7bac64
23 files changed
+308 -34

No files matched your search

@@ -0,0 +1,81 @@
namespace SmartB2B.Optima.Tests;
// Atrapa granicy COM sprawdza przebieg rzeczywistej fasady bez instalacji Optimy.
public sealed class FakeOrderSession
{
public FakeOrderDocument Document { get; } = new();
public bool Saved { get; private set; }
public FakeOrderCollection CreateObject(string name) => new(name, Document);
public void Save() => Saved = true;
}
public sealed class FakeOrderCollection(string name, FakeOrderDocument document)
{
public object? Item(string filter) => name switch
{
"CDN.DefinicjeDokumentow" => new FakeOrderDefinition(),
"CDN.Kontrahenci" or "CDN.Magazyny" or "CDN.Towary" => new object(),
"CDN.Waluty" => new[] { "PLN", "EUR", "USD", "CZK" }
.Any(currency => filter == $"WNa_Symbol='{currency}'") ? new object() : null,
_ => throw new InvalidOperationException($"Nieoczekiwana kolekcja: {name}")
};
public FakeOrderDocument AddNew() => name == "CDN.DokumentyHaMag"
? document : throw new InvalidOperationException($"Nieoczekiwana kolekcja: {name}");
}
public sealed class FakeOrderDefinition
{
public int Klasa => 308;
}
public sealed class FakeOrderDocument
{
public int Rodzaj { get; set; }
public int TypDokumentu { get; set; }
public object? Podmiot { get; set; }
public int Bufor { get; set; }
public int MagazynZrodlowyID { get; set; }
public string? NumerObcy { get; set; }
public string? Uwagi { get; set; }
public string WalutaSymbol { get; set; } = "PLN";
public int Export { get; set; }
public int TypNB => 1;
public int ID => 42;
public string NumerPelny => "RO/42/2026";
public FakeOrderElements Elementy { get; }
public decimal RazemNettoWal => Elementy.Items.Sum(line => line.Net);
public decimal RazemBruttoWal => Elementy.Items.Sum(line => line.Net * (line.ExportWhenProductAssigned == 0 ? 1.23m : 1m));
public FakeOrderDocument() => Elementy = new(this);
}
public sealed class FakeOrderElements(FakeOrderDocument document)
{
public List<FakeOrderElement> Items { get; } = [];
public FakeOrderElement AddNew()
{
var item = new FakeOrderElement(document);
Items.Add(item);
return item;
}
}
public sealed class FakeOrderElement(FakeOrderDocument document)
{
public int ExportWhenProductAssigned { get; private set; }
public string? CurrencyWhenProductAssigned { get; private set; }
public object Towar
{
set
{
ExportWhenProductAssigned = document.Export;
CurrencyWhenProductAssigned = document.WalutaSymbol;
}
}
public double Ilosc { get; set; }
public decimal Cena0 { get; set; }
public double Rabat { get; set; }
public decimal Net => Convert.ToDecimal(Ilosc) * Cena0 * (1m - Convert.ToDecimal(Rabat) / 100m);
}
+83 -1
View File
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Globalization;
using Newtonsoft.Json.Linq;
using SmartB2B.Optima.Adapter;
using SmartB2B.Optima.Tests;
using SmartB2B.Sync.Contracts;
using SmartB2B.Sync.Service.Diagnostics;
using SmartB2B.Sync.Service.Orders;
@@ -18,6 +19,9 @@ 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),
("Flagi z WampOrder rozróżniają zamówienia krajowe i zagraniczne", HandlerMapsInternationalFlags),
("Fasada ustawia walutę i rodzaj transakcji przed pozycjami", FacadeMapsInternationalOrders),
("Fasada odrzuca nieznaną walutę przed zapisem", FacadeRejectsUnknownCurrency),
("Handler przyjmuje i pomija opcjonalne extraFields", HandlerMapsOptionalExtraFields),
("Handler odrzuca błędne extraFields", HandlerRejectsInvalidExtraFields),
("Handler loguje pełne wywołanie place_order", HandlerLogsFullInvocation),
@@ -34,8 +38,16 @@ var tests = new (string Name, Func<Task> Run)[]
};
var failures = 0;
var skipped = 0;
foreach (var test in tests)
{
if (!OperatingSystem.IsWindows() &&
(test.Run == (Func<Task>)FacadeRunsOnSingleStaThread || test.Run == (Func<Task>)AdapterSerializesComCalls))
{
skipped++;
Console.WriteLine($"SKIP: {test.Name}: test COM STA wymaga Windows.");
continue;
}
try
{
await test.Run();
@@ -48,7 +60,7 @@ foreach (var test in tests)
}
}
Console.WriteLine($"Wynik: {tests.Length - failures}/{tests.Length} testów zaliczonych.");
Console.WriteLine($"Wynik: {tests.Length - skipped - failures}/{tests.Length - skipped} testów zaliczonych, pominięto: {skipped}.");
return failures == 0 ? 0 : 1;
static Task NetPriceIsBackwardCompatible()
@@ -115,6 +127,76 @@ static async Task HandlerMapsOptimaContract()
AssertEqual(123m, request.Items[0].UnitPriceGross, "brutto");
}
static async Task HandlerMapsInternationalFlags()
{
foreach (var (international, eu) in new[] { (false, true), (true, true), (true, false) })
{
using var adapter = new CapturingAdapter();
var arguments = new Dictionary<string, object?>(CreateInvocation().KeywordArguments)
{
["companyIsInternational"] = new JValue(international),
["companyIsInEuropeanUnion"] = new JValue(eu)
};
await new PlaceOrderHandler(adapter, CreateConfiguration())
.HandleAsync(new RpcInvocation([], arguments), CancellationToken.None);
AssertEqual(international, adapter.LastRequest?.CompanyIsInternational, "companyIsInternational");
AssertEqual(eu, adapter.LastRequest?.CompanyIsInEuropeanUnion, "companyIsInEuropeanUnion");
}
using var legacyAdapter = new CapturingAdapter();
await new PlaceOrderHandler(legacyAdapter, CreateConfiguration())
.HandleAsync(CreateInvocation(), CancellationToken.None);
AssertEqual(false, legacyAdapter.LastRequest?.CompanyIsInternational, "brak flagi zagranicznej");
AssertEqual(false, legacyAdapter.LastRequest?.CompanyIsInEuropeanUnion, "brak flagi unijnej");
}
static Task FacadeMapsInternationalOrders()
{
foreach (var currency in new[] { "PLN", "EUR", "USD", "CZK" })
foreach (var (international, eu, export) in new[] { (false, true, 0), (true, true, 3), (true, false, 1) })
{
var session = new FakeOrderSession();
var result = LateBoundOptimaComFacade.CreateOrderInSession(session, new OrderRequest
{
CurrencyIso = currency,
CustomerCode = "123",
WarehouseCode = "7",
DocumentDefinition = "RO",
CompanyIsInternational = international,
CompanyIsInEuropeanUnion = eu,
Items = [new OrderLineRequest { ProductCode = "TOWAR", Quantity = 2m,
UnitPriceNet = 100m, UnitPriceGross = 123m, Discount = 10m }]
}, false);
AssertEqual(true, session.Saved, "zapis dokumentu");
AssertEqual(currency, session.Document.WalutaSymbol, "waluta dokumentu");
AssertEqual(export, session.Document.Export, "rodzaj transakcji");
AssertEqual(export, session.Document.Elementy.Items.Single().ExportWhenProductAssigned, "rodzaj przed towarem");
AssertEqual(currency, session.Document.Elementy.Items.Single().CurrencyWhenProductAssigned, "waluta przed towarem");
AssertEqual(180m, result.Net, "wynik netto w walucie");
AssertEqual(international ? 180m : 221.40m, result.Gross, "wynik brutto w walucie");
}
return Task.CompletedTask;
}
static Task FacadeRejectsUnknownCurrency()
{
var session = new FakeOrderSession();
try
{
LateBoundOptimaComFacade.CreateOrderInSession(session, new OrderRequest
{
CurrencyIso = "XXX", CustomerCode = "123", WarehouseCode = "7", DocumentDefinition = "RO",
Items = [CreateLine(10m, 12.30m)]
}, false);
throw new InvalidOperationException("Oczekiwano błędu nieznanej waluty.");
}
catch (ErpOperationException exception) when (exception.ErrorUri == "eu.smartb2b.erp.currency_not_found")
{
AssertEqual(false, session.Saved, "brak zapisu dla nieznanej waluty");
}
return Task.CompletedTask;
}
static async Task DiagnosticsReadsOpenLogs()
{
var directory = Path.Combine(Path.GetTempPath(), $"smartb2b-log-test-{Guid.NewGuid():N}");