Ekaer validation tightened, SignalR tags, doc updates

- Enforce `weight > 0` and `value > 0` for each trade item in `EkaerTradeCardValidator` (NAV 2021+ requirement); add tests for zero/missing values.
- Add grid layout roaming tags (90010–90012) to `AcSignalRTags` for key-value layout storage.
- Suppress info-level logging for SignalR ping messages in `AcBinaryHubProtocol`.
- Update EKÁER and SignalR documentation with new rules, tags, and latest legal references (2026-07).
- Add new build/test scripts to `settings.local.json`.
This commit is contained in:
Loretta 2026-07-10 15:36:58 +02:00
parent dc8e01250d
commit e9438e4374
10 changed files with 74 additions and 15 deletions

View File

@ -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(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(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\" 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)"
] ]
} }
} }

View File

@ -46,6 +46,7 @@ public sealed class EkaerTradeCardValidatorTests
ProductVtsz = "08081010", ProductVtsz = "08081010",
ProductName = "Alma", ProductName = "Alma",
Weight = 123.5m, Weight = 123.5m,
Value = 250_000,
}); });
return operation; return operation;
} }
@ -96,6 +97,30 @@ public sealed class EkaerTradeCardValidatorTests
Assert.IsTrue(HasError(Sut.Validate(op), "címzett"), "üres/whitespace címzettnév → hiba"); 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) ------------------------------ // ---- Séma-szint (generált DataAnnotations) ------------------------------
[TestMethod] [TestMethod]

View File

@ -10,7 +10,7 @@ namespace AyCode.Services.Nav.Ekaer;
/// <item>a generált modellek <see cref="ValidationAttribute"/>-jai (Required / RegularExpression / Min-MaxLength), /// <item>a generált modellek <see cref="ValidationAttribute"/>-jai (Required / RegularExpression / Min-MaxLength),
/// rekurzívan a tradeCard + tételek + jármű + helyszínek fölött — ezt a <see cref="Validator"/> végzi;</item> /// rekurzívan a tradeCard + tételek + jármű + helyszínek fölött — ezt a <see cref="Validator"/> végzi;</item>
/// <item>az XSD-ben NEM jelölt, de a NAV által megkövetelt üzleti szabályok (vonó jármű, legalább egy tétel, /// <item>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.</item> /// tétel-tömeg/-érték &gt; 0, feladó/címzett alapadatok) — ezeket kézzel ellenőrizzük.</item>
/// </list> /// </list>
/// </remarks> /// </remarks>
public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator 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), // 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. // 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")); 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). // 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli).

View File

@ -3,6 +3,8 @@
> **Forrás:** [`eKAERManagementService_2.2.pdf`](eKAERManagementService_2.2.pdf) + [`ekaermanagement.xsd`](ekaermanagement.xsd) / [`common.xsd`](common.xsd). > **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**. > Ez **LLM-barát kivonat** a gyakori dolgokról — ütközés esetén **a PDF/XSD a hiteles forrás**.
> Online: <https://ekaer.nav.gov.hu/faq/?page_id=9> > Online: <https://ekaer.nav.gov.hu/faq/?page_id=9>
>
> **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 ## Transport

View File

@ -86,11 +86,15 @@ Mezők: `name`, `VATNumber`, `phone`, `e-mail`, `country`, `zipCode`, `city`, `s
| Kód | Jelentés | Biztosíték? | | 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 | | `A` | Saját tulajdonú termék (kivezetve 2015.03.01 után — új bejelentésnél nem adható) | van |
| `W` | Bérmunka | nincs | | `W` | Bérmunka | nincs |
| `O` | Egyéb | 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:** **A `tradeReason` a `tradeType`-tól függően korlátozott:**
- `E` (export): `S`, `W`, `O` - `E` (export): `S`, `W`, `O`
- `I` (import): `S`, `W`, `O` - `I` (import): `S`, `W`, `O`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -261,7 +261,10 @@ public class AcBinaryHubProtocol : IHubProtocol
public void WriteMessage(HubMessage message, IBufferWriter<byte> output) public void WriteMessage(HubMessage message, IBufferWriter<byte> 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 // AsyncSegment: chunked protocol framing for messages with streamable arguments
if (_protocolMode == BinaryProtocolMode.AsyncSegment if (_protocolMode == BinaryProtocolMode.AsyncSegment
@ -327,7 +330,7 @@ public class AcBinaryHubProtocol : IHubProtocol
bw.Flush(); bw.Flush();
Unsafe.WriteUnaligned(ref lengthSpan[0], totalPayload); 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) if (_logger?.IsEnabled(LogLevel.Debug) == true)
_logger.LogDebug("WriteMessage {MessageType} payloadSize={PayloadSize}", message.GetType().Name, totalPayload); _logger.LogDebug("WriteMessage {MessageType} payloadSize={PayloadSize}", message.GetType().Name, totalPayload);
@ -654,7 +657,12 @@ public class AcBinaryHubProtocol : IHubProtocol
return false; return false;
_logger?.LogTrace("TryParseMessage parsing payloadLength={PayloadLength} inputLength={InputLength}", payloadLength, input.Length); _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); message = ParseMessage(ref reader, payloadLength, binder);
@ -662,7 +670,7 @@ public class AcBinaryHubProtocol : IHubProtocol
{ {
input = input.Slice(LengthPrefixSize + payloadLength); 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); if (_logger?.IsEnabled(LogLevel.Debug) == true) _logger.LogDebug("TryParseMessage parsed {MessageType}", message.GetType().Name);
return true; return true;

View File

@ -6,4 +6,11 @@ public class AcSignalRTags
public const int PingTag = 90001; public const int PingTag = 90001;
public const int EchoTag = 90002; public const int EchoTag = 90002;
/// <summary>
/// Grid layout roaming (key-value store): key = composite grid-layout key, value = layout JSON, userId = owning user.
/// </summary>
public const int GetGridLayoutTag = 90010;
public const int SaveGridLayoutTag = 90011;
public const int DeleteGridLayoutTag = 90012;
} }

View File

@ -20,7 +20,7 @@ Custom binary SignalR protocol, client infrastructure, message tagging, and seri
### Message Tagging ### Message Tagging
- **`SignalMessageTagAttribute.cs`** — Three attributes: `TagAttribute` (base, int messageTag), `SignalRAttribute` (server method routing + client notification), `SignalRSendToClientAttribute` (client-side receive). - **`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`. - **`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. - **`SendToClientType.cs`** — Enum: None, Others, Caller, All.