Add FruitBank EKÁER mapping layer and validation docs

- 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
This commit is contained in:
Loretta 2026-06-02 15:45:46 +02:00
parent b541405640
commit 2fb1151e5d
8 changed files with 511 additions and 5 deletions

View File

@ -42,7 +42,17 @@
"Bash(awk '{print toupper\\($1\\)}')", "Bash(awk '{print toupper\\($1\\)}')",
"Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" status --short)", "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/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\")"
] ]
} }
} }

View File

@ -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;
/// <inheritdoc cref="IFruitBankEkaerService"/>
/// <remarks>
/// A teljes lánc: <c>map</c> (<see cref="IShippingToEkaerMapper"/>, FruitBank.Common) →
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A NAV-fiók
/// hitelesítő adatait (<c>INavCredentials</c>) és a <see cref="EkaerMappingOptions"/>-t a DI szolgáltatja.
/// </remarks>
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<EkaerSubmitResult> 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);
}
}

View File

@ -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;
/// <summary>
/// A FruitBank szerver-oldali EKÁER-fogyasztója: egy <see cref="Shipping"/>-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 <c>AyCode.Services</c>-ben él.
/// </summary>
public interface IFruitBankEkaerService
{
/// <summary>
/// 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 <see cref="EkaerSubmitResult"/>.
/// </summary>
Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,36 @@
using AyCode.Services.Nav.Ekaer.Models;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// A <see cref="ShippingToEkaerMapper"/> konfiguráció-függő bemenetei, amelyek NEM a <c>Shipping</c>-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.
/// </summary>
/// <remarks>Ezek később a szerver-oldali plugin beállításaiból (settings) töltődnek — lásd a plugin <c>docs/EKAER/EKAER_TODO.md</c> #2.</remarks>
public sealed class EkaerMappingOptions
{
/// <summary>A bejelentő (FruitBank) neve — a tradeCard <c>destinationName</c>-je bejövő relációban.</summary>
public string? DestinationName { get; set; }
/// <summary>A bejelentő adószáma (<c>destinationVatNumber</c>). Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
public string? DestinationVatNumber { get; set; }
/// <summary>A bejelentő országkódja (2 betű). Alapértelmezés: <c>HU</c>.</summary>
public string DestinationCountryCode { get; set; } = "HU";
/// <summary>A bejelentő címe egybeírva (<c>destinationAddress</c>, max 200).</summary>
public string? DestinationAddress { get; set; }
/// <summary>
/// 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 <see cref="LocationType"/>-ként adjuk át (a plugin-konfigból összeállítva).
/// </summary>
public LocationType? UnloadLocation { get; set; }
/// <summary>
/// A bejelentő saját országkódja a tradeType meghatározásához: ha a feladó (beszállító) országa
/// ezzel egyezik → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import). Alapértelmezés: <c>HU</c>.
/// </summary>
public string HomeCountryCode { get; set; } = "HU";
}

View File

@ -0,0 +1,26 @@
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Entities;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// FruitBank domain → NAV EKÁER tradeCard leképezés. Egy bejövő <see cref="Shipping"/>-ből
/// EKÁER tradeCard műveleteket állít elő (dokumentumonként egyet).
/// </summary>
/// <remarks>
/// A NAV-protokollt és az authentikációt NEM kezeli — az az <c>AyCode.Services.Nav</c> réteg
/// (<c>NavReportServiceBase</c> / <c>EkaerManageService</c>) 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 <c>docs/EKAER/README.md</c> és <c>EKAER_TODO.md</c> fájljaiban.
/// </remarks>
public interface IShippingToEkaerMapper
{
/// <summary>
/// Leképezi a <paramref name="shipping"/> minden <c>ShippingDocument</c>-jét egy-egy EKÁER tradeCard műveletre.
/// </summary>
/// <param name="shipping">A bejövő szállítmány. A fuvarozó/jármű a Shipping szintjén, az eladó/tételek a dokumentum szintjén élnek.</param>
/// <param name="options">A konfiguráció-függő adatok (destination cég, lerakodási hely, home country).</param>
/// <param name="operation">A tradeCard művelet típusa. Alapértelmezés: <see cref="OperationType.Create"/>.</param>
/// <returns>Dokumentumonként egy <see cref="TradeCardOperationType"/>, a beérkezés sorrendjében indexelve.</returns>
IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create);
}

View File

@ -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;
/// <inheritdoc cref="IShippingToEkaerMapper"/>
/// <remarks>
/// Tiszta (állapotmentes) leképező. A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be,
/// hogy elkerüljük a <c>Models.Common.TradeCardType</c> enum és a <c>Models.TradeCardType</c> osztály névütközését.
/// </remarks>
public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
{
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create)
{
ArgumentNullException.ThrowIfNull(shipping);
ArgumentNullException.ThrowIfNull(options);
var operations = new List<TradeCardOperationType>();
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;
}
/// <summary>
/// Bejövő (beszerzés) reláció: belföldi feladó → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import,
/// közösségből belföldre). Az export (<c>E</c>) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7).
/// </summary>
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),
};
/// <summary>
/// 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 <c>Partner</c> entitásban nincs — lásd EKAER_TODO #6.)
/// </summary>
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,
};
}
/// <summary>Egybeírt cím (<c>sellerAddress</c>/<c>destinationAddress</c>, max 200): "1234 Budapest Fő utca 1.".</summary>
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);
}
/// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
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);
}
/// <summary>Országkód normalizálása. Pattern: <c>[A-Z]{1,maxLen}</c> (seller/location: 2, jármű: 3).</summary>
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);
}
/// <summary>Rendszám normalizálása. Pattern: <c>[A-Z0-9ÖŐÜŰ]{4,15}</c> — a kötőjel/szóköz NEM engedett, ezért eltávolítjuk.</summary>
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);
}
/// <summary>Irányítószám normalizálása. Pattern: <c>[A-Z0-9 -]{2,10}</c> vagy üres.</summary>
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];
}

View File

@ -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;
/// <summary>
/// Unit tesztek a <see cref="ShippingToEkaerMapper"/>-re — a FruitBank <c>Shipping</c> → NAV EKÁER tradeCard
/// leképezésre. Tisztán memóriában felépített entitásokon fut (nincs hálózat/DB), determinisztikus.
/// </summary>
[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<ArgumentNullException>(() => Mapper.MapShipping(null!, CreateOptions()));
[TestMethod]
public void MapShipping_NullOptions_Throws()
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(CreateShipping(), null!));
}

View File

@ -38,10 +38,10 @@ public sealed class OrderClientTests
var stockTakings = await _signalRClient.GetStockTakings(true); var stockTakings = await _signalRClient.GetStockTakings(true);
Assert.IsNotNull(stockTakings); 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 != 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] [TestMethod]
@ -50,9 +50,9 @@ public sealed class OrderClientTests
var stockTakingItems = await _signalRClient.GetStockTakingItems(); var stockTakingItems = await _signalRClient.GetStockTakingItems();
Assert.IsNotNull(stockTakingItems); 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] [TestMethod]