EKÁER: unify consignment mapping & obligation logic
- Introduced EkaerConsignment model for direction-agnostic mapping (inbound/outbound), centralizing normalization and aggregation. - Refactored IShippingToEkaerMapper and service interfaces to use new model and expose obligation evaluation. - Added EkaerReportability logic for robust, threshold-based reporting obligation checks with error handling. - Updated UI and SignalR to support detailed creation results and user feedback for skipped entries. - Enhanced tests and documentation to cover new mapping, evaluation, and legal context. - Minor config/protocol adjustments for improved reliability.
This commit is contained in:
parent
6f46aaebb0
commit
c722a7b242
|
|
@ -69,7 +69,11 @@
|
|||
"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(find \"H:\\\\\\\\Applications\\\\\\\\Mango\" -name \"Order.cs\" -path \"*/Domain/Orders/*\" 2>&1 | head -3)",
|
||||
"Bash(f=/h/Applications/Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/Components/Grids/MgGridBase.razor; echo \"FILE exists: $\\(test -f $f && echo yes\\)\"; echo \"=== DxGrid nyitó tag + környéke ===\"; grep -n \"DxGrid\\\\|CustomizeDataRowFilter\\\\|CustomizeElement\\\\|@attributes\\\\|CustomizeEditModel\" \"$f\" 2>/dev/null | head -15)"
|
||||
"Bash(f=/h/Applications/Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/Components/Grids/MgGridBase.razor; echo \"FILE exists: $\\(test -f $f && echo yes\\)\"; echo \"=== DxGrid nyitó tag + környéke ===\"; grep -n \"DxGrid\\\\|CustomizeDataRowFilter\\\\|CustomizeElement\\\\|@attributes\\\\|CustomizeEditModel\" \"$f\" 2>/dev/null | head -15)",
|
||||
"WebFetch(domain:www.ekaer-feladas.hu)",
|
||||
"WebFetch(domain:net.jogtar.hu)",
|
||||
"WebFetch(domain:www.itrack.hu)",
|
||||
"WebFetch(domain:docplayer.hu)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
|||
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapOrder(order, _settings.Company), currency);
|
||||
}
|
||||
|
||||
public EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents)
|
||||
=> EkaerReportability.Evaluate(_mapper.ToConsignment(documents, _settings.Company), _settings);
|
||||
|
||||
public EkaerObligationResult EvaluateObligation(OrderDto order)
|
||||
=> EkaerReportability.Evaluate(_mapper.ToConsignment(order, _settings.Company), _settings);
|
||||
|
||||
/// <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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using AyCode.Services.Nav.Ekaer;
|
|||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Services.Ekaer;
|
||||
|
||||
namespace FruitBank.Common.Server.Services.Ekaer;
|
||||
|
||||
|
|
@ -34,4 +35,14 @@ public interface IFruitBankEkaerService
|
|||
/// 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);
|
||||
|
||||
/// <summary>
|
||||
/// Eldönti egy bejövő (Shipping, Partner) CSOPORT EKÁER-kötelezettségét: külföldi (a feladó és a címzett
|
||||
/// országkódja eltér) → mindig kötelező; belföld → az AGGREGÁLT tömeg/érték a küszöbhöz; érvénytelen országkód →
|
||||
/// <see cref="EkaerObligation.DataError"/>. A küszöb-summa a csoport ÖSSZES dokumentumának tételeire megy.
|
||||
/// </summary>
|
||||
EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents);
|
||||
|
||||
/// <summary>Eldönti egy kimenő rendelés EKÁER-kötelezettségét (ugyanaz a logika, a rendelés tételeire).</summary>
|
||||
EkaerObligationResult EvaluateObligation(OrderDto order);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
/// <summary>A „hiányzó EKÁER-sorok létrehozása" (reconciliation gomb) eredménye a SignalR-dróton: hány Pending sor
|
||||
/// jött létre, és a felhasználónak szóló sima string üzenetek (adathiány / formátumhiba miatt kihagyott jelöltek —
|
||||
/// pl. érvénytelen országkód). A küszöb alatti, nem kötelező jelölteknél NINCS üzenet. A megjelenítés a hívó dolga.</summary>
|
||||
[AcBinarySerializable(false, false, false, false, false, false)]
|
||||
public sealed class EkaerCreateResult
|
||||
{
|
||||
public int CreatedCount { get; set; }
|
||||
public List<string> Messages { get; set; } = [];
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ public interface IFruitBankDataControllerCommon
|
|||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory);
|
||||
public Task<EkaerHistory?> GenerateEkaerXmlDocument(int foreignKey, bool isOutgoing);
|
||||
public Task<EkaerHistory?> CreateEkaerHistory(int foreignKey, bool isOutgoing);
|
||||
public Task<int> CreateMissingEkaerHistories(DateTime fromDate);
|
||||
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate);
|
||||
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter);
|
||||
#endregion EkaerHistory
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Irány-független, normalizált szállítmány-modell. A bejövő (<c>ShippingDocument</c>-csoport) és a kimenő
|
||||
/// (<c>OrderDto</c>) forrás EBBE képződik (a <see cref="IShippingToEkaerMapper"/> adapterei), és innen épül MIND a
|
||||
/// NAV tradeCard (<see cref="IShippingToEkaerMapper.BuildTradeCard"/>), MIND a bejelentés-kötelezettség
|
||||
/// (<see cref="EkaerReportability"/>). Így az irányfüggő tudás a két adapterre szorul, a közös logika egy helyen van.
|
||||
/// Szerver-oldali köztes típus — NEM megy a SignalR-dróton.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A fel-/lerakodási hely és a jármű már a NAV-típus (<see cref="LocationType"/>, <see cref="BasicVehicleDetailType"/>),
|
||||
/// mert a saját telephely (<c>company.Site</c>) kész LocationType (Phone/Email/FELIR-mezőkkel) — azt átalakítás nélkül,
|
||||
/// MEZŐVESZTÉS nélkül kell átengedni. A normalizálás (kötelezettséghez számít) a feladó/címzett országkódjára és a
|
||||
/// tételek tömeg/érték-aggregálására korlátozódik.
|
||||
/// </remarks>
|
||||
public sealed class EkaerConsignment
|
||||
{
|
||||
/// <summary>A forrás azonosítója (bejövőnél ShippingDocument.Id, kimenőnél Order.Id). Csoport-kiértékelésnél
|
||||
/// (több dokumentum) az első forrásé — a kötelezettség-döntés nem használja.</summary>
|
||||
public int ForeignKey { get; init; }
|
||||
|
||||
public bool IsOutgoing { get; init; }
|
||||
|
||||
/// <summary>Számla-pénznem (ISO 4217). A tétel <see cref="EkaerLine.ValueHuf"/> már KISZÁMOLT HUF-ban.</summary>
|
||||
public string? Currency { get; init; }
|
||||
|
||||
/// <summary>Feladó / eladó (a kötelezettség az országkódját nézi).</summary>
|
||||
public EkaerEndpoint Seller { get; init; } = new();
|
||||
|
||||
/// <summary>Címzett / vevő (a kötelezettség az országkódját nézi).</summary>
|
||||
public EkaerEndpoint Buyer { get; init; } = new();
|
||||
|
||||
/// <summary>Felrakodási hely (NAV LocationType — pl. a saját telephely átengedve).</summary>
|
||||
public LocationType? LoadLocation { get; init; }
|
||||
|
||||
/// <summary>Lerakodási hely (NAV LocationType — pl. a saját telephely átengedve).</summary>
|
||||
public LocationType? UnloadLocation { get; init; }
|
||||
|
||||
public IReadOnlyList<EkaerLine> Lines { get; init; } = [];
|
||||
|
||||
public BasicVehicleDetailType? Vehicle { get; init; }
|
||||
public BasicVehicleDetailType? Trailer { get; init; }
|
||||
public string? CarrierName { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Egy fél (feladó/címzett) normalizálatlan adatai a NAV seller/destination mezőkhöz ÉS a kötelezettség
|
||||
/// országkód-vizsgálatához. (A cím egysoros; a tagolt fel-/lerakodási helyet a <see cref="EkaerConsignment.LoadLocation"/>
|
||||
/// / <see cref="EkaerConsignment.UnloadLocation"/> hordozza.)</summary>
|
||||
public sealed class EkaerEndpoint
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? VatNumber { get; init; }
|
||||
public string? CountryCode { get; init; }
|
||||
public string? FullAddress { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Egy normalizált tétel. A <see cref="WeightKg"/> és a <see cref="ValueHuf"/> már KISZÁMOLT (HUF-ban),
|
||||
/// hogy a küszöb-summa és a tradeCard ugyanazt használja (egyetlen érték-forrás).</summary>
|
||||
public sealed class EkaerLine
|
||||
{
|
||||
public string ExternalId { get; init; } = string.Empty;
|
||||
public string? Vtsz { get; init; }
|
||||
public string? Name { get; init; }
|
||||
public double WeightKg { get; init; }
|
||||
public long? ValueHuf { get; init; }
|
||||
public TradeReasonType TradeReason { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
using AyCode.Services.Nav.Ekaer;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <summary>Egy szállítmány EKÁER bejelentés-kötelezettsége.</summary>
|
||||
public enum EkaerObligation
|
||||
{
|
||||
/// <summary>Kötelező bejelenteni (sort kell létrehozni).</summary>
|
||||
Required,
|
||||
/// <summary>Nem kötelező (belföld, küszöb alatt) — NEM hiba, nincs üzenet.</summary>
|
||||
NotRequired,
|
||||
/// <summary>A döntés nem hozható meg adathiány/formátumhiba miatt (pl. érvénytelen országkód) — NINCS sor, az okok az üzenetekben.</summary>
|
||||
DataError,
|
||||
}
|
||||
|
||||
/// <summary>A kötelezettség-kiértékelés eredménye: a döntés + (csak DataError esetén) a felhasználónak szóló okok.</summary>
|
||||
public sealed class EkaerObligationResult
|
||||
{
|
||||
public EkaerObligation Obligation { get; private init; }
|
||||
public IReadOnlyList<string> Errors { get; private init; } = [];
|
||||
|
||||
public static EkaerObligationResult Required { get; } = new() { Obligation = EkaerObligation.Required };
|
||||
public static EkaerObligationResult NotRequired { get; } = new() { Obligation = EkaerObligation.NotRequired };
|
||||
public static EkaerObligationResult DataError(IReadOnlyList<string> errors) =>
|
||||
new() { Obligation = EkaerObligation.DataError, Errors = errors };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EKÁER bejelentés-kötelezettség eldöntése egy normalizált <see cref="EkaerConsignment"/>-re — KÖZÖS a bejövő
|
||||
/// (document-csoport) és a kimenő (order) ágon. A NAV mindkét irányban büntet (a felesleges bejelentés ÉS a kimaradás
|
||||
/// is bírság), ezért: a bizonytalan adatot (érvénytelen országkód) NEM döntjük el magától → <see cref="EkaerObligation.DataError"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sorrend: (1) az AGGREGÁLT tömeg/érték küszöb FELETT → Required, az országkódtól FÜGGETLENÜL — belföld+küszöb
|
||||
/// felett és külföld egyaránt kötelező; az országkód-hibát ilyenkor a generate-validálás jelzi, a sor létrejön;
|
||||
/// (2) küszöb ALATT → itt számít az ország: hiányzó/érvénytelen országkód → DataError (a foreign-vs-belföld nem
|
||||
/// dönthető el); (3) küszöb alatt, érvényes országkódok: külföld (eltér) → Required, belföld (egyezik) → NotRequired.
|
||||
/// A 13/2020. (XII. 23.) PM rendelet alapján a küszöb feladó→címzett→jármű relációra aggregált — a hívó már
|
||||
/// így állítja össze a <see cref="EkaerConsignment.Lines"/>-t (bejövőnél a (Shipping, Partner) csoport tételei).
|
||||
/// </remarks>
|
||||
public static class EkaerReportability
|
||||
{
|
||||
public static EkaerObligationResult Evaluate(EkaerConsignment consignment, IEkaerSettings settings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(consignment);
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
// (1) Küszöb FELETT → kötelező, FÜGGETLENÜL az országkódtól: belföld+küszöb felett ÉS külföld egyaránt kötelező.
|
||||
// Az országkód-hibát ilyenkor NEM itt blokkoljuk — a sor létrejön, a hibát a generate-validálás jelzi.
|
||||
var totalWeight = consignment.Lines.Sum(l => l.WeightKg);
|
||||
var totalValueHuf = consignment.Lines.Sum(l => l.ValueHuf ?? 0L);
|
||||
if (totalWeight >= settings.ThresholdWeightKg || totalValueHuf >= settings.ThresholdValueHuf)
|
||||
return EkaerObligationResult.Required;
|
||||
|
||||
// (2) Küszöb ALATT: itt MÁR az ország dönt — csak a külföldi (eltérő országkód) reláció kötelező. Ehhez érvényes
|
||||
// ISO-2 országkódok kellenek; hiányzó/érvénytelen → a foreign-vs-belföld nem dönthető el → DataError.
|
||||
var subject = Subject(consignment);
|
||||
var errors = new List<string>();
|
||||
if (!IsValidCountry(consignment.Seller.CountryCode))
|
||||
errors.Add($"{subject}: a feladó országkódja hiányzik vagy érvénytelen ('{consignment.Seller.CountryCode}').");
|
||||
if (!IsValidCountry(consignment.Buyer.CountryCode))
|
||||
errors.Add($"{subject}: a címzett országkódja hiányzik vagy érvénytelen ('{consignment.Buyer.CountryCode}').");
|
||||
if (errors.Count > 0)
|
||||
return EkaerObligationResult.DataError(errors);
|
||||
|
||||
// (3) Küszöb alatt, érvényes országkódok: külföld (eltér) → kötelező, belföld (egyezik) → nem kötelező (üzenet nélkül).
|
||||
return CountryEquals(consignment.Seller.CountryCode, consignment.Buyer.CountryCode)
|
||||
? EkaerObligationResult.NotRequired
|
||||
: EkaerObligationResult.Required;
|
||||
}
|
||||
|
||||
/// <summary>Érvényes EKÁER országkód: pontosan 2 ASCII betű (ISO-2). Üres / más hosszúságú → érvénytelen.</summary>
|
||||
private static bool IsValidCountry(string? code)
|
||||
{
|
||||
var c = code?.Trim();
|
||||
return c is { Length: 2 } && c.All(char.IsAsciiLetter);
|
||||
}
|
||||
|
||||
private static bool CountryEquals(string? a, string? b) => string.Equals(a?.Trim(), b?.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string Subject(EkaerConsignment c)
|
||||
=> c.IsOutgoing ? $"Rendelés #{c.ForeignKey}" : string.IsNullOrWhiteSpace(c.Seller.Name) ? $"Szállítólevél #{c.ForeignKey}" : c.Seller.Name!;
|
||||
}
|
||||
|
|
@ -37,4 +37,17 @@ public interface IShippingToEkaerMapper
|
|||
/// a kimenő fuvar-adat (OrderDto) — amíg az nincs bekötve, üresen marad (a validátor jelzi).
|
||||
/// </summary>
|
||||
TradeCardType MapOrder(OrderDto order, EkaerCompanyInfo company);
|
||||
|
||||
/// <summary>
|
||||
/// Bejövő forrás → normalizált <see cref="EkaerConsignment"/>. Egy vagy több <see cref="ShippingDocument"/>-tel
|
||||
/// hívható: EGY dokumentummal a tradeCard-generáláshoz, a (Shipping, Partner) CSOPORTtal a kötelezettség-summához
|
||||
/// (a tételek összevonva). A hívó gondoskodik róla, hogy a csoport azonos partneré és azonos Shippingé legyen.
|
||||
/// </summary>
|
||||
EkaerConsignment ToConsignment(IReadOnlyCollection<ShippingDocument> documents, EkaerCompanyInfo company);
|
||||
|
||||
/// <summary>Kimenő forrás → normalizált <see cref="EkaerConsignment"/> (egy rendelés = egy szállítmány).</summary>
|
||||
EkaerConsignment ToConsignment(OrderDto order, EkaerCompanyInfo company);
|
||||
|
||||
/// <summary>Normalizált szállítmány → NAV tradeCard. KÖZÖS a két irányra; a <c>MapDocument</c>/<c>MapOrder</c> ezt hívja.</summary>
|
||||
TradeCardType BuildTradeCard(EkaerConsignment consignment);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,19 @@ namespace FruitBank.Common.Services.Ekaer;
|
|||
|
||||
/// <inheritdoc cref="IShippingToEkaerMapper"/>
|
||||
/// <remarks>
|
||||
/// Tiszta (állapotmentes) leképező. A feladót és a saját céget egységesen <see cref="ICompanyInfoBase"/>-ként kezeli.
|
||||
/// A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be a <c>Models.TradeCardType</c> osztály
|
||||
/// és a <c>Models.Common.TradeCardType</c> enum névütközése miatt.
|
||||
/// 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" mindig HU; minden más feladó-ország import.</summary>
|
||||
/// <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;
|
||||
/// bekötéskor innen jön). HUF → nincs átváltás (rate 1).</summary>
|
||||
/// <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;
|
||||
|
|
@ -30,6 +32,8 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
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);
|
||||
|
|
@ -41,13 +45,13 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
// 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++;
|
||||
var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate);
|
||||
operations.Add(new TradeCardOperationType
|
||||
{
|
||||
Index = index,
|
||||
Operation = operation,
|
||||
TradeCard = BuildTradeCard(shipping, document, company, rateToHuf),
|
||||
TradeCard = BuildTradeCard(ToConsignment([document], company)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -58,130 +62,184 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(document);
|
||||
ArgumentNullException.ThrowIfNull(company);
|
||||
|
||||
var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate);
|
||||
return BuildTradeCard(document.Shipping, document, company, rateToHuf);
|
||||
return BuildTradeCard(ToConsignment([document], company));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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 = EkaerValueCalculator.ResolveRateToHuf(OutboundCurrency, _settings.EurHufRate);
|
||||
var rateToHuf = SafeRateToHuf(OutboundCurrency); // jelenleg minden HUF
|
||||
|
||||
var tradeCard = new TradeCardType
|
||||
var lines = (order.OrderItemDtos ?? [])
|
||||
.Select(item => new EkaerLine
|
||||
{
|
||||
// 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,
|
||||
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();
|
||||
|
||||
// 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ő.
|
||||
return new EkaerConsignment
|
||||
{
|
||||
ForeignKey = order.Id,
|
||||
IsOutgoing = true,
|
||||
Currency = OutboundCurrency,
|
||||
Seller = CompanyEndpoint(company),
|
||||
Buyer = CustomerEndpoint(customer),
|
||||
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.
|
||||
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.
|
||||
};
|
||||
|
||||
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)
|
||||
// ── 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)
|
||||
{
|
||||
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase
|
||||
ArgumentNullException.ThrowIfNull(consignment);
|
||||
|
||||
var tradeCard = new TradeCardType
|
||||
{
|
||||
TradeType = ResolveTradeType(seller),
|
||||
TradeType = ResolveTradeType(consignment),
|
||||
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),
|
||||
SellerName = consignment.Seller.Name,
|
||||
SellerVatNumber = NormalizeVatNumber(consignment.Seller.VatNumber),
|
||||
SellerCountry = NormalizeCountryCode(consignment.Seller.CountryCode, 2),
|
||||
SellerAddress = Truncate(consignment.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),
|
||||
DestinationName = consignment.Buyer.Name,
|
||||
DestinationVatNumber = NormalizeVatNumber(consignment.Buyer.VatNumber),
|
||||
DestinationCountry = NormalizeCountryCode(consignment.Buyer.CountryCode, 2),
|
||||
DestinationAddress = Truncate(consignment.Buyer.FullAddress, 200),
|
||||
|
||||
// Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név.
|
||||
CarrierText = shipping?.CargoPartner?.Name,
|
||||
CarrierText = consignment.CarrierName,
|
||||
|
||||
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye.
|
||||
UnloadLocation = company.Site,
|
||||
LoadLocation = BuildLoadLocation(seller),
|
||||
// A helyek kész NAV LocationType-ok (a saját telephely érintetlenül) — átengedve.
|
||||
LoadLocation = consignment.LoadLocation,
|
||||
UnloadLocation = consignment.UnloadLocation,
|
||||
};
|
||||
|
||||
// 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);
|
||||
if (consignment.Vehicle != null) tradeCard.Vehicle = consignment.Vehicle;
|
||||
if (consignment.Trailer != null) tradeCard.Vehicle2 = consignment.Trailer;
|
||||
|
||||
foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item, rateToHuf));
|
||||
foreach (var line in consignment.Lines) tradeCard.Items.Add(BuildItem(line));
|
||||
return tradeCard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Belföldi feladó (HU) → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import). A NAV EKÁER magyar,
|
||||
/// így a belföld mindig HU; az export (<c>E</c>) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7).
|
||||
/// </summary>
|
||||
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()
|
||||
/// <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)
|
||||
{
|
||||
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
|
||||
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,
|
||||
};
|
||||
|
||||
/// <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()
|
||||
// ── 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()
|
||||
{
|
||||
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
|
||||
Name = partner?.Name,
|
||||
VatNumber = partner?.TaxId,
|
||||
CountryCode = partner?.CountryCode,
|
||||
FullAddress = partner?.FullAddress,
|
||||
};
|
||||
|
||||
private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new()
|
||||
private static EkaerEndpoint CompanyEndpoint(EkaerCompanyInfo company) => new()
|
||||
{
|
||||
PlateNumber = NormalizePlateNumber(truck.LicencePlate),
|
||||
Country = NormalizeCountryCode(truck.CountryCode, 3),
|
||||
Name = company.Name,
|
||||
VatNumber = company.TaxId,
|
||||
CountryCode = company.CountryCode,
|
||||
FullAddress = company.FullAddress,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 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.)
|
||||
/// </summary>
|
||||
private static LocationType? BuildLoadLocation(ICompanyInfoBase? seller)
|
||||
/// <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
|
||||
|
|
@ -195,16 +253,7 @@ 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>
|
||||
/// <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;
|
||||
|
|
@ -219,6 +268,33 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
using FruitBank.Common.Services.Ekaer;
|
||||
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tesztek a <see cref="EkaerReportability"/> bejelentés-kötelezettség-eldöntésére. Tisztán memóriában felépített
|
||||
/// <see cref="EkaerConsignment"/>-eken fut (nincs hálózat/DB), determinisztikus. Küszöb: 200 kg / 250 000 Ft.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class EkaerReportabilityTests
|
||||
{
|
||||
private static readonly EkaerSettings Settings = new()
|
||||
{
|
||||
EurHufRate = 356,
|
||||
ThresholdWeightKg = 200,
|
||||
ThresholdValueHuf = 250_000,
|
||||
};
|
||||
|
||||
private static EkaerConsignment Consignment(string? sellerCountry, string? buyerCountry, params EkaerLine[] lines) => new()
|
||||
{
|
||||
Seller = new EkaerEndpoint { Name = "Feladó", CountryCode = sellerCountry },
|
||||
Buyer = new EkaerEndpoint { Name = "Címzett", CountryCode = buyerCountry },
|
||||
Lines = lines,
|
||||
};
|
||||
|
||||
private static EkaerLine Line(double weightKg, long? valueHuf) =>
|
||||
new() { ExternalId = "1", WeightKg = weightKg, ValueHuf = valueHuf, TradeReason = TradeReasonType.A };
|
||||
|
||||
// ---- Belföld + küszöb ----------------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void Domestic_BelowBothThresholds_NotRequired()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(100, 100_000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
|
||||
Assert.AreEqual(0, result.Errors.Count, "küszöb alatt NINCS üzenet (nem hiba)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Domestic_WeightAtOrOverThreshold_Required()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(200, 0)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "a tömeg eléri a küszöböt → kötelező (VAGY-logika)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Domestic_ValueOverThreshold_Required()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(10, 300_000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "az érték átlépi a küszöböt → kötelező");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Domestic_AggregatedOverThreshold_Required()
|
||||
{
|
||||
// Egyenként 200 kg ALATT, EGYÜTT fölötte → kötelező. Ez a (Shipping, Partner)-aggregálás lényege.
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(120, 0), Line(120, 0)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation);
|
||||
}
|
||||
|
||||
// ---- Külföld (a két országkód eltér) → küszöb nélkül kötelező ------------
|
||||
|
||||
[TestMethod]
|
||||
public void CrossBorder_BelowThreshold_StillRequired()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment("DE", "HU", Line(1, 1)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "eltérő országkód → mindig kötelező, küszöb nélkül");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SameForeignCountryBothEnds_TreatedAsDomesticThreshold()
|
||||
{
|
||||
// Mindkét vég azonos (nem HU) ország → NEM határátlépő → a küszöb dönt.
|
||||
var result = EkaerReportability.Evaluate(Consignment("DE", "DE", Line(1, 1)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
|
||||
}
|
||||
|
||||
// ---- Adathiba: érvénytelen/hiányzó országkód → DataError CSAK küszöb alatt ----
|
||||
|
||||
[TestMethod]
|
||||
public void BelowThreshold_MissingSellerCountry_DataError()
|
||||
{
|
||||
// Küszöb alatt MÁR számít az ország (foreign-vs-belföld) — hiányzó országkód → nem dönthető el → DataError.
|
||||
var result = EkaerReportability.Evaluate(Consignment(null, "HU", Line(10, 1000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
|
||||
Assert.IsTrue(result.Errors.Count > 0, "a hiányzó országkódot jelezni kell a felhasználónak");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BelowThreshold_InvalidSellerCountry_NotIso2_DataError()
|
||||
{
|
||||
// Teljes név (nem ISO-2) → érvénytelen → küszöb alatt nem dönthető el → DataError.
|
||||
var result = EkaerReportability.Evaluate(Consignment("Magyarország", "HU", Line(10, 1000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BelowThreshold_MissingBuyerCountry_DataError()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "", Line(10, 1000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverThreshold_InvalidCountry_Required_NotDataError()
|
||||
{
|
||||
// Küszöb FELETT (tömeg 500 ≥ 200) a kötelezettség az országkódtól FÜGGETLEN → a sor létrejön (a hibát a
|
||||
// generate-validálás jelzi), NEM DataError.
|
||||
var result = EkaerReportability.Evaluate(Consignment(null, "Magyarország", Line(500, 0)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation);
|
||||
Assert.AreEqual(0, result.Errors.Count);
|
||||
}
|
||||
|
||||
// ---- Hibás / üres adatok (robusztusság) ----------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void NoLines_Domestic_NotRequired()
|
||||
{
|
||||
// Üres szállítmány (nincs tétel) → tömeg/érték 0 → küszöb alatt → belföld → nem kötelező (NEM dob).
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU"), Settings);
|
||||
Assert.AreEqual(EkaerObligation.NotRequired, result.Obligation);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BothCountriesInvalid_BelowThreshold_DataError_WithTwoMessages()
|
||||
{
|
||||
var result = EkaerReportability.Evaluate(Consignment(null, "", Line(10, 1000)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.DataError, result.Obligation);
|
||||
Assert.AreEqual(2, result.Errors.Count, "mindkét hibás országkódot külön jelezni kell");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NullLineValue_HandledAsZero_WeightDecides()
|
||||
{
|
||||
// Hiányzó (null) tétel-érték (pl. külföldi deviza árfolyam nélkül) → 0-ként számít, NEM dob; a tömeg dönt.
|
||||
var result = EkaerReportability.Evaluate(Consignment("HU", "HU", Line(500, null)), Settings);
|
||||
Assert.AreEqual(EkaerObligation.Required, result.Obligation, "a tömeg átlépi a küszöböt — a null érték nem akadály");
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,28 @@ public sealed class ShippingToEkaerMapperTests
|
|||
},
|
||||
};
|
||||
|
||||
private static ShippingDocument CreateInboundDocument(int itemId, double weight, string sellerCountry = "HU") => new()
|
||||
{
|
||||
Country = sellerCountry,
|
||||
Partner = new Partner
|
||||
{
|
||||
Name = "Beszállító Kft",
|
||||
TaxId = "12345678-2-42",
|
||||
CountryCode = sellerCountry,
|
||||
PostalCode = "1011",
|
||||
City = "Budapest",
|
||||
Street = "Fő utca 1",
|
||||
},
|
||||
ShippingItems = [new ShippingItem
|
||||
{
|
||||
Id = itemId,
|
||||
Name = "Alma",
|
||||
ProductDto = new ProductDto { Gtin = "08081010", Name = "Alma" },
|
||||
MeasuredGrossWeight = weight,
|
||||
UnitPriceOnDocument = 5.0,
|
||||
}],
|
||||
};
|
||||
|
||||
// ---- Granularitás / index ----------------------------------------------
|
||||
|
||||
[TestMethod]
|
||||
|
|
@ -216,4 +238,55 @@ public sealed class ShippingToEkaerMapperTests
|
|||
[TestMethod]
|
||||
public void MapShipping_NullCompany_Throws()
|
||||
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(CreateShipping(), null!));
|
||||
|
||||
// ---- Köztes modell: ToConsignment (bejövő) ------------------------------
|
||||
|
||||
[TestMethod]
|
||||
public void ToConsignment_Inbound_MultipleDocuments_CombinesLinesAndAggregatesWeight()
|
||||
{
|
||||
// Két szállítólevél (azonos partner, azonos Shipping) → egy szállítmány, MINDKÉT tétellel — a küszöb-aggregáláshoz.
|
||||
var consignment = Mapper.ToConsignment(
|
||||
[CreateInboundDocument(itemId: 1, weight: 100), CreateInboundDocument(itemId: 2, weight: 150)],
|
||||
CreateCompany());
|
||||
|
||||
Assert.AreEqual(2, consignment.Lines.Count, "a csoport ÖSSZES tétele egy szállítmányban");
|
||||
Assert.AreEqual(250d, consignment.Lines.Sum(l => l.WeightKg), 0.001, "a tömeg a dokumentumokból összegződik");
|
||||
Assert.IsFalse(consignment.IsOutgoing);
|
||||
Assert.AreEqual("HU", consignment.Seller.CountryCode, "feladó = a beszállító partner");
|
||||
Assert.AreEqual("HU", consignment.Buyer.CountryCode, "címzett = a saját cég");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToConsignment_Inbound_BuildTradeCard_PreservesCompanySiteAsUnloadLocation()
|
||||
{
|
||||
var company = CreateCompany();
|
||||
var tradeCard = Mapper.BuildTradeCard(Mapper.ToConsignment([CreateInboundDocument(1, 100)], company));
|
||||
Assert.AreSame(company.Site, tradeCard.UnloadLocation, "a saját telephely érintetlenül (mezővesztés nélkül) megy át");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToConsignment_Inbound_EmptyDocuments_Throws()
|
||||
=> Assert.ThrowsExactly<ArgumentException>(() => Mapper.ToConsignment([], CreateCompany()));
|
||||
|
||||
[TestMethod]
|
||||
public void ToConsignment_Inbound_NullPartner_DoesNotThrow_SellerCountryNull()
|
||||
{
|
||||
// Hibás adat: nincs Partner a szállítólevélen → az adapter NEM dob; a feladó-ország null (ezt a kötelezettség-
|
||||
// értékelő / generate-validálás kezeli, nem a leképező).
|
||||
var doc = new ShippingDocument { Partner = null, ShippingItems = [new ShippingItem { Id = 1, MeasuredGrossWeight = 50 }] };
|
||||
var consignment = Mapper.ToConsignment([doc], CreateCompany());
|
||||
|
||||
Assert.IsNull(consignment.Seller.CountryCode, "nincs partner → nincs feladó-ország");
|
||||
Assert.AreEqual(1, consignment.Lines.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToConsignment_Inbound_NullShippingItems_EmptyLines()
|
||||
{
|
||||
// Hibás/hiányos adat: nincs tétel a szállítólevélen → üres Lines, NEM dob.
|
||||
var doc = new ShippingDocument { Partner = new Partner { CountryCode = "HU" }, ShippingItems = null };
|
||||
var consignment = Mapper.ToConsignment([doc], CreateCompany());
|
||||
|
||||
Assert.AreEqual(0, consignment.Lines.Count);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,17 @@
|
|||
@using AyCode.Services.Nav
|
||||
@using AyCode.Services.Nav.Ekaer.Models
|
||||
@using AyCode.Utils.Extensions
|
||||
@using FruitBank.Common.Dtos
|
||||
@using FruitBank.Common.Entities
|
||||
@using FruitBankHybrid.Shared.Databases
|
||||
@using FruitBankHybrid.Shared.Extensions
|
||||
@using FruitBankHybrid.Shared.Services.Loggers
|
||||
@using FruitBankHybrid.Shared.Services.SignalRs
|
||||
|
||||
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
|
||||
@inject FruitBankSignalRClient FruitBankSignalRClient
|
||||
@inject IJSRuntime JS
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<MgGridWithInfoPanel ShowInfoPanel="@IsMasterGrid">
|
||||
<GridContent>
|
||||
|
|
@ -147,6 +150,10 @@
|
|||
/// hogy a fül-számlálók azonnal frissüljenek, mert a sor átkerülhet másik tabra.</summary>
|
||||
[Parameter] public EventCallback OnDataChanged { get; set; }
|
||||
|
||||
/// <summary>A szülő (oldal) értesítése a hosszú művelet (sorok létrehozása) elejéről (true) / végéről (false) —
|
||||
/// hogy az oldal Loading panelje megjelenjen/eltűnjön.</summary>
|
||||
[Parameter] public EventCallback<bool> OnBusyChanged { get; set; }
|
||||
|
||||
public bool IsMasterGrid => ParentDataItem == null;
|
||||
|
||||
private LoggerClient<GridEkaerHistory> _logger;
|
||||
|
|
@ -238,13 +245,23 @@
|
|||
{
|
||||
if (_creatingMissing) return;
|
||||
_creatingMissing = true;
|
||||
await OnBusyChanged.InvokeAsync(true); // Loading panel BE — a művelet lassú lehet (kimenő ág)
|
||||
|
||||
EkaerCreateResult? result = null;
|
||||
try
|
||||
{
|
||||
var createdCount = await FruitBankSignalRClient.CreateMissingEkaerHistories(CreateFromDate);
|
||||
_logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; fromDate: {CreateFromDate:yyyy.MM.dd}");
|
||||
result = await FruitBankSignalRClient.CreateMissingEkaerHistories(CreateFromDate);
|
||||
var createdCount = result?.CreatedCount ?? 0;
|
||||
_logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; messages: {result?.Messages.Count ?? 0}; fromDate: {CreateFromDate:yyyy.MM.dd}");
|
||||
|
||||
if (createdCount > 0) await ReloadDataFromDb(true);
|
||||
foreach (var message in result?.Messages ?? [])
|
||||
_logger.Warning($"CreateMissingEkaerHistories; skipped: {message}");
|
||||
|
||||
if (createdCount > 0)
|
||||
{
|
||||
await ReloadDataFromDb(true);
|
||||
await NotifyDataChanged(); // a fül-számlálók frissítése (új sorok)
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -253,7 +270,12 @@
|
|||
finally
|
||||
{
|
||||
_creatingMissing = false;
|
||||
await OnBusyChanged.InvokeAsync(false); // Loading panel KI
|
||||
}
|
||||
|
||||
// Az adathiány/formátumhiba miatt KIHAGYOTT jelöltek üzenetei popupban — ezeket javítani kell, mielőtt bekerülhetnek.
|
||||
if (result?.Messages is { Count: > 0 } skipped)
|
||||
await DialogService.ShowMessageBoxAsync("EKÁER — kihagyott tételek", string.Join(Environment.NewLine, skipped), MessageBoxRenderStyle.Warning);
|
||||
}
|
||||
|
||||
private async Task OnCopyClick(EkaerHistory ekaerHistory)
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@
|
|||
|
||||
<DxTabs ActiveTabIndexChanged="(i) => OnActiveTabChanged(i)" RenderMode="TabsRenderMode.OnDemand" AllowTabReorder="false">
|
||||
<DxTabPage Text="Beküldésre váró">
|
||||
<GridEkaerHistory @ref="gridEkaerHistoryPending" Filter="EkaerHistoryFilter.ToSubmit" OnDataChanged="RefreshNeedsCompletionCountAsync"></GridEkaerHistory>
|
||||
<GridEkaerHistory @ref="gridEkaerHistoryPending" Filter="EkaerHistoryFilter.ToSubmit" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
|
||||
</DxTabPage>
|
||||
<DxTabPage Text="@NeedsCompletionTabText">
|
||||
<GridEkaerHistory @ref="gridEkaerHistoryNeedsCompletion" Filter="EkaerHistoryFilter.ToSubmit" OnDataChanged="RefreshNeedsCompletionCountAsync"></GridEkaerHistory>
|
||||
<GridEkaerHistory @ref="gridEkaerHistoryNeedsCompletion" Filter="EkaerHistoryFilter.ToSubmit" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
|
||||
</DxTabPage>
|
||||
<DxTabPage Text="Elküldött">
|
||||
<GridEkaerHistory @ref="gridEkaerHistorySent" Filter="EkaerHistoryFilter.Sent" OnDataChanged="RefreshNeedsCompletionCountAsync"></GridEkaerHistory>
|
||||
<GridEkaerHistory @ref="gridEkaerHistorySent" Filter="EkaerHistoryFilter.Sent" OnDataChanged="RefreshNeedsCompletionCountAsync" OnBusyChanged="OnGridBusyChanged"></GridEkaerHistory>
|
||||
</DxTabPage>
|
||||
</DxTabs>
|
||||
</DxLoadingPanel>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using AyCode.Core.Loggers;
|
|||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Models;
|
||||
using FruitBankHybrid.Shared.Components.Grids.Ekaers;
|
||||
using FruitBankHybrid.Shared.Databases;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Mango.Nop.Core.Loggers;
|
||||
|
|
@ -45,6 +46,13 @@ public partial class Ekaer : ComponentBase
|
|||
await RefreshNeedsCompletionCountAsync();
|
||||
}
|
||||
|
||||
/// <summary>A grid jelzi a hosszú művelet (sorok létrehozása) elejét (true) / végét (false) → az oldal Loading panelje meg/elrejtve.</summary>
|
||||
private Task OnGridBusyChanged(bool busy)
|
||||
{
|
||||
LoadingPanelVisibility.Visible = busy;
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>A „Pótlásra váró" rekordok számának frissítése a fül feliratához (a count endpointról, flag-szűrővel).</summary>
|
||||
private async Task RefreshNeedsCompletionCountAsync()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
|||
public Task<EkaerHistory?> UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory);
|
||||
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<int> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<int>(SignalRTags.CreateMissingEkaerHistories, fromDate);
|
||||
public Task<EkaerCreateResult?> CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync<EkaerCreateResult?>(SignalRTags.CreateMissingEkaerHistories, fromDate);
|
||||
public Task<int> GetEkaerHistoryCount(EkaerHistoryFilter filter) => GetByIdAsync<int>(SignalRTags.GetEkaerHistoryCount, filter);
|
||||
#endregion EkaerHistory
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"ProtocolMode": "Bytes",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "Coalesced",
|
||||
"FlushTimeout": "00:00:10"
|
||||
"FlushTimeout": "00:00:15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"ProtocolMode": "Bytes",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "Coalesced",
|
||||
"FlushTimeout": "00:00:10"
|
||||
"FlushTimeout": "00:00:15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"ProtocolMode": "Bytes",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "Coalesced",
|
||||
"FlushTimeout": "00:00:10"
|
||||
"FlushTimeout": "00:00:15"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue