Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/docs/EKAER/README.md

90 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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>();
```