belépés jó, rgisztráción kicsit faragni kell még

This commit is contained in:
Roo
2026-06-05 14:45:02 +00:00
parent f03b5f3916
commit b84b1bab41
7 changed files with 570 additions and 42 deletions

View File

@@ -294,9 +294,15 @@ async def complete_kyc(kyc_in: UserKYCComplete, db: AsyncSession = Depends(get_d
return {"status": "success", "message": "Fiók aktiválva."} return {"status": "success", "message": "Fiók aktiválva."}
@router.get("/me") @router.get("/me")
async def get_current_user_profile(current_user: User = Depends(get_current_user)): async def get_current_user_profile(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
""" """
Return current user profile (alias for /users/me). Return current user profile (alias for /users/me).
Uses _build_user_response() to ensure proper address field mapping.
""" """
from app.schemas.user import UserResponse from app.schemas.user import UserResponse
return UserResponse.model_validate(current_user) from app.api.v1.endpoints.users import _build_user_response
response_data = _build_user_response(current_user)
return UserResponse.model_validate(response_data)

View File

@@ -68,15 +68,15 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None) -> dic
address_data = None address_data = None
if person.address: if person.address:
address_data = { address_data = {
"zip": getattr(person.address, 'zip', None) or getattr(getattr(person.address, 'postal_code', None), 'zip_code', None), "address_zip": getattr(person.address, 'zip', None) or getattr(getattr(person.address, 'postal_code', None), 'zip_code', None),
"city": getattr(person.address, 'city', None) or getattr(getattr(person.address, 'postal_code', None), 'city', None), "address_city": getattr(person.address, 'city', None) or getattr(getattr(person.address, 'postal_code', None), 'city', None),
"street_name": person.address.street_name, "address_street_name": person.address.street_name,
"street_type": person.address.street_type, "address_street_type": person.address.street_type,
"house_number": person.address.house_number, "address_house_number": person.address.house_number,
"stairwell": person.address.stairwell, "address_stairwell": person.address.stairwell,
"floor": person.address.floor, "address_floor": person.address.floor,
"door": person.address.door, "address_door": person.address.door,
"parcel_id": person.address.parcel_id, "address_hrsz": person.address.parcel_id,
"full_address_text": person.address.full_address_text, "full_address_text": person.address.full_address_text,
"latitude": person.address.latitude, "latitude": person.address.latitude,
"longitude": person.address.longitude, "longitude": person.address.longitude,
@@ -249,13 +249,12 @@ async def update_my_person(
) )
if addr_id: if addr_id:
person.address_id = addr_id person.address_id = addr_id
# EXPLICIT betöltjük az új címet a memóriába, hogy felülírjuk a régit!
person.address = await db.get(Address, addr_id)
try: try:
await db.commit() await db.commit()
# Refresh to get the latest state
await db.refresh(person) await db.refresh(person)
if person.address:
await db.refresh(person.address)
except SQLAlchemyError as e: except SQLAlchemyError as e:
await db.rollback() await db.rollback()
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")

View File

@@ -6,16 +6,17 @@ from datetime import date, datetime
class AddressResponse(BaseModel): class AddressResponse(BaseModel):
"""Beágyazott cím adatok a PersonResponse számára.""" """Beágyazott cím adatok a PersonResponse számára.
zip: Optional[str] = None A mezőnevek address_ előtaggal egyeznek a frontend által várt PersonUpdate formátummal."""
city: Optional[str] = None address_zip: Optional[str] = None
street_name: Optional[str] = None address_city: Optional[str] = None
street_type: Optional[str] = None address_street_name: Optional[str] = None
house_number: Optional[str] = None address_street_type: Optional[str] = None
stairwell: Optional[str] = None address_house_number: Optional[str] = None
floor: Optional[str] = None address_stairwell: Optional[str] = None
door: Optional[str] = None address_floor: Optional[str] = None
parcel_id: Optional[str] = None address_door: Optional[str] = None
address_hrsz: Optional[str] = None
full_address_text: Optional[str] = None full_address_text: Optional[str] = None
latitude: Optional[float] = None latitude: Optional[float] = None
longitude: Optional[float] = None longitude: Optional[float] = None

View File

@@ -0,0 +1,251 @@
# 🚗 Service Finder - Jármű API és Frontend Architektúra Elemzés
> **Verzió:** 1.0
> **Dátum:** 2026-06-05
> **Szerző:** Rendszer-Architect
> **Cél:** A backend jármű API teljes áttekintése és frontend csoportosítási javaslat
---
## 1. 📊 Backend API Réteg - Teljes Végpont Térkép
A FastAPI routerek a `backend/app/api/v1/api.py` fájlban vannak összefogva. Az összes járművel kapcsolatos végpont az alábbi prefixek alatt fut:
### 1.1 `/assets` prefix - Jármű (Asset) Menedzsment
**Router:** `backend/app/api/v1/endpoints/assets.py`
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|---------|---------|-----|---------------------|
| GET | /assets/vehicles | Felhasználó járműveinek listázása (Garázs). Paginált. Támogatja: személyes mód (owner/operator) és vállalati mód (szervezet garázsai/Branch-ek). | Asset - vehicle.assets tábla |
| POST | /assets/vehicles | Új jármű hozzáadása vagy meglévő igénylése. Ellenőrzi a járműlimitet, kezeli a VIN duplikációt, XP jutalmat ad. | Asset |
| GET | /assets/{asset_id} | Jármű részletes adatai (katalógus adatokkal együtt). Jogosultság ellenőrzés (tulajdonos/szervezet). | Asset + AssetCatalog |
| GET | /assets/{asset_id}/financial-summary | Pénzügyi riport - kategóriákra bontott összesítés (Local/EUR). | AssetCost |
| GET | /assets/{asset_id}/costs | Tételes költséglista lapozással. | AssetCost |
| GET | /assets/{asset_id}/maintenance | Szerviznapló - karbantartási költségek listája. | AssetCost - cost_category='maintenance' |
| POST | /assets/{asset_id}/maintenance | Szervizbejegyzés hozzáadása (date, odometer, description, cost). Létrehoz AssetEvent-et is. | AssetCost + AssetEvent |
### 1.2 `/catalog` prefix - Jármű Katalógus
**Router:** `backend/app/api/v1/endpoints/catalog.py`
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|---------|---------|-----|---------------------|
| GET | /catalog/makes | Márkák listázása (opcionálisan vehicle_class szerint szűrve). | AssetCatalog |
| GET | /catalog/models | Típusok listázása adott márkához. | AssetCatalog |
| GET | /catalog/generations | Generációk/Évjáratok adott márka+típushoz. | AssetCatalog |
| GET | /catalog/engines | Motorváltozatok és technikai specifikációk (ID, engine_code, fuel_type, factory_data). | AssetCatalog |
### 1.3 `/vehicles` prefix - Jármű Értékelések
**Router:** `backend/app/api/v1/endpoints/vehicles.py`
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|---------|---------|-----|---------------------|
| POST | /vehicles/{vehicle_id}/ratings | Jármű értékelése 4 dimenzióban (driving_experience, reliability, comfort, consumption_satisfaction 1-10). Egy user csak egyszer értékelhet. | VehicleUserRating |
| GET | /vehicles/{vehicle_id}/ratings | Értékelések lekérése adott járműhöz. | VehicleUserRating |
### 1.4 `/expenses` prefix - Flotta Költségek (TCO)
**Router:** `backend/app/api/v1/endpoints/expenses.py`
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|---------|---------|-----|---------------------|
| POST | /expenses/ | Költség rögzítése (üzemanyag, szerviz, adó, biztosítás). Dynamic Gatekeeper: draft jármű limit ellenőrzés. | AssetCost + SystemParameter |
### 1.5 `/analytics` prefix - Jármű Analitika
**Router:** `backend/app/api/v1/endpoints/analytics.py`
| Metódus | Végpont | Cél | Adatbázis kapcsolat |
|---------|---------|-----|---------------------|
| GET | /analytics/{vehicle_id}/summary | TCO összesítő járművenként (user costs, lifetime costs, benchmark). | AssetCost, VehicleModelDefinition |
| GET | /analytics/dashboard | Dashboard analitika (mock data - havi költségek, üzemanyag-hatékonyság, cost/km trendek). | Jelenleg mock |
### 1.6 További kapcsolódó végpontok
| Metódus | Végpont | Cél | Router helye |
|---------|---------|-----|-------------|
| POST | /documents/upload/{parent_type}/{parent_id} | Dokumentumfeltöltés járműhöz | documents.py |
| POST | /evidence/scan-registration | OCR regisztrációs dokumentum szkennelés | evidence.py |
| GET | /search/match | Geo-keresés PostGIS segítségével | search.py |
| POST | /services/hunt | Szerviz vadászat indítása | services.py |
| GET | /reports/summary/{vehicle_id} | Jármű összesítő jelentés | reports.py |
| GET | /reports/trends/{vehicle_id} | Jármű trend jelentés | reports.py |
---
## 2. 🗄️ Adatbázis Modell Réteg - Jármű Domain
### 2.1 Core Asset Model - A "Digital Twin"
Az Asset modell (backend/app/models/vehicle/asset.py) a központi entitás:
```
Asset ──┬── AssetCatalog (katalógus kapcsolat)
├── AssetFinancials (pénzügyi adatok)
├── AssetCost (költségnapló)
├── AssetEvent (eseménynapló - Digitális Szervizkönyv)
├── VehicleLogbook (útnyilvántartás)
├── AssetInspection (ellenőrzési napló)
├── AssetReview (értékelések)
├── AssetTelemetry (telemetria)
├── AssetAssignment (szervezeti hozzárendelés)
├── VehicleOwnership (tulajdonosváltás)
└── ServiceRequest (szervizkérések)
```
### 2.2 Főbb Modellek Leírása
| Modell | Tábla (séma) | Cél |
|--------|-------------|-----|
| Asset | vehicle.assets | Fizikai eszköz (Digital Twin) - Minden adat itt fut össze |
| AssetCatalog | vehicle.vehicle_catalog | Jármű katalógus mesteradatok |
| VehicleModelDefinition | vehicle.vehicle_model_definitions | Multi-Tier MDM Master - technikai igazságforrás |
| VehicleType | vehicle.vehicle_types | Jármű kategóriák |
| CostCategory | vehicle.cost_categories | Standardizált költségkategória fa |
| AssetCost | vehicle.asset_costs | Üzemeltetési költség (TCO) |
| AssetEvent | vehicle.asset_events | Digitális Szervizkönyv események |
| VehicleOdometerState | vehicle.vehicle_odometer_states | Km óra állapot és becslés |
| VehicleUserRating | vehicle.vehicle_user_ratings | Jármű értékelés 4 dimenzióban |
---
## 3. 🔌 Jelenlegi Frontend Implementáció
### 3.1 Garázs Nézet (DashboardView.vue)
A frontend egy egységes garázs nézetet használ, ahol az összes jármű egy grid-ben jelenik meg:
- Adatforrás: GET /api/v1/assets/vehicles -> vehicleStore.fetchVehicles()
- Megjelenítés: Minden jármű egy kártyán, amely tartalmazza az EU-s rendszámtáblát, márka+modell nevet, gyártási évet, állapot score-t, futott km-t, státusz badge-t
- UI élmény: Zen mód (UI elrejtése), 3D téreffektusok
### 3.2 Hiányzó Frontend Funkciók
A backend által biztosított API-k közül a frontend CSAK a járműlista lekérést használja. Az alábbi funkciók NINCSENEK implementálva:
- Jármű hozzáadása (POST /assets/vehicles)
- Jármű részletes adatlapja (GET /assets/{id})
- Költségnapló megjelenítése/rögzítése
- Szerviznapló (karbantartási rekordok)
- Katalógus böngésző (márka -> modell -> generáció -> motor)
- TCO analitika és dashboard
- Jármű értékelés
- Dokumentumfeltöltés
- Útnyilvántartás (logbook)
---
## 4. 🎯 Frontend Csoportosítási Javaslat
### 4.1 Javasolt Menü Szerkezet
Az alábbi csoportosítás a DDD (Domain-Driven Design) elveket követi, és a felhasználói élményt (UX) helyezi előtérbe:
```
GARÁZS (fő nézet)
├── 🔧 JÁRMŰVEK
│ ├── Garázs áttekintés <- Jelenlegi DashboardView
│ ├── Jármű hozzáadása <- POST /assets/vehicles
│ ├── Katalógus böngésző <- GET /catalog/makes -> models -> engines
│ └── Jármű részletek <- GET /assets/{id}
├── 📊 PÉNZÜGYEK & TCO
│ ├── Költségnapló <- GET /assets/{id}/costs, POST /expenses
│ ├── Pénzügyi riport <- GET /assets/{id}/financial-summary
│ ├── TCO Analitika <- GET /analytics/{id}/summary
│ └── Dashboard analitika <- GET /analytics/dashboard
├── 🔧 KARBANTARTÁS
│ ├── Szerviznapló <- GET /assets/{id}/maintenance
│ ├── Új szerviz bejegyzés <- POST /assets/{id}/maintenance
│ ├── Eseménynapló <- AssetEvent adatok
│ └── Km óra állapot <- GET /admin/odometer/{id}
├── 📄 DOKUMENTUMOK
│ ├── Feltöltés <- POST /documents/upload/vehicle/{id}
│ └── OCR szkennelés <- POST /evidence/scan-registration
└── ⭐ KÖZÖSSÉG
├── Értékelés <- POST /vehicles/{id}/ratings
└── Vélemények <- GET /vehicles/{id}/ratings
```
### 4.2 Javasolt Vue Router Struktúra
```
/dashboard Garázs áttekintés (DashboardView)
/vehicles/add Jármű hozzáadása
/vehicles/catalog Katalógus böngésző
/vehicles/{id} Jármű részletes nézet
/vehicles/{id}/costs Költségek
/vehicles/{id}/maintenance Szerviznapló
/vehicles/{id}/analytics TCO Analitika
/vehicles/{id}/documents Dokumentumok
/vehicles/{id}/ratings Értékelések
```
### 4.3 Javasolt Store Bővítés
A jelenlegi vehicleStore bővítendő az alábbiakkal:
```
ÚJ state-ek:
- selectedVehicle Aktuálisan kiválasztott jármű
- vehicleCosts Költségnapló tételek
- maintenanceRecords Szerviz rekordok
- financialSummary Pénzügyi összesítő
ÚJ katalógus state-ek:
- catalogMakes Márkák listája
- catalogModels Modellek listája
- catalogGenerations Generációk listája
- catalogEngines Motorváltozatok listája
ÚJ action-ök:
- fetchVehicleDetail(id)
- createVehicle(payload)
- fetchCosts(assetId)
- createExpense(payload)
- fetchMaintenance(assetId)
- fetchFinancialSummary(assetId)
- fetchAnalytics(assetId)
- fetchMakes(vehicleClass?)
- fetchModels(make)
- fetchGenerations(make, model)
- fetchEngines(make, model, gen)
```
### 4.4 UX/UI Javaslatok
1. **Garázs Szűrők:** A járművek szűrhetők legyenek járműosztály (personal, motorcycle, commercial), státusz (active, draft, maintenance) és márka szerint.
2. **Jármű Kártya Információs Rétegek:**
- 1. réteg (alap): Rendszám, márka, modell, státusz - látszik a grid-ben
- 2. réteg (hover): Gyors infók (km, állapot, utolsó szerviz)
- 3. réteg (kattintás): Teljes részletes nézet
3. **TCO Dashboard:** A garázs nézet tetején egy összesítő sáv (teljes flotta km, havi költség, átlagos állapot). Minden járműkártyán egy "TCO" gomb.
4. **Katalógus Böngésző:** Lépcsőzetes dropdown-ok (márka -> modell -> generáció -> motor). Gyors hozzáadás: "Add this to my garage" gomb.
---
## 5. 📝 Összefoglalás
A backend rendelkezésre álló API végpontok lefedik a járműkezelés teljes életciklusát:
| Fázis | API Végpontok | Állapot |
|-------|--------------|---------|
| Felfedezés (Katalógus) | /catalog/makes, /catalog/models, /catalog/generations, /catalog/engines | Backend kész |
| Létrehozás | POST /assets/vehicles | Backend kész |
| Megtekintés (Garázs) | GET /assets/vehicles, GET /assets/{id} | Backend kész, Frontend részleges |
| Költségkövetés (TCO) | POST /expenses/, GET /assets/{id}/costs, GET /assets/{id}/financial-summary | Backend kész, Frontend hiányzik |
| Karbantartás | GET/POST /assets/{id}/maintenance | Backend kész, Frontend hiányzik |
| Analitika | GET /analytics/{id}/summary, GET /analytics/dashboard | Backend kész (részben mock), Frontend hiányzik |
| Értékelés | GET/POST /vehicles/{id}/ratings | Backend kész, Frontend hiányzik |
| Dokumentumok | POST /documents/upload/{parent_type}/{parent_id} | Backend kész, Frontend hiányzik |
**Következtetés:** A backend erős alapokon áll a járműkezeléshez. A frontenden a garázs nézet (DashboardView) csak a járművek listázását valósítja meg. A javasolt csoportosítás (Járművek -> Pénzügyek -> Karbantartás -> Dokumentumok -> Közösség) egy logikus, felhasználó-központú navigációs struktúrát adna a teljes jármű életciklus kezeléséhez.

View File

@@ -3,6 +3,12 @@ import { ref, computed } from 'vue'
import router from '../router' import router from '../router'
import api from '../api/axios' import api from '../api/axios'
/**
* Backend now sends address fields directly on the person object
* with address_ prefix (e.g. address_city, address_zip, address_floor).
* This interface is kept for backward compatibility but the actual
* data comes as flat fields on PersonData.
*/
export interface AddressData { export interface AddressData {
zip: string | null zip: string | null
city: string | null city: string | null
@@ -28,7 +34,20 @@ export interface PersonData {
identity_docs: Record<string, any> | null identity_docs: Record<string, any> | null
ice_contact: Record<string, any> | null ice_contact: Record<string, any> | null
is_active: boolean is_active: boolean
/** @deprecated Backend now sends address_* fields flat on person */
address: AddressData | null address: AddressData | null
// Flat address fields (new backend format)
address_zip: string | null
address_city: string | null
address_street_name: string | null
address_street_type: string | null
address_house_number: string | null
address_stairwell: string | null
address_floor: string | null
address_door: string | null
address_hrsz: string | null
address_parcel_id: string | null
address_full_text: string | null
} }
export interface UserProfile { export interface UserProfile {

View File

@@ -403,13 +403,13 @@
<div v-else class="text-sm space-y-1"> <div v-else class="text-sm space-y-1">
<p class="text-white font-medium"> <p class="text-white font-medium">
<template v-if="authStore.user?.person?.address?.zip || authStore.user?.person?.address?.city"> <template v-if="authStore.user?.person?.address?.address_zip || authStore.user?.person?.address?.address_city">
{{ authStore.user?.person?.address?.zip }} {{ authStore.user?.person?.address?.city }}, {{ authStore.user?.person?.address?.address_zip }} {{ authStore.user?.person?.address?.address_city }},
</template> </template>
{{ authStore.user?.person?.address?.street_name }} {{ authStore.user?.person?.address?.street_type }} {{ authStore.user?.person?.address?.house_number }} {{ authStore.user?.person?.address?.address_street_name }} {{ authStore.user?.person?.address?.address_street_type }} {{ authStore.user?.person?.address?.address_house_number }}
</p> </p>
<p v-if="authStore.user?.person?.address?.stairwell || authStore.user?.person?.address?.floor || authStore.user?.person?.address?.door" class="text-white font-medium"> <p v-if="authStore.user?.person?.address?.address_stairwell || authStore.user?.person?.address?.address_floor || authStore.user?.person?.address?.address_door" class="text-white font-medium">
Lépcsőház: {{ authStore.user?.person?.address?.stairwell || '' }}, Emelet: {{ authStore.user?.person?.address?.floor || '' }}, Ajtó: {{ authStore.user?.person?.address?.door || '' }} Lépcsőház: {{ authStore.user?.person?.address?.address_stairwell || '' }}, Emelet: {{ authStore.user?.person?.address?.address_floor || '' }}, Ajtó: {{ authStore.user?.person?.address?.address_door || '' }}
</p> </p>
</div> </div>
</div> </div>
@@ -427,8 +427,8 @@
<!-- 🩻 DIAGNOSTIC JSON DUMP --> <!-- 🩻 DIAGNOSTIC JSON DUMP -->
<div class="rounded-2xl border border-white/10 bg-black/40 p-4 mt-6 backdrop-blur-xl shadow-xl shadow-black/20"> <div class="rounded-2xl border border-white/10 bg-black/40 p-4 mt-6 backdrop-blur-xl shadow-xl shadow-black/20">
<details> <details>
<summary class="text-xs font-semibold text-[#00E5A0] cursor-pointer select-none mb-2">🩻 RÖNTGEN: authStore.user?.person?.address</summary> <summary class="text-xs font-semibold text-[#00E5A0] cursor-pointer select-none mb-2">🩻 RÖNTGEN: person (address_* fields)</summary>
<pre class="text-xs text-green-400 bg-black p-4 mt-2 overflow-auto rounded-lg max-h-64">{{ JSON.stringify(authStore.user?.person?.address, null, 2) }}</pre> <pre class="text-xs text-green-400 bg-black p-4 mt-2 overflow-auto rounded-lg max-h-64">{{ JSON.stringify(authStore.user?.person, null, 2) }}</pre>
</details> </details>
</div> </div>
@@ -575,7 +575,6 @@ const isPageLoading = ref(true)
// ── Computed helpers ────────────────────────────────────────────────── // ── Computed helpers ──────────────────────────────────────────────────
const person = computed(() => authStore.user?.person ?? null) const person = computed(() => authStore.user?.person ?? null)
const address = computed(() => person.value?.address ?? null)
const identityDocs = computed(() => person.value?.identity_docs ?? null) const identityDocs = computed(() => person.value?.identity_docs ?? null)
const isCityLoading = ref(false) const isCityLoading = ref(false)
@@ -641,7 +640,6 @@ function formatDate(dateVal: string | null | undefined): string {
function startEditing() { function startEditing() {
const p = person.value const p = person.value
const a = address.value
const docs = identityDocs.value const docs = identityDocs.value
editForm.first_name = p?.first_name || '' editForm.first_name = p?.first_name || ''
@@ -659,15 +657,16 @@ function startEditing() {
editForm.license_expiry = formatDate(docs?.LICENSE?.expiry_date) || '' editForm.license_expiry = formatDate(docs?.LICENSE?.expiry_date) || ''
editForm.license_categories = docs?.LICENSE?.categories || '' editForm.license_categories = docs?.LICENSE?.categories || ''
// Address // Address — backend sends address_* fields nested inside person.address
editForm.address_zip = a?.zip || '' const addr = p?.address
editForm.address_city = a?.city || '' editForm.address_zip = addr?.address_zip || ''
editForm.address_street_name = a?.street_name || '' editForm.address_city = addr?.address_city || ''
editForm.address_street_type = a?.street_type || '' editForm.address_street_name = addr?.address_street_name || ''
editForm.address_house_number = a?.house_number || '' editForm.address_street_type = addr?.address_street_type || ''
editForm.address_stairwell = a?.stairwell || '' editForm.address_house_number = addr?.address_house_number || ''
editForm.address_floor = a?.floor || '' editForm.address_stairwell = addr?.address_stairwell || ''
editForm.address_door = a?.door || '' editForm.address_floor = addr?.address_floor || ''
editForm.address_door = addr?.address_door || ''
saveMessage.value = '' saveMessage.value = ''
isEditing.value = true isEditing.value = true

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Playwright E2E Test: Profile Address Persistence (Stairwell / Floor / Door)
This test:
1. Logs in as the configured test user (via LoginModal on the landing page).
2. Navigates to the Profile page (/profile).
3. Enters edit mode, fills every address field (city, zip, street, house number,
stairwell, floor, door), and saves.
4. Reloads the page and verifies that the stairwell, floor, and door values
persisted after the page refresh.
Requirements:
- playwright (Python) + dotenv
- .env.test file at the project root with TEST_USER_EMAIL and TEST_USER_PASS
Usage:
python tests/active/e2e_profile_address.py
To see the browser visually, change HEADLESS=False below.
"""
import os
import sys
import re
from pathlib import Path
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout
# ── Configuration ──────────────────────────────────────────────────────────
BASE_URL = "https://app.servicefinder.hu"
HEADLESS = True # Set to False for visual debugging
SLOW_MO = 0 # ms delay between actions (useful for debugging)
ACTION_TIMEOUT = 15_000 # ms default timeout for element lookups
# ── Load .env.test from project root ──────────────────────────────────────
ENV_PATH = Path(__file__).resolve().parents[1] / ".env.test"
if not ENV_PATH.exists():
raise FileNotFoundError(
f".env.test not found at {ENV_PATH}. "
"Create it with TEST_USER_EMAIL and TEST_USER_PASS."
)
load_dotenv(dotenv_path=ENV_PATH, override=True)
TEST_EMAIL = os.getenv("TEST_USER_EMAIL")
TEST_PASS = os.getenv("TEST_USER_PASS")
if not TEST_EMAIL or not TEST_PASS:
raise ValueError(
"TEST_USER_EMAIL and/or TEST_USER_PASS are missing from .env.test. "
f"Loaded from: {ENV_PATH}"
)
# ── Test data ──────────────────────────────────────────────────────────────
ADDRESS_DATA = {
"address_city": "Dunakeszi",
"address_zip": "2120",
"address_street_name": "Határ",
"address_street_type": "utca",
"address_house_number": "8",
"address_stairwell": "A",
"address_floor": "3",
"address_door": "5",
}
# ═══════════════════════════════════════════════════════════════════════════
# Helper functions
# ═══════════════════════════════════════════════════════════════════════════
def log(msg: str) -> None:
"""Simple stdout logger."""
print(f"[E2E] {msg}")
def fill_input(page, placeholder: str, value: str) -> None:
"""Locate an <input> by its placeholder and fill it."""
locator = page.locator(f'input[placeholder="{placeholder}"]')
locator.wait_for(state="visible", timeout=ACTION_TIMEOUT)
locator.fill(value)
log(f" Filled '{placeholder}' → '{value}'")
# ═══════════════════════════════════════════════════════════════════════════
# Main test flow
# ═══════════════════════════════════════════════════════════════════════════
def main() -> None:
with sync_playwright() as pw:
# ── 1. Launch browser (local or remote via BROWSERLESS_URL) ──
browserless_url = os.getenv("BROWSERLESS_URL")
if browserless_url:
# Connect to a remote Browserless / Chrome container
browser = pw.chromium.connect_over_cdp(browserless_url)
log(f"Connected to remote browser at {browserless_url}")
else:
# Local fallback
browser = pw.chromium.launch(
headless=os.getenv("HEADLESS", "True").lower() == "true",
slow_mo=SLOW_MO,
)
log(f"Browser launched locally (headless={os.getenv('HEADLESS', 'True')})")
context = browser.new_context(
viewport={"width": 1280, "height": 800},
locale="hu-HU",
)
page = context.new_page()
log(f"Browser launched (headless={HEADLESS})")
# ── 2. Open landing page & login ─────────────────────────────
log("Navigating to landing page …")
page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
# Click the "Garázs Nyitása" button to open the LoginModal
login_trigger = page.locator("button:has-text('Garázs Nyitása')")
login_trigger.wait_for(state="visible", timeout=ACTION_TIMEOUT)
login_trigger.click()
log("Login modal opened")
# Fill email & password
page.fill("#login-email", TEST_EMAIL)
page.fill("#login-password", TEST_PASS)
log("Login credentials entered")
# Click the submit button (the first .btn-premium inside the visible form)
submit_btn = page.locator("form button[type='submit']").first
submit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
submit_btn.click()
log("Login submitted waiting for navigation …")
# Wait for redirect away from landing page (to /dashboard or /complete-kyc)
try:
page.wait_for_url(
lambda url: "/dashboard" in url or "/complete-kyc" in url,
timeout=15_000,
)
except PwTimeout:
# If still on landing, maybe login failed dump state
log(f"URL after login attempt: {page.url}")
page.screenshot(path="/tmp/e2e_login_fail.png")
raise AssertionError(
"Login failed did not redirect to dashboard. "
"Check credentials or UI state."
)
log(f"Login successful current URL: {page.url}")
# ── 3. Navigate to Profile page ──────────────────────────────
log("Navigating to /profile …")
page.goto(f"{BASE_URL}/profile", wait_until="networkidle", timeout=20_000)
# Wait for the profile page to finish loading (spinner disappears)
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
# Also wait until the loading spinner is gone
page.wait_for_function(
"() => !document.querySelector('.animate-spin')",
timeout=15_000,
)
log("Profile page loaded")
# ── 4. Enter edit mode ───────────────────────────────────────
edit_btn = page.locator("button:has-text('Szerkesztés')")
edit_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
edit_btn.click()
log("Edit mode activated")
# ── 5. Fill in all address fields ────────────────────────────
log("Filling address form …")
fill_input(page, "Irányítószám", ADDRESS_DATA["address_zip"])
fill_input(page, "Város", ADDRESS_DATA["address_city"])
fill_input(page, "Utca", ADDRESS_DATA["address_street_name"])
fill_input(page, "Közterület jellege", ADDRESS_DATA["address_street_type"])
fill_input(page, "Házszám", ADDRESS_DATA["address_house_number"])
fill_input(page, "Lépcsőház", ADDRESS_DATA["address_stairwell"])
fill_input(page, "Emelet", ADDRESS_DATA["address_floor"])
fill_input(page, "Ajtó", ADDRESS_DATA["address_door"])
# ── 6. Save ──────────────────────────────────────────────────
save_btn = page.locator("button:has-text('Mentés')")
save_btn.wait_for(state="visible", timeout=ACTION_TIMEOUT)
save_btn.click()
log("Save clicked waiting for confirmation …")
# Wait for the success message to appear
success_locator = page.locator("text=A profil adatok és okmányok sikeresen frissítve!")
try:
success_locator.wait_for(state="visible", timeout=10_000)
log("Save succeeded success message visible")
except PwTimeout:
# Check for error message
error_locator = page.locator(".text-red-400")
if error_locator.is_visible():
error_text = error_locator.text_content()
log(f"Save FAILED with error: {error_text}")
page.screenshot(path="/tmp/e2e_save_fail.png")
raise AssertionError(f"Save failed: {error_text}")
page.screenshot(path="/tmp/e2e_save_timeout.png")
raise AssertionError(
"Save timeout neither success nor error message appeared."
)
# ── 7. Reload and verify persistence ─────────────────────────
log("Reloading page to verify persistence …")
page.reload(wait_until="networkidle")
page.wait_for_selector("text=Profil beállítások", timeout=ACTION_TIMEOUT)
page.wait_for_function(
"() => !document.querySelector('.animate-spin')",
timeout=15_000,
)
log("Page reloaded")
# Read the display text in the read-only address section
# The read-only view shows: Lépcsőház: …, Emelet: …, Ajtó: …
page_body = page.locator("body").inner_text()
log("── Read-only address block ──")
# Use a regex to extract values
stairwell_match = re.search(r"Lépcsőház:\s*(\S+)", page_body)
floor_match = re.search(r"Emelet:\s*(\S+)", page_body)
door_match = re.search(r"Ajtó:\s*(\S+)", page_body)
stairwell_val = stairwell_match.group(1) if stairwell_match else ""
floor_val = floor_match.group(1) if floor_match else ""
door_val = door_match.group(1) if door_match else ""
log(f" Stairwell (Lépcsőház): '{stairwell_val}'")
log(f" Floor (Emelet): '{floor_val}'")
log(f" Door (Ajtó): '{door_val}'")
# ── 8. Assertion: all three values must be present ───────────
errors = []
if not stairwell_val or stairwell_val in ("—", "null", "None"):
errors.append(f"stairwell (expected '{ADDRESS_DATA['address_stairwell']}', got '{stairwell_val}')")
if not floor_val or floor_val in ("—", "null", "None"):
errors.append(f"floor (expected '{ADDRESS_DATA['address_floor']}', got '{floor_val}')")
if not door_val or door_val in ("—", "null", "None"):
errors.append(f"door (expected '{ADDRESS_DATA['address_door']}', got '{door_val}')")
if errors:
page.screenshot(path="/tmp/e2e_persistence_fail.png")
raise AssertionError(
"Hibás backend mentés! Az emelet/ajtó adatok elvesztek az oldal "
f"frissítése után.\n Missing/empty fields: {', '.join(errors)}"
)
log("Sikeres E2E teszt: Az adatok megmaradtak a backend frissítés után is!")
log("ALL CHECKS PASSED.\n")