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 bejövő (ShippingDocument-csoport) és a kimenő (OrderDto) forrás /// előbb egy irány-független -re képződik ( /// / ), majd EBBŐL épül a NAV tradeCard (). /// Így az irányfüggő tudás a két adapterre szorul, a NAV-build közös. A fel-/lerakodási hely és a jármű már a NAV-típus /// (a saját telephely kész , mezővesztés nélkül átengedve); a feladó/címzett normalizálása a /// build-ben történik. A TradeType/TradeReasonType enumokat aliasszal hozzuk be a névütközés miatt. /// public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper { /// A NAV EKÁER magyar rendszer — a „belföld" alapértéke HU (a vevő-ország feloldásáig). 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). private const string OutboundCurrency = "HUF"; private readonly IEkaerSettings _settings; public ShippingToEkaerMapper(IEkaerSettings settings) => _settings = settings ?? throw new ArgumentNullException(nameof(settings)); // ── Belépési pontok (vékony wrapperek a köztes modell + a közös build köré) ─────────────────── 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 ?? []) { document.Shipping ??= shipping; // a vontató/fuvarozó a Shippingről jön index++; operations.Add(new TradeCardOperationType { Index = index, Operation = operation, TradeCard = BuildTradeCard(ToConsignment([document], company)), }); } return operations; } public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(company); return BuildTradeCard(ToConsignment([document], company)); } public TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(company); return BuildTradeCard(ToConsignment(order, company)); } // ── Adapterek: forrás → normalizált EkaerConsignment (az EGYETLEN irányfüggő rész) ──────────── /// Bejövő: egy vagy több ShippingDocument (a hívó (Shipping, Partner)-re csoportosít) → egy /// szállítmány a csoport ÖSSZES tételével. Egy dokumentummal a generáláshoz, a csoporttal a kötelezettség-summához. public EkaerConsignment ToConsignment(IReadOnlyCollection documents, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(documents); ArgumentNullException.ThrowIfNull(company); if (documents.Count == 0) throw new ArgumentException("Legalább egy ShippingDocument szükséges.", nameof(documents)); var first = documents.First(); var partner = first.Partner; // a csoport azonos partneré (a hívó (Shipping, Partner)-re csoportosít) var shipping = first.Shipping; // azonos Shipping; a kapunál lehet null (a jármű ott nem kell) var rateToHuf = SafeRateToHuf(partner?.Currency); var lines = documents .SelectMany(d => d.ShippingItems ?? []) .Select(item => new EkaerLine { ExternalId = item.Id.ToString(), TradeReason = TradeReasonType.A, // bejövő áru = beszerzés Vtsz = NormalizeVtsz(item.ProductDto?.Gtin), Name = item.ProductName, WeightKg = item.MeasuredGrossWeight, ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), }) .ToList(); return new EkaerConsignment { ForeignKey = first.Id, IsOutgoing = false, Currency = partner?.Currency, Seller = PartnerEndpoint(partner), Buyer = CompanyEndpoint(company), LoadLocation = BuildLocation(partner), // a beszállító telephelye (a PartnerDepot-bekötés külön feladat) UnloadLocation = company.Site, // a saját telephely (kész LocationType — mezővesztés nélkül átengedve) Lines = lines, Vehicle = BuildVehicle(shipping?.CargoTruck), Trailer = BuildVehicle(shipping?.CargoTrailer), CarrierName = shipping?.CargoPartner?.Name, }; } /// Kimenő: egy rendelés → egy szállítmány. Nincs Shipping; a kötelezettséget a rendelésre nézzük. public EkaerConsignment ToConsignment(OrderDto order, EkaerCompanyInfo company) { ArgumentNullException.ThrowIfNull(order); ArgumentNullException.ThrowIfNull(company); var customer = order.Customer; var rateToHuf = SafeRateToHuf(OutboundCurrency); // jelenleg minden HUF var lines = (order.OrderItemDtos ?? []) .Select(item => new EkaerLine { ExternalId = item.Id.ToString(), TradeReason = TradeReasonType.S, // kimenő áru = értékesítés Vtsz = NormalizeVtsz(item.ProductDto?.Gtin), Name = item.ProductDto?.Name, WeightKg = item.GrossWeight, ValueHuf = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), }) .ToList(); return new EkaerConsignment { ForeignKey = order.Id, IsOutgoing = true, Currency = OutboundCurrency, Seller = CompanyEndpoint(company), Buyer = CustomerEndpoint(customer), LoadLocation = company.Site, UnloadLocation = BuildCustomerLocation(customer), Lines = lines, // Kimenőnél a VEVŐ veszi át / viszi el az árut → ő a fuvarozó. CarrierName = customer?.Company, // A vonó jármű (rendszám) a customer-hez még nincs bekötve → üresen marad, a felrakodás megkezdéséig pótolandó // (a validátor warningolja). Amint bekötik, a Vehicle is innen jön. }; } // ── Közös build: normalizált szállítmány → NAV tradeCard (a korábbi két ág helyett egy) ─────── public TradeCardType BuildTradeCard(EkaerConsignment consignment) { ArgumentNullException.ThrowIfNull(consignment); var tradeCard = new TradeCardType { TradeType = ResolveTradeType(consignment), ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat SellerName = consignment.Seller.Name, SellerVatNumber = NormalizeVatNumber(consignment.Seller.VatNumber), SellerCountry = NormalizeCountryCode(consignment.Seller.CountryCode, 2), SellerAddress = Truncate(consignment.Seller.FullAddress, 200), DestinationName = consignment.Buyer.Name, DestinationVatNumber = NormalizeVatNumber(consignment.Buyer.VatNumber), DestinationCountry = NormalizeCountryCode(consignment.Buyer.CountryCode, 2), DestinationAddress = Truncate(consignment.Buyer.FullAddress, 200), CarrierText = consignment.CarrierName, // A helyek kész NAV LocationType-ok (a saját telephely érintetlenül) — átengedve. LoadLocation = consignment.LoadLocation, UnloadLocation = consignment.UnloadLocation, }; if (consignment.Vehicle != null) tradeCard.Vehicle = consignment.Vehicle; if (consignment.Trailer != null) tradeCard.Vehicle2 = consignment.Trailer; foreach (var line in consignment.Lines) tradeCard.Items.Add(BuildItem(line)); return tradeCard; } /// Belföld (a feladó és a címzett országkódja megegyezik) → D; eltérő országok → kimenőnél /// export (E), bejövőnél import (I). private static TradeType ResolveTradeType(EkaerConsignment c) { if (string.Equals(c.Seller.CountryCode?.Trim(), c.Buyer.CountryCode?.Trim(), StringComparison.OrdinalIgnoreCase)) return TradeType.D; return c.IsOutgoing ? TradeType.E : TradeType.I; } private static TradeCardItemType BuildItem(EkaerLine line) => new() { ItemExternalId = line.ExternalId, TradeReason = line.TradeReason, ProductVtsz = line.Vtsz, // már normalizált (az adapterben) ProductName = line.Name, Weight = (decimal)line.WeightKg, Value = line.ValueHuf, }; // ── Forrás → normalizálatlan végpont (feladó/címzett: név/adószám/ország/egysoros cím) ───────── private static EkaerEndpoint PartnerEndpoint(ICompanyInfoBase? partner) => new() { Name = partner?.Name, VatNumber = partner?.TaxId, CountryCode = partner?.CountryCode, FullAddress = partner?.FullAddress, }; private static EkaerEndpoint CompanyEndpoint(EkaerCompanyInfo company) => new() { Name = company.Name, VatNumber = company.TaxId, CountryCode = company.CountryCode, FullAddress = company.FullAddress, }; /// A vevő mint fél. Ország jelenleg HU — a Customer.CountryId→ISO feloldás (export E) külön feladat. private static EkaerEndpoint CustomerEndpoint(Customer? customer) => new() { Name = customer?.Company, VatNumber = customer?.VatNumber, CountryCode = HomeCountry, FullAddress = ComposeCustomerAddress(customer), }; // ── Forrás → NAV LocationType / jármű (a saját telephelyet az adapter közvetlenül adja) ──────── /// Felrakodási hely a beszállító adataiból (bejövő). A NAV magyar feladónál a Phone/Email-t is kéri, ami /// az entitásban nincs — lásd EKAER_TODO #6. private static LocationType? BuildLocation(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, }; } /// Lerakodási hely a vevő adataiból (kimenő). 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, }; } private static BasicVehicleDetailType? BuildVehicle(CargoTruck? truck) { if (truck is null) return null; return new BasicVehicleDetailType { PlateNumber = NormalizePlateNumber(truck.LicencePlate), Country = NormalizeCountryCode(truck.CountryCode, 3), }; } /// A számla-pénznem → HUF szorzó, NEM dobó változat (a kapu külföldinél küszöb nélkül jelent, ott az érték /// nem kell): HUF → 1; külföldi + érvényes árfolyam → árfolyam; külföldi + hiányzó árfolyam → 0 (a tétel-érték null lesz). /// Generáláskor a service config-kapuja (TryConfigError) előbb elvágja a hiányzó-árfolyamos külföldi esetet. private double SafeRateToHuf(string? currency) => EkaerValueCalculator.IsHuf(currency) ? 1d : (_settings.EurHufRate > 0 ? _settings.EurHufRate : 0d); /// 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()); } // ── Normalizálók (NAV pattern-ek) ───────────────────────────────────────────────────────────── /// 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]; }