# logic_spec_opten_integration.md — Opten REST API Integrációs Terv ## Modul Célja A jelenlegi mock `GET /lookup-tax/{tax_number}` végpont lecserélése valós [Opten REST API](https://www.opten.hu/) hívásra. A frontend [`CompanyOnboardingView.vue`](frontend/src/views/organization/CompanyOnboardingView.vue:654) `lookupTaxNumber()` függvénye már készen áll a valós adatok fogadására — csak a backend oldali integráció hiányzik. ## Masterbook 2.0 Illeszkedés - **DDD Séma:** `identity.organizations` — a cégadatok automatikus kitöltése csökkenti a hibalehetőséget. - **Geo-logika:** Csak magyar (HU) adószámok esetén aktív. Más országok esetén a mock marad, vagy külön integráció később. --- ## Érintett Fájlok és Műveletek | # | Fájl | Művelet | Leírás | |---|------|---------|--------| | 1 | [`backend/app/core/config.py`](backend/app/core/config.py:11) | ✏️ Módosítás | `OPTEN_API_KEY`, `OPTEN_API_BASE_URL`, `OPTEN_CACHE_TTL` hozzáadása a `Settings` osztályhoz | | 2 | [`backend/app/services/opten_service.py`](backend/app/services/opten_service.py) | ➕ **ÚJ fájl** | `OptenService` osztály: `httpx.AsyncClient` + `tenacity` retry logika | | 3 | [`backend/app/schemas/organization.py`](backend/app/schemas/organization.py) | ➕ **ÚJ fájl** | `TaxLookupResponse` Pydantic séma | | 4 | [`backend/app/api/v1/endpoints/organizations.py`](backend/app/api/v1/endpoints/organizations.py:125) | ✏️ Módosítás | Mock végpont → valós `OptenService.lookup_by_tax_number()` hívás | | 5 | [`backend/requirements.txt`](backend/requirements.txt:17) | 🟢 **Nincs teendő** | `httpx` már szerepel. `tenacity` opcionális (retry logikához) | | 6 | [`frontend/src/views/organization/CompanyOnboardingView.vue`](frontend/src/views/organization/CompanyOnboardingView.vue:654) | 🟢 **Már kész** | `lookupTaxNumber()` teljes mértékben használható | --- ## Adatmodell — TaxLookupResponse ```python # backend/app/schemas/organization.py from pydantic import BaseModel from typing import Optional class TaxLookupResponse(BaseModel): full_name: str name: str display_name: Optional[str] = None address_zip: str address_city: str address_street_name: str address_street_type: Optional[str] = None address_house_number: str legal_form: Optional[str] = None # pl. "Kft.", "Bt.", "Zrt." status: Optional[str] = None # pl. "active", "dissolved" ``` ### Frontend Kompatibilitás A fenti mezők teljes mértékben lefedik a frontend [`CompanyOnboardingView.vue:654-684`](frontend/src/views/organization/CompanyOnboardingView.vue:654) által várt mezőket. Az új `legal_form` és `status` mezők a frontend `tax_lookup_result` objektumban tárolódhatnak későbbi felhasználásra. --- ## Service Réteg — OptenService ### Osztály Felépítése ```python # backend/app/services/opten_service.py import logging from typing import Optional import httpx from tenacity import retry, stop_after_attempt, wait_exponential logger = logging.getLogger(__name__) class OptenService: def __init__(self, api_key: str, base_url: str = "https://api.opten.hu/v2"): self.api_key = api_key self.base_url = base_url self.client = httpx.AsyncClient( base_url=self.base_url, headers={"X-Api-Key": self.api_key, "Accept": "application/json"}, timeout=10.0 ) @retry(stop=stop_after_attempt(2), wait=wait_exponential(multiplier=1, min=1, max=5)) async def lookup_by_tax_number(self, tax_number: str) -> dict: """Lekérdezi az Opten API-t adószám alapján.""" normalized = self._normalize_tax_number(tax_number) response = await self.client.get(f"/company/{normalized}") response.raise_for_status() return self._map_opten_response(response.json()) def _normalize_tax_number(self, raw: str) -> str: """Csak számjegyeket tart meg (8 vagy 10 vagy 11 karakter).""" digits = "".join(c for c in raw if c.isdigit()) # HU adószám: 8 számjegy (egyéni) vagy 10-11 számjegy (cég, áfával) return digits[:11] def _map_opten_response(self, data: dict) -> dict: """Opten API válasz → TaxLookupResponse formátumba.""" return { "full_name": data.get("cegnev", ""), "name": data.get("rovitett_cegnev", data.get("cegnev", "")), "display_name": data.get("rovitett_cegnev"), "address_zip": data.get("iranyitoszam", ""), "address_city": data.get("telepules", ""), "address_street_name": data.get("kozt_nev", ""), "address_street_type": data.get("kozt_jelleg", ""), "address_house_number": str(data.get("hazszam", "")), "legal_form": data.get("cegforma", ""), "status": data.get("ceg_statusz", ""), } async def close(self): await self.client.aclose() ``` ### Gráciális Fallback Ha az `OPTEN_API_KEY` nincs beállítva (üres string), a végpont NEM dob hibát, hanem visszaadja a mock adatokat: ```python # backend/app/api/v1/endpoints/organizations.py @router.get("/lookup-tax/{tax_number}", response_model=TaxLookupResponse) async def lookup_tax_number(tax_number: str, db: AsyncSession = Depends(get_db)): settings = get_settings() if not settings.OPTEN_API_KEY: logger.warning("OPTEN_API_KEY not set — returning mock data") return await _mock_lookup(tax_number) service = OptenService(settings.OPTEN_API_KEY, settings.OPTEN_API_BASE_URL) try: result = await service.lookup_by_tax_number(tax_number) return result except httpx.HTTPStatusError as e: return await _handle_opten_error(e) finally: await service.close() ``` --- ## Geo-Logika | Ország | Forrás | Státusz | |--------|--------|---------| | Magyarország (HU) | Opten REST API | ✅ Ezen terv | | Más EU országok | Mock (később: saját provider) | ⏳ Tervezett | Csak magyar adószámok esetén aktív az Opten hívás. A `_normalize_tax_number()` metódus alapján: ha a szám nem felel meg a HU formátumnak (8-11 számjegy), a végpont mock adatokat ad vissza. --- ## Hibakezelési Mátrix | Opten Hiba | HTTP Státusz | Felhasználói Üzenet | |------------|-------------|---------------------| | 404 — Nincs ilyen cég | 404 | "Nem található cég ezzel az adószámmal." | | 403 — Rossz API kulcs | 500 | "Belső szolgáltatói hiba. Kérjük, próbáld később." | | 429 — Rate limit | 429 | "Túl sok lekérdezés. Kérjük, várj egy percet." | | 422 — Érvénytelen adószám | 422 | "Érvénytelen adószám formátum." | | 504 — Gateway timeout | 504 | "A szolgáltató nem válaszol. Próbáld újra később." | | Ismeretlen hiba | 502 | "Váratlan hiba történt. Próbáld újra később." | ### `_handle_opten_error()` pszeudokód ```python async def _handle_opten_error(e: httpx.HTTPStatusError) -> dict: status_map = { 404: (404, "Nem található cég ezzel az adószámmal."), 403: (500, "Belső szolgáltatói hiba."), 429: (429, "Túl sok lekérdezés. Várj egy percet."), 422: (422, "Érvénytelen adószám formátum."), 504: (504, "A szolgáltató nem válaszol."), } http_code, msg = status_map.get(e.response.status_code, (502, "Váratlan hiba.")) raise HTTPException(status_code=http_code, detail=msg) ``` --- ## Biztonsági Megfontolások | # | Intézkedés | Leírás | |---|-----------|--------| | 1 | `.env`-ben tartás | `OPTEN_API_KEY` csak a `.env` fájlban, SOHOHA hardcode-olva | | 2 | Rate limiting | Nginx/API Gateway szintű korlátozás a `/lookup-tax/` végpontra | | 3 | Audit log | Minden Opten hívás naplózása: tax_number, timestamp, sikeresség | | 4 | Cache | `OPTEN_CACHE_TTL` (alapértelmezett: 3600s) — redis cache a gyakori ismétlődésekhez | | 5 | Adatvédelem | A visszaadott adatok csak a szervezet létrehozásához használhatók | --- ## Teszt Terv ### Unit tesztek | Teszt | Leírás | |-------|--------| | `test_normalize_tax_number` | 8, 10, 11 jegyű, kötőjeles, szóközös bemenetek | | `test_map_opten_response` | Opten JSON → TaxLookupResponse mezőtérkép | | `test_mock_fallback` | Ha nincs API kulcs, mock adat jön | | `test_error_mapping` | Minden hibaeset lefedése | ### Integrációs tesztek ```bash # Teszt élő Opten API-val (ha van API kulcs) docker compose exec sf_api python3 -c " from app.services.opten_service import OptenService import asyncio async def test(): s = OptenService('teszt_kulcs') result = await s.lookup_by_tax_number('12345678') print(result) await s.close() asyncio.run(test()) " ``` --- ## Végrehajtási Terv (6 Kártya) ```mermaid graph TD A[1. Config: OPTEN_API_KEY] --> B[2. Séma: TaxLookupResponse] A --> C[3. Service: OptenService] B --> D[4. Endpoint: Mock → Valós] C --> D D --> E[5. Tesztek] E --> F[6. Dokumentáció] ``` | # | Kártya Név | Függőség | Becsült Méret | |---|-----------|----------|---------------| | 1 | Config: `OPTEN_API_KEY` hozzáadása | Nincs | S (< 10 sor) | | 2 | Séma: `TaxLookupResponse` Pydantic | 1 | S (< 20 sor) | | 3 | Service: `OptenService` osztály | Nincs | M (~60 sor) | | 4 | Endpoint: Mock → valós hívás | 2, 3 | S (~20 sor) | | 5 | Tesztek (unit + smoke) | 4 | M (~50 sor) | | 6 | Dokumentáció frissítése | 5 | S (< 20 sor) | --- ## Kockázatok - **Opten API kulcs beszerzése** — a fejlesztés mock módban is végezhető. - **Opten API változás** — a `_map_opten_response()` metódus könnyen adaptálható. - **Éles rate limit** — szükség esetén Opten előfizetés frissítése.