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]; }