diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 558045a..aa94c8a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -147,7 +147,10 @@ "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" mv \"AyCode.Services.Server/docs/SIGNALR_DATASOURCE\" \"AyCode.Services/docs/SIGNALR_DATASOURCE\")", "Bash(awk 'NR==135' \"H:/Applications/Aycode/Source/AyCode.Core/.github/LLM_PROTOCOL_DECISIONS.md\")", "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" status --short)", - "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" reset)" + "Bash(git -C \"H:/Applications/Aycode/Source/AyCode.Core\" reset)", + "Bash(dotnet build *)", + "PowerShell(dotnet build AyCode.Services\\\\AyCode.Services.csproj --nologo -v q 2>$null)", + "PowerShell(dotnet test AyCode.Services.Server.Tests\\\\AyCode.Services.Server.Tests.csproj --nologo --no-restore --filter \"FullyQualifiedName~AcBinaryHubProtocolConcurrencyTests\" 2>$null)" ] } } diff --git a/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs b/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs index 0784d9e..17ed562 100644 --- a/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs +++ b/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs @@ -46,6 +46,7 @@ public sealed class EkaerTradeCardValidatorTests ProductVtsz = "08081010", ProductName = "Alma", Weight = 123.5m, + Value = 250_000, }); return operation; } @@ -96,6 +97,30 @@ public sealed class EkaerTradeCardValidatorTests Assert.IsTrue(HasError(Sut.Validate(op), "címzett"), "üres/whitespace címzettnév → hiba"); } + [TestMethod] + public void Validate_ZeroWeight_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].Weight = 0; + Assert.IsTrue(HasError(Sut.Validate(op), "tömege"), "0 tömeg (hiányzó súly a doksin) → üzleti szabály hiba"); + } + + [TestMethod] + public void Validate_MissingValue_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].Value = null; + Assert.IsTrue(HasError(Sut.Validate(op), "értéke"), "hiányzó tétel-érték → üzleti szabály hiba (NAV 2021-től)"); + } + + [TestMethod] + public void Validate_ZeroValue_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].Value = 0; + Assert.IsTrue(HasError(Sut.Validate(op), "értéke"), "0 tétel-érték → üzleti szabály hiba (NAV 2021-től)"); + } + // ---- Séma-szint (generált DataAnnotations) ------------------------------ [TestMethod] diff --git a/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs b/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs index 97aa306..158f5cd 100644 --- a/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs +++ b/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs @@ -10,7 +10,7 @@ namespace AyCode.Services.Nav.Ekaer; /// a generált modellek -jai (Required / RegularExpression / Min-MaxLength), /// rekurzívan a tradeCard + tételek + jármű + helyszínek fölött — ezt a végzi; /// az XSD-ben NEM jelölt, de a NAV által megkövetelt üzleti szabályok (vonó jármű, legalább egy tétel, -/// feladó/címzett alapadatok) — ezeket kézzel ellenőrizzük. +/// tétel-tömeg/-érték > 0, feladó/címzett alapadatok) — ezeket kézzel ellenőrizzük. /// /// public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator @@ -81,6 +81,12 @@ public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator // a sémán (a null bukik a Required-en, az "" nem, mert a RegularExpression az üreset validnak veszi), // ezért üzleti szabállyal lezárjuk. A { Length: 0 } property-pattern a null-ra NEM illeszkedik → nincs dupla hiba. if (item.ProductVtsz is { Length: 0 }) errors.Add(Error($"{itemPath}.productVtsz", "a tétel VTSZ-száma (productVtsz) nem lehet üres")); + + // A tétel bruttó tömege kötelező és pozitív — a 0 azt jelzi, hogy a forrás-dokumentumról hiányzik a súly (adathiba). + if (item.Weight <= 0) errors.Add(Error($"{itemPath}.weight", "a tétel bruttó tömege (weight) kötelező és nem lehet 0")); + + // A NAV 2021.01.01-től minden tételre value > 0-t követel — a hiányzó/0 érték (pl. ismeretlen ár) blokkoló adathiba. + if (item.Value is not > 0) errors.Add(Error($"{itemPath}.value", "a tétel értéke (value) kötelező és pozitív (NAV-követelmény 2021-től)")); } // 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli). diff --git a/AyCode.Services/Nav/docs/EKAER_INTERFACE.md b/AyCode.Services/Nav/docs/EKAER_INTERFACE.md index 5b7fcdb..ed1b6e4 100644 --- a/AyCode.Services/Nav/docs/EKAER_INTERFACE.md +++ b/AyCode.Services/Nav/docs/EKAER_INTERFACE.md @@ -3,6 +3,8 @@ > **Forrás:** [`eKAERManagementService_2.2.pdf`](eKAERManagementService_2.2.pdf) + [`ekaermanagement.xsd`](ekaermanagement.xsd) / [`common.xsd`](common.xsd). > Ez **LLM-barát kivonat** a gyakori dolgokról — ütközés esetén **a PDF/XSD a hiteles forrás**. > Online: +> +> **Verzió-állapot (2026-07-10, a NAV-oldalról ellenőrizve):** a **2.2 az aktuális** interfész-verzió, újabb nincs bejelentve. A NAV a 2.2 dokumentációt **2025-05-14-én frissítette** („Lejárt jelszó kezelése" — `EXPIRED_PASSWORD` eset viselkedése). **A helyi PDF EZ a friss kiadás** (byte-azonos méret az online-nal, letöltve 2026-06-01) — nem kell újra letölteni. ## Transport diff --git a/AyCode.Services/Nav/docs/EKAER_TRADECARD.md b/AyCode.Services/Nav/docs/EKAER_TRADECARD.md index c164630..5f9800b 100644 --- a/AyCode.Services/Nav/docs/EKAER_TRADECARD.md +++ b/AyCode.Services/Nav/docs/EKAER_TRADECARD.md @@ -86,11 +86,15 @@ Mezők: `name`, `VATNumber`, `phone`, `e-mail`, `country`, `zipCode`, `city`, `s | Kód | Jelentés | Biztosíték? | |---|---|---| -| `S` | Termékértékesítés / -beszerzés | **van** | +| `S` | Termékértékesítés **/ -beszerzés** — EGY kód MINDKÉT irányra (kimenő eladás ÉS bejövő beszerzés) | **van** | | `A` | Saját tulajdonú termék (kivezetve 2015.03.01 után — új bejelentésnél nem adható) | van | | `W` | Bérmunka | nincs | | `O` | Egyéb | nincs | +> ⚠️ **Gyakori félreértés:** az `A` NEM „beszerzés" (angol *acquisition* alapján kézenfekvő, de TÉVES) — a `common.xsd` szerint `A` = „Saját tulajdonú termék - Törvény változás miatt kivezetve." A **beszerzésre is az `S`** való; belföldön (`D`) ráadásul CSAK az `S` megengedett (lásd lent). +> +> ⛔ **Az `ekaermanagement.xsd` tétel-kommentje ELAVULT és félrevezet:** a `TradeCardItemGroup`/`tradeReason` elem inline dokumentációja („S: Értékesítés **A: Beszerzés** W: Bérmunka O: Egyéb") a kivezetés ELŐTTI állapot — NE ebből dolgozz. A mérvadó a `common.xsd` `TradeReasonType` enum-annotációja + a PDF (webről is megerősítve, 2026-07-10: az `A` 2015.03.01 után nem adható). + **A `tradeReason` a `tradeType`-tól függően korlátozott:** - `E` (export): `S`, `W`, `O` - `I` (import): `S`, `W`, `O` diff --git a/AyCode.Services/Nav/docs/EKAER_VALIDATION.md b/AyCode.Services/Nav/docs/EKAER_VALIDATION.md index b2f2148..ae04e82 100644 --- a/AyCode.Services/Nav/docs/EKAER_VALIDATION.md +++ b/AyCode.Services/Nav/docs/EKAER_VALIDATION.md @@ -64,7 +64,7 @@ A `BaseResultType` (funcCode/reasonCode/msg) a `Nav/NavInterfaces.cs` `INavResul A fenti szabályokat a **`EkaerTradeCardValidator`** (`Nav/Ekaer/`) ellenőrzi a leképzett tradeCard-okon, **beküldés előtt**, hibalistával (nem áll meg az első hibánál): - **Séma-szint:** a generált modellek `[Required]` / `[RegularExpression]` / hossz-attribútumai (rekurzívan a tradeCard + tételek + jármű + helyszínek fölött), a `System.ComponentModel.DataAnnotations.Validator`-ral. A `*Value`/`*Specified` segéd-property-ket kihagyja (a default `DateTime` különben tévesen bukna a timestamp-patternen). -- **Üzleti szabályok** (az XSD-ben nem `[Required]`, de a NAV megköveteli): legalább egy tétel, feladó/címzett név+adószám+ország. A **vonó jármű (rendszám)** viszont **NEM blokkoló hiba, hanem `Warning`** (lásd a *Súlyosság* szakaszt) — a NAV csak a *felrakodás megkezdéséig* / `Finalize`-ig kéri (`TC_FINALIZE_VEHICLE_DATA_EMPTY`), `Create`-nél még hiányozhat (pl. a vevő viszi el az árut, beadáskor még nincs rendszám). +- **Üzleti szabályok** (az XSD-ben nem `[Required]`, de a NAV megköveteli): legalább egy tétel, feladó/címzett név+adószám+ország, és tételenként **`weight > 0`** + **`value > 0`** (2026-07; a value a NAV-nál 2021.01.01-től követelmény — a 0/hiányzó érték blokkoló `Error`, mert a forrás-dokumentumról hiányzó adatot jelez). A **vonó jármű (rendszám)** viszont **NEM blokkoló hiba, hanem `Warning`** (lásd a *Súlyosság* szakaszt) — a NAV csak a *felrakodás megkezdéséig* / `Finalize`-ig kéri (`TC_FINALIZE_VEHICLE_DATA_EMPTY`), `Create`-nél még hiányozhat (pl. a vevő viszi el az árut, beadáskor még nincs rendszám). - **`productVtsz` üres-string:** a modellen `[Required(AllowEmptyStrings=true)]` + pattern miatt az `""` elcsúszna a sémán (a `null` bukik, az `""` nem) — ezért a validátor külön lezárja. ### Súlyosság: `Warning` vs `Error` (2026-06) @@ -76,4 +76,4 @@ A validátor üzenetei **súlyozottak**: `EkaerValidationMessage : ValidationRes A **`EkaerSubmitService`** (`Nav/Ekaer/`) a kapu: `Validate` → **csak ha `HasErrors()`** áll meg (`EkaerSubmitResult.Invalid`); a `Warning`-ok **átmennek** (a beküldés mehet, a hiányt később pótolják), egyébként `EkaerManageService.ManageAsync`. A fogyasztó (FruitBank) ezt `Generated` / `GeneratedWithWarning` státuszra képezi. -> ⚠️ **Nyitott (value):** a tétel `value` a NAV szerint **2021-től > 0 kötelező** (lásd a Tétel-validáció szakaszt), a validátor viszont **jelenleg nem követeli** — mert a FruitBank-mapper a deviza/FX tisztázásáig nem tölti. Amíg ez nyitott, a NAV **elutasíthatja** a beküldést. Lezárandó: a value kitöltése (deviza→HUF) + a validátorban `value > 0` üzleti szabály. (Plugin `EKAER_TODO` #3/#10.) +> ✅ **Lezárva (2026-07, value + weight):** a validátor tételenként kényszeríti a **`value > 0`**-t (NAV-követelmény 2021-től) és a **`weight > 0`**-t — mindkettő blokkoló `Error` (a 0/hiány a forrás-dokumentum adathibája, szándékosan nincs fallback). A fogyasztó (FruitBank) oldalán a value a `Partner.Currency` + EUR-HUF config-árfolyam alapján, a weight a szállítólevélen szereplő bruttóból (`GrossWeightOnDocument`) töltődik. (Plugin `EKAER_TODO` #3/#4/#10.) diff --git a/AyCode.Services/Nav/docs/README.md b/AyCode.Services/Nav/docs/README.md index ef6fff2..cfdcea4 100644 --- a/AyCode.Services/Nav/docs/README.md +++ b/AyCode.Services/Nav/docs/README.md @@ -21,16 +21,20 @@ A NAV adatszolgáltatás (jelenleg EKÁER) hivatalos forrásai + **LLM-barát ki **Olvasási sorrend új belépőnek:** `EKAER_INTERFACE` → `EKAER_TRADECARD` → `EKAER_OPERATIONS` → `EKAER_VALIDATION`. -## Jogszabályi alap, kötelezettség, hozzáférés (kontextus, 2026-06) +## Jogszabályi alap, kötelezettség, hozzáférés (kontextus, 2026-07 — webről ellenőrizve) A fenti PDF/XSD a **HOGYAN** (a 2.2 interfész — a tradeCard beküldése). Néhány **általános** dolog, ami tisztázódott (a forrásból ellenőrizve): -**Jogszabály (a MIKOR kötelező):** a hatályos rendelet a **13/2020. (XII. 23.) PM rendelet** (). A leképezést/kötelezettséget érintő elvek: -- az EKÁER-egység = **egy feladó → egy címzett, egy gépjárművel**; **§ 12:** EGY EKÁER-szám több *fel-/kirakodási címet* és több *terméktételt* is fedhet — **nem** szállítólevelenként kell darabolni; -- a bejelentés-kötelezettség **küszöbe erre a relációra AGGREGÁLT** (egy fuvarozás keretében ugyanazon feladótól ugyanazon címzett részére ugyanazon gépjárművel — § 4 i); +**Jogszabály (a MIKOR kötelező):** a hatályos rendelet a **13/2020. (XII. 23.) PM rendelet** (). A leképezést/kötelezettséget érintő elvek (2026-07 állapot): +- **Csak a bejelentésköteles (kockázatos) termék** EKÁER-köteles (2021-01-01 óta; a nem-kockázatos áru EGYÁLTALÁN nem) — a lista az **51/2014. (XII. 31.) NGM rendelet** 1. melléklete (VTSZ-alapú). Utolsó módosítás: **25/2025. (VIII. 18.) NGM** — 2025-08-18-tól egyes kertészeti termékek KIKERÜLTEK a listából (pl. vöröshagyma-dughagyma 07031011, friss borsó 07081000, friss bab 07082000, csemegekukorica 07099960, egyes vetőmagok). +- **Küszöb (4. § i):** mentes a fuvarozás, ha az **500 kg bruttó tömeget ÉS az 1 millió Ft adó nélküli értéket** sem haladja meg → kötelező, ha **BÁRMELYIK átlépi**. (A 200 kg / 250 eFt-os „kockázatos élelmiszer"-küszöb a 2021 ELŐTTI rezsim [5/2015 NGM] — MÁR NEM hatályos.) **A mentesség IRÁNY-FÜGGETLEN** (a § szövegében nincs irány-megkötés): a küszöb alatti fuvar **importnál/exportnál SEM** kötelező — a „külföldi reláció küszöb nélkül mindig kötelező" vélekedés a régi rezsimből ered, a hatályos jogban NEM áll (szó szerint ellenőrizve, 2026-07-10). +- az EKÁER-egység = **egy feladó → egy címzett, egy gépjárművel**; **§ 12 (4):** EGY EKÁER-szám több *fel-/kirakodási címet* és több *terméktételt* is fedhet — **nem** szállítólevelenként kell darabolni; **§ 12 (2):** gépjármű = jármű + vontatott pótkocsi/félpótkocsi együtt; +- a bejelentés-kötelezettség **küszöbe erre a relációra AGGREGÁLT** (egy fuvarozás keretében ugyanazon feladótól ugyanazon címzett részére ugyanazon gépjárművel); +- **további mentességek:** honvédség/rendvédelem, postai küldemény, jövedéki termék (4. §); **20 km-en belüli** fuvarozás útszakasz-mentesítése (5. §); +- **kockázati biztosíték** (17-18. §): az érték **15%-a**, az előző **45 nap** bejelentései alapján görgetve; formája elkülönített letét vagy garancia; **mentes**: a „megbízható adózó", ill. a 2+ éve működő, köztartozásmentes adózó (18. § (2)); - a NAV **mindkét irányban büntet:** az **aluljelentés ÉS a túljelentés** is bírság — a kötelezettség-döntésnek pontosnak kell lennie. -> A konkrét küszöb-kategóriák/értékek a **jogszabálytól és a beállítástól függően változnak**, és a **fogyasztó** (pl. FruitBank) dönti el a kötelezettséget — itt szándékosan **nincs érték bedrótozva**. A fogyasztó-oldali kapu + a dátumozott küszöb-referencia: a FruitBank plugin `docs/EKAER/`. +> A konkrét küszöb-értékek a **jogszabálytól és a beállítástól függően változnak**, és a **fogyasztó** (pl. FruitBank) dönti el a kötelezettséget — itt szándékosan **nincs érték bedrótozva**; a fenti számok **dátumozott jogszabályi referenciák**. A fogyasztó-oldali kapu: a FruitBank plugin `docs/EKAER/`. **Hozzáférés (kié a `user` + `signingKey`):** az **ADÓZÓé** (a bejelentő cégé), **nem a fejlesztőé** — az adózó Ügyfélkapun, elsődleges felhasználóként állítja be a kulcsot, a szoftver az ő credentialját használja (`INavCredentials`). Részletek + a 2024–25-ös „NAV M2M" alternatíva: [`EKAER_INTERFACE.md`](EKAER_INTERFACE.md) → *Hozzáférés*. diff --git a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs index f50ed03..36b87bf 100644 --- a/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs +++ b/AyCode.Services/SignalRs/AcBinaryHubProtocol.cs @@ -261,7 +261,10 @@ public class AcBinaryHubProtocol : IHubProtocol public void WriteMessage(HubMessage message, IBufferWriter output) { - _logger?.LogInformation("Serialize start"); + // Keepalive pings (~15 s, both directions) would flood the log channel at Information + // level — real messages keep the start/end brackets (hang diagnostics, ACCORE-SBP-I-T3W9). + var isPing = message is PingMessage; + if (!isPing) _logger?.LogInformation("Serialize start"); // AsyncSegment: chunked protocol framing for messages with streamable arguments if (_protocolMode == BinaryProtocolMode.AsyncSegment @@ -327,7 +330,7 @@ public class AcBinaryHubProtocol : IHubProtocol bw.Flush(); Unsafe.WriteUnaligned(ref lengthSpan[0], totalPayload); - _logger?.LogInformation("Serialize end totalSentSize={TotalSentSize}", LengthPrefixSize + totalPayload); + if (!isPing) _logger?.LogInformation("Serialize end totalSentSize={TotalSentSize}", LengthPrefixSize + totalPayload); if (_logger?.IsEnabled(LogLevel.Debug) == true) _logger.LogDebug("WriteMessage {MessageType} payloadSize={PayloadSize}", message.GetType().Name, totalPayload); @@ -654,7 +657,12 @@ public class AcBinaryHubProtocol : IHubProtocol return false; _logger?.LogTrace("TryParseMessage parsing payloadLength={PayloadLength} inputLength={InputLength}", payloadLength, input.Length); - _logger?.LogInformation("Deserialize start"); + + // Keepalive ping frame (payload = single MsgPing byte) — skip the Information-level + // brackets (~15 s spam, both directions). Wire-level check: the routing-fallback that + // SURFACES PingMessage.Instance for real payloads (see ParseMessage) must keep its brackets. + var isPing = payloadLength == 1 && reader.TryPeek(out var firstByte) && firstByte == MsgPing; + if (!isPing) _logger?.LogInformation("Deserialize start"); message = ParseMessage(ref reader, payloadLength, binder); @@ -662,7 +670,7 @@ public class AcBinaryHubProtocol : IHubProtocol { input = input.Slice(LengthPrefixSize + payloadLength); - _logger?.LogInformation("Deserialize end"); + if (!isPing) _logger?.LogInformation("Deserialize end"); if (_logger?.IsEnabled(LogLevel.Debug) == true) _logger.LogDebug("TryParseMessage parsed {MessageType}", message.GetType().Name); return true; diff --git a/AyCode.Services/SignalRs/AcSignalRTags.cs b/AyCode.Services/SignalRs/AcSignalRTags.cs index 95b8ecd..f7f4995 100644 --- a/AyCode.Services/SignalRs/AcSignalRTags.cs +++ b/AyCode.Services/SignalRs/AcSignalRTags.cs @@ -6,4 +6,11 @@ public class AcSignalRTags public const int PingTag = 90001; public const int EchoTag = 90002; + + /// + /// Grid layout roaming (key-value store): key = composite grid-layout key, value = layout JSON, userId = owning user. + /// + public const int GetGridLayoutTag = 90010; + public const int SaveGridLayoutTag = 90011; + public const int DeleteGridLayoutTag = 90012; } \ No newline at end of file diff --git a/AyCode.Services/SignalRs/README.md b/AyCode.Services/SignalRs/README.md index 2a36729..7b2cfc2 100644 --- a/AyCode.Services/SignalRs/README.md +++ b/AyCode.Services/SignalRs/README.md @@ -20,7 +20,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri ### Message Tagging - **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive). -- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`. +- **`AcSignalRTags.cs`** — Static constants: `None`, `PingTag`, `EchoTag`; grid-layout roaming tags `GetGridLayoutTag` / `SaveGridLayoutTag` / `DeleteGridLayoutTag` (90010-12) — key-value layout storage contract (`key`, [`layoutJson`,] `userId`), implemented by consumer hubs. - **`SignalRCrudTags.cs`** — Sealed class bundling 5 independent CRUD tag integers. `GetMessageTagByTrackingState()` maps `TrackingState` -> tag. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md`. - **`SendToClientType.cs`** — Enum: None, Others, Caller, All.