FruitBankHybridApp/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs

169 lines
7.8 KiB
C#

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