Refactor EKÁER mapping: unify company info, doc updates

- Introduced ICompanyInfoBase for standardized company/partner data; refactored PartnerBase and interfaces to implement it
- Replaced EkaerMappingOptions with EkaerCompanyInfo; updated all usages, constructors, and tests
- Refactored EKÁER mapping logic to use ICompanyInfoBase; improved normalization and address handling
- Added regex/validation for plate numbers and country codes; new error codes
- Added Currency to PartnerBase; updated grids to display it
- Updated ProductDto doc for GTIN/VTSZ data model issue
- Enabled validation in CargoTruck grid
- Added DMODEL topic docs: TOPIC_CODES.md, DATAMODEL_ISSUES.md, README.md
- Removed obsolete files and updated settings.local.json
- General code and doc improvements for maintainability
This commit is contained in:
Loretta 2026-06-03 16:58:47 +02:00
parent 2fb1151e5d
commit 0a1287ce67
16 changed files with 190 additions and 138 deletions

View File

@ -52,7 +52,9 @@
"Bash(find \"H:\\\\Applications\\\\FruitBankHybridApp\\\\FruitBank.Common\\\\Source\" -type f -name \"*Shipping*.cs\" 2>/dev/null | head -10)",
"Bash(find \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Services\" -type d | head -20)",
"Bash(rm -f H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/EkaerMappingOptions.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/IShippingToEkaerMapper.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/ShippingToEkaerMapper.cs)",
"Bash(rmdir \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer\")"
"Bash(rmdir \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer\")",
"Bash(rm -f \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Infrastructure/NavCredentials.cs\" *)",
"Bash(rm -f \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Entities/IPostalParty.cs\" *)"
]
}
}

13
.github/TOPIC_CODES.md vendored Normal file
View File

@ -0,0 +1,13 @@
# Topic Codes — FruitBankHybridApp (`FBANKAPP`)
Per-repo topic registry for the FruitBankHybridApp product, per the **per-repo extension convention** in `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`. Lists ONLY this repo's own topic codes; inherited (ACCORE / AyCode.Blazor / Mango.Nop) topics are reached through `@repo.own-dep-repos`.
**Foundational conventions are defined once at the framework layer — not restated here:**
- ID format, type codes (`I`/`T`/`B`/`C`), ID rules, Status vocabulary, archival, registry-maintenance workflow → `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`
- `<PREFIX>` (this repo = `FBANKAPP`, from the `@repo.prefix` in `.github/copilot-instructions.md`) + `<RAND>` spec → `AyCode.Core/.github/REPO_PREFIXES.md`
## This repo's own topic codes
| Code | Topic | Scope | Docs location |
|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|
| `DMODEL` | DATA-MODEL | FruitBank entitások adatmodell-normalizálása: nopCommerce referencia-FK-k szabad string helyett (Country/Currency), azonosító-szétválasztások (GTIN/VTSZ), és hasonló átmeneti adatmodell-megoldások. **Általános** — nem funkció-specifikus (pl. nem EKÁER). | `FruitBank.Common/docs/DATAMODEL/` |

View File

@ -8,20 +8,20 @@ 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 <see cref="EkaerMappingOptions"/>-t a DI szolgáltatja.
/// <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.
/// </remarks>
public sealed class FruitBankEkaerService : IFruitBankEkaerService
{
private readonly IShippingToEkaerMapper _mapper;
private readonly IEkaerSubmitService _submitService;
private readonly EkaerMappingOptions _options;
private readonly EkaerCompanyInfo _company;
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerMappingOptions options)
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerCompanyInfo company)
{
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
_options = options ?? throw new ArgumentNullException(nameof(options));
_company = company ?? throw new ArgumentNullException(nameof(company));
}
public Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default)
@ -29,7 +29,7 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService
ArgumentNullException.ThrowIfNull(shipping);
// map (FruitBank.Common) → submit: validate → send (AyCode.Services)
var operations = _mapper.MapShipping(shipping, _options, operation);
var operations = _mapper.MapShipping(shipping, _company, operation);
return _submitService.SubmitAsync(operations, cancellationToken);
}
}

View File

@ -34,7 +34,7 @@ public class ProductDto : MgProductDto, IProductDto
/// A nopCommerce <c>Product.Gtin</c> oszlopa. <b>Átmenetileg az EKÁER VTSZ-t (vámtarifaszámot) tárolja</b> —
/// a jövőbeli <c>ShippingToEkaerMapper</c> innen olvassa a <c>tradeCardItem.productVtsz</c> értékét.
/// ⚠️ A GTIN ≠ VTSZ (a GTIN globális kereskedelmi cikkszám, a VTSZ vámtarifaszám). Külön mezőbe választandó —
/// lásd <c>Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md#mgfbankplug-ekaer-i-t3x8</c>.
/// lásd <c>FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md#fbankapp-dmodel-i-p6x4</c>.
/// </summary>
[LinqToDB.Mapping.Column(nameof(Product.Gtin))]
[ToonDescription(Purpose = "nopCommerce Gtin column — holds the EKÁER VTSZ (customs tariff number) used as the trade-card item productVtsz in NAV road-freight reporting.")]

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Core.Consts;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
@ -17,10 +19,14 @@ public sealed class CargoTruck: MgEntityBase, ICargoTruck
[ToonDescription(Purpose = "FK to the owning transport company (CargoPartner) — the carrier, not the goods supplier.")]
public int CargoPartnerId { get; set; }
[Association(ThisKey = nameof(CargoPartnerId), OtherKey = nameof(CargoPartner.Id), CanBeNull = true)]
[LinqToDB.Mapping.Association(ThisKey = nameof(CargoPartnerId), OtherKey = nameof(CargoPartner.Id), CanBeNull = true)]
public CargoPartner CargoPartner { get; set; }
[RegularExpression(AcRegExpression.CountryCodeMax3Mask, ErrorMessage = "A jármű országkódja 13 nagybetű (pl. HU).")]
public string CountryCode { get; set; }
[Required(ErrorMessage = "A rendszám kötelező.")]
[RegularExpression(AcRegExpression.PlateNumberMask, ErrorMessage = "A rendszám csak nagybetű és szám lehet, kötőjel/szóköz nélkül (pl. ABC123).")]
public string LicencePlate { get; set; }
[ToonDescription(Purpose = "Discriminates the shared table: false = tractor/truck unit, true = trailer.")]

View File

@ -1,6 +1,11 @@
using FruitBank.Common.Interfaces;
using AyCode.Core.Serializers.Toons;
using AyCode.Entities;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations.Schema;
using AyCode.Core.Interfaces;
namespace FruitBank.Common.Entities;
@ -11,6 +16,9 @@ public abstract class PartnerBase : MgEntityBase, IPartnerBase
public string CertificationNumber { get; set; }
public string CountryCode { get; set; }
[ToonDescription(Purpose = "ISO 4217 currency code the company trades and settles in with this partner (e.g. EUR, HUF). For supplier partners it is the source currency for converting shipping-item values to example: HUF in NAV EKÁER reporting.")]
public string Currency { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string State { get; set; }
@ -18,6 +26,10 @@ public abstract class PartnerBase : MgEntityBase, IPartnerBase
public string City { get; set; }
public string Street { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "The PostalCode + City + Street joined into a single-line postal address (non-empty parts).")]
public string? FullAddress => this.ComposeFullAddress();
[SkipValuesOnUpdate]
public DateTime Created { get; set; }

View File

@ -1,4 +1,6 @@
using AyCode.Interfaces.Entities;
using AyCode.Core.Interfaces;
using AyCode.Entities;
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.TimeStampInfo;
using FruitBank.Common.Entities;
@ -14,16 +16,11 @@ public interface IPartner : IPartnerBase
List<ShippingDocument>? ShippingDocuments { get; set; }
}
public interface IPartnerBase : IEntityInt, ITimeStampInfo
public interface IPartnerBase : ICompanyInfoBase, IEntityInt, ITimeStampInfo
{
string Name { get; set; }
string TaxId { get; set; }
string CertificationNumber { get; set; }
string CountryCode { get; set; }
string PostalCode { get; set; }
string Currency { get; set; }
string Country { get; set; }
string State { get; set; }
string County { get; set; }
string City { get; set; }
string Street { get; set; }
}

View File

@ -1,36 +0,0 @@
using AyCode.Services.Nav.Ekaer.Models;
namespace FruitBank.Common.Services.Ekaer;
/// <summary>
/// A <see cref="ShippingToEkaerMapper"/> konfiguráció-függő bemenetei, amelyek NEM a <c>Shipping</c>-ből
/// származnak: a bejelentő (FruitBank) mint címzett/destination cégadatai, a lerakodási hely (saját raktár),
/// és a saját országkód a tradeType irány meghatározásához.
/// </summary>
/// <remarks>Ezek később a szerver-oldali plugin beállításaiból (settings) töltődnek — lásd a plugin <c>docs/EKAER/EKAER_TODO.md</c> #2.</remarks>
public sealed class EkaerMappingOptions
{
/// <summary>A bejelentő (FruitBank) neve — a tradeCard <c>destinationName</c>-je bejövő relációban.</summary>
public string? DestinationName { get; set; }
/// <summary>A bejelentő adószáma (<c>destinationVatNumber</c>). Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
public string? DestinationVatNumber { get; set; }
/// <summary>A bejelentő országkódja (2 betű). Alapértelmezés: <c>HU</c>.</summary>
public string DestinationCountryCode { get; set; } = "HU";
/// <summary>A bejelentő címe egybeírva (<c>destinationAddress</c>, max 200).</summary>
public string? DestinationAddress { get; set; }
/// <summary>
/// A lerakodási / raktározási hely (a saját raktár). Magyar cím esetén a Name/VatNumber/Phone/Email
/// kitöltése kötelező — ezért kész <see cref="LocationType"/>-ként adjuk át (a plugin-konfigból összeállítva).
/// </summary>
public LocationType? UnloadLocation { get; set; }
/// <summary>
/// A bejelentő saját országkódja a tradeType meghatározásához: ha a feladó (beszállító) országa
/// ezzel egyezik → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import). Alapértelmezés: <c>HU</c>.
/// </summary>
public string HomeCountryCode { get; set; } = "HU";
}

View File

@ -1,3 +1,4 @@
using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Entities;
@ -8,19 +9,17 @@ namespace FruitBank.Common.Services.Ekaer;
/// EKÁER tradeCard műveleteket állít elő (dokumentumonként egyet).
/// </summary>
/// <remarks>
/// A NAV-protokollt és az authentikációt NEM kezeli — az az <c>AyCode.Services.Nav</c> réteg
/// (<c>NavReportServiceBase</c> / <c>EkaerManageService</c>) felelőssége. A tényleges NAV-bejelentést
/// a szerver-oldali (nopCommerce plugin) service végzi, amely ezt a leképezőt használja. A leképezés
/// tisztázott pontjai és nyitott döntései a plugin <c>docs/EKAER/README.md</c> és <c>EKAER_TODO.md</c> fájljaiban.
/// A NAV-protokollt és az authentikációt NEM kezeli — az az <c>AyCode.Services.Nav</c> réteg felelőssége.
/// A feladót (beszállító <c>Partner</c>) és a saját céget (<see cref="EkaerCompanyInfo"/>) egységesen,
/// <see cref="AyCode.Entities.ICompanyInfoBase"/>-ként kezeli.
/// </remarks>
public interface IShippingToEkaerMapper
{
/// <summary>
/// Leképezi a <paramref name="shipping"/> minden <c>ShippingDocument</c>-jét egy-egy EKÁER tradeCard műveletre.
/// </summary>
/// <param name="shipping">A bejövő szállítmány. A fuvarozó/jármű a Shipping szintjén, az eladó/tételek a dokumentum szintjén élnek.</param>
/// <param name="options">A konfiguráció-függő adatok (destination cég, lerakodási hely, home country).</param>
/// <param name="shipping">A bejövő szállítmány (fuvarozó/jármű a Shipping szintjén, eladó/tételek a dokumentum szintjén).</param>
/// <param name="company">A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely.</param>
/// <param name="operation">A tradeCard művelet típusa. Alapértelmezés: <see cref="OperationType.Create"/>.</param>
/// <returns>Dokumentumonként egy <see cref="TradeCardOperationType"/>, a beérkezés sorrendjében indexelve.</returns>
IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create);
IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create);
}

View File

@ -1,3 +1,6 @@
using AyCode.Core.Interfaces;
using AyCode.Entities;
using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Entities;
using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType;
@ -7,15 +10,19 @@ namespace FruitBank.Common.Services.Ekaer;
/// <inheritdoc cref="IShippingToEkaerMapper"/>
/// <remarks>
/// Tiszta (állapotmentes) leképező. A <c>TradeType</c>/<c>TradeReasonType</c> enumokat aliasszal hozzuk be,
/// hogy elkerüljük a <c>Models.Common.TradeCardType</c> enum és a <c>Models.TradeCardType</c> osztály névütközését.
/// 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.
/// </remarks>
public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
{
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create)
/// <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";
public IReadOnlyList<TradeCardOperationType> MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create)
{
ArgumentNullException.ThrowIfNull(shipping);
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(company);
var operations = new List<TradeCardOperationType>();
var index = 0;
@ -28,43 +35,43 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
{
Index = index,
Operation = operation,
TradeCard = BuildTradeCard(shipping, document, options),
TradeCard = BuildTradeCard(shipping, document, company),
});
}
return operations;
}
private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerMappingOptions options)
private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerCompanyInfo company)
{
var seller = document.Partner; // a beszállító (feladó)
var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase
var tradeCard = new TradeCardType
{
TradeType = ResolveTradeType(seller, options),
ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthatja a bejelentést
TradeType = ResolveTradeType(seller),
ModByCarrierEnabled = false, // mi jelentünk; a fuvarozó alapból nem módosíthat
// Feladó / eladó = a beszállító (ShippingDocument.Partner)
// Feladó / eladó = a beszállító
SellerName = seller?.Name,
SellerVatNumber = NormalizeVatNumber(seller?.TaxId),
SellerCountry = NormalizeCountryCode(seller?.CountryCode, 2),
SellerAddress = BuildAddress(seller),
SellerAddress = Truncate(seller?.FullAddress, 200),
// Címzett = a bejelentő (FruitBank) — bejövő relációban (lásd EKAER_TODO #1)
DestinationName = options.DestinationName,
DestinationVatNumber = NormalizeVatNumber(options.DestinationVatNumber),
DestinationCountry = NormalizeCountryCode(options.DestinationCountryCode, 2),
DestinationAddress = options.DestinationAddress,
// 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),
// Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító (Carrier) nincs, csak szöveges név.
// Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név.
CarrierText = shipping.CargoPartner?.Name,
// Lerakodási hely = saját raktár (konfigból); felrakodás = a beszállító telephelye.
UnloadLocation = options.UnloadLocation,
// Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye.
UnloadLocation = company.UnloadLocation,
LoadLocation = BuildLoadLocation(seller),
};
// Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2), saját rendszámmal/országgal.
// 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);
@ -73,11 +80,11 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
}
/// <summary>
/// Bejövő (beszerzés) reláció: belföldi feladó → <c>D</c> (belföld-belföld), egyébként → <c>I</c> (import,
/// közösségből belföldre). Az export (<c>E</c>) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7).
/// 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(Partner? seller, EkaerMappingOptions options)
=> string.Equals(seller?.CountryCode, options.HomeCountryCode, StringComparison.OrdinalIgnoreCase)
private static TradeType ResolveTradeType(ICompanyInfoBase? seller)
=> string.Equals(seller?.CountryCode, HomeCountry, StringComparison.OrdinalIgnoreCase)
? TradeType.D
: TradeType.I;
@ -86,10 +93,10 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
ItemExternalId = item.Id.ToString(),
// Bejövő áru = beszerzés → A. (Enum: S=értékesítés, A=beszerzés, W=bérmunka, O=egyéb.) Lásd EKAER_TODO #9.
TradeReason = TradeReasonType.A,
ProductVtsz = item.ProductDto?.Gtin, // VTSZ — átmenetileg a Gtin oszlopban (MGFBANKPLUG-EKAER-I-T3X8)
ProductVtsz = 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.
// Value (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3, #10.
};
private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new()
@ -99,13 +106,12 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
};
/// <summary>
/// Felrakodási hely a beszállító címéből. (Magyar feladónál a NAV kötelezővé teszi a Phone/Email-t,
/// ami a <c>Partner</c> entitásban nincs — lásd EKAER_TODO #6.)
/// 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(Partner? seller)
private static LocationType? BuildLoadLocation(ICompanyInfoBase? seller)
{
if (seller == null) return null;
if (seller is null) return null;
return new LocationType
{
Name = seller.Name,
@ -117,21 +123,11 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
};
}
/// <summary>Egybeírt cím (<c>sellerAddress</c>/<c>destinationAddress</c>, max 200): "1234 Budapest Fő utca 1.".</summary>
private static string? BuildAddress(PartnerBase? partner)
{
if (partner == null) return null;
var parts = new[] { partner.PostalCode, partner.City, partner.Street }.Where(p => !string.IsNullOrWhiteSpace(p));
var address = string.Join(" ", parts).Trim(); return Truncate(string.IsNullOrWhiteSpace(address) ? null : address, 200);
}
/// <summary>Adószám normalizálása. Pattern: <c>[0-9A-Z-]{1,15}</c>.</summary>
private static string? NormalizeVatNumber(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var cleaned = new string(value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c == '-').ToArray());
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c == '-')]);
return Truncate(EmptyToNull(cleaned), 15);
}
@ -139,16 +135,14 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
private static string? NormalizeCountryCode(string? value, int maxLen)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var cleaned = new string([.. value.ToUpperInvariant().Where(char.IsAsciiLetter)]);
return Truncate(EmptyToNull(cleaned), maxLen);
}
/// <summary>Rendszám normalizálása. Pattern: <c>[A-Z0-9ÖŐÜŰ]{4,15}</c> — a kötőjel/szóköz NEM engedett, ezért eltávolítjuk.</summary>
/// <summary>Rendszám normalizálása. Pattern: <c>[A-Z0-9ÖŐÜŰ]{4,15}</c> — kötőjel/szóköz NEM engedett.</summary>
private static string? NormalizePlateNumber(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is 'Ö' or 'Ő' or 'Ü' or 'Ű')]);
return Truncate(EmptyToNull(cleaned), 15);
}
@ -157,12 +151,12 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper
private static string? NormalizeZipCode(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var cleaned = new string([.. value.ToUpperInvariant().Where(c => char.IsAsciiLetterOrDigit(c) || c is ' ' or '-')]);
return Truncate(EmptyToNull(cleaned), 10);
}
private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value;
private static string? Truncate(string? value, int maxLen) => value == null ? null : value.Length <= maxLen ? value : value[..maxLen];
private static string? Truncate(string? value, int maxLen)
=> value is null ? null : value.Length <= maxLen ? value : value[..maxLen];
}

View File

@ -0,0 +1,48 @@
# DATA-MODEL — Known Issues
> Companion to [`README.md`](README.md). Topic `DMODEL`, prefix `FBANKAPP` → entry IDs `FBANKAPP-DMODEL-I-<RAND>` (issue) / `-B-` (bug).
> ID format, Status vocabulary, type codes, archival → `../../../.github/TOPIC_CODES.md` (→ framework registry).
Scope: a FruitBank entitások adatmodell-normalizálási teendői (nopCommerce referencia-FK-k, azonosító-szétválasztások).
## Active entries
## FBANKAPP-DMODEL-I-K3D9: A Partner-entitások nopCommerce referencia-mezői szabad string-ek, FK helyett
**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell / normalizálás)
A `PartnerBase` (és így `Partner` / `CargoPartner`) + a `CargoTruck` a nopCommerce referencia-adatait **szabad string-ként** tárolja, ahelyett hogy a megfelelő nopCommerce tábla **Id-jára FK-zna**:
| Mező | Jelenlegi | Helyes (hosszú táv) |
|---|---|---|
| `CountryCode` (string) | szabad szöveg (pl. `"HU"`) | nopCommerce **`Country.Id`** FK; a megjelenített kód a `Country.TwoLetterIsoCode`-ból — `PartnerBase` + `CargoTruck` |
| `Currency` (string) | szabad szöveg (pl. `"EUR"`) | nopCommerce **`Currency.Id`** FK; a megjelenített kód a `Currency.CurrencyCode`-ból — `PartnerBase` |
**Hatás:** működik (a string-eket kézzel töltjük), de **nincs hivatkozás-integritás**, és a kódok elgépelhetők / inkonzisztensek lehetnek. Az EKÁER-leképezés a string pontosságára támaszkodik (`seller`/`destination`/`vehicle` `Country`, és a value→HUF deviza forrás-pénzneme).
**Javítási irány:** FK-oszlop (`CountryId`, `CurrencyId`) + navigation a nopCommerce táblákra; a megjelenített/exportált kód a referencia-entitásból.
**Affected:**
- `FruitBank.Common/Entities/PartnerBase.cs``CountryCode`, `Currency`
- `FruitBank.Common/Entities/CargoTruck.cs``CountryCode`
- felhasználó: `FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs` (a `*Country` + a deviza-konverzió pontossága)
## FBANKAPP-DMODEL-I-P6X4: A `Product.Gtin` átmenetileg a VTSZ-t tárolja — szétválasztandó
**Status:** Open · **Priority:** P3 · **Type:** I (adatmodell / átmeneti megoldás)
> **Supersedes** `MGFBANKPLUG-EKAER-I-T3X8` — áthelyezve az EKÁER-topicból ide, mert a GTIN/VTSZ szétválasztás **általános** adatmodell-kérdés (a GTIN globális termékazonosító, a VTSZ vámtarifaszám), nem EKÁER-specifikus.
Az EKÁER `tradeCardItem.productVtsz` (kötelező, 8 jegyű vámtarifaszám) forrása jelenleg a nopCommerce **`Product.Gtin`** oszlop (a `ProductDto.Gtin`-en keresztül). A GTIN és a VTSZ **fogalmilag különböző**:
- **GTIN** — globális kereskedelmi cikkszám (vonalkód-azonosító, EAN/UPC).
- **VTSZ** — vámtarifaszám (a termék vám-/statisztikai besorolása).
Egy termékhez a kettő nem azonos; a `Gtin` oszlop VTSZ-ként való használata **átmeneti** megoldás az EKÁER-integráció beindításához.
**Hatás:** jelenleg nincs üzemszerű gond (a `Gtin` mező szabad, és a VTSZ-t tölthetjük bele). Hosszú távon viszont, ha a valódi GTIN-re is szükség lesz, a kettő ütközik.
**Javítási irány:** külön `Vtsz` mező/`GenericAttribute` a `Product`-on, és a `ShippingToEkaerMapper` onnan olvasson — a `Gtin` maradjon a valódi GTIN.
**Affected:**
- `FruitBank.Common/Dtos/ProductDto.cs``Gtin` property (a `[Column(nameof(Product.Gtin))]` jelöléssel, summary-ban megjelölve)
- `FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs` → a `productVtsz` forrása

View File

@ -0,0 +1,12 @@
# DATA-MODEL — FruitBank adatmodell-normalizálás
Topic `DMODEL`, prefix `FBANKAPP` → entry ID-k `FBANKAPP-DMODEL-I-<RAND>` (issue) / `-T-` (TODO) / `-B-` (bug).
ID-formátum, Status, type-kódok, archiválás → `../../../.github/TOPIC_CODES.md` (→ framework registry).
A FruitBank entitások (`Partner`, `CargoPartner`, `CargoTruck`, `ProductDto`) **adatmodell-átmenetiségei**: olyan mezők, amelyek hosszú távon a nopCommerce referencia-tábláira FK-znának, vagy külön mezőbe válnának — de jelenleg szabad string / átmeneti megoldás.
> Ezek **általános** adatmodell-kérdések, NEM funkció-specifikusak. Az EKÁER-, pre-order- stb. doksik csak **hivatkoznak** ide (nem duplikálják).
## Companion fájlok
- [`DATAMODEL_ISSUES.md`](DATAMODEL_ISSUES.md) — az aktív adatmodell-issue-k.

View File

@ -1,3 +1,4 @@
using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
@ -58,13 +59,14 @@ public sealed class ShippingToEkaerMapperTests
return shipping;
}
private static EkaerMappingOptions CreateOptions() => new()
private static EkaerCompanyInfo CreateCompany() => new()
{
DestinationName = "FruitBank Kft",
DestinationVatNumber = "98765432-2-41",
DestinationCountryCode = "HU",
DestinationAddress = "1102 Budapest Raktár utca 5",
HomeCountryCode = "HU",
Name = "FruitBank Kft",
TaxId = "98765432-2-41",
CountryCode = "HU",
PostalCode = "1102",
City = "Budapest",
Street = "Raktar utca 5",
UnloadLocation = new LocationType
{
Name = "FruitBank Raktár",
@ -72,7 +74,7 @@ public sealed class ShippingToEkaerMapperTests
Country = "HU",
ZipCode = "1102",
City = "Budapest",
Street = "Raktár utca",
Street = "Raktar utca",
StreetNumber = "5",
},
};
@ -90,7 +92,7 @@ public sealed class ShippingToEkaerMapperTests
ShippingItems = [],
});
var ops = Mapper.MapShipping(shipping, CreateOptions());
var ops = Mapper.MapShipping(shipping, CreateCompany());
Assert.AreEqual(2, ops.Count, "dokumentumonként egy tradeCard");
Assert.AreEqual(1, ops[0].Index);
@ -101,14 +103,14 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_NullDocuments_ReturnsEmpty()
{
var ops = Mapper.MapShipping(new Shipping { ShippingDocuments = null }, CreateOptions());
var ops = Mapper.MapShipping(new Shipping { ShippingDocuments = null }, CreateCompany());
Assert.AreEqual(0, ops.Count);
}
[TestMethod]
public void MapShipping_HonorsExplicitOperation()
{
var ops = Mapper.MapShipping(CreateShipping(), CreateOptions(), OperationType.Modify);
var ops = Mapper.MapShipping(CreateShipping(), CreateCompany(), OperationType.Modify);
Assert.AreEqual(OperationType.Modify, ops[0].Operation);
}
@ -117,15 +119,15 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_DomesticSeller_TradeTypeDomestic()
{
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "HU"), CreateOptions());
Assert.AreEqual(TradeType.D, ops[0].TradeCard.TradeType, "belföldi feladó → D");
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "HU"), CreateCompany());
Assert.AreEqual(TradeType.D, ops[0].TradeCard.TradeType, "belföldi (HU) feladó → D");
}
[TestMethod]
public void MapShipping_ForeignSeller_TradeTypeImport()
{
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "DE"), CreateOptions());
Assert.AreEqual(TradeType.I, ops[0].TradeCard.TradeType, "külföldi feladó → I (import)");
var ops = Mapper.MapShipping(CreateShipping(sellerCountry: "DE"), CreateCompany());
Assert.AreEqual(TradeType.I, ops[0].TradeCard.TradeType, "nem-HU feladó → I (import) — a NAV EKÁER magyar");
}
// ---- Tétel-leképezés ----------------------------------------------------
@ -133,7 +135,7 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_MapsItemFields()
{
var item = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard.Items[0];
var item = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard.Items[0];
Assert.AreEqual("08081010", item.ProductVtsz, "productVtsz = ProductDto.Gtin");
Assert.AreEqual("Alma", item.ProductName);
@ -147,29 +149,30 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_MapsSellerFromPartner()
{
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard;
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard;
Assert.AreEqual("Beszállító Kft", tradeCard.SellerName);
Assert.AreEqual("12345678-2-42", tradeCard.SellerVatNumber);
Assert.AreEqual("HU", tradeCard.SellerCountry);
StringAssert.Contains(tradeCard.SellerAddress, "Budapest");
StringAssert.Contains(tradeCard.SellerAddress, "Budapest", "a sellerAddress a Partner FullAddress-éből jön");
}
[TestMethod]
public void MapShipping_MapsDestinationAndUnloadFromOptions()
public void MapShipping_MapsDestinationAndUnloadFromCompany()
{
var options = CreateOptions();
var tradeCard = Mapper.MapShipping(CreateShipping(), options)[0].TradeCard;
var company = CreateCompany();
var tradeCard = Mapper.MapShipping(CreateShipping(), company)[0].TradeCard;
Assert.AreEqual("FruitBank Kft", tradeCard.DestinationName);
Assert.AreEqual("HU", tradeCard.DestinationCountry);
Assert.AreSame(options.UnloadLocation, tradeCard.UnloadLocation, "a lerakodási hely a konfigbó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");
}
[TestMethod]
public void MapShipping_MapsCarrierTextFromCargoPartner()
{
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateOptions())[0].TradeCard;
var tradeCard = Mapper.MapShipping(CreateShipping(), CreateCompany())[0].TradeCard;
Assert.AreEqual("Fuvaros Zrt", tradeCard.CarrierText);
}
@ -178,7 +181,7 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_NormalizesLicencePlate_RemovesHyphenAndUppercases()
{
var tradeCard = Mapper.MapShipping(CreateShipping(plate: "abc-123"), CreateOptions())[0].TradeCard;
var tradeCard = Mapper.MapShipping(CreateShipping(plate: "abc-123"), CreateCompany())[0].TradeCard;
Assert.AreEqual("ABC123", tradeCard.Vehicle!.PlateNumber, "a NAV pattern nem enged kötőjelet, és nagybetűs");
Assert.AreEqual("HU", tradeCard.Vehicle.Country);
}
@ -186,7 +189,7 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_WithTrailer_MapsVehicle2()
{
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: true), CreateOptions())[0].TradeCard;
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: true), CreateCompany())[0].TradeCard;
Assert.IsNotNull(tradeCard.Vehicle, "vonó jármű");
Assert.IsNotNull(tradeCard.Vehicle2, "vontatmány");
Assert.AreEqual("XYZ789", tradeCard.Vehicle2!.PlateNumber);
@ -195,7 +198,7 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_NoTrailer_Vehicle2Null()
{
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: false), CreateOptions())[0].TradeCard;
var tradeCard = Mapper.MapShipping(CreateShipping(withTrailer: false), CreateCompany())[0].TradeCard;
Assert.IsNotNull(tradeCard.Vehicle);
Assert.IsNull(tradeCard.Vehicle2, "nincs pótkocsi → nincs vehicle2");
}
@ -204,9 +207,9 @@ public sealed class ShippingToEkaerMapperTests
[TestMethod]
public void MapShipping_NullShipping_Throws()
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(null!, CreateOptions()));
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(null!, CreateCompany()));
[TestMethod]
public void MapShipping_NullOptions_Throws()
public void MapShipping_NullCompany_Throws()
=> Assert.ThrowsExactly<ArgumentNullException>(() => Mapper.MapShipping(CreateShipping(), null!));
}

View File

@ -29,6 +29,7 @@
<DxGridDataColumn FieldName="Name" />
<DxGridDataColumn FieldName="TaxId" />
<DxGridDataColumn FieldName="CertificationNumber" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.Currency)" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.CountryCode)" />
<DxGridDataColumn FieldName="PostalCode" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.Country)" />

View File

@ -23,7 +23,7 @@
SignalRClient="FruitBankSignalRClient"
Logger="_logger"
CssClass="@GridCss"
ValidationEnabled="false"
ValidationEnabled="true"
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />

View File

@ -29,6 +29,7 @@
<DxGridDataColumn FieldName="Name" />
<DxGridDataColumn FieldName="TaxId" />
<DxGridDataColumn FieldName="CertificationNumber" />
<DxGridDataColumn FieldName="@nameof(CargoPartner.Currency)" />
<DxGridDataColumn FieldName="@nameof(Partner.CountryCode)" />
<DxGridDataColumn FieldName="PostalCode" />
<DxGridDataColumn FieldName="@nameof(Partner.Country)" />