EKÁER config refactor: centralize settings, add audit rate
Centralized EKÁER configuration in new EkaerSettings class (company info, EUR-HUF rate, thresholds). Refactored service and mapper to use EkaerSettings. Added EkaerValueCalculator for HUF value logic. Extended EkaerHistory with ConversionRate for audit. Added IsEkaer to Partner. Updated mapping, VTSZ normalization, UI grid, CSS, and tests. Added diagnostic commands to settings.local.json.
This commit is contained in:
parent
ea29ad18bd
commit
f23aebff2d
File diff suppressed because one or more lines are too long
|
|
@ -9,22 +9,22 @@ namespace FruitBank.Common.Server.Services.Ekaer;
|
|||
/// <inheritdoc cref="IFruitBankEkaerService"/>
|
||||
/// <remarks>
|
||||
/// A teljes lánc: <c>map</c> (<see cref="IShippingToEkaerMapper"/>, FruitBank.Common) →
|
||||
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A NAV-fiók hitelesítő adatait
|
||||
/// (<c>INavCredentials</c>) és a saját cégadatokat (<see cref="EkaerCompanyInfo"/>) a DI szolgáltatja.
|
||||
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A saját cégadatot
|
||||
/// (<see cref="EkaerSettings.Company"/>) és a NAV-fiók hitelesítő adatait a DI szolgáltatja.
|
||||
/// </remarks>
|
||||
public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
||||
{
|
||||
private readonly IShippingToEkaerMapper _mapper;
|
||||
private readonly IEkaerSubmitService _submitService;
|
||||
private readonly IEkaerTradeCardValidator _validator;
|
||||
private readonly EkaerCompanyInfo _company;
|
||||
private readonly EkaerSettings _settings;
|
||||
|
||||
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerCompanyInfo company)
|
||||
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerSettings settings)
|
||||
{
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
|
||||
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
|
||||
_company = company ?? throw new ArgumentNullException(nameof(company));
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
public Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default)
|
||||
|
|
@ -32,7 +32,7 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
|||
ArgumentNullException.ThrowIfNull(shipping);
|
||||
|
||||
// map (FruitBank.Common) → submit: validate → send (AyCode.Services)
|
||||
var operations = _mapper.MapShipping(shipping, _company, operation);
|
||||
var operations = _mapper.MapShipping(shipping, _settings.Company, operation);
|
||||
return _submitService.SubmitAsync(operations, cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -42,17 +42,29 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
|
|||
|
||||
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
|
||||
// value csendben 0 lenne. Hibás configgal nem generálunk félrevezető XML-t, hanem jelezzük a hibát.
|
||||
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
|
||||
{
|
||||
Index = 1,
|
||||
Operation = OperationType.Create,
|
||||
TradeCard = _mapper.MapDocument(document, _company),
|
||||
TradeCard = _mapper.MapDocument(document, _settings.Company),
|
||||
};
|
||||
|
||||
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(operation.TradeCard);
|
||||
// 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.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
|
|
@ -37,6 +38,10 @@ public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo
|
|||
[ToonDescription(Purpose = "The generated NAV EKÁER tradeCard request XML exactly as it was (or will be) submitted — audit copy and source of the read-only detail view. Null until the first Generate.")]
|
||||
public string? XmlDoc { get; set; }
|
||||
|
||||
[Column(DataType = DataType.DecFloat)]
|
||||
[ToonDescription(Purpose = "The conversion rate actually applied when computing this declaration's item values to HUF: 1 for domestic (HUF) suppliers (no conversion), the FX rate (e.g. EUR→HUF) for foreign suppliers. Null before the first Generate. Currency-agnostic by design — works for any source currency. Audit trail: the invoice amount times this rate yields the HUF value in the tradeCard; the NAV schema has no rate field, so this column preserves how the value was derived.")]
|
||||
public double? ConversionRate { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "The EKÁER number (TCN) assigned by NAV after a successful submission. Null until the declaration is accepted.")]
|
||||
public string? EkaerNumber { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ namespace FruitBank.Common.Entities;
|
|||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
|
||||
public sealed class Partner : PartnerBase, IPartner
|
||||
{
|
||||
[ToonDescription(Purpose = "Whether shipments from this supplier are subject to NAV EKÁER reporting. Default true; set false for partners that do not require EKÁER (e.g. multiple companies sharing a wholesale-market address where no road transport between them occurs).")]
|
||||
public bool IsEkaer { get; set; } = true;
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(PartnerDepot.PartnerId), CanBeNull = true)]
|
||||
public List<PartnerDepot>? PartnerDepots { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ public interface ICargoPartner : IPartnerBase
|
|||
|
||||
public interface IPartner : IPartnerBase
|
||||
{
|
||||
bool IsEkaer { get; set; }
|
||||
List<PartnerDepot>? PartnerDepots { get; set; }
|
||||
List<ShippingDocument>? ShippingDocuments { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using AyCode.Services.Nav.Ekaer;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// A teljes EKÁER konfiguráció egy helyen (configból, <c>appsettings.json</c> „Ekaer" szekció): a bejelentő
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class EkaerSettings
|
||||
{
|
||||
/// <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();
|
||||
|
||||
/// <summary>EUR→HUF átváltási árfolyam a tétel-érték HUF-ra számításához. Forrás: MNB napi középárfolyam
|
||||
/// (a NAV nem közöl külön EKÁER-árfolyamot). SZÁNDÉKOSAN nincs default: ha a config nem töltődik be, 0 marad,
|
||||
/// és a külföldi érték-átváltás hibát dob (<see cref="EkaerValueCalculator.ResolveRateToHuf"/>) — így elavult /
|
||||
/// beégetett árfolyammal soha nem számolunk.</summary>
|
||||
public double EurHufRate { get; set; }
|
||||
|
||||
/// <summary>Tömeg-küszöb kg-ban: e felett (vagy az érték-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 200 kg.
|
||||
/// 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>
|
||||
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.
|
||||
/// 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>
|
||||
public long ThresholdValueHuf { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Services.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// A tétel-érték HUF-ra számítása — közös a leképezés (tradeCardItem.value) és a küszöb-kapu
|
||||
/// (<c>CreateMissingEkaerHistories</c>) között, hogy a kétféle számítás ne csúszhasson el egymástól.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A forrás-pénznem a <c>Partner.Currency</c> (ISO 4217). Jelenleg csak HUF és EUR van használatban
|
||||
/// (100%-ban a partnerből következik): HUF → nincs átváltás, minden más → EUR-ként, az árfolyammal.
|
||||
/// Ha valaha más deviza is megjelenik, ezt a feltevést bővíteni kell (akkor per-deviza árfolyam kell).
|
||||
/// </remarks>
|
||||
public static class EkaerValueCalculator
|
||||
{
|
||||
public static bool IsHuf(string? currency) => string.Equals(currency?.Trim(), "HUF", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>A forrás-pénznem → HUF szorzó. HUF → 1 (árfolyam nem kell). Minden más (EUR) → a megadott
|
||||
/// EUR-HUF árfolyam; ha az hiányzik/0, <see cref="InvalidOperationException"/> — soha nem számolunk elavult /
|
||||
/// üres árfolyammal (a config-default szándékosan nincs).</summary>
|
||||
public static double ResolveRateToHuf(string? currency, double eurHufRate)
|
||||
{
|
||||
if (IsHuf(currency)) return 1d;
|
||||
if (eurHufRate <= 0)
|
||||
throw new InvalidOperationException(
|
||||
"EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — külföldi (nem HUF) értéket nem lehet HUF-ra számolni.");
|
||||
return eurHufRate;
|
||||
}
|
||||
|
||||
/// <summary>Egy tétel értéke a számla pénznemében (egységár × dokumentum-mennyiség), átváltás előtt.</summary>
|
||||
public static double ItemLineValue(ShippingItem item) => item.UnitPriceOnDocument * item.QuantityOnDocument;
|
||||
|
||||
/// <summary>Egy tétel értéke HUF-ban, egészre kerekítve. A NAV <c>value > 0</c>-t vár — 0/ismeretlen
|
||||
/// esetén <c>null</c> (a mezőt nem töltjük, a validátor jelzi, ha kötelező lenne).</summary>
|
||||
public static long? ItemValueHuf(ShippingItem item, double rateToHuf)
|
||||
{
|
||||
var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero);
|
||||
return huf > 0 ? (long)huf : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,11 @@ 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>
|
||||
private const string HomeCountry = "HU";
|
||||
|
||||
private readonly EkaerSettings _settings;
|
||||
|
||||
public ShippingToEkaerMapper(EkaerSettings settings)
|
||||
=> _settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
|
||||
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(shipping);
|
||||
|
|
@ -31,11 +36,12 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
foreach (var document in shipping.ShippingDocuments ?? [])
|
||||
{
|
||||
index++;
|
||||
var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate);
|
||||
operations.Add(new TradeCardOperationType
|
||||
{
|
||||
Index = index,
|
||||
Operation = operation,
|
||||
TradeCard = BuildTradeCard(shipping, document, company),
|
||||
TradeCard = BuildTradeCard(shipping, document, company, rateToHuf),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -47,10 +53,11 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
ArgumentNullException.ThrowIfNull(document);
|
||||
ArgumentNullException.ThrowIfNull(company);
|
||||
|
||||
return BuildTradeCard(document.Shipping, document, company);
|
||||
var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate);
|
||||
return BuildTradeCard(document.Shipping, document, company, rateToHuf);
|
||||
}
|
||||
|
||||
private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company)
|
||||
private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company, double rateToHuf)
|
||||
{
|
||||
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase
|
||||
|
||||
|
|
@ -83,7 +90,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
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));
|
||||
foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item, rateToHuf));
|
||||
return tradeCard;
|
||||
}
|
||||
|
||||
|
|
@ -96,15 +103,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
? TradeType.D
|
||||
: TradeType.I;
|
||||
|
||||
private static TradeCardItemType BuildItem(ShippingItem item) => new()
|
||||
private static TradeCardItemType BuildItem(ShippingItem item, double rateToHuf) => 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 (FBANKAPP-DMODEL-I-P6X4)
|
||||
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 (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3, #10.
|
||||
Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // beszerzési érték HUF-ban (Partner.Currency → árfolyam); 0/ismeretlen → null
|
||||
};
|
||||
|
||||
private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new()
|
||||
|
|
@ -163,6 +170,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
|
|||
return Truncate(EmptyToNull(cleaned), 10);
|
||||
}
|
||||
|
||||
/// <summary>VTSZ normalizálása. Pattern: <c>[0-9]{4,8}</c> — a forrásban tagolt formátum is lehet
|
||||
/// („0805 10 00", „0805.10.00"), ezért csak a számjegyek maradnak; a 4-nél rövidebbet a validátor jelzi.</summary>
|
||||
private static string? NormalizeVtsz(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var cleaned = new string([.. value.Where(char.IsAsciiDigit)]);
|
||||
return Truncate(EmptyToNull(cleaned), 8);
|
||||
}
|
||||
|
||||
private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value;
|
||||
|
||||
private static string? Truncate(string? value, int maxLen)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace FruitBankHybrid.Shared.Tests.Ekaer;
|
|||
[TestClass]
|
||||
public sealed class ShippingToEkaerMapperTests
|
||||
{
|
||||
private static readonly ShippingToEkaerMapper Mapper = new();
|
||||
private static readonly ShippingToEkaerMapper Mapper = new(new EkaerSettings());
|
||||
|
||||
// ---- Helpers ------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.Status)" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.EkaerNumber)" Caption="EKÁER szám" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.SentDate)" DisplayFormat="yyyy.MM.dd HH:mm" />
|
||||
@* Audit: a value-számításhoz alkalmazott árfolyam (HUF feladónál 1). ReadOnly — generáláskor töltődik. *@
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ConversionRate)" Caption="Árfolyam" ReadOnly="true" DisplayFormat="0.00##" />
|
||||
<DxGridDataColumn FieldName="@nameof(EkaerHistory.ErrorText)" Caption="Hiba" ReadOnly="true">
|
||||
<CellDisplayTemplate>
|
||||
@if (!string.IsNullOrWhiteSpace(context.DisplayText))
|
||||
|
|
@ -97,7 +99,9 @@
|
|||
<ToolbarItemsExtended>
|
||||
@* A dátumválasztó editor csak Template-ben mehet toolbarba; a gomb natív DxToolbarItem,
|
||||
hogy a toolbar stílusát/igazítását kapja (lásd: "Ai process..." minta a GridShippingDocument-ben). *@
|
||||
<DxToolbarItem BeginGroup="true" CssClass="ekaer-create-from-date-item">
|
||||
@* dxbl-toolbar-edit: a DevExpress téma saját opt-in osztálya editor-t hordozó toolbar-itemre —
|
||||
ettől kapja a beépített középre igazítást és a toolbarba simuló editor-stílust. *@
|
||||
<DxToolbarItem BeginGroup="true" CssClass="dxbl-toolbar-edit ekaer-create-from-date-item">
|
||||
<Template Context="toolbarItemContext">
|
||||
<DxDateEdit @bind-Date="CreateFromDate"
|
||||
Format="yyyy.MM.dd"
|
||||
|
|
@ -194,6 +198,7 @@
|
|||
// A sor frissítése helyben — a grid ugyanazt a példányt mutatja.
|
||||
ekaerHistory.Status = updated.Status;
|
||||
ekaerHistory.XmlDoc = updated.XmlDoc;
|
||||
ekaerHistory.ConversionRate = updated.ConversionRate;
|
||||
ekaerHistory.ErrorText = updated.ErrorText;
|
||||
ekaerHistory.EkaerNumber = updated.EkaerNumber;
|
||||
ekaerHistory.SentDate = updated.SentDate;
|
||||
|
|
|
|||
|
|
@ -72,10 +72,11 @@ h1:focus {
|
|||
width: 9rem;
|
||||
}
|
||||
|
||||
/* A Template-es toolbar-elem konténere alapból teljes magasságra nyúlik, és az editor felülre csúszik —
|
||||
középre igazítjuk, hogy egy vonalban legyen a többi (natív) toolbar-gombbal. */
|
||||
.ekaer-create-from-date-item .dxbl-toolbar-item-content,
|
||||
.ekaer-create-from-date-item .dxbl-toolbar-item-template {
|
||||
/* A Template-es toolbar-item tartalma alapból felülre csúszik. Az elsődleges igazítást a DevExpress saját
|
||||
dxbl-toolbar-edit osztálya adja (a téma-CSS szállítja, a CssClass-ban kapja meg az item) — ez itt a
|
||||
fallback, ami a markup-szerkezettől függetlenül középre igazít. */
|
||||
.ekaer-create-from-date-item,
|
||||
.ekaer-create-from-date-item > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
|
|
|||
Loading…
Reference in New Issue