using AyCode.Core.Interfaces; using AyCode.Entities; using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer.Models; using FruitBank.Common.Dtos; using FruitBank.Common.Entities; using Nop.Core.Domain.Customers; 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 feladót és a saját céget egységesen -ként kezeli. /// A TradeType/TradeReasonType enumokat aliasszal hozzuk be a Models.TradeCardType osztály /// és a Models.Common.TradeCardType enum névütközése miatt. /// public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper { /// A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import. private const string HomeCountry = "HU"; /// Kimenő pénznem — jelenleg minden HUF (a vevő/rendelés devizája az OrderDto-ban még nincs leképezve; /// bekötéskor innen jön). HUF → nincs átváltás (rate 1). private const string OutboundCurrency = "HUF"; private readonly IEkaerSettings _settings; public ShippingToEkaerMapper(IEkaerSettings settings) => _settings = settings ?? throw new ArgumentNullException(nameof(settings)); public IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create) { ArgumentNullException.ThrowIfNull(shipping); ArgumentNullException.ThrowIfNull(company); 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++; var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate); operations.Add(new TradeCardOperationType { Index = index, Operation = operation, TradeCard = BuildTradeCard(shipping, document, company, rateToHuf), }); } return operations; } public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(company); var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate); return BuildTradeCard(document.Shipping, document, company, rateToHuf); } /// public TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(company); var customer = order.Customer; var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(OutboundCurrency, _settings.EurHufRate); var tradeCard = new TradeCardType { // Kimenő belföldi értékesítés. (Export `E` + külföldi deviza: későbbi — jelenleg minden HUF/belföld.) TradeType = TradeType.D, ModByCarrierEnabled = false, // Eladó = mi (kimenő relációban) SellerName = company.Name, SellerVatNumber = NormalizeVatNumber(company.TaxId), SellerCountry = NormalizeCountryCode(company.CountryCode, 2), SellerAddress = Truncate(company.FullAddress, 200), // Címzett = a vevő. Ország jelenleg HU (belföld); a Customer.CountryId-feloldás + export: későbbi. DestinationName = customer?.Company, DestinationVatNumber = NormalizeVatNumber(customer?.VatNumber), DestinationCountry = HomeCountry, DestinationAddress = Truncate(ComposeCustomerAddress(customer), 200), // Felrakodás = a mi telephelyünk; lerakodás = a vevő. LoadLocation = company.Site, UnloadLocation = BuildCustomerLocation(customer), // Vonó jármű / fuvarozó: a kimenő fuvar-adat (OrderDto) bekötéséig nincs forrás — üresen marad, // a validátor jelzi (mint a bejövőnél kezdetben). TODO: order. → Vehicle / CarrierText. }; foreach (var item in order.OrderItemDtos ?? []) tradeCard.Items.Add(BuildOutboundItem(item, rateToHuf)); return tradeCard; } private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company, double rateToHuf) { var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase var tradeCard = new TradeCardType { TradeType = ResolveTradeType(seller), ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat // Feladó / eladó = a beszállító SellerName = seller?.Name, SellerVatNumber = NormalizeVatNumber(seller?.TaxId), SellerCountry = NormalizeCountryCode(seller?.CountryCode, 2), SellerAddress = Truncate(seller?.FullAddress, 200), // Címzett = a bejelentő saját cége (bejövő relációban) DestinationName = company.Name, DestinationVatNumber = NormalizeVatNumber(company.TaxId), DestinationCountry = NormalizeCountryCode(company.CountryCode, 2), DestinationAddress = Truncate(company.FullAddress, 200), // Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név. CarrierText = shipping?.CargoPartner?.Name, // Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye. UnloadLocation = company.Site, LoadLocation = BuildLoadLocation(seller), }; // Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2). 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, rateToHuf)); return tradeCard; } /// /// Belföldi feladó (HU) → D (belföld-belföld), egyébként → I (import). A NAV EKÁER magyar, /// így a belföld mindig HU; az export (E) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7). /// private static TradeType ResolveTradeType(ICompanyInfoBase? seller) => string.Equals(seller?.CountryCode, HomeCountry, StringComparison.OrdinalIgnoreCase) ? TradeType.D : TradeType.I; private static TradeCardItemType BuildItem(ShippingItem item, double rateToHuf) => 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 = NormalizeVtsz(item.ProductDto?.Gtin), // VTSZ — átmenetileg a Gtin oszlopban (FBANKAPP-DMODEL-I-P6X4) ProductName = item.ProductName, Weight = (decimal)item.MeasuredGrossWeight, // bruttó tömeg kg-ban (lásd EKAER_TODO #4) Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // beszerzési érték HUF-ban (Partner.Currency → árfolyam); 0/ismeretlen → null }; /// Kimenő (Order) tétel → tradeCardItem. Értékesítés (S); bruttó tömeg a palettákból; nettó érték HUF-ban. private static TradeCardItemType BuildOutboundItem(OrderItemDto item, double rateToHuf) => new() { ItemExternalId = item.Id.ToString(), // Kimenő áru = értékesítés → S. (Enum: S=értékesítés, A=beszerzés, W=bérmunka, O=egyéb.) TradeReason = TradeReasonType.S, ProductVtsz = NormalizeVtsz(item.ProductDto?.Gtin), // VTSZ — átmenetileg a Gtin oszlopban (FBANKAPP-DMODEL-I-P6X4) ProductName = item.ProductDto?.Name, Weight = (decimal)item.GrossWeight, // bruttó tömeg kg-ban (OrderItemPallets összegéből) Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // nettó értékesítési érték HUF-ban; 0/ismeretlen → null }; private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new() { PlateNumber = NormalizePlateNumber(truck.LicencePlate), Country = NormalizeCountryCode(truck.CountryCode, 3), }; /// /// Felrakodási hely a beszállító adataiból. (Magyar feladónál a NAV a Phone/Email-t is kéri, ami az /// entitásban nincs — lásd EKAER_TODO #6.) /// private static LocationType? BuildLoadLocation(ICompanyInfoBase? seller) { if (seller is 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, }; } /// A vevő egysoros címe (irsz + város + utca) a NAV destinationAddress-hez. private static string? ComposeCustomerAddress(Customer? customer) { if (customer is null) return null; var parts = new[] { customer.ZipPostalCode, customer.City, customer.StreetAddress, customer.StreetAddress2 } .Where(p => !string.IsNullOrWhiteSpace(p)); return EmptyToNull(string.Join(" ", parts).Trim()); } /// Lerakodási hely a vevő adataiból (kimenő reláció). Ország jelenleg HU (belföld). private static LocationType? BuildCustomerLocation(Customer? customer) { if (customer is null) return null; return new LocationType { Name = customer.Company, VatNumber = NormalizeVatNumber(customer.VatNumber), Country = HomeCountry, ZipCode = NormalizeZipCode(customer.ZipPostalCode), City = customer.City, Street = customer.StreetAddress, }; } /// 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 == '-')]); 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} — kötőjel/szóköz NEM engedett. 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); } /// VTSZ normalizálása. Pattern: [0-9]{4,8} — a forrásban tagolt formátum is lehet /// („0805 10 00", „0805.10.00"), ezért csak a számjegyek maradnak; a 4-nél rövidebbet a validátor jelzi. private static string? NormalizeVtsz(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var cleaned = new string([.. value.Where(char.IsAsciiDigit)]); return Truncate(EmptyToNull(cleaned), 8); } private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value; private static string? Truncate(string? value, int maxLen) => value is null ? null : value.Length <= maxLen ? value : value[..maxLen]; }