Files
service-finder/plans/logic_spec_opten_integration.md
2026-06-10 08:06:07 +00:00

9.6 KiB
Raw Permalink Blame History

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.