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.