90 lines
12 KiB
Markdown
90 lines
12 KiB
Markdown
# EKÁER — NAV közúti áruforgalom-bejelentés (FruitBank áttekintő)
|
||
|
||
Topic `EKAER`, prefix `MGFBANKPLUG` → entry ID-k `MGFBANKPLUG-EKAER-I-<RAND>` (issue) / `-T-` (TODO) / `-B-` (bug).
|
||
|
||
A FruitBank EKÁER-bejelentés **szerver-oldali** (a kliens már standalone WASM, böngészőből CORS miatt nem hívhat NAV-ot — a kliens SignalR-en triggerel). A tényleges logika viszont **rétegekre bontva** él, nem ebben a pluginban; ez a folder a **belépő/áttekintő** + a FruitBank-/nopCommerce-specifikus nyitott döntések.
|
||
|
||
> A NAV-protokoll részletei (transport, auth, üzenet-szerkezet) az `AyCode.Core` repóban: `AyCode.Services/Nav/docs/EKAER_*.md`. Itt **nem** ismételjük.
|
||
|
||
## A teljes EKÁER-kép (4 réteg)
|
||
|
||
| Réteg | Hol | Mit ad |
|
||
|---|---|---|
|
||
| **NAV/EKÁER framework** | `AyCode.Services/Nav/` + `Nav/Ekaer/` (AyCode.Core) | transport, auth (SHA-512), generált modellek, **`EkaerTradeCardValidator`** (NAV-séma + üzleti szabályok), **`EkaerSubmitService`** (validate→send), `EkaerManageService` (HTTP). Általános — **nem** FruitBank-specifikus. |
|
||
| **FruitBank leképezés** | `FruitBank.Common/Services/Ekaer/` | **`ShippingToEkaerMapper`** — a forrást (bejövő `ShippingDocument`-csoport / kimenő `OrderDto`) egy irány-független **`EkaerConsignment`**-re képezi (`ToConsignment`), abból épül a `tradeCard` (`BuildTradeCard`). **`EkaerReportability`** — bejelentés-kötelezettség (küszöb/külföld). **`EkaerValueCalculator`** — tétel-érték HUF-ban. |
|
||
| **FruitBank fogyasztó** | `FruitBank.Common.Server/Services/Ekaer/` | **`FruitBankEkaerService`** — szerver-oldali orchestráció: credentials/options + map → submit |
|
||
| **nopCommerce-integráció (ez a plugin)** | ez a folder + a plugin kód | DI-bekötés, NAV-fiók settings, a VTSZ-forrás (`Product.Gtin`), SignalR-trigger a kliens felől, és ez a doksi |
|
||
|
||
## A beküldési lánc
|
||
|
||
```
|
||
FruitBankEkaerService.SubmitShippingAsync(shipping) // FruitBank.Common.Server
|
||
→ ShippingToEkaerMapper.MapShipping(...) // FruitBank.Common (map)
|
||
→ EkaerSubmitService.SubmitAsync(ops) // AyCode.Services (validate → send)
|
||
→ EkaerTradeCardValidator.Validate(ops) // hibalista → ha nem üres: NEM küld
|
||
→ EkaerManageService.ManageAsync(request) // auth + HTTP POST a NAV-nak
|
||
```
|
||
Validációs hiba → **hibalista** (`EkaerSubmitResult.Invalid`), nem megy ki kérés. NAV-oldali hiba → `NavReportException` propagál.
|
||
|
||
> **A ténylegesen bekötött beküldő-út (2026-07):** a fenti `SubmitShippingAsync` (shipping-szintű, dokumentumonkénti) az AUTOMATA út — még NINCS triggerelve (lásd a `MapShipping`-drift a TODO-ban). A használt beküldés a **`SendEkaerToNav(ekaerHistoryId)`** endpoint (grid „Beküldés" gomb): egy legenerált `EkaerHistory` **tárolt `XmlDoc`-ját** deserializálja → create-`TradeCardOperationType` → `EkaerSubmitService.SubmitAsync` (ugyanaz a validate→send). A kettő granularitása eltér — egységesítés az automata bekötés előtt.
|
||
|
||
**További belépési pontok** (`FruitBankDataController` SignalR-endpointok, kliens felől — a grid gombjaival — triggerelve):
|
||
- **`CreateMissingEkaerHistories(fromDate)`** — reconciliation-kapu: a dátumtól még le nem fedett szállítólevelekre/rendelésekre `Pending` `EkaerHistory` sorokat hoz létre, **csak ha kötelező** (lásd a kapu-szakaszt lentebb). Bejövőnél a `(Shipping, Partner, PartnerDepot)` csoport **EGY** `EkaerHistory`-t kap, a csoport dokumentumai pedig `EkaerHistoryMapping` sorokként kapcsolódnak; kimenőnél egy rendelés = egy `EkaerHistory` + egy mapping. `EkaerCreateResult`-ot ad vissza (létrehozott szám + a kihagyott tételek üzenetei).
|
||
- **`GenerateEkaerXmlDocument(ekaerHistoryId)`** — egy `EkaerHistory`-deklarációra (a mappingben rögzített forrás-dokumentumok ÖSSZES tételét egy tradeCard-ba vonva) legenerálja + validálja a tradeCard XML-t (NEM küld NAV-ot); a `Status` / `ErrorText` / `ConversionRate` töltődik.
|
||
- **`SendEkaerToNav(ekaerHistoryId)`** (2026-07) — a legenerált (`Generated`/`GeneratedWithWarning`) deklaráció **TÁROLT `XmlDoc`-ját** create-műveletként beküldi a NAV-nak (`FruitBankEkaerService.SendEkaerAsync` → `EkaerSubmitService.SubmitAsync` → `EkaerManageService`). Siker → `Sent` (tiszta) / `SentWithMissingData` (warningos generálásból, pl. hiányzó rendszám) + `EkaerNumber` (a NAV `tcn`) + `SentDate`; NAV-/transport-hiba → `SendError` + `ErrorText` (a `reasonCode`/`msg`). A grid „Beküldés" gombja hívja, **megerősítő dialógus** után. A kézi út (Letöltés → NAV-web) átmeneti, ez váltja ki.
|
||
|
||
## Companion fájlok
|
||
|
||
- [`EKAER_ISSUES.md`](EKAER_ISSUES.md) — ismert problémák (pl. `MGFBANKPLUG-EKAER-I-T3X8`: GTIN≠VTSZ).
|
||
- [`EKAER_TODO.md`](EKAER_TODO.md) — a `Shipping → tradeCard` mapping nyitott üzleti döntései.
|
||
|
||
## A leképezés (tisztázott pontok)
|
||
|
||
| EKÁER | FruitBank forrás |
|
||
|---|---|
|
||
| `seller*` (feladó) | `ShippingDocument.Partner` (a beszállító) |
|
||
| `carrierText` (szállítmányozó — **opcionális**, NAV nem követeli) | **bejövő:** `Shipping.CargoPartner.Name` (valós, külön fuvarozó); **kimenő:** — **üres** (a vevő maga viszi el, ő már a címzett; nincs külön fuvarozónk) |
|
||
| `vehicle` / `vehicle2` | `Shipping.CargoTruck` / `CargoTrailer` (`LicencePlate` + `CountryCode`, normalizálva) |
|
||
| `loadLocation` (felrakodás) | **bejövő:** a `ShippingDocument.PartnerDepot` **címe** (kötelező — **T-D8R4** kész; a `VatNumber` a Partneré, a depot `TaxId`-ja passthrough); **kimenő:** `company.Site` (configból) |
|
||
| `unloadLocation` (lerakodás) | **bejövő:** `company.Site`; **kimenő:** az `order.Site` snapshot (fallback: a vevő fő címe) |
|
||
| tétel `productVtsz` | `ShippingItem.ProductDto.Gtin` (átmenetileg — lásd `EKAER_ISSUES.md`) |
|
||
| tétel `productName` | `ShippingItem.ProductName` (a VTSZ-szerinti csoportosítás + a csoport-név `Category`-ből: TODO **T-V9G3**) |
|
||
| tétel `weight` | `ShippingItem.GrossWeightOnDocument` — a **szállítólevélen szereplő** bruttó (a bejelentés a kísérő dokumentum adatairól megy, mint a value; NEM a későbbi mérlegelésről) |
|
||
| tétel `tradeReason` | **`S` MINDKÉT irányra** — az XSD szerint `S` = „Termék értékesítés / beszerzés" (egy kód), `A` = saját tulajdonú termék (**kivezetve**), `W`=bérmunka, `O`=egyéb; belföldön (`D`) csak `S` adható. (A bejövő korábban tévesen `A`-t küldött — javítva 2026-07-10.) |
|
||
|
||
A nyitott pontok (tradeType, destination, value/deviza, granularitás, kimenő irány): [`EKAER_TODO.md`](EKAER_TODO.md).
|
||
|
||
## Reconciliation-kapu, kötelezettség, életciklus (2026-06)
|
||
|
||
Munkafolyamat: a **kapu** létrehozza a sorokat → a user **generál** → **beküld** (grid „Beküldés" gomb → `SendEkaerToNav` → NAV, megerősítés után; a NAV-válasz — `tcn` / hiba — automatikusan rögzül). A Letöltés → NAV-web kézi út átmeneti (a beküldés bekötése előtti időszakra).
|
||
|
||
**`EkaerHistory` életciklus** (egy sor = egy tradeCard; a forrás-rekordok az **`EkaerHistoryMapping`** junctionön át kapcsolódnak — bejövő: a `(Shipping, Partner, PartnerDepot)` csoport N `ShippingDocument`-je, kimenő: 1 `Order` —, az `EkaerHistory` maga **nem** hordoz forrás-kulcsot, az irány az `IsOutgoing`; a `Status` int-oszlop + `[NotColumn]` enum-nézet; a mapping a `GetAll(loadRelations: true)`-zal töltődik a `Mappings` asszociációba):
|
||
`Pending` (a kapu hozta létre) → **Generate** → `Generated` / `GeneratedWithWarning` (küldhető, de pótlandó — pl. hiányzó rendszám) / `ValidationError` (blokkoló) → **Beküldés** (`SendEkaerToNav`) → `Sent` (tiszta) / `SentWithMissingData` (warningosból, pótlásra vár) / `SendError` (a NAV-hívás bukott — újraküldhető). A grid 3 tabja ezekre szűr (szerver-oldali `EkaerHistoryFilter`), a warning ⚠️ / error ⛔ ikonnal megkülönböztetve.
|
||
|
||
**Bejelentés-kötelezettség** — `EkaerReportability.Evaluate(consignment, settings)`, a kapu hívja egységenként:
|
||
- **tételenkénti előfeltétel (2026-07):** minden tétel **bruttó súlya > 0** kell legyen — 0 súlyú tétel (kitöltetlen dokumentum-súly / méretlen rendelés) → `DataError` tételes üzenettel, a csoport sora NEM jön létre (a 0 csendben alul-számolná a küszöb-summát); pótlás után a következő futás felveszi;
|
||
- **egység:** bejövőnél a `(Shipping, Partner, PartnerDepot)` csoport (= feladó+telephely→címzett→jármű) — a tömeg/érték a csoport **ÖSSZES** dokumentumára **aggregálva** (13/2020 PM rendelet), és a csoport EGY `EkaerHistory`-t kap; kimenőnél egy rendelés;
|
||
ℹ️ **Ismert, VÁLLALT eltérés (T-H7F2, ügyfél-döntés 2026-07-10):** a jogszabályi aggregálási egység telephely NÉLKÜLI (feladó→címzett→jármű, §12 (4)) — a depot-szintű summa a több-telephelyes belföldi partnernél elvileg alul-jelenthet (2×300 kg két depotról). Vállalva, mert ritka, tételenként küszöb alatti, és a nap-pontosságú `loadDate` mellett két külön fuvarként számolható el; az [`I-F5D9`](EKAER_ISSUES.md)-cel együtt kezelendő, ha jogszabályi szabályra váltás jön — lásd `EKAER_TODO.md` T-H7F2;
|
||
- **küszöb felett** → kötelező, az országkódtól **függetlenül** (az országkód-hibát a generate-validálás jelzi);
|
||
- **küszöb alatt** → csak a **külföldi** reláció kötelező (a feladó és a címzett országkódja **eltér**); belföld → nem; hiányzó/érvénytelen (nem ISO-2) országkód → **`DataError`** (a sor kimarad + üzenet a popupban);
|
||
ℹ️ **Kimondottan az ügyfél kérése (megerősítve 2026-07-10):** a hatályos jog szerint a küszöb-mentesség irány-független (13/2020 PM 4.§ i — küszöb alatti import SEM lenne kötelező), az ügyfél viszont **minden külföldről bejövőt jelenteni kér** — a minimumnál szigorúbb, önkéntes bejelentés. Nem kizárt, hogy a közeljövőben a jogszabályi szabályra kell átírni — a követés: [`EKAER_ISSUES.md`](EKAER_ISSUES.md) **I-F5D9**;
|
||
- **mentesség:** bejövőnél `Partner.IsEkaer = false`, és a Partner nélküli (félkész) szállítólevél; kimenőnél a **Customer `isEkaer` GenericAttribute explicit `false`** értéke (T-C3M8, 2026-07: `bool?`-olvasás, a hiányzó attribútum = köteles — fail-safe; az admin-widget checkboxa is pipáltat mutat, amíg explicit ki nem veszik) — mind csendben kimaradnak;
|
||
- **telephely-kapu (T-D8R4, 2026-07):** kötelező bejelentésnél a `PartnerDepotId` is kötelező — hiányzó telephelyű csoportra NEM jön létre sor, üzenet a popupba (a depot-check a kötelezettség-döntés UTÁN fut: küszöb alatti belföldi csoportra nem szól feleslegesen). A generate-út `LoadWith(sd => sd.PartnerDepot)`-tal tölt, és a `FruitBankEkaerService.TryDepotError` guard hiányzó/betöltetlen depotnál `ValidationError`-t ad (nincs partner-központi-címes, félrevezető XML). A csoportkulcsban a `PartnerId` **szándékosan** benne marad: null-depotnál különben különböző partnerek dokumentumai egy csoportba olvadnának.
|
||
|
||
> **A küszöb configból jön** (`Ekaer:Thresholds:WeightKg` / `:ValueHuf`) — a kódba **NINCS drótozva**, mert a NAV-jogszabály és a beállítás szerint **változhat**. A konkrét, dátumozott értékeket és a termékkategóriákat lásd [`EKAER_TODO.md`](EKAER_TODO.md) **T-W3R8**. A NAV **mindkét irányban büntet** (alul- ÉS túljelentés), ezért a küszöbnek pontosnak kell lennie.
|
||
|
||
**Egyesített leképezés:** a bejövő/kimenő forrás egy **`EkaerConsignment`** köztes modellre képződik (két adapter), és **abból** épül MIND a tradeCard (`BuildTradeCard`), MIND a kötelezettség-döntés — így az irányfüggő rész két adapterre szorul, a közös logika egy helyen van. A `MapDocument` / `MapOrder` / `MapShipping` ennek vékony wrappere.
|
||
|
||
## Bekötés a szerveren (DI)
|
||
|
||
A plugin (vagy a szerver-startup) köti be a láncot — a NAV-fiók titkos adatai a szerver config/secret store-jából:
|
||
```csharp
|
||
services.AddSingleton<INavCredentials>(/* NAV-fiók: User/Password/SigningKey/TaxNumber/BaseUrl */);
|
||
services.AddHttpClient<EkaerManageService>(/* NAV TLS 1.2 */);
|
||
services.AddScoped<IEkaerTradeCardValidator, EkaerTradeCardValidator>();
|
||
services.AddScoped<IEkaerSubmitService, EkaerSubmitService>();
|
||
services.AddScoped<IShippingToEkaerMapper, ShippingToEkaerMapper>();
|
||
services.AddSingleton<IEkaerSettings>(/* EkaerSettings: Ekaer:Company + Site + küszöbök + EUR-HUF árfolyam — configból */);
|
||
services.AddScoped<IFruitBankEkaerService, FruitBankEkaerService>();
|
||
```
|