From 2fb1151e5d9f61764dd79364a26089c4cc927c1d Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 2 Jun 2026 15:45:46 +0200 Subject: [PATCH] =?UTF-8?q?Add=20FruitBank=20EK=C3=81ER=20mapping=20layer?= =?UTF-8?q?=20and=20validation=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced IFruitBankEkaerService and implementation for mapping and submitting Shipping as EKÁER tradeCards - Added IShippingToEkaerMapper and ShippingToEkaerMapper for domain-to-NAV mapping with normalization logic - Created EkaerMappingOptions for config-driven mapping inputs - Added unit tests for mapping logic (ShippingToEkaerMapperTests) - Expanded EKAER_VALIDATION.md with C# validation implementation details - Updated README.md to clarify mapping/submission flow and project boundaries - Improved OrderClientTests assertions for nullability and clarity - Updated settings.local.json with new dev workflow commands --- .claude/settings.local.json | 12 +- .../Services/Ekaer/FruitBankEkaerService.cs | 35 +++ .../Services/Ekaer/IFruitBankEkaerService.cs | 19 ++ .../Services/Ekaer/EkaerMappingOptions.cs | 36 +++ .../Services/Ekaer/IShippingToEkaerMapper.cs | 26 +++ .../Services/Ekaer/ShippingToEkaerMapper.cs | 168 ++++++++++++++ .../Ekaer/ShippingToEkaerMapperTests.cs | 212 ++++++++++++++++++ .../OrderClientTests.cs | 8 +- 8 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs create mode 100644 FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs create mode 100644 FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs create mode 100644 FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs create mode 100644 FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs create mode 100644 FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2ef6a2f4..9fdc5ea2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -42,7 +42,17 @@ "Bash(awk '{print toupper\\($1\\)}')", "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" status --short)", "Bash(git -C \"H:/Applications/Mango/Source/FruitBankHybridApp\" status --short)", - "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common\" status --short)" + "Bash(git -C \"H:/Applications/Mango/Source/NopCommerce.Common\" status --short)", + "Bash(Get-ChildItem -Path \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\" -Directory)", + "Bash(Select-Object Name)", + "Bash(ICargo)", + "Bash(IShipping\")", + "Bash(ls -la \"H:\\\\Applications\\\\Mango\\\\Source\\\\NopCommerce.Common\\\\4.70\\\\Plugins\\\\Nop.Plugin.Misc.AIPlugin\" 2>/dev/null | head -30)", + "PowerShell(ls \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Interfaces\\\\\" | where {$_.Name -match 'IPartner|ICargo|IShipping'} | select Name)", + "Bash(find \"H:\\\\Applications\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Source\" -type f -name \"*Shipping*.cs\" 2>/dev/null | head -10)", + "Bash(find \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services\" -type d | head -20)", + "Bash(rm -f H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/EkaerMappingOptions.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/IShippingToEkaerMapper.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/ShippingToEkaerMapper.cs)", + "Bash(rmdir \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer\")" ] } } diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs new file mode 100644 index 00000000..2e457d1a --- /dev/null +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -0,0 +1,35 @@ +using AyCode.Services.Nav.Ekaer; +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common.Entities; +using FruitBank.Common.Services.Ekaer; + +namespace FruitBank.Common.Server.Services.Ekaer; + +/// +/// +/// A teljes lánc: map (, FruitBank.Common) → +/// validate → send (, AyCode.Services). A NAV-fiók +/// hitelesítő adatait (INavCredentials) és a -t a DI szolgáltatja. +/// +public sealed class FruitBankEkaerService : IFruitBankEkaerService +{ + private readonly IShippingToEkaerMapper _mapper; + private readonly IEkaerSubmitService _submitService; + private readonly EkaerMappingOptions _options; + + public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerMappingOptions options) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _submitService = submitService ?? throw new ArgumentNullException(nameof(submitService)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(shipping); + + // map (FruitBank.Common) → submit: validate → send (AyCode.Services) + var operations = _mapper.MapShipping(shipping, _options, operation); + return _submitService.SubmitAsync(operations, cancellationToken); + } +} diff --git a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs new file mode 100644 index 00000000..d9f13b9c --- /dev/null +++ b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs @@ -0,0 +1,19 @@ +using AyCode.Services.Nav.Ekaer; +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common.Entities; + +namespace FruitBank.Common.Server.Services.Ekaer; + +/// +/// A FruitBank szerver-oldali EKÁER-fogyasztója: egy -et leképez EKÁER tradeCard-okra +/// (a mapperrel), majd beküldi (az általános submit-orchestrátorral). Ez a vékony, projekt-specifikus réteg; +/// az általános NAV/EKÁER logika (validátor, submit, manage) az AyCode.Services-ben él. +/// +public interface IFruitBankEkaerService +{ + /// + /// Leképezi és beküldi a szállítmányt. Az eredmény vagy validációs hibák (nem ment ki kérés), + /// vagy a NAV-válasz — lásd . + /// + Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default); +} diff --git a/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs b/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs new file mode 100644 index 00000000..88122619 --- /dev/null +++ b/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs @@ -0,0 +1,36 @@ +using AyCode.Services.Nav.Ekaer.Models; + +namespace FruitBank.Common.Services.Ekaer; + +/// +/// A konfiguráció-függő bemenetei, amelyek NEM a Shipping-ből +/// származnak: a bejelentő (FruitBank) mint címzett/destination cégadatai, a lerakodási hely (saját raktár), +/// és a saját országkód a tradeType irány meghatározásához. +/// +/// Ezek később a szerver-oldali plugin beállításaiból (settings) töltődnek — lásd a plugin docs/EKAER/EKAER_TODO.md #2. +public sealed class EkaerMappingOptions +{ + /// A bejelentő (FruitBank) neve — a tradeCard destinationName-je bejövő relációban. + public string? DestinationName { get; set; } + + /// A bejelentő adószáma (destinationVatNumber). Pattern: [0-9A-Z-]{1,15}. + public string? DestinationVatNumber { get; set; } + + /// A bejelentő országkódja (2 betű). Alapértelmezés: HU. + public string DestinationCountryCode { get; set; } = "HU"; + + /// A bejelentő címe egybeírva (destinationAddress, max 200). + public string? DestinationAddress { get; set; } + + /// + /// A lerakodási / raktározási hely (a saját raktár). Magyar cím esetén a Name/VatNumber/Phone/Email + /// kitöltése kötelező — ezért kész -ként adjuk át (a plugin-konfigból összeállítva). + /// + public LocationType? UnloadLocation { get; set; } + + /// + /// A bejelentő saját országkódja a tradeType meghatározásához: ha a feladó (beszállító) országa + /// ezzel egyezik → D (belföld-belföld), egyébként → I (import). Alapértelmezés: HU. + /// + public string HomeCountryCode { get; set; } = "HU"; +} diff --git a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs new file mode 100644 index 00000000..47af2cbc --- /dev/null +++ b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs @@ -0,0 +1,26 @@ +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common.Entities; + +namespace FruitBank.Common.Services.Ekaer; + +/// +/// FruitBank domain → NAV EKÁER tradeCard leképezés. Egy bejövő -ből +/// EKÁER tradeCard műveleteket állít elő (dokumentumonként egyet). +/// +/// +/// A NAV-protokollt és az authentikációt NEM kezeli — az az AyCode.Services.Nav réteg +/// (NavReportServiceBase / EkaerManageService) felelőssége. A tényleges NAV-bejelentést +/// a szerver-oldali (nopCommerce plugin) service végzi, amely ezt a leképezőt használja. A leképezés +/// tisztázott pontjai és nyitott döntései a plugin docs/EKAER/README.md és EKAER_TODO.md fájljaiban. +/// +public interface IShippingToEkaerMapper +{ + /// + /// Leképezi a minden ShippingDocument-jét egy-egy EKÁER tradeCard műveletre. + /// + /// A bejövő szállítmány. A fuvarozó/jármű a Shipping szintjén, az eladó/tételek a dokumentum szintjén élnek. + /// A konfiguráció-függő adatok (destination cég, lerakodási hely, home country). + /// A tradeCard művelet típusa. Alapértelmezés: . + /// Dokumentumonként egy , a beérkezés sorrendjében indexelve. + IReadOnlyList MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create); +} diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs new file mode 100644 index 00000000..74a76355 --- /dev/null +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -0,0 +1,168 @@ +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common.Entities; +using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType; +using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType; + +namespace FruitBank.Common.Services.Ekaer; + +/// +/// +/// Tiszta (állapotmentes) leképező. A TradeType/TradeReasonType enumokat aliasszal hozzuk be, +/// hogy elkerüljük a Models.Common.TradeCardType enum és a Models.TradeCardType osztály névütközését. +/// +public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper +{ + public IReadOnlyList MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create) + { + ArgumentNullException.ThrowIfNull(shipping); + ArgumentNullException.ThrowIfNull(options); + + var operations = new List(); + var index = 0; + + // Granularitás: egy ShippingDocument → egy tradeCard (lásd EKAER_TODO #5). + foreach (var document in shipping.ShippingDocuments ?? []) + { + index++; + operations.Add(new TradeCardOperationType + { + Index = index, + Operation = operation, + TradeCard = BuildTradeCard(shipping, document, options), + }); + } + + return operations; + } + + private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerMappingOptions options) + { + var seller = document.Partner; // a beszállító (feladó) + + var tradeCard = new TradeCardType + { + TradeType = ResolveTradeType(seller, options), + ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthatja a bejelentést + + // Feladó / eladó = a beszállító (ShippingDocument.Partner) + SellerName = seller?.Name, + SellerVatNumber = NormalizeVatNumber(seller?.TaxId), + SellerCountry = NormalizeCountryCode(seller?.CountryCode, 2), + SellerAddress = BuildAddress(seller), + + // Címzett = a bejelentő (FruitBank) — bejövő relációban (lásd EKAER_TODO #1) + DestinationName = options.DestinationName, + DestinationVatNumber = NormalizeVatNumber(options.DestinationVatNumber), + DestinationCountry = NormalizeCountryCode(options.DestinationCountryCode, 2), + DestinationAddress = options.DestinationAddress, + + // Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító (Carrier) nincs, csak szöveges név. + CarrierText = shipping.CargoPartner?.Name, + + // Lerakodási hely = saját raktár (konfigból); felrakodás = a beszállító telephelye. + UnloadLocation = options.UnloadLocation, + LoadLocation = BuildLoadLocation(seller), + }; + + // Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2), saját rendszámmal/országgal. + if (shipping.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); + if (shipping.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); + + foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item)); + return tradeCard; + } + + /// + /// Bejövő (beszerzés) reláció: belföldi feladó → D (belföld-belföld), egyébként → I (import, + /// közösségből belföldre). Az export (E) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7). + /// + private static TradeType ResolveTradeType(Partner? seller, EkaerMappingOptions options) + => string.Equals(seller?.CountryCode, options.HomeCountryCode, StringComparison.OrdinalIgnoreCase) + ? TradeType.D + : TradeType.I; + + private static TradeCardItemType BuildItem(ShippingItem item) => new() + { + ItemExternalId = item.Id.ToString(), + // Bejövő áru = beszerzés → A. (Enum: S=értékesítés, A=beszerzés, W=bérmunka, O=egyéb.) Lásd EKAER_TODO #9. + TradeReason = TradeReasonType.A, + ProductVtsz = item.ProductDto?.Gtin, // VTSZ — átmenetileg a Gtin oszlopban (MGFBANKPLUG-EKAER-I-T3X8) + ProductName = item.ProductName, + Weight = (decimal)item.MeasuredGrossWeight, // bruttó tömeg kg-ban (lásd EKAER_TODO #4) + // Value (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3. + }; + + private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new() + { + PlateNumber = NormalizePlateNumber(truck.LicencePlate), + Country = NormalizeCountryCode(truck.CountryCode, 3), + }; + + /// + /// Felrakodási hely a beszállító címéből. (Magyar feladónál a NAV kötelezővé teszi a Phone/Email-t, + /// ami a Partner entitásban nincs — lásd EKAER_TODO #6.) + /// + private static LocationType? BuildLoadLocation(Partner? seller) + { + if (seller == null) return null; + + return new LocationType + { + Name = seller.Name, + VatNumber = NormalizeVatNumber(seller.TaxId), + Country = NormalizeCountryCode(seller.CountryCode, 2), + ZipCode = NormalizeZipCode(seller.PostalCode), + City = seller.City, + Street = seller.Street, + }; + } + + /// Egybeírt cím (sellerAddress/destinationAddress, max 200): "1234 Budapest Fő utca 1.". + private static string? BuildAddress(PartnerBase? partner) + { + if (partner == null) return null; + + var parts = new[] { partner.PostalCode, partner.City, partner.Street }.Where(p => !string.IsNullOrWhiteSpace(p)); + var address = string.Join(" ", parts).Trim(); return Truncate(string.IsNullOrWhiteSpace(address) ? null : address, 200); + } + + /// Adószám normalizálása. Pattern: [0-9A-Z-]{1,15}. + private static string? NormalizeVatNumber(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + + var cleaned = new string(value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c == '-').ToArray()); + return Truncate(EmptyToNull(cleaned), 15); + } + + /// Országkód normalizálása. Pattern: [A-Z]{1,maxLen} (seller/location: 2, jármű: 3). + private static string? NormalizeCountryCode(string? value, int maxLen) + { + if (string.IsNullOrWhiteSpace(value)) return null; + + var cleaned = new string([.. value.ToUpperInvariant().Where(char.IsAsciiLetter)]); + return Truncate(EmptyToNull(cleaned), maxLen); + } + + /// Rendszám normalizálása. Pattern: [A-Z0-9ÖŐÜŰ]{4,15} — a kötőjel/szóköz NEM engedett, ezért eltávolítjuk. + private static string? NormalizePlateNumber(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + + var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is 'Ö' or 'Ő' or 'Ü' or 'Ű')]); + return Truncate(EmptyToNull(cleaned), 15); + } + + /// Irányítószám normalizálása. Pattern: [A-Z0-9 -]{2,10} vagy üres. + private static string? NormalizeZipCode(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + + var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is ' ' or '-')]); + return Truncate(EmptyToNull(cleaned), 10); + } + + private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value; + + private static string? Truncate(string? value, int maxLen) => value == null ? null : value.Length <= maxLen ? value : value[..maxLen]; +} diff --git a/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs new file mode 100644 index 00000000..eff81443 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs @@ -0,0 +1,212 @@ +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common.Dtos; +using FruitBank.Common.Entities; +using FruitBank.Common.Services.Ekaer; +using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType; +using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType; + +namespace FruitBankHybrid.Shared.Tests.Ekaer; + +/// +/// Unit tesztek a -re — a FruitBank Shipping → NAV EKÁER tradeCard +/// leképezésre. Tisztán memóriában felépített entitásokon fut (nincs hálózat/DB), determinisztikus. +/// +[TestClass] +public sealed class ShippingToEkaerMapperTests +{ + private static readonly ShippingToEkaerMapper Mapper = new(); + + // ---- Helpers ------------------------------------------------------------ + + private static Shipping CreateShipping(string sellerCountry = "HU", string plate = "ABC-123", bool withTrailer = true) + { + var item = new ShippingItem + { + Id = 1, + Name = "Alma", + ProductDto = new ProductDto { Gtin = "08081010", Name = "Alma" }, + MeasuredGrossWeight = 123.5, + MeasuredQuantity = 10, + UnitPriceOnDocument = 5.0, + }; + + var document = new ShippingDocument + { + Country = sellerCountry, + Partner = new Partner + { + Name = "Beszállító Kft", + TaxId = "12345678-2-42", + CountryCode = sellerCountry, + PostalCode = "1011", + City = "Budapest", + Street = "Fő utca 1", + }, + ShippingItems = [item], + }; + + var shipping = new Shipping + { + CargoPartner = new CargoPartner { Name = "Fuvaros Zrt", CountryCode = "HU" }, + CargoTruck = new CargoTruck { LicencePlate = plate, CountryCode = "HU", IsTrailer = false }, + ShippingDocuments = [document], + }; + + if (withTrailer) + shipping.CargoTrailer = new CargoTruck { LicencePlate = "XYZ-789", CountryCode = "HU", IsTrailer = true }; + + return shipping; + } + + private static EkaerMappingOptions CreateOptions() => new() + { + DestinationName = "FruitBank Kft", + DestinationVatNumber = "98765432-2-41", + DestinationCountryCode = "HU", + DestinationAddress = "1102 Budapest Raktár utca 5", + HomeCountryCode = "HU", + UnloadLocation = new LocationType + { + Name = "FruitBank Raktár", + VatNumber = "98765432-2-41", + Country = "HU", + ZipCode = "1102", + City = "Budapest", + Street = "Raktár utca", + StreetNumber = "5", + }, + }; + + // ---- Granularitás / index ---------------------------------------------- + + [TestMethod] + public void MapShipping_ProducesOneOperationPerDocument_WithSequentialIndex() + { + var shipping = CreateShipping(); + shipping.ShippingDocuments!.Add(new ShippingDocument + { + Country = "HU", + Partner = new Partner { Name = "Másik Beszállító", CountryCode = "HU" }, + ShippingItems = [], + }); + + var ops = Mapper.MapShipping(shipping, CreateOptions()); + + Assert.AreEqual(2, ops.Count, "dokumentumonként egy tradeCard"); + Assert.AreEqual(1, ops[0].Index); + Assert.AreEqual(2, ops[1].Index); + Assert.AreEqual(OperationType.Create, ops[0].Operation, "alapértelmezett művelet: Create"); + } + + [TestMethod] + public void MapShipping_NullDocuments_ReturnsEmpty() + { + var ops = Mapper.MapShipping(new Shipping { ShippingDocuments = null }, CreateOptions()); + Assert.AreEqual(0, ops.Count); + } + + [TestMethod] + public void MapShipping_HonorsExplicitOperation() + { + var ops = Mapper.MapShipping(CreateShipping(), CreateOptions(), OperationType.Modify); + Assert.AreEqual(OperationType.Modify, ops[0].Operation); + } + + // ---- tradeType irány ---------------------------------------------------- + + [TestMethod] + public void MapShipping_DomesticSeller_TradeTypeDomestic() + { + var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "HU"), CreateOptions()); + Assert.AreEqual(TradeType.D, ops[0].TradeCard.TradeType, "belföldi feladó → D"); + } + + [TestMethod] + public void MapShipping_ForeignSeller_TradeTypeImport() + { + var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "DE"), CreateOptions()); + Assert.AreEqual(TradeType.I, ops[0].TradeCard.TradeType, "külföldi feladó → I (import)"); + } + + // ---- Tétel-leképezés ---------------------------------------------------- + + [TestMethod] + public void MapShipping_MapsItemFields() + { + var item = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard.Items[0]; + + Assert.AreEqual("08081010", item.ProductVtsz, "productVtsz = ProductDto.Gtin"); + Assert.AreEqual("Alma", item.ProductName); + Assert.AreEqual(123.5m, item.Weight, "weight = MeasuredGrossWeight (bruttó)"); + Assert.AreEqual(TradeReasonType.A, item.TradeReason, "bejövő áru = beszerzés = A"); + Assert.IsNull(item.Value, "a HUF érték a deviza tisztázásáig nincs kitöltve"); + } + + // ---- Eladó (seller*) ---------------------------------------------------- + + [TestMethod] + public void MapShipping_MapsSellerFromPartner() + { + var tradeCard = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard; + + Assert.AreEqual("Beszállító Kft", tradeCard.SellerName); + Assert.AreEqual("12345678-2-42", tradeCard.SellerVatNumber); + Assert.AreEqual("HU", tradeCard.SellerCountry); + StringAssert.Contains(tradeCard.SellerAddress, "Budapest"); + } + + [TestMethod] + public void MapShipping_MapsDestinationAndUnloadFromOptions() + { + var options = CreateOptions(); + var tradeCard = Mapper.MapShipping(CreateShipping(), options)[0].TradeCard; + + Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName); + Assert.AreEqual("HU", tradeCard.DestinationCountry); + Assert.AreSame(options.UnloadLocation, tradeCard.UnloadLocation, "a lerakodási hely a konfigból jön"); + } + + [TestMethod] + public void MapShipping_MapsCarrierTextFromCargoPartner() + { + var tradeCard = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard; + Assert.AreEqual("Fuvaros Zrt", tradeCard.CarrierText); + } + + // ---- Járművek + rendszám-normalizálás ---------------------------------- + + [TestMethod] + public void MapShipping_NormalizesLicencePlate_RemovesHyphenAndUppercases() + { + var tradeCard = Mapper.MapShipping(CreateShipping(plate: "abc-123"), CreateOptions())[0].TradeCard; + Assert.AreEqual("ABC123", tradeCard.Vehicle!.PlateNumber, "a NAV pattern nem enged kötőjelet, és nagybetűs"); + Assert.AreEqual("HU", tradeCard.Vehicle.Country); + } + + [TestMethod] + public void MapShipping_WithTrailer_MapsVehicle2() + { + var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: true), CreateOptions())[0].TradeCard; + Assert.IsNotNull(tradeCard.Vehicle, "vonó jármű"); + Assert.IsNotNull(tradeCard.Vehicle2, "vontatmány"); + Assert.AreEqual("XYZ789", tradeCard.Vehicle2!.PlateNumber); + } + + [TestMethod] + public void MapShipping_NoTrailer_Vehicle2Null() + { + var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: false), CreateOptions())[0].TradeCard; + Assert.IsNotNull(tradeCard.Vehicle); + Assert.IsNull(tradeCard.Vehicle2, "nincs pótkocsi → nincs vehicle2"); + } + + // ---- Védőkorlátok ------------------------------------------------------- + + [TestMethod] + public void MapShipping_NullShipping_Throws() + => Assert.ThrowsExactly(() => Mapper.MapShipping(null!, CreateOptions())); + + [TestMethod] + public void MapShipping_NullOptions_Throws() + => Assert.ThrowsExactly(() => Mapper.MapShipping(CreateShipping(), null!)); +} diff --git a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs index cedb2b34..a711f963 100644 --- a/FruitBankHybrid.Shared.Tests/OrderClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/OrderClientTests.cs @@ -38,10 +38,10 @@ public sealed class OrderClientTests var stockTakings = await _signalRClient.GetStockTakings(true); Assert.IsNotNull(stockTakings); - Assert.IsTrue(stockTakings.Count != 0); + Assert.IsNotEmpty(stockTakings); Assert.IsTrue(stockTakings.All(o => o.StockTakingItems != null)); - Assert.IsTrue(stockTakings.All(o => o.StockTakingItems.All(oi => oi.Product != null && oi.Product.Id == oi.ProductId))); + Assert.IsTrue(stockTakings.All(o => o.StockTakingItems!.All(oi => oi.Product != null && oi.Product.Id == oi.ProductId))); } [TestMethod] @@ -50,9 +50,9 @@ public sealed class OrderClientTests var stockTakingItems = await _signalRClient.GetStockTakingItems(); Assert.IsNotNull(stockTakingItems); - Assert.IsTrue(stockTakingItems.Count != 0); + Assert.IsNotEmpty(stockTakingItems); - Assert.IsTrue(stockTakingItems.All(oi => oi.StockTaking != null && oi.Product != null && oi.Product.Id == oi.ProductId)); + Assert.IsTrue(stockTakingItems.All(oi => oi is { StockTaking: not null, Product: not null } && oi.Product.Id == oi.ProductId)); } [TestMethod]