344 lines
16 KiB
C#
344 lines
16 KiB
C#
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;
|
|
|
|
/// <inheritdoc cref="IShippingToEkaerMapper"/>
|
|
/// <remarks>
|
|
/// Tiszta (állapotmentes) leképező. A bejövő (<c>ShippingDocument</c>-csoport) és a kimenő (<c>OrderDto</c>) forrás
|
|
/// előbb egy irány-független <see cref="EkaerConsignment"/>-re képződik (<see cref="ToConsignment(IReadOnlyCollection{ShippingDocument}, EkaerCompanyInfo)"/>
|
|
/// / <see cref="ToConsignment(OrderDto, EkaerCompanyInfo)"/>), majd EBBŐL épül a NAV tradeCard (<see cref="BuildTradeCard"/>).
|
|
/// Í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 <see cref="LocationType"/>, mezővesztés nélkül átengedve); a feladó/címzett normalizálása a
|
|
/// build-ben történik. A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be a névütközés miatt.
|
|
/// </remarks>
|
|
public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|
{
|
|
/// <summary>A NAV EKÁER magyar rendszer — a „belföld" alapértéke HU (a vevő-ország feloldásáig).</summary>
|
|
private const string HomeCountry = "HU";
|
|
|
|
/// <summary>Kimenő pénznem — jelenleg minden HUF (a vevő/rendelés devizája az OrderDto-ban még nincs leképezve).</summary>
|
|
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<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(shipping);
|
|
ArgumentNullException.ThrowIfNull(company);
|
|
|
|
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 ?? [])
|
|
{
|
|
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) ────────────
|
|
|
|
/// <summary>Bejövő: egy vagy több <c>ShippingDocument</c> (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.</summary>
|
|
public EkaerConsignment ToConsignment(IReadOnlyCollection<ShippingDocument> 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,
|
|
};
|
|
}
|
|
|
|
/// <summary>Kimenő: egy rendelés → egy szállítmány. Nincs Shipping; a kötelezettséget a rendelésre nézzük.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Belföld (a feladó és a címzett országkódja megegyezik) → <c>D</c>; eltérő országok → kimenőnél
|
|
/// export (<c>E</c>), bejövőnél import (<c>I</c>).</summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>A vevő mint fél. Ország jelenleg HU — a Customer.CountryId→ISO feloldás (export E) külön feladat.</summary>
|
|
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) ────────
|
|
|
|
/// <summary>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.</summary>
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// <summary>Lerakodási hely a vevő adataiból (kimenő). Ország jelenleg HU (belföld).</summary>
|
|
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),
|
|
};
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
private double SafeRateToHuf(string? currency)
|
|
=> EkaerValueCalculator.IsHuf(currency) ? 1d : (_settings.EurHufRate > 0 ? _settings.EurHufRate : 0d);
|
|
|
|
/// <summary>A vevő egysoros címe (irsz + város + utca) a NAV <c>destinationAddress</c>-hez.</summary>
|
|
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) ─────────────────────────────────────────────────────────────
|
|
|
|
/// <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 == '-')]);
|
|
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> — kötőjel/szóköz NEM engedett.</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);
|
|
}
|
|
|
|
/// <summary>VTSZ normalizálása. Pattern: <c>[0-9]{4,8}</c> — 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.</summary>
|
|
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];
|
|
}
|