9.6 KiB
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 hívásra. A frontend CompanyOnboardingView.vue 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 |
✏️ 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 |
➕ ÚJ fájl | OptenService osztály: httpx.AsyncClient + tenacity retry logika |
| 3 | backend/app/schemas/organization.py |
➕ ÚJ fájl | TaxLookupResponse Pydantic séma |
| 4 | backend/app/api/v1/endpoints/organizations.py |
✏️ Módosítás | Mock végpont → valós OptenService.lookup_by_tax_number() hívás |
| 5 | backend/requirements.txt |
🟢 Nincs teendő | httpx már szerepel. tenacity opcionális (retry logikához) |
| 6 | frontend/src/views/organization/CompanyOnboardingView.vue |
🟢 Már kész | lookupTaxNumber() teljes mértékben használható |
Adatmodell — TaxLookupResponse
# 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 á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
# 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:
# 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
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
# 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)
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.