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

233 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 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.