Add EKÁER XML support for outgoing orders, refactor config
- Introduced IEkaerSettings interface and refactored code to use it for DI/testability - Added support for generating EKÁER XML from outgoing orders (OrderDto) - Extended service and mapper interfaces for outgoing order handling - Implemented MapOrder and value calculation for order items - Replaced UnloadLocation with Site in EkaerCompanyInfo for general site usage - Updated tests, SignalR/controller interfaces, and UI to support new features and signatures - Improved documentation and code clarity throughout
This commit is contained in:
parent
f23aebff2d
commit
9383041504
|
|
@ -67,7 +67,8 @@
|
||||||
"Bash(grep -c -a \"ConversionRate\" FruitBank.Common.dll)",
|
"Bash(grep -c -a \"ConversionRate\" FruitBank.Common.dll)",
|
||||||
"Bash(grep -c -a \"ConversionRate\" FruitBank.Common.Server.dll)",
|
"Bash(grep -c -a \"ConversionRate\" FruitBank.Common.Server.dll)",
|
||||||
"Bash(grep -c -a \"EurHufRate\" FruitBank.Common.dll FruitBank.Common.Server.dll)",
|
"Bash(grep -c -a \"EurHufRate\" FruitBank.Common.dll FruitBank.Common.Server.dll)",
|
||||||
"Bash(ls -la --time-style=+%H:%M FruitBank.Common.dll FruitBank.Common.Server.dll)"
|
"Bash(ls -la --time-style=+%H:%M FruitBank.Common.dll FruitBank.Common.Server.dll)",
|
||||||
|
"Bash(find \"H:\\\\\\\\Applications\\\\\\\\Mango\" -name \"Order.cs\" -path \"*/Domain/Orders/*\" 2>&1 | head -3)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AyCode.Services.Nav;
|
using AyCode.Services.Nav;
|
||||||
using AyCode.Services.Nav.Ekaer;
|
using AyCode.Services.Nav.Ekaer;
|
||||||
using AyCode.Services.Nav.Ekaer.Models;
|
using AyCode.Services.Nav.Ekaer.Models;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
using FruitBank.Common.Services.Ekaer;
|
using FruitBank.Common.Services.Ekaer;
|
||||||
|
|
||||||
|
|
@ -17,9 +18,9 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
||||||
private readonly IShippingToEkaerMapper _mapper;
|
private readonly IShippingToEkaerMapper _mapper;
|
||||||
private readonly IEkaerSubmitService _submitService;
|
private readonly IEkaerSubmitService _submitService;
|
||||||
private readonly IEkaerTradeCardValidator _validator;
|
private readonly IEkaerTradeCardValidator _validator;
|
||||||
private readonly EkaerSettings _settings;
|
private readonly IEkaerSettings _settings;
|
||||||
|
|
||||||
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerSettings settings)
|
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, IEkaerSettings settings)
|
||||||
{
|
{
|
||||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||||
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
|
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
|
||||||
|
|
@ -39,35 +40,44 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
||||||
public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null)
|
public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(document);
|
ArgumentNullException.ThrowIfNull(document);
|
||||||
|
|
||||||
ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false };
|
ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false };
|
||||||
|
|
||||||
// Config-ellenőrzés a leképezés ELŐTT: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a
|
var currency = document.Partner?.Currency;
|
||||||
// value csendben 0 lenne. Hibás configgal nem generálunk félrevezető XML-t, hanem jelezzük a hibát.
|
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapDocument(document, _settings.Company), currency);
|
||||||
if (!EkaerValueCalculator.IsHuf(document.Partner?.Currency) && _settings.EurHufRate <= 0)
|
}
|
||||||
{
|
|
||||||
ekaerHistory.Status = EkaerStatus.ValidationError;
|
|
||||||
ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható.";
|
|
||||||
return ekaerHistory;
|
|
||||||
}
|
|
||||||
|
|
||||||
var operation = new TradeCardOperationType
|
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
|
||||||
{
|
{
|
||||||
Index = 1,
|
ArgumentNullException.ThrowIfNull(order);
|
||||||
Operation = OperationType.Create,
|
ekaerHistory ??= new EkaerHistory { ForeignKey = order.Id, IsOutgoing = true };
|
||||||
TradeCard = _mapper.MapDocument(document, _settings.Company),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Kimenő pénznem: jelenleg minden HUF (a deviza az OrderDto-ba kerül, amint bekötik) → ConversionRate = 1.
|
||||||
|
const string currency = "HUF";
|
||||||
|
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapOrder(order, _settings.Company), currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Config-kapu: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a leképezés ELŐTT
|
||||||
|
/// ValidationError (nincs félrevezető XML). <c>null</c> = rendben, mehet a generálás.</summary>
|
||||||
|
private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency)
|
||||||
|
{
|
||||||
|
if (EkaerValueCalculator.IsHuf(currency) || _settings.EurHufRate > 0) return null;
|
||||||
|
|
||||||
|
ekaerHistory.Status = EkaerStatus.ValidationError;
|
||||||
|
ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható.";
|
||||||
|
return ekaerHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Közös befejezés (bejövő/kimenő): validál, szerializál; az XML validációs hibánál IS tárolódik
|
||||||
|
/// (a detail-nézethez), és rögzíti a ténylegesen alkalmazott árfolyamot (HUF → 1, külföldi → FX-ráta).</summary>
|
||||||
|
private EkaerHistory Finalize(EkaerHistory ekaerHistory, TradeCardType tradeCard, string? currency)
|
||||||
|
{
|
||||||
|
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
|
||||||
var errors = _validator.Validate(operation);
|
var errors = _validator.Validate(operation);
|
||||||
|
|
||||||
// Az XML validációs hibánál IS tárolódik — a detail-nézetben így látszik, mi hiányzik.
|
ekaerHistory.XmlDoc = NavXmlHelper.Serialize(tradeCard);
|
||||||
ekaerHistory.XmlDoc = NavXmlHelper.Serialize(operation.TradeCard);
|
ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(currency, _settings.EurHufRate);
|
||||||
// Audit: a value-számításhoz TÉNYLEGESEN alkalmazott árfolyam (HUF → 1, külföldi → FX-ráta) — pont az,
|
|
||||||
// amivel a value készült. A NAV-sémában nincs árfolyam-mező, csak a HUF value; ez az oszlop őrzi meg, hogyan.
|
|
||||||
ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate);
|
|
||||||
ekaerHistory.Status = errors.Count == 0 ? EkaerStatus.Generated : EkaerStatus.ValidationError;
|
ekaerHistory.Status = errors.Count == 0 ? EkaerStatus.Generated : EkaerStatus.ValidationError;
|
||||||
ekaerHistory.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage));
|
ekaerHistory.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage));
|
||||||
|
|
||||||
return ekaerHistory;
|
return ekaerHistory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AyCode.Services.Nav.Ekaer;
|
using AyCode.Services.Nav.Ekaer;
|
||||||
using AyCode.Services.Nav.Ekaer.Models;
|
using AyCode.Services.Nav.Ekaer.Models;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Server.Services.Ekaer;
|
namespace FruitBank.Common.Server.Services.Ekaer;
|
||||||
|
|
@ -26,4 +27,11 @@ public interface IFruitBankEkaerService
|
||||||
/// <param name="document">A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek).</param>
|
/// <param name="document">A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek).</param>
|
||||||
/// <param name="ekaerHistory">A dokumentum meglévő rekordja (újrageneráláskor); <c>null</c> → új rekord készül.</param>
|
/// <param name="ekaerHistory">A dokumentum meglévő rekordja (újrageneráláskor); <c>null</c> → új rekord készül.</param>
|
||||||
EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null);
|
EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Egy kimenő rendelésből (<see cref="OrderDto"/>) legenerálja az EKÁER tradeCard XML-t és validálja —
|
||||||
|
/// a (meglévő vagy új) <see cref="EkaerHistory"/> rekordot tölti (<c>IsOutgoing = true</c>). Mi vagyunk az
|
||||||
|
/// ELADÓ, a vevő a CÍMZETT (belföldi értékesítés). NEM perzisztál és NEM hív NAV-ot — a mentés a hívó dolga.
|
||||||
|
/// </summary>
|
||||||
|
EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public interface IFruitBankDataControllerCommon
|
||||||
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey);
|
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey);
|
||||||
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
|
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory);
|
||||||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
|
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
|
||||||
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int shippingDocumentId);
|
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing);
|
||||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
||||||
public Task<int> CreateMissingEkaerHistories(DateTime fromDate);
|
public Task<int> CreateMissingEkaerHistories(DateTime fromDate);
|
||||||
#endregion EkaerHistory
|
#endregion EkaerHistory
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace FruitBank.Common.Services.Ekaer;
|
||||||
/// cégadata + a küszöbök + az árfolyam. A küszöbök és az árfolyam évente / jogszabály szerint változhatnak,
|
/// cégadata + a küszöbök + az árfolyam. A küszöbök és az árfolyam évente / jogszabály szerint változhatnak,
|
||||||
/// ezért configban élnek — nem a kódban beégetve.
|
/// ezért configban élnek — nem a kódban beégetve.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EkaerSettings
|
public sealed class EkaerSettings : IEkaerSettings
|
||||||
{
|
{
|
||||||
/// <summary>A bejelentő saját cégadatai (címzett a bejövő relációban) + a lerakodási hely.</summary>
|
/// <summary>A bejelentő saját cégadatai (címzett a bejövő relációban) + a lerakodási hely.</summary>
|
||||||
public EkaerCompanyInfo Company { get; set; } = new();
|
public EkaerCompanyInfo Company { get; set; } = new();
|
||||||
|
|
@ -23,6 +23,7 @@ public sealed class EkaerSettings
|
||||||
public double ThresholdWeightKg { get; set; }
|
public double ThresholdWeightKg { get; set; }
|
||||||
|
|
||||||
/// <summary>Érték-küszöb HUF-ban (nettó): e felett (vagy a tömeg-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 250 000 Ft.
|
/// <summary>Érték-küszöb HUF-ban (nettó): e felett (vagy a tömeg-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 250 000 Ft.
|
||||||
/// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé).</summary>
|
/// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé).
|
||||||
public long ThresholdValueHuf { get; set; }
|
/// <c>int</c>: a legmagasabb küszöb 5 millió Ft, bőven belefér.</summary>
|
||||||
|
public int ThresholdValueHuf { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Services.Ekaer;
|
namespace FruitBank.Common.Services.Ekaer;
|
||||||
|
|
@ -37,4 +38,17 @@ public static class EkaerValueCalculator
|
||||||
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
|
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
|
||||||
return huf > 0 ? (long)huf : null;
|
return huf > 0 ? (long)huf : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Kimenő (Order) változatok ────────────────────────────────────────────
|
||||||
|
// Az érték a NETTÓ (ÁFA nélküli) egységár × mennyiség — az EKÁER „nettó érték"-et vár.
|
||||||
|
|
||||||
|
/// <summary>Egy rendelés-tétel nettó értéke a számla pénznemében (UnitPriceExclTax × mennyiség), átváltás előtt.</summary>
|
||||||
|
public static double ItemLineValue(OrderItemDto item) => (double)item.UnitPriceExclTax * item.Quantity;
|
||||||
|
|
||||||
|
/// <summary>Egy rendelés-tétel értéke HUF-ban, egészre kerekítve. 0/ismeretlen → <c>null</c> (lásd a bejövő párját).</summary>
|
||||||
|
public static long? ItemValueHuf(OrderItemDto item, double rateToHuf)
|
||||||
|
{
|
||||||
|
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
|
||||||
|
return huf > 0 ? (long)huf : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using AyCode.Services.Nav.Ekaer;
|
using AyCode.Services.Nav.Ekaer;
|
||||||
using AyCode.Services.Nav.Ekaer.Models;
|
using AyCode.Services.Nav.Ekaer.Models;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Services.Ekaer;
|
namespace FruitBank.Common.Services.Ekaer;
|
||||||
|
|
@ -29,4 +30,11 @@ public interface IShippingToEkaerMapper
|
||||||
/// <c>document.Shipping</c>-ből jönnek; ha az nincs betöltve, ezek üresen maradnak (a validátor jelzi).
|
/// <c>document.Shipping</c>-ből jönnek; ha az nincs betöltve, ezek üresen maradnak (a validátor jelzi).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company);
|
TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EGY kimenő rendelést (<see cref="FruitBank.Common.Dtos.OrderDto"/>) képez le egy tradeCard-dá: a bejelentő
|
||||||
|
/// (mi) az ELADÓ, a vevő a CÍMZETT, az irány belföldi értékesítés (<c>D</c>/<c>S</c>). A vonó jármű forrása
|
||||||
|
/// a kimenő fuvar-adat (OrderDto) — amíg az nincs bekötve, üresen marad (a validátor jelzi).
|
||||||
|
/// </summary>
|
||||||
|
TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ using AyCode.Core.Interfaces;
|
||||||
using AyCode.Entities;
|
using AyCode.Entities;
|
||||||
using AyCode.Services.Nav.Ekaer;
|
using AyCode.Services.Nav.Ekaer;
|
||||||
using AyCode.Services.Nav.Ekaer.Models;
|
using AyCode.Services.Nav.Ekaer.Models;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
|
using Nop.Core.Domain.Customers;
|
||||||
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
|
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
|
||||||
using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType;
|
using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType;
|
||||||
|
|
||||||
|
|
@ -19,9 +21,13 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
||||||
/// <summary>A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import.</summary>
|
/// <summary>A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import.</summary>
|
||||||
private const string HomeCountry = "HU";
|
private const string HomeCountry = "HU";
|
||||||
|
|
||||||
private readonly EkaerSettings _settings;
|
/// <summary>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).</summary>
|
||||||
|
private const string OutboundCurrency = "HUF";
|
||||||
|
|
||||||
public ShippingToEkaerMapper(EkaerSettings settings)
|
private readonly IEkaerSettings _settings;
|
||||||
|
|
||||||
|
public ShippingToEkaerMapper(IEkaerSettings settings)
|
||||||
=> _settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
=> _settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||||
|
|
||||||
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
|
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
|
||||||
|
|
@ -57,6 +63,45 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
||||||
return BuildTradeCard(document.Shipping, document, company, rateToHuf);
|
return BuildTradeCard(document.Shipping, document, company, rateToHuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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.<transport> → 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)
|
private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company, double rateToHuf)
|
||||||
{
|
{
|
||||||
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase
|
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase
|
||||||
|
|
@ -82,7 +127,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
||||||
CarrierText = shipping?.CargoPartner?.Name,
|
CarrierText = shipping?.CargoPartner?.Name,
|
||||||
|
|
||||||
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye.
|
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye.
|
||||||
UnloadLocation = company.UnloadLocation,
|
UnloadLocation = company.Site,
|
||||||
LoadLocation = BuildLoadLocation(seller),
|
LoadLocation = BuildLoadLocation(seller),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -114,6 +159,18 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
||||||
Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // beszerzési érték HUF-ban (Partner.Currency → árfolyam); 0/ismeretlen → null
|
Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // beszerzési érték HUF-ban (Partner.Currency → árfolyam); 0/ismeretlen → null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>Kimenő (Order) tétel → tradeCardItem. Értékesítés (<c>S</c>); bruttó tömeg a palettákból; nettó érték HUF-ban.</summary>
|
||||||
|
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()
|
private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new()
|
||||||
{
|
{
|
||||||
PlateNumber = NormalizePlateNumber(truck.LicencePlate),
|
PlateNumber = NormalizePlateNumber(truck.LicencePlate),
|
||||||
|
|
@ -138,6 +195,30 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Lerakodási hely a vevő adataiból (kimenő reláció). 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
|
/// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
|
||||||
private static string? NormalizeVatNumber(string? value)
|
private static string? NormalizeVatNumber(string? value)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ namespace FruitBankHybrid.Shared.Tests.Ekaer;
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public sealed class ShippingToEkaerMapperTests
|
public sealed class ShippingToEkaerMapperTests
|
||||||
{
|
{
|
||||||
private static readonly ShippingToEkaerMapper Mapper = new(new EkaerSettings());
|
// EurHufRate > 0 kell: a teszt-partnernek nincs Currency-je (null → külföldiként kezelt), és árfolyam nélkül
|
||||||
|
// a value-számítás ResolveRateToHuf-ja dobna. A value-t a tesztek nem ellenőrzik; ez csak az átváltáshoz kell.
|
||||||
|
private static readonly ShippingToEkaerMapper Mapper = new(new EkaerSettings { EurHufRate = 356 });
|
||||||
|
|
||||||
// ---- Helpers ------------------------------------------------------------
|
// ---- Helpers ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -67,15 +69,17 @@ public sealed class ShippingToEkaerMapperTests
|
||||||
PostalCode = "1102",
|
PostalCode = "1102",
|
||||||
City = "Budapest",
|
City = "Budapest",
|
||||||
Street = "Raktar utca 5",
|
Street = "Raktar utca 5",
|
||||||
UnloadLocation = new LocationType
|
Site = new LocationType
|
||||||
{
|
{
|
||||||
Name = "FruitBank Raktár",
|
Name = "FruitBank Raktár",
|
||||||
VatNumber = "98765432-2-41",
|
VatNumber = "98765432-2-41",
|
||||||
|
Phone = "+36301234567", // NAV-formátum: + / 06 prefix kötelező
|
||||||
|
Email = "raktar@fruitbank.hu",
|
||||||
Country = "HU",
|
Country = "HU",
|
||||||
ZipCode = "1102",
|
ZipCode = "1239",
|
||||||
City = "Budapest",
|
City = "Budapest",
|
||||||
Street = "Raktar utca",
|
Street = "Nagykőrösi út",
|
||||||
StreetNumber = "5",
|
StreetNumber = "353",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -166,7 +170,7 @@ public sealed class ShippingToEkaerMapperTests
|
||||||
Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName);
|
Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName);
|
||||||
Assert.AreEqual("HU", tradeCard.DestinationCountry);
|
Assert.AreEqual("HU", tradeCard.DestinationCountry);
|
||||||
StringAssert.Contains(tradeCard.DestinationAddress, "Budapest", "a destinationAddress a company FullAddress-éből jön");
|
StringAssert.Contains(tradeCard.DestinationAddress, "Budapest", "a destinationAddress a company FullAddress-éből jön");
|
||||||
Assert.AreSame(company.UnloadLocation, tradeCard.UnloadLocation, "a lerakodási hely a cégadatból jön");
|
Assert.AreSame(company.Site, tradeCard.UnloadLocation, "a lerakodási hely a cég telephelyéből jön");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ namespace FruitBankHybrid.Shared.Tests
|
||||||
|
|
||||||
foreach (var shippingDocument in shippingDocuments)
|
foreach (var shippingDocument in shippingDocuments)
|
||||||
{
|
{
|
||||||
var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id);
|
var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id, false);
|
||||||
|
|
||||||
// A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon.
|
// A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon.
|
||||||
Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}");
|
Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}");
|
||||||
|
|
@ -105,8 +105,8 @@ namespace FruitBankHybrid.Shared.Tests
|
||||||
|
|
||||||
var shippingDocumentId = shippingDocuments[0].Id;
|
var shippingDocumentId = shippingDocuments[0].Id;
|
||||||
|
|
||||||
var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId);
|
var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false);
|
||||||
var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId);
|
var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId, false);
|
||||||
|
|
||||||
Assert.IsNotNull(first);
|
Assert.IsNotNull(first);
|
||||||
Assert.IsNotNull(second);
|
Assert.IsNotNull(second);
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,10 @@
|
||||||
|
|
||||||
private readonly HashSet<int> _generatingIds = [];
|
private readonly HashSet<int> _generatingIds = [];
|
||||||
|
|
||||||
// Elküldött bejelentést nem generálunk újra némán (az már a NAV-nál van — módosítás külön művelet lesz);
|
// Elküldött bejelentést nem generálunk újra némán (az már a NAV-nál van — módosítás külön művelet lesz).
|
||||||
// kimenő (Order) generálás még nincs implementálva.
|
// Bejövő és kimenő (Order) is generálható; a kimenőnél a fuvar-adat bekötéséig a validátor jelzi a hiányt.
|
||||||
private bool CanGenerate(EkaerHistory ekaerHistory)
|
private bool CanGenerate(EkaerHistory ekaerHistory)
|
||||||
=> ekaerHistory.Status != EkaerStatus.Sent && !ekaerHistory.IsOutgoing && !_generatingIds.Contains(ekaerHistory.Id);
|
=> ekaerHistory.Status != EkaerStatus.Sent && !_generatingIds.Contains(ekaerHistory.Id);
|
||||||
|
|
||||||
// Hibás (validációs hibás) bejelentés XML-jét nem adjuk a user kezébe — ne kerülhessen kézzel a NAV-hoz.
|
// Hibás (validációs hibás) bejelentés XML-jét nem adjuk a user kezébe — ne kerülhessen kézzel a NAV-hoz.
|
||||||
private static bool CanCopy(EkaerHistory ekaerHistory)
|
private static bool CanCopy(EkaerHistory ekaerHistory)
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.ForeignKey);
|
var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.ForeignKey, ekaerHistory.IsOutgoing);
|
||||||
|
|
||||||
if (updated == null)
|
if (updated == null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||||
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync<List<EkaerHistory>>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]);
|
public Task<List<EkaerHistory>?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync<List<EkaerHistory>>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]);
|
||||||
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory);
|
public Task<EkaerHistory?> AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory);
|
||||||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
|
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
|
||||||
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int shippingDocumentId) => GetByIdAsync<EkaerHistory?>(SignalRTags.GenerateEkaerXmlDocument, shippingDocumentId);
|
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.GenerateEkaerXmlDocument, [foreignKey, isOutgoing]);
|
||||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
|
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync<EkaerHistory?>(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]);
|
||||||
public Task<int> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<int>(SignalRTags.CreateMissingEkaerHistories, fromDate);
|
public Task<int> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<int>(SignalRTags.CreateMissingEkaerHistories, fromDate);
|
||||||
#endregion EkaerHistory
|
#endregion EkaerHistory
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue