diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9fdc5ea2..977e2229 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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\" *)" ] } } diff --git a/.github/TOPIC_CODES.md b/.github/TOPIC_CODES.md new file mode 100644 index 00000000..b892f8e2 --- /dev/null +++ b/.github/TOPIC_CODES.md @@ -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` +- `` (this repo = `FBANKAPP`, from the `@repo.prefix` in `.github/copilot-instructions.md`) + `` 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/` | diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index 2e457d1a..616cb277 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -8,20 +8,20 @@ namespace FruitBank.Common.Server.Services.Ekaer; /// /// /// A teljes lánc: map (, FruitBank.Common) → -/// validate → send (, AyCode.Services). A NAV-fiók -/// hitelesítő adatait (INavCredentials) és a -t a DI szolgáltatja. +/// validate → send (, AyCode.Services). A NAV-fiók hitelesítő adatait +/// (INavCredentials) és a saját cégadatokat () a DI szolgáltatja. /// 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 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); } } diff --git a/FruitBank.Common/Dtos/ProductDto.cs b/FruitBank.Common/Dtos/ProductDto.cs index 2fc2ba8d..d0572ac4 100644 --- a/FruitBank.Common/Dtos/ProductDto.cs +++ b/FruitBank.Common/Dtos/ProductDto.cs @@ -34,7 +34,7 @@ public class ProductDto : MgProductDto, IProductDto /// A nopCommerce Product.Gtin oszlopa. Átmenetileg az EKÁER VTSZ-t (vámtarifaszámot) tárolja — /// a jövőbeli ShippingToEkaerMapper innen olvassa a tradeCardItem.productVtsz é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 Nop.Plugin.Misc.AIPlugin/docs/EKAER/EKAER_ISSUES.md#mgfbankplug-ekaer-i-t3x8. + /// lásd FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md#fbankapp-dmodel-i-p6x4. /// [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.")] diff --git a/FruitBank.Common/Entities/CargoTruck.cs b/FruitBank.Common/Entities/CargoTruck.cs index 90c6d5c5..efd864d9 100644 --- a/FruitBank.Common/Entities/CargoTruck.cs +++ b/FruitBank.Common/Entities/CargoTruck.cs @@ -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 1–3 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.")] diff --git a/FruitBank.Common/Entities/PartnerBase.cs b/FruitBank.Common/Entities/PartnerBase.cs index 74bd8f7a..331e8529 100644 --- a/FruitBank.Common/Entities/PartnerBase.cs +++ b/FruitBank.Common/Entities/PartnerBase.cs @@ -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; } diff --git a/FruitBank.Common/Interfaces/IPartner.cs b/FruitBank.Common/Interfaces/IPartner.cs index b757dc7e..5518e234 100644 --- a/FruitBank.Common/Interfaces/IPartner.cs +++ b/FruitBank.Common/Interfaces/IPartner.cs @@ -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? 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; } } \ No newline at end of file diff --git a/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs b/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs deleted file mode 100644 index 88122619..00000000 --- a/FruitBank.Common/Services/Ekaer/EkaerMappingOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using AyCode.Services.Nav.Ekaer.Models; - -namespace FruitBank.Common.Services.Ekaer; - -/// -/// A konfiguráció-függő bemenetei, amelyek NEM a Shipping-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. -/// -/// Ezek később a szerver-oldali plugin beállításaiból (settings) töltődnek — lásd a plugin docs/EKAER/EKAER_TODO.md #2. -public sealed class EkaerMappingOptions -{ - /// A bejelentő (FruitBank) neve — a tradeCard destinationName-je bejövő relációban. - public string? DestinationName { get; set; } - - /// A bejelentő adószáma (destinationVatNumber). Pattern: [0-9A-Z-]{1,15}. - public string? DestinationVatNumber { get; set; } - - /// A bejelentő országkódja (2 betű). Alapértelmezés: HU. - public string DestinationCountryCode { get; set; } = "HU"; - - /// A bejelentő címe egybeírva (destinationAddress, max 200). - public string? DestinationAddress { get; set; } - - /// - /// 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 -ként adjuk át (a plugin-konfigból összeállítva). - /// - public LocationType? UnloadLocation { get; set; } - - /// - /// A bejelentő saját országkódja a tradeType meghatározásához: ha a feladó (beszállító) országa - /// ezzel egyezik → D (belföld-belföld), egyébként → I (import). Alapértelmezés: HU. - /// - public string HomeCountryCode { get; set; } = "HU"; -} diff --git a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs index 47af2cbc..a3430daf 100644 --- a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs @@ -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). /// /// -/// A NAV-protokollt és az authentikációt NEM kezeli — az az AyCode.Services.Nav réteg -/// (NavReportServiceBase / EkaerManageService) 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 docs/EKAER/README.md és EKAER_TODO.md fájljaiban. +/// A NAV-protokollt és az authentikációt NEM kezeli — az az AyCode.Services.Nav réteg felelőssége. +/// A feladót (beszállító Partner) és a saját céget () egységesen, +/// -ként kezeli. /// public interface IShippingToEkaerMapper { /// /// Leképezi a minden ShippingDocument-jét egy-egy EKÁER tradeCard műveletre. /// - /// A bejövő szállítmány. A fuvarozó/jármű a Shipping szintjén, az eladó/tételek a dokumentum szintjén élnek. - /// A konfiguráció-függő adatok (destination cég, lerakodási hely, home country). + /// A bejövő szállítmány (fuvarozó/jármű a Shipping szintjén, eladó/tételek a dokumentum szintjén). + /// A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely. /// A tradeCard művelet típusa. Alapértelmezés: . - /// Dokumentumonként egy , a beérkezés sorrendjében indexelve. - IReadOnlyList MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create); + IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create); } diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs index 74a76355..c038c745 100644 --- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -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; /// /// -/// Tiszta (állapotmentes) leképező. A TradeType/TradeReasonType enumokat aliasszal hozzuk be, -/// hogy elkerüljük a Models.Common.TradeCardType enum és a Models.TradeCardType osztály névütközését. +/// Tiszta (állapotmentes) leképező. A feladót és a saját céget egységesen -ként kezeli. +/// A TradeType/TradeReasonType enumokat aliasszal hozzuk be a Models.TradeCardType osztály +/// és a Models.Common.TradeCardType enum névütközése miatt. /// public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper { - public IReadOnlyList MapShipping(Shipping shipping, EkaerMappingOptions options, OperationType operation = OperationType.Create) + /// A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import. + private const string HomeCountry = "HU"; + + public IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create) { ArgumentNullException.ThrowIfNull(shipping); - ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(company); var operations = new List(); 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 } /// - /// Bejövő (beszerzés) reláció: belföldi feladó → D (belföld-belföld), egyébként → I (import, - /// közösségből belföldre). Az export (E) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7). + /// Belföldi feladó (HU) → D (belföld-belföld), egyébként → I (import). A NAV EKÁER magyar, + /// így a belföld mindig HU; az export (E) jelenleg nincs leképezve (lásd EKAER_TODO #1, #7). /// - 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 }; /// - /// 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 Partner 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.) /// - 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 }; } - /// Egybeírt cím (sellerAddress/destinationAddress, max 200): "1234 Budapest Fő utca 1.". - 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); - } - /// Adószám normalizálása. Pattern: [0-9A-Z-]{1,15}. 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); } - /// Rendszám normalizálása. Pattern: [A-Z0-9ÖŐÜŰ]{4,15} — a kötőjel/szóköz NEM engedett, ezért eltávolítjuk. + /// Rendszám normalizálása. Pattern: [A-Z0-9ÖŐÜŰ]{4,15} — kötőjel/szóköz NEM engedett. 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]; } diff --git a/FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md b/FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md new file mode 100644 index 00000000..f6d1c55a --- /dev/null +++ b/FruitBank.Common/docs/DATAMODEL/DATAMODEL_ISSUES.md @@ -0,0 +1,48 @@ +# DATA-MODEL — Known Issues + +> Companion to [`README.md`](README.md). Topic `DMODEL`, prefix `FBANKAPP` → entry IDs `FBANKAPP-DMODEL-I-` (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 diff --git a/FruitBank.Common/docs/DATAMODEL/README.md b/FruitBank.Common/docs/DATAMODEL/README.md new file mode 100644 index 00000000..aa86e41b --- /dev/null +++ b/FruitBank.Common/docs/DATAMODEL/README.md @@ -0,0 +1,12 @@ +# DATA-MODEL — FruitBank adatmodell-normalizálás + +Topic `DMODEL`, prefix `FBANKAPP` → entry ID-k `FBANKAPP-DMODEL-I-` (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. diff --git a/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs index eff81443..52b441fe 100644 --- a/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs +++ b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs @@ -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(() => Mapper.MapShipping(null!, CreateOptions())); + => Assert.ThrowsExactly(() => Mapper.MapShipping(null!, CreateCompany())); [TestMethod] - public void MapShipping_NullOptions_Throws() + public void MapShipping_NullCompany_Throws() => Assert.ThrowsExactly(() => Mapper.MapShipping(CreateShipping(), null!)); } diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor index b408b12c..07ea7f23 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor @@ -29,6 +29,7 @@ + diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor index 43f987bf..3663e202 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor @@ -23,7 +23,7 @@ SignalRClient="FruitBankSignalRClient" Logger="_logger" CssClass="@GridCss" - ValidationEnabled="false" + ValidationEnabled="true" OnGridFocusedRowChanged="Grid_FocusedRowChanged"> diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor index b9f725cf..0bc66eb8 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor @@ -29,6 +29,7 @@ +