2026.03.30 front és garázs logika

This commit is contained in:
Roo
2026-03-30 06:32:22 +00:00
parent ba8b6579ef
commit 2508ae7452
108 changed files with 3184 additions and 115 deletions

View File

@@ -5,6 +5,7 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload, joinedload
from app.db.session import get_db
from app.core.security import decode_token, DEFAULT_RANK_MAP
@@ -51,7 +52,7 @@ async def get_current_token_payload(
return payload
async def get_current_user(
db: AsyncSession = Depends(get_db),
db: AsyncSession = Depends(get_db),
payload: Dict = Depends(get_current_token_payload)
) -> User:
"""
@@ -60,17 +61,19 @@ async def get_current_user(
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token azonosítási hiba."
)
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés
result = await db.execute(select(User).where(User.id == int(user_id)))
user = result.scalar_one_or_none()
# JAVÍTVA: Modern SQLAlchemy 2.0 aszinkron lekérdezés with eager loading
# We need to load the person relationship to avoid lazy loading issues
stmt = select(User).where(User.id == int(user_id)).options(joinedload(User.person))
result = await db.execute(stmt)
user = result.unique().scalar_one_or_none()
if not user or user.is_deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND,
detail="A felhasználó nem található."
)
return user

View File

@@ -30,34 +30,69 @@ async def get_user_vehicles(
This endpoint returns a paginated list of vehicles that the authenticated user
has access to (either as owner or through organization membership).
Garage-centric logic: In corporate mode, only returns vehicles physically
parked in the active organization's garages (branches).
"""
# Query assets where user is owner or organization member
from sqlalchemy import or_
from sqlalchemy import or_, select
# First, get user's organization memberships
from app.models.marketplace.organization import OrganizationMember
org_stmt = select(OrganizationMember.organization_id).where(
OrganizationMember.user_id == current_user.id
)
org_result = await db.execute(org_stmt)
user_org_ids = [row[0] for row in org_result.all()]
# Build query: assets owned by user OR assets in user's organizations
stmt = (
select(Asset)
.where(
or_(
Asset.owner_person_id == current_user.id,
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
Asset.operator_person_id == current_user.id,
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
if current_user.scope_id is None:
# Personal mode: only show vehicles owned/operated personally (no organization)
stmt = (
select(Asset)
.where(
or_(
Asset.owner_org_id.is_(None),
Asset.operator_org_id.is_(None)
),
or_(
Asset.owner_person_id == current_user.person_id,
Asset.operator_person_id == current_user.person_id
)
)
.order_by(Asset.created_at.desc())
.offset(skip)
.limit(limit)
.options(selectinload(Asset.catalog))
)
else:
# Corporate mode: only show vehicles belonging to the active organization's garages
try:
scope_org_id = int(current_user.scope_id)
except (ValueError, TypeError):
# If scope_id is not a valid integer, treat as no organization
scope_org_id = None
if scope_org_id is None:
# Fallback: no valid organization, return empty list
return []
# First, get all branch IDs (garages) for this organization
from app.models.marketplace.organization import Branch
branch_stmt = select(Branch.id).where(
Branch.organization_id == scope_org_id,
Branch.is_deleted == False,
Branch.status == "active"
)
branch_result = await db.execute(branch_stmt)
branch_ids = [row[0] for row in branch_result.all()]
if not branch_ids:
# Organization has no active garages, return empty list
return []
# Query assets that are in any of the organization's garages
stmt = (
select(Asset)
.where(
Asset.branch_id.in_(branch_ids),
Asset.status == "active"
)
.order_by(Asset.created_at.desc())
.offset(skip)
.limit(limit)
.options(selectinload(Asset.catalog))
)
.order_by(Asset.created_at.desc())
.offset(skip)
.limit(limit)
.options(selectinload(Asset.catalog))
)
result = await db.execute(stmt)
assets = result.scalars().all()
@@ -170,25 +205,16 @@ async def create_or_claim_vehicle(
- XP jutalom adása a felhasználónak
"""
try:
# Determine organization ID: use provided or default to user's first organization
org_id = payload.organization_id
if org_id is None:
# Get user's organization memberships
from app.models.marketplace.organization import OrganizationMember
org_stmt = select(OrganizationMember.organization_id).where(
OrganizationMember.user_id == current_user.id
).limit(1)
org_result = await db.execute(org_stmt)
user_org = org_result.scalar_one_or_none()
if user_org is None:
# User has no organization - create a personal organization or use default
# For now, raise an error (in future, we could create a personal org)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="No organization found for user. Please specify an organization_id or join/create an organization first."
)
org_id = user_org
# Determine organization ID based on user's active scope (garage isolation)
# The owner_org_id MUST be set to the current_user.scope_id.
# If the scope is null, it stays null (personal).
org_id = None
if current_user.scope_id is not None:
try:
org_id = int(current_user.scope_id)
except (ValueError, TypeError):
# If scope_id is not a valid integer, treat as personal (no organization)
pass
asset = await AssetService.create_or_claim_vehicle(
db=db,

View File

@@ -106,7 +106,7 @@ async def onboard_organization(
return {"organization_id": new_org.id, "status": new_org.status}
@router.get("/my", response_model=List[CorpOnboardResponse])
@router.get("/my", response_model=List[dict])
async def get_my_organizations(
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user)
@@ -120,4 +120,19 @@ async def get_my_organizations(
result = await db.execute(stmt)
orgs = result.scalars().all()
return [{"organization_id": o.id, "status": o.status} for o in orgs]
# Return full organization details
return [
{
"organization_id": o.id,
"status": o.status,
"name": o.name,
"full_name": o.full_name,
"display_name": o.display_name,
"tax_number": o.tax_number,
"country_code": o.country_code,
"is_active": o.is_active,
"is_deleted": o.is_deleted,
"subscription_plan": o.subscription_plan
}
for o in orgs
]

View File

@@ -1,10 +1,11 @@
#/opt/docker/dev/service_finder/backend/app/api/v1/endpoints/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError
from typing import Dict, Any
from app.api.deps import get_db, get_current_user
from app.schemas.user import UserResponse, UserUpdate
from app.schemas.user import UserResponse, UserUpdate, ActiveOrganizationUpdate
from app.models.identity import User
from app.services.trust_engine import TrustEngine
@@ -70,8 +71,12 @@ async def read_users_me(
# Create a response dictionary with the active_organization_id
# Get first_name and last_name from person relation if available
person = current_user.person
first_name = person.first_name if person else ""
last_name = person.last_name if person else ""
# Safe extraction with fallback to empty string
first_name = ""
last_name = ""
if person:
first_name = getattr(person, 'first_name', '')
last_name = getattr(person, 'last_name', '')
response_data = {
"id": current_user.id,
@@ -141,6 +146,59 @@ async def update_user_preferences(
else:
raise HTTPException(status_code=400, detail=f"Invalid field: {field}")
await db.commit()
await db.refresh(current_user)
return current_user
try:
await db.commit()
await db.refresh(current_user)
except SQLAlchemyError as e:
await db.rollback()
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
# Return the Pydantic model instead of raw SQLAlchemy object
return UserResponse.model_validate(current_user)
@router.patch("/me/active-organization", response_model=UserResponse)
async def update_active_organization(
update_data: ActiveOrganizationUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Update the user's active organization (scope_id).
Accepts an organization_id (UUID/string) or None to revert to personal mode.
"""
# Extract organization_id from request
org_id = update_data.organization_id
# Validate that the user has access to this organization if org_id is provided
if org_id is not None:
from sqlalchemy import select
from app.models.marketplace.organization import OrganizationMember
# Check if user is a member of the organization
stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.user_id == current_user.id
)
result = await db.execute(stmt)
member = result.scalar_one_or_none()
if not member:
raise HTTPException(
status_code=403,
detail="You are not a member of this organization"
)
# Update user's scope_id
current_user.scope_id = org_id
try:
await db.commit()
await db.refresh(current_user)
except SQLAlchemyError as e:
await db.rollback()
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
# Return updated user data
return UserResponse.model_validate(current_user)

View File

@@ -57,6 +57,10 @@ class Asset(Base):
catalog_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("vehicle.vehicle_catalog.id"))
current_organization_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
# Garage-centric hierarchy
branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet.branches.id"))
relocation_performed: Mapped[bool] = mapped_column(Boolean, server_default=text('false'), default=False)
# Identity kapcsolatok
owner_person_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey("identity.persons.id"))
owner_org_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("fleet.organizations.id"))
@@ -144,7 +148,8 @@ class AssetFinancials(Base):
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
financing_type: Mapped[str] = mapped_column(String(50))
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
financing_type: Mapped[str] = mapped_column(String(50))
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")

View File

@@ -39,6 +39,7 @@ class AssetResponse(BaseModel):
# Státusz és ellenőrzés
status: str
data_status: Optional[str] = None
is_verified: bool
verification_method: Optional[str] = None
catalog_match_score: Optional[float] = None

View File

@@ -25,4 +25,7 @@ class UserUpdate(BaseModel):
first_name: Optional[str] = None
last_name: Optional[str] = None
preferred_language: Optional[str] = None
ui_mode: Optional[str] = None
ui_mode: Optional[str] = None
class ActiveOrganizationUpdate(BaseModel):
organization_id: Optional[str] = None # UUID/string or None to revert to personal mode

View File

@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""
Garázs adatok ellenőrzése és javítása.
Ez a szkript ellenőrzi a teszt felhasználó szervezeti státuszát,
és felosztja a járműveket privát és céges flotta között.
Futtatás: docker compose exec sf_api python -m app.scripts.check_and_fix_garage_data
"""
import asyncio
import sys
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
# Add the backend directory to the path
sys.path.insert(0, '/app')
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.models.marketplace.organization import Organization, OrganizationMember
from app.models.vehicle.asset import Asset
# AssetCatalog is inside the asset module
async def main():
"""Fő végrehajtási logika."""
print("=" * 60)
print("GARÁZS ADATOK ELLENŐRZÉSE ÉS JAVÍTÁSA")
print("=" * 60)
async with AsyncSessionLocal() as db:
# 1. Keressük meg a teszt felhasználót
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
result = await db.execute(stmt)
test_user = result.scalar_one_or_none()
if not test_user:
print("❌ HIBA: A teszt felhasználó (tester_pro@profibot.hu) nem található!")
return
print(f" ✅ Teszt felhasználó megtalálva: ID={test_user.id}, Email={test_user.email}")
# 2. Ellenőrizzük, hogy a felhasználóhoz tartozik-e szervezet
print("\n2. SZERVEZETI TAGSÁG ELLENŐRZÉSE...")
org_stmt = (
select(Organization)
.join(OrganizationMember)
.where(OrganizationMember.user_id == test_user.id)
.where(Organization.is_deleted == False)
.where(Organization.is_active == True)
)
org_result = await db.execute(org_stmt)
user_organizations = org_result.scalars().all()
if user_organizations:
print(f" ✅ A felhasználó már tagja {len(user_organizations)} szervezetnek:")
for org in user_organizations:
print(f" - {org.name} (ID: {org.id}, Adószám: {org.tax_number})")
target_org = user_organizations[0]
else:
print(" A felhasználó nem tagja egyetlen szervezetnek sem. Új szervezet létrehozása...")
# Új szervezet létrehozása
new_org = Organization(
full_name="Teszt Flotta Kft.",
name="Teszt Flotta",
display_name="Teszt Flotta Kft.",
tax_number="12345678-2-42",
reg_number="01-23-456789",
country_code="HU",
language="hu",
default_currency="HUF",
address_zip="1234",
address_city="Budapest",
address_street_name="Teszt utca",
address_street_type="utca",
address_house_number="1",
folder_slug="teszt-flotta",
org_type="business",
status="active",
is_active=True,
is_deleted=False,
subscription_plan="FREE",
base_asset_limit=10,
owner_id=test_user.id
)
db.add(new_org)
await db.flush() # ID generáláshoz
await db.refresh(new_org)
# Szervezeti tagság létrehozása (ADMIN szerepkör)
org_member = OrganizationMember(
organization_id=new_org.id,
user_id=test_user.id,
role="ADMIN"
)
db.add(org_member)
await db.commit()
await db.refresh(new_org)
target_org = new_org
print(f" ✅ Új szervezet létrehozva: {target_org.name} (ID: {target_org.id})")
# 3. A felhasználó összes járművének lekérdezése
print("\n3. FELHASZNÁLÓ JÁRMŰVEINEK LEKÉRDEZÉSE...")
asset_stmt = (
select(Asset)
.where(Asset.owner_person_id == test_user.id)
.options(selectinload(Asset.catalog))
)
asset_result = await db.execute(asset_stmt)
user_assets = asset_result.scalars().all()
print(f" ✅ Összesen {len(user_assets)} jármű található a felhasználóhoz.")
if not user_assets:
print(" Nincsenek járművek a felhasználóhoz. Nincs mit felosztani.")
return
# 4. Járművek felosztása privát és céges között
print("\n4. JÁRMŰVEK FELOSZTÁSA PRIVÁT ÉS CÉGES FLOTTA KÖZÖTT...")
# Számoljuk meg, hány jármű van már privát és hány céges
private_count = 0
corporate_count = 0
for asset in user_assets:
if asset.owner_org_id is None:
private_count += 1
else:
corporate_count += 1
print(f" Jelenlegi állapot: {private_count} privát, {corporate_count} céges jármű")
# Ha minden jármű ugyanabban a kategóriában van, felosztjuk őket
if private_count == 0 or corporate_count == 0:
print(" Járművek újraelosztása 50-50% arányban...")
# Felosztás fele-fele arányban
half_index = len(user_assets) // 2
for i, asset in enumerate(user_assets):
if i < half_index:
# Első fele: maradjon privát (owner_org_id = None)
if asset.owner_org_id is not None:
asset.owner_org_id = None
print(f" 🚗 {asset.id}: Privát módra állítva")
else:
# Második fele: legyen céges (owner_org_id = target_org.id)
if asset.owner_org_id != target_org.id:
asset.owner_org_id = target_org.id
print(f" 🏢 {asset.id}: Céges flottához rendelve (Szervezet: {target_org.name})")
await db.commit()
print(f"{half_index} jármű privát, {len(user_assets) - half_index} jármű céges módra állítva.")
else:
print(" ✅ A járművek már megfelelően fel vannak osztva. Nincs szükség módosításra.")
# 5. Végeredmény összefoglaló
print("\n" + "=" * 60)
print("VÉGEREDMÉNY ÖSSZEFOGLALÓ")
print("=" * 60)
# Új lekérdezés a frissített adatokhoz
asset_result = await db.execute(asset_stmt)
user_assets = asset_result.scalars().all()
private_assets = [a for a in user_assets if a.owner_org_id is None]
corporate_assets = [a for a in user_assets if a.owner_org_id == target_org.id]
other_assets = [a for a in user_assets if a.owner_org_id not in [None, target_org.id]]
print(f"\n📊 TESZT FELHASZNÁLÓ ÁLLAPOTA:")
print(f" • Email: {test_user.email}")
print(f" • User ID: {test_user.id}")
print(f" • Aktív szervezet: {target_org.name} (ID: {target_org.id})")
print(f" • Szerepkör a szervezetben: ADMIN")
print(f"\n🚗 JÁRMŰVEGYÜTT ÁLLAPOTA:")
print(f" • Összes jármű: {len(user_assets)} db")
print(f" • Privát garázs (owner_org_id = NULL): {len(private_assets)} db")
print(f" • Céges flotta ({target_org.name}): {len(corporate_assets)} db")
if other_assets:
print(f" • Egyéb szervezetekhez rendelve: {len(other_assets)} db")
print(f"\n📋 PRIVÁT JÁRMŰVEK:")
for asset in private_assets[:5]: # Csak az első 5-öt mutatjuk
catalog_name = asset.catalog.make + " " + asset.catalog.model if asset.catalog else "Ismeretlen"
print(f"{catalog_name} (Asset ID: {asset.id})")
if len(private_assets) > 5:
print(f" • ... és még {len(private_assets) - 5} további")
print(f"\n🏢 CÉGES JÁRMŰVEK:")
for asset in corporate_assets[:5]:
catalog_name = asset.catalog.make + " " + asset.catalog.model if asset.catalog else "Ismeretlen"
print(f"{catalog_name} (Asset ID: {asset.id})")
if len(corporate_assets) > 5:
print(f" • ... és még {len(corporate_assets) - 5} további")
print("\n" + "=" * 60)
print("A tesztadatok sikeresen előkészítve!")
print("Most tesztelhető a Garage UI switcher funkció.")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""
Egyszerűbb változat: Garázs adatok ellenőrzése és javítása.
"""
import asyncio
import sys
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
sys.path.insert(0, '/app')
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.models.marketplace.organization import Organization, OrganizationMember
from app.models.vehicle.asset import Asset
async def main():
print("=" * 60)
print("GARÁZS ADATOK ELLENŐRZÉSE (Egyszerű változat)")
print("=" * 60)
async with AsyncSessionLocal() as db:
# 1. Keressük meg a teszt felhasználót
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
result = await db.execute(stmt)
test_user = result.scalar_one_or_none()
if not test_user:
print("❌ HIBA: A teszt felhasználó nem található!")
return
print(f" ✅ Teszt felhasználó: ID={test_user.id}, Email={test_user.email}")
# 2. Ellenőrizzük a szervezeti tagságot
print("\n2. SZERVEZETI TAGSÁG ELLENŐRZÉSE...")
org_stmt = (
select(Organization)
.join(OrganizationMember)
.where(OrganizationMember.user_id == test_user.id)
.where(Organization.is_deleted == False)
)
org_result = await db.execute(org_stmt)
user_orgs = org_result.scalars().all()
if user_orgs:
print(f" ✅ A felhasználó tagja {len(user_orgs)} szervezetnek:")
for org in user_orgs:
print(f" - {org.name} (ID: {org.id})")
target_org = user_orgs[0]
else:
print(" Nincs szervezet. Új létrehozása...")
# Egyszerű szervezet létrehozás
new_org = Organization(
full_name="Teszt Flotta Kft.",
name="Teszt Flotta",
display_name="Teszt Flotta Kft.",
tax_number="12345678-2-42",
country_code="HU",
folder_slug="teszt-flotta-123",
org_type="business",
status="active",
is_active=True,
owner_id=test_user.id
)
db.add(new_org)
await db.flush()
org_member = OrganizationMember(
organization_id=new_org.id,
user_id=test_user.id,
role="ADMIN"
)
db.add(org_member)
await db.commit()
await db.refresh(new_org)
target_org = new_org
print(f" ✅ Új szervezet: {target_org.name} (ID: {target_org.id})")
# 3. Járművek lekérdezése
print("\n3. JÁRMŰVEK LEKÉRDEZÉSE...")
asset_stmt = (
select(Asset)
.where(Asset.owner_person_id == test_user.id)
)
asset_result = await db.execute(asset_stmt)
assets = asset_result.scalars().all()
print(f" ✅ Összesen {len(assets)} jármű található.")
if not assets:
print(" Nincsenek járművek. Nincs mit felosztani.")
return
# 4. Jelenlegi felosztás
private = [a for a in assets if a.owner_org_id is None]
corporate = [a for a in assets if a.owner_org_id == target_org.id]
other = [a for a in assets if a.owner_org_id not in [None, target_org.id]]
print(f"\n JELENLEGI ÁLLAPOT:")
print(f" • Privát: {len(private)} db")
print(f" • Céges ({target_org.name}): {len(corporate)} db")
if other:
print(f" • Egyéb: {len(other)} db")
# 5. Ha nincs elég adat, felosztjuk
if len(private) == 0 or len(corporate) == 0:
print("\n Járművek felosztása...")
half = len(assets) // 2
for i, asset in enumerate(assets):
if i < half:
asset.owner_org_id = None
else:
asset.owner_org_id = target_org.id
await db.commit()
print(f" ✅ Felosztva: {half} privát, {len(assets)-half} céges")
else:
print("\n ✅ A járművek már megfelelően fel vannak osztva.")
# 6. Végeredmény
print("\n" + "=" * 60)
print("VÉGEREDMÉNY:")
print("=" * 60)
print(f"\n📋 TESZT FELHASZNÁLÓ:")
print(f" • Email: {test_user.email}")
print(f" • User ID: {test_user.id}")
print(f" • Szervezet: {target_org.name} (ID: {target_org.id})")
# Új lekérdezés
asset_result = await db.execute(asset_stmt)
assets = asset_result.scalars().all()
private = [a for a in assets if a.owner_org_id is None]
corporate = [a for a in assets if a.owner_org_id == target_org.id]
print(f"\n🚗 JÁRMŰVEGYÜTT:")
print(f" • Összesen: {len(assets)} db")
print(f" • Privát garázs: {len(private)} db")
print(f" • Céges flotta: {len(corporate)} db")
print("\n" + "=" * 60)
print("KÉSZ! A tesztadatok előkészítve.")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
Check Person data for User 28.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
from app.models.identity.identity import User, Person
async def check_person():
async with AsyncSessionLocal() as db:
# Get User 28
user_stmt = select(User).where(User.id == 28)
user_result = await db.execute(user_stmt)
test_user = user_result.scalar_one_or_none()
if not test_user:
print("❌ User 28 not found")
return
print(f"✅ User 28:")
print(f" - User ID: {test_user.id}")
print(f" - Person ID: {test_user.person_id}")
print(f" - Email: {test_user.email}")
# Get the Person record
if test_user.person_id:
person_stmt = select(Person).where(Person.id == test_user.person_id)
person_result = await db.execute(person_stmt)
person = person_result.scalar_one_or_none()
if person:
print(f"\n✅ Person {person.id}:")
print(f" - First name: {person.first_name}")
print(f" - Last name: {person.last_name}")
print(f" - Date of birth: {person.date_of_birth}")
print(f" - Gender: {person.gender}")
else:
print(f"\n❌ Person with ID {test_user.person_id} not found")
else:
print("\n❌ User has no person_id")
# Check if there's a Person with ID 28
person28_stmt = select(Person).where(Person.id == 28)
person28_result = await db.execute(person28_stmt)
person28 = person28_result.scalar_one_or_none()
if person28:
print(f"\n⚠️ Person with ID 28 exists:")
print(f" - First name: {person28.first_name}")
print(f" - Last name: {person28.last_name}")
print(f" - Date of birth: {person28.date_of_birth}")
print(f" - Gender: {person28.gender}")
# Check which user is linked to this person
user_for_person28_stmt = select(User).where(User.person_id == 28)
user_for_person28_result = await db.execute(user_for_person28_stmt)
user_for_person28 = user_for_person28_result.scalar_one_or_none()
if user_for_person28:
print(f" - Linked to User: {user_for_person28.email} (ID: {user_for_person28.id})")
else:
print(f" - Not linked to any user")
if __name__ == "__main__":
asyncio.run(check_person())

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
Check detailed vehicle data for MNO-345 and PQR-678.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
from app.models.vehicle.asset import Asset
async def check_details():
async with AsyncSessionLocal() as db:
# Get specific vehicles
asset_stmt = select(Asset).where(
Asset.license_plate.in_(["MNO-345", "PQR-678"])
)
asset_result = await db.execute(asset_stmt)
assets = asset_result.scalars().all()
print(f"✅ Found {len(assets)} vehicles:")
for asset in assets:
print(f"\n Vehicle: {asset.license_plate}")
print(f" - Asset ID: {asset.id}")
print(f" - Owner Person ID: {asset.owner_person_id}")
print(f" - Operator Person ID: {asset.operator_person_id}")
print(f" - Owner Org ID: {asset.owner_org_id}")
print(f" - Operator Org ID: {asset.operator_org_id}")
print(f" - Branch ID: {asset.branch_id}")
print(f" - Current Org ID: {asset.current_organization_id}")
print(f" - Status: {asset.status}")
print(f" - Data Status: {asset.data_status}")
# Check personal mode conditions
owner_org_none = asset.owner_org_id is None
operator_org_none = asset.operator_org_id is None
print(f" - Owner Org is None: {owner_org_none}")
print(f" - Operator Org is None: {operator_org_none}")
# Check if it would appear in personal mode
would_appear_personal = (owner_org_none or operator_org_none)
print(f" - Would appear in personal mode: {would_appear_personal}")
# Check corporate mode conditions
print(f" - Would appear in corporate mode (org 15): {asset.current_organization_id == 15}")
print(f" - Would appear in corporate mode (org 21): {asset.current_organization_id == 21}")
if __name__ == "__main__":
asyncio.run(check_details())

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""
Teszt járművek létrehozása a teszt felhasználóhoz.
"""
import asyncio
import uuid
import sys
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
sys.path.insert(0, '/app')
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.models.marketplace.organization import Organization, OrganizationMember
from app.models.vehicle.asset import Asset
from app.models.vehicle.vehicle_definitions import VehicleModelDefinition
async def main():
print("=" * 60)
print("TESZT JÁRMŰVEK LÉTREHOZÁSA")
print("=" * 60)
async with AsyncSessionLocal() as db:
# 1. Keressük meg a teszt felhasználót
print("\n1. TESZT FELHASZNÁLÓ KERESÉSE...")
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
result = await db.execute(stmt)
test_user = result.scalar_one_or_none()
if not test_user:
print("❌ HIBA: A teszt felhasználó nem található!")
return
print(f" ✅ Teszt felhasználó: ID={test_user.id}")
# 2. Keressük meg a szervezetet
print("\n2. SZERVEZET KERESÉSE...")
org_stmt = (
select(Organization)
.join(OrganizationMember)
.where(OrganizationMember.user_id == test_user.id)
.where(Organization.is_deleted == False)
.limit(1)
)
org_result = await db.execute(org_stmt)
organization = org_result.scalar_one_or_none()
if not organization:
print("❌ HIBA: Nincs szervezet a felhasználóhoz!")
return
print(f" ✅ Szervezet: {organization.name} (ID: {organization.id})")
# 3. Keressünk néhány járműmodellt a katalógusból
print("\n3. JÁRMŰMODELLEK KERESÉSE A KATALÓGUSBÓL...")
catalog_stmt = select(VehicleModelDefinition).limit(10)
catalog_result = await db.execute(catalog_stmt)
catalog_models = catalog_result.scalars().all()
if not catalog_models:
print("❌ HIBA: Nincsenek járműmodellek a katalógusban!")
return
print(f"{len(catalog_models)} járműmodell található a katalógusban.")
# 4. Hozzunk létre teszt járműveket
print("\n4. TESZT JÁRMŰVEK LÉTREHOZÁSA...")
test_vehicles = [
# (név, rendszám, catalog_id, privát vagy céges)
("Privát Audi", "ABC-123", catalog_models[0].id, True),
("Privát BMW", "DEF-456", catalog_models[1].id, True),
("Privát Mercedes", "GHI-789", catalog_models[2].id, True),
("Céges Ford", "JKL-012", catalog_models[3].id, False),
("Céges Toyota", "MNO-345", catalog_models[4].id, False),
("Céges Volkswagen", "PQR-678", catalog_models[5].id, False),
]
created_count = 0
for name, license_plate, catalog_id, is_private in test_vehicles:
# Ellenőrizzük, hogy már létezik-e ilyen rendszámú jármű
existing_stmt = select(Asset).where(Asset.license_plate == license_plate)
existing_result = await db.execute(existing_stmt)
if existing_result.scalar_one_or_none():
print(f" ⚠️ '{license_plate}' rendszámú jármű már létezik, kihagyva.")
continue
# Új Asset létrehozása
new_asset = Asset(
catalog_id=catalog_id,
license_plate=license_plate,
name=name,
owner_person_id=test_user.id,
owner_org_id=None if is_private else organization.id,
status="active",
price=15000000 if is_private else 20000000, # 15-20 millió HUF
currency="HUF",
individual_equipment={},
created_at=datetime.now()
)
db.add(new_asset)
created_count += 1
mode_text = "Privát" if is_private else "Céges"
print(f"{mode_text} jármű létrehozva: {name} ({license_plate})")
await db.commit()
# 5. Végeredmény
print("\n" + "=" * 60)
print("VÉGEREDMÉNY:")
print("=" * 60)
# Járművek számolása
private_stmt = select(Asset).where(
Asset.owner_person_id == test_user.id,
Asset.owner_org_id.is_(None)
)
private_result = await db.execute(private_stmt)
private_count = len(private_result.scalars().all())
corporate_stmt = select(Asset).where(
Asset.owner_person_id == test_user.id,
Asset.owner_org_id == organization.id
)
corporate_result = await db.execute(corporate_stmt)
corporate_count = len(corporate_result.scalars().all())
print(f"\n📊 ÖSSZEFOGLALÓ:")
print(f" • Felhasználó: {test_user.email}")
print(f" • Szervezet: {organization.name}")
print(f" • Új járművek létrehozva: {created_count}")
print(f" • Összes privát jármű: {private_count} db")
print(f" • Összes céges jármű: {corporate_count} db")
print("\n" + "=" * 60)
print("KÉSZ! A teszt járművek létrehozva.")
print("Most már tesztelhető a Garage UI switcher.")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Debug the scope_id issue.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
from app.models.identity.identity import User
from app.core.security import decode_token
# Token from verification test (hardcoded for now, we'll get it dynamically)
TEST_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsInJvbGUiOiJhZG1pbiIsInJhbmsiOjEsInNjb3BlX2xldmVsIjoib3JnYW5pemF0aW9uIiwic2NvcGVfaWQiOiIyOCIsImV4cCI6MTc3NDg2MjQwMCwiaWF0IjoxNzQzMzI2NDAwLCJ0eXBlIjoiYWNjZXNzIn0.4Q9n2vQ8q3V7X6Y5Z8A9B0C1D2E3F4G5H6I7J8K9L0"
async def debug():
async with AsyncSessionLocal() as db:
# Decode token
payload = decode_token(TEST_TOKEN)
print(f"✅ Token payload:")
print(f" - sub: {payload.get('sub')}")
print(f" - scope_id: {payload.get('scope_id')}")
print(f" - scope_level: {payload.get('scope_level')}")
print(f" - role: {payload.get('role')}")
# Get user from database
user_id = payload.get('sub')
if user_id:
user_stmt = select(User).where(User.id == int(user_id))
user_result = await db.execute(user_stmt)
user = user_result.scalar_one_or_none()
if user:
print(f"\n✅ User from database (ID: {user.id}):")
print(f" - scope_id: {user.scope_id}")
print(f" - scope_level: {user.scope_level}")
print(f" - person_id: {user.person_id}")
# Check what the assets endpoint would see
print(f"\n🔍 Assets endpoint logic:")
print(f" - current_user.scope_id: {user.scope_id}")
print(f" - Type: {type(user.scope_id)}")
print(f" - Is None? {user.scope_id is None}")
print(f" - == 'None'? {user.scope_id == 'None'}")
print(f" - == ''? {user.scope_id == ''}")
if user.scope_id is None:
print(" → Would go to PERSONAL mode")
else:
print(" → Would go to CORPORATE mode")
try:
scope_org_id = int(user.scope_id)
print(f" → scope_org_id: {scope_org_id}")
except (ValueError, TypeError):
print(f" → scope_org_id: None (invalid)")
if __name__ == "__main__":
asyncio.run(debug())

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Diagnostic script to understand why vehicle filtering isn't working correctly.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from sqlalchemy import select, or_
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
from app.models.vehicle.asset import Asset
from app.models.identity.identity import User
async def diagnose():
async with AsyncSessionLocal() as db:
# Get User 28
user_stmt = select(User).where(User.id == 28)
user_result = await db.execute(user_stmt)
test_user = user_result.scalar_one_or_none()
if not test_user:
print("❌ User 28 not found")
return
print(f"✅ Found User 28:")
print(f" - User ID: {test_user.id}")
print(f" - Person ID: {test_user.person_id}")
print(f" - Email: {test_user.email}")
# Get all vehicles
asset_stmt = select(Asset).where(
or_(
Asset.license_plate == "MNO-345",
Asset.license_plate == "PQR-678"
)
)
asset_result = await db.execute(asset_stmt)
assets = asset_result.scalars().all()
print(f"\n✅ Found {len(assets)} vehicles:")
for asset in assets:
print(f"\n Vehicle: {asset.license_plate}")
print(f" - Asset ID: {asset.id}")
print(f" - Owner Person ID: {asset.owner_person_id}")
print(f" - Operator Person ID: {asset.operator_person_id}")
print(f" - Owner Org ID: {asset.owner_org_id}")
print(f" - Operator Org ID: {asset.operator_org_id}")
print(f" - Branch ID: {asset.branch_id}")
print(f" - Current Org ID: {asset.current_organization_id}")
# Check if it matches user's person_id
matches_owner = asset.owner_person_id == test_user.person_id
matches_operator = asset.operator_person_id == test_user.person_id
print(f" - Matches owner_person_id ({test_user.person_id}): {matches_owner}")
print(f" - Matches operator_person_id ({test_user.person_id}): {matches_operator}")
# Now test the actual query logic from assets.py
print(f"\n🔍 Testing the actual query logic:")
# Personal mode query (from assets.py lines 41-57)
stmt = (
select(Asset)
.where(
or_(
Asset.owner_org_id.is_(None),
Asset.operator_org_id.is_(None)
),
or_(
Asset.owner_person_id == test_user.person_id,
Asset.operator_person_id == test_user.person_id
)
)
.order_by(Asset.created_at.desc())
)
result = await db.execute(stmt)
personal_assets = result.scalars().all()
print(f" Personal mode query returns {len(personal_assets)} vehicles:")
for asset in personal_assets:
print(f" - {asset.license_plate}")
# Corporate mode query (for organization 15)
print(f"\n🔍 Testing corporate mode query (org_id = 15):")
stmt = (
select(Asset)
.where(
Asset.current_organization_id == 15,
Asset.branch_id.is_not(None)
)
.order_by(Asset.created_at.desc())
)
result = await db.execute(stmt)
corporate_assets = result.scalars().all()
print(f" Corporate mode query returns {len(corporate_assets)} vehicles:")
for asset in corporate_assets:
print(f" - {asset.license_plate}")
if __name__ == "__main__":
asyncio.run(diagnose())

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""
Fix asset person IDs - update owner_person_id from User ID to Person ID.
"""
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.session import AsyncSessionLocal
from app.models.vehicle.asset import Asset
from app.models.identity.identity import User
async def fix_asset_person_ids():
async with AsyncSessionLocal() as db:
# Get User 28
user_stmt = select(User).where(User.id == 28)
user_result = await db.execute(user_stmt)
test_user = user_result.scalar_one_or_none()
if not test_user:
print("❌ User 28 not found")
return
print(f"✅ Found User 28:")
print(f" - User ID: {test_user.id}")
print(f" - Person ID: {test_user.person_id}")
print(f" - Email: {test_user.email}")
if not test_user.person_id:
print("❌ User has no person_id")
return
# Find assets with owner_person_id = 28 (User ID)
asset_stmt = select(Asset).where(
Asset.owner_person_id == 28
)
asset_result = await db.execute(asset_stmt)
assets = asset_result.scalars().all()
print(f"\n✅ Found {len(assets)} assets with owner_person_id = 28:")
for asset in assets:
print(f" - {asset.license_plate} (Asset ID: {asset.id})")
if assets:
# Update them to have person_id = 29
update_stmt = (
update(Asset)
.where(Asset.owner_person_id == 28)
.values(owner_person_id=test_user.person_id)
)
result = await db.execute(update_stmt)
await db.commit()
print(f"\n✅ Updated {result.rowcount} assets:")
print(f" - Changed owner_person_id from 28 to {test_user.person_id}")
# Verify the update
asset_stmt = select(Asset).where(
Asset.owner_person_id == test_user.person_id
)
asset_result = await db.execute(asset_stmt)
updated_assets = asset_result.scalars().all()
print(f"\n✅ Verification - Found {len(updated_assets)} assets with owner_person_id = {test_user.person_id}:")
for asset in updated_assets:
print(f" - {asset.license_plate}")
else:
print("\n No assets found with owner_person_id = 28")
# Also check for operator_person_id = 28
operator_asset_stmt = select(Asset).where(
Asset.operator_person_id == 28
)
operator_asset_result = await db.execute(operator_asset_stmt)
operator_assets = operator_asset_result.scalars().all()
print(f"\n✅ Found {len(operator_assets)} assets with operator_person_id = 28:")
for asset in operator_assets:
print(f" - {asset.license_plate} (Asset ID: {asset.id})")
if operator_assets:
# Update them to have person_id = 29
update_stmt = (
update(Asset)
.where(Asset.operator_person_id == 28)
.values(operator_person_id=test_user.person_id)
)
result = await db.execute(update_stmt)
await db.commit()
print(f"\n✅ Updated {result.rowcount} operator assets:")
print(f" - Changed operator_person_id from 28 to {test_user.person_id}")
if __name__ == "__main__":
asyncio.run(fix_asset_person_ids())

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Fix test user garage data and split vehicles between private and org branches.
This script:
1. Fetches User 28 (tester_pro@profibot.hu)
2. Creates a Private Organization for User 28 (if missing)
3. Creates a Private Branch (Garage) named "Teszt Pro - Saját Garázs"
4. Fetches the 2 vehicles owned by User 28 (MNO-345 and PQR-678)
5. Splits them: Assigns MNO-345 to the newly created Private Branch
6. Keeps PQR-678 in the Org 15 Branch (b2060e1d...)
7. Commits changes
Run inside sf_api container:
docker compose exec sf_api python -m app.scripts.fix_test_user_garage
"""
import asyncio
import sys
import uuid
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
# Add the backend directory to the path
sys.path.insert(0, '/app')
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.models.marketplace.organization import Organization, OrganizationMember, Branch, OrgType, OrgUserRole
from app.models.vehicle.asset import Asset
async def main():
"""Main execution logic."""
print("=" * 60)
print("FIX TEST USER GARAGE DATA SCRIPT")
print("=" * 60)
async with AsyncSessionLocal() as db:
# 1. Fetch User 28 (tester_pro@profibot.hu)
print("\n1. FETCHING TEST USER...")
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
result = await db.execute(stmt)
test_user = result.scalar_one_or_none()
if not test_user:
print("❌ ERROR: Test user (tester_pro@profibot.hu) not found!")
return
print(f" ✅ Test user found: ID={test_user.id}, Email={test_user.email}")
# 2. Check if user already has a private organization
print("\n2. CHECKING FOR PRIVATE ORGANIZATION...")
org_stmt = (
select(Organization)
.join(OrganizationMember)
.where(OrganizationMember.user_id == test_user.id)
.where(Organization.org_type == OrgType.individual)
.where(Organization.is_deleted == False)
)
result = await db.execute(org_stmt)
private_org = result.scalar_one_or_none()
if not private_org:
print(" ⚠️ No private organization found, creating one...")
# Create private organization
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
private_org = Organization(
full_name=f"Private Organization for {test_user.email}",
name=f"Private_{test_user.id}",
display_name=f"Teszt Pro - Privát",
folder_slug=f"priv_{test_user.id}",
org_type=OrgType.individual,
status="active",
is_active=True,
is_deleted=False,
country_code="HU",
language="hu",
default_currency="HUF",
first_registered_at=now,
current_lifecycle_started_at=now,
subscription_plan="FREE",
base_asset_limit=1,
purchased_extra_slots=0,
notification_settings={"notify_owner": True, "alert_days_before": [30, 15, 7, 1]},
external_integration_config={},
created_at=now,
is_ownership_transferable=True
)
db.add(private_org)
await db.flush() # Get the ID
# Add user as owner of the organization
org_member = OrganizationMember(
organization_id=private_org.id,
user_id=test_user.id,
person_id=test_user.person_id,
role=OrgUserRole.OWNER,
is_verified=True
)
db.add(org_member)
await db.commit()
print(f" ✅ Created private organization: ID={private_org.id}, Name={private_org.name}")
else:
print(f" ✅ Private organization already exists: ID={private_org.id}, Name={private_org.name}")
# 3. Check/create private branch (garage)
print("\n3. CHECKING/CREATING PRIVATE BRANCH (GARAGE)...")
branch_stmt = select(Branch).where(
Branch.organization_id == private_org.id,
Branch.name.ilike("%Teszt Pro - Saját Garázs%"),
Branch.is_deleted == False
)
result = await db.execute(branch_stmt)
private_branch = result.scalar_one_or_none()
if not private_branch:
private_branch = Branch(
id=uuid.uuid4(),
organization_id=private_org.id,
name="Teszt Pro - Saját Garázs",
is_main=True,
status="active",
is_deleted=False,
postal_code="1234",
city="Budapest",
street_name="Teszt utca",
house_number="1"
)
db.add(private_branch)
await db.commit()
print(f" ✅ Created private branch: ID={private_branch.id}, Name={private_branch.name}")
else:
print(f" ✅ Private branch already exists: ID={private_branch.id}, Name={private_branch.name}")
# 4. Fetch the 2 vehicles by license plate (regardless of owner)
print("\n4. FETCHING VEHICLES BY LICENSE PLATE...")
# Find vehicles by license plate
vehicles_stmt = select(Asset).where(
Asset.license_plate.in_(["MNO-345", "PQR-678"])
)
result = await db.execute(vehicles_stmt)
vehicles = result.scalars().all()
print(f" Found {len(vehicles)} vehicles with plates MNO-345 or PQR-678")
# 5. Find Org 15 branch (b2060e1d...)
print("\n5. FINDING ORG 15 BRANCH...")
# First find Org 15
org15_stmt = select(Organization).where(
Organization.id == 15,
Organization.is_deleted == False
)
result = await db.execute(org15_stmt)
org15 = result.scalar_one_or_none()
if not org15:
print("❌ ERROR: Organization 15 not found!")
return
print(f" ✅ Organization 15 found: ID={org15.id}, Name={org15.name}")
# Find a branch in Org 15
org15_branch_stmt = select(Branch).where(
Branch.organization_id == 15,
Branch.is_deleted == False
).limit(1)
result = await db.execute(org15_branch_stmt)
org15_branch = result.scalar_one_or_none()
if not org15_branch:
print("❌ ERROR: No branch found in Organization 15!")
return
print(f" ✅ Org 15 branch found: ID={org15_branch.id}, Name={org15_branch.name}")
# 6. Split vehicles
print("\n6. SPLITTING VEHICLES BETWEEN BRANCHES...")
updated_count = 0
for vehicle in vehicles:
if vehicle.license_plate == "MNO-345":
# Assign to private branch
vehicle.branch_id = private_branch.id
vehicle.current_organization_id = private_org.id
print(f" ✅ Assigned MNO-345 to private branch: {private_branch.name}")
updated_count += 1
elif vehicle.license_plate == "PQR-678":
# Keep in Org 15 branch
vehicle.branch_id = org15_branch.id
vehicle.current_organization_id = 15
print(f" ✅ Kept PQR-678 in Org 15 branch: {org15_branch.name}")
updated_count += 1
if updated_count > 0:
await db.commit()
print(f"\n✅ Successfully updated {updated_count} vehicles")
else:
print("\n⚠️ No vehicles needed updating")
# 7. Verify the split
print("\n7. VERIFICATION...")
for plate in ["MNO-345", "PQR-678"]:
verify_stmt = select(Asset).where(Asset.license_plate == plate).options(selectinload(Asset.catalog))
result = await db.execute(verify_stmt)
vehicle = result.scalar_one_or_none()
if vehicle:
branch_name = "Unknown"
if vehicle.branch_id == private_branch.id:
branch_name = "Private Branch"
elif vehicle.branch_id == org15_branch.id:
branch_name = "Org 15 Branch"
print(f" {plate}: Branch ID={vehicle.branch_id} ({branch_name}), Org ID={vehicle.current_organization_id}")
else:
print(f" {plate}: Not found")
print("\n" + "=" * 60)
print("SCRIPT COMPLETED SUCCESSFULLY")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""
Migration script to assign existing vehicles to their organization's default garage (branch).
This fixes the issue where existing vehicles have branch_id = NULL after the column was added.
Logic:
1. For each Asset with owner_org_id or operator_org_id
2. Find the default Branch (Garage) for that Organization (is_main = True)
3. Update Asset.branch_id to that Branch's UUID
4. If no default branch exists, create one
"""
import asyncio
import logging
import sys
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import AsyncSessionLocal
from app.models.vehicle.asset import Asset
from app.models.marketplace.organization import Branch, Organization
from app.models.identity import User
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def get_or_create_default_branch(session: AsyncSession, organization_id: int) -> Branch:
"""Get the default branch (is_main = True) for an organization, create if doesn't exist."""
# Try to find existing default branch
stmt = select(Branch).where(
Branch.organization_id == organization_id,
Branch.is_main == True,
Branch.is_deleted == False
)
result = await session.execute(stmt)
branch = result.scalar_one_or_none()
if branch:
logger.info(f"Found default branch {branch.id} for organization {organization_id}")
return branch
# If no default branch exists, create one
logger.warning(f"No default branch found for organization {organization_id}, creating one...")
# Get organization name for branch naming
org_stmt = select(Organization).where(Organization.id == organization_id)
org_result = await session.execute(org_stmt)
organization = org_result.scalar_one_or_none()
org_name = organization.name if organization else f"Organization {organization_id}"
# Create default branch
import uuid
from datetime import datetime
from sqlalchemy.sql import func
new_branch = Branch(
id=uuid.uuid4(),
organization_id=organization_id,
name=f"{org_name} - Main Garage",
is_main=True,
status="active",
is_deleted=False,
created_at=datetime.utcnow()
)
session.add(new_branch)
await session.flush()
logger.info(f"Created default branch {new_branch.id} for organization {organization_id}")
return new_branch
async def migrate_vehicles_to_garages():
"""Main migration function."""
async with AsyncSessionLocal() as session:
try:
# Get all assets that have organization ownership but no branch_id
stmt = select(Asset).where(
(Asset.owner_org_id.is_not(None) | Asset.operator_org_id.is_not(None)),
Asset.branch_id.is_(None)
)
result = await session.execute(stmt)
assets = result.scalars().all()
logger.info(f"Found {len(assets)} assets without branch assignment")
updated_count = 0
skipped_count = 0
for asset in assets:
# Determine which organization to use (prefer owner, fallback to operator)
org_id = asset.owner_org_id or asset.operator_org_id
if not org_id:
logger.warning(f"Asset {asset.id} has no organization reference, skipping")
skipped_count += 1
continue
# Get or create default branch for the organization
branch = await get_or_create_default_branch(session, org_id)
# Update the asset
update_stmt = (
update(Asset)
.where(Asset.id == asset.id)
.values(branch_id=branch.id, relocation_performed=True)
)
await session.execute(update_stmt)
logger.info(f"Updated asset {asset.id} with branch {branch.id} (org {org_id})")
updated_count += 1
# Commit all changes
await session.commit()
logger.info(f"Migration completed: {updated_count} assets updated, {skipped_count} skipped")
# Also update assets that already have branch_id but need relocation_performed flag
if updated_count > 0:
stmt = select(Asset).where(
Asset.branch_id.is_not(None),
Asset.relocation_performed == False
)
result = await session.execute(stmt)
assets_without_flag = result.scalars().all()
for asset in assets_without_flag:
update_stmt = (
update(Asset)
.where(Asset.id == asset.id)
.values(relocation_performed=True)
)
await session.execute(update_stmt)
await session.commit()
logger.info(f"Updated relocation_performed flag for {len(assets_without_flag)} assets")
return updated_count
except Exception as e:
await session.rollback()
logger.error(f"Migration failed: {e}")
raise
async def verify_migration():
"""Verify the migration results."""
async with AsyncSessionLocal() as session:
# Count assets with branch_id
stmt = select(Asset).where(Asset.branch_id.is_not(None))
result = await session.execute(stmt)
assets_with_branch = result.scalars().all()
# Count assets without branch_id but with organizations
stmt = select(Asset).where(
(Asset.owner_org_id.is_not(None) | Asset.operator_org_id.is_not(None)),
Asset.branch_id.is_(None)
)
result = await session.execute(stmt)
assets_still_missing = result.scalars().all()
logger.info(f"Verification:")
logger.info(f" - Assets with branch_id: {len(assets_with_branch)}")
logger.info(f" - Assets still missing branch_id: {len(assets_still_missing)}")
if assets_still_missing:
logger.warning("Some assets still missing branch_id:")
for asset in assets_still_missing[:5]: # Show first 5
logger.warning(f" Asset {asset.id}: owner_org={asset.owner_org_id}, operator_org={asset.operator_org_id}")
return len(assets_with_branch), len(assets_still_missing)
if __name__ == "__main__":
logger.info("Starting vehicle-to-garage migration...")
try:
# Run migration
updated = asyncio.run(migrate_vehicles_to_garages())
# Verify
with_branch, missing = asyncio.run(verify_migration())
if missing == 0:
logger.info("✅ Migration successful! All organizational vehicles now have branch assignments.")
else:
logger.warning(f"⚠️ Migration incomplete: {missing} assets still lack branch_id")
sys.exit(1)
except Exception as e:
logger.error(f"❌ Migration failed: {e}")
sys.exit(1)

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""
Test the assets API endpoint directly.
"""
import asyncio
import aiohttp
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
async def test_assets_api():
base_url = "http://sf_api:8000"
# First, login to get token
async with aiohttp.ClientSession() as session:
# Login
login_data = {
"username": "tester_pro@profibot.hu",
"password": "Test123!"
}
print("1. Logging in...")
async with session.post(f"{base_url}/api/v1/auth/login", data=login_data) as resp:
if resp.status != 200:
print(f"❌ Login failed: {resp.status}")
text = await resp.text()
print(f"Response: {text}")
return
login_result = await resp.json()
token = login_result["access_token"]
print(f"✅ Login successful, token: {token[:50]}...")
# Set personal mode (scope_id = null)
print("\n2. Setting personal mode (scope_id = null)...")
headers = {"Authorization": f"Bearer {token}"}
patch_data = {"organization_id": None}
async with session.patch(
f"{base_url}/api/v1/users/me/active-organization",
json=patch_data,
headers=headers
) as resp:
if resp.status != 200:
print(f"❌ PATCH failed: {resp.status}")
text = await resp.text()
print(f"Response: {text}")
return
patch_result = await resp.json()
print(f"✅ PATCH successful")
print(f" scope_id: {patch_result.get('scope_id')}")
print(f" scope_level: {patch_result.get('scope_level')}")
# Get vehicles in personal mode
print("\n3. Getting vehicles in personal mode...")
async with session.get(
f"{base_url}/api/v1/assets/vehicles",
headers=headers
) as resp:
if resp.status != 200:
print(f"❌ GET vehicles failed: {resp.status}")
text = await resp.text()
print(f"Response: {text}")
return
vehicles_result = await resp.json()
print(f"✅ GET vehicles successful")
print(f" Found {len(vehicles_result)} vehicles")
for i, vehicle in enumerate(vehicles_result[:5]): # Show first 5
print(f" {i+1}. {vehicle.get('license_plate')} (ID: {vehicle.get('id')})")
if len(vehicles_result) > 5:
print(f" ... and {len(vehicles_result) - 5} more")
# Now test corporate mode (org_id = 15)
print("\n4. Setting corporate mode (org_id = 15)...")
patch_data_corp = {"organization_id": "15"}
async with session.patch(
f"{base_url}/api/v1/users/me/active-organization",
json=patch_data_corp,
headers=headers
) as resp:
if resp.status != 200:
print(f"❌ PATCH corporate failed: {resp.status}")
text = await resp.text()
print(f"Response: {text}")
return
patch_corp_result = await resp.json()
print(f"✅ PATCH corporate successful")
print(f" scope_id: {patch_corp_result.get('scope_id')}")
# Get vehicles in corporate mode
print("\n5. Getting vehicles in corporate mode...")
async with session.get(
f"{base_url}/api/v1/assets/vehicles",
headers=headers
) as resp:
if resp.status != 200:
print(f"❌ GET corporate vehicles failed: {resp.status}")
text = await resp.text()
print(f"Response: {text}")
return
vehicles_corp_result = await resp.json()
print(f"✅ GET corporate vehicles successful")
print(f" Found {len(vehicles_corp_result)} vehicles")
for i, vehicle in enumerate(vehicles_corp_result[:5]):
print(f" {i+1}. {vehicle.get('license_plate')}")
if __name__ == "__main__":
asyncio.run(test_assets_api())

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
Backend verification test for scope switcher functionality.
Simulates the frontend:
1. Login as User 28 (tester_pro@profibot.hu) - get JWT token
2. Call PATCH /active-organization with organization_id = null
3. Call GET /vehicles - assert it returns exactly 1 car (MNO-345)
4. Call PATCH /active-organization with organization_id = 15
5. Call GET /vehicles - assert it returns exactly 1 car (PQR-678)
Run inside sf_api container:
docker compose exec sf_api python -m app.scripts.verify_scope_switcher
"""
import asyncio
import sys
import httpx
import json
from typing import Dict, Any
# Add the backend directory to the path
sys.path.insert(0, '/app')
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.core.security import create_tokens
from app.core.config import settings
async def get_auth_token() -> str:
"""Get JWT token for test user by simulating login."""
async with AsyncSessionLocal() as db:
# Find the test user
from sqlalchemy import select
stmt = select(User).where(User.email == "tester_pro@profibot.hu")
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
raise Exception("Test user not found")
# Create token directly (simulating login)
from app.core.security import DEFAULT_RANK_MAP
ranks = DEFAULT_RANK_MAP # Simplified
token_data = {
"sub": str(user.id),
"role": user.role.value if hasattr(user.role, 'value') else str(user.role),
"rank": ranks.get(user.role.value.upper(), 10),
"scope_level": user.scope_level or "individual",
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
}
access_token, _ = create_tokens(data=token_data)
return access_token
async def make_api_request(method: str, endpoint: str, token: str, data: Dict = None) -> Dict[str, Any]:
"""Make HTTP request to the API."""
base_url = "http://localhost:8000/api/v1"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
async with httpx.AsyncClient(timeout=30.0) as client:
url = f"{base_url}{endpoint}"
if method == "GET":
response = await client.get(url, headers=headers)
elif method == "PATCH":
response = await client.patch(url, headers=headers, json=data)
else:
raise ValueError(f"Unsupported method: {method}")
response.raise_for_status()
return response.json()
async def main():
"""Main verification test."""
print("=" * 60)
print("SCOPE SWITCHER VERIFICATION TEST")
print("=" * 60)
try:
# 1. Get auth token
print("\n1. AUTHENTICATING AS TEST USER...")
token = await get_auth_token()
print(f" ✅ Token obtained (length: {len(token)})")
# 2. Test personal mode (organization_id = null)
print("\n2. TESTING PERSONAL MODE (organization_id = null)...")
patch_data = {"organization_id": None}
patch_response = await make_api_request("PATCH", "/users/me/active-organization", token, patch_data)
print(f" ✅ PATCH response: {json.dumps(patch_response, indent=2)}")
# Check if scope_id was updated
if patch_response.get("scope_id") is not None:
print(" ❌ ERROR: scope_id should be null in personal mode!")
return
# 3. Get vehicles in personal mode
print("\n3. GETTING VEHICLES IN PERSONAL MODE...")
vehicles_response = await make_api_request("GET", "/assets/vehicles", token)
vehicles_count = len(vehicles_response)
print(f" ✅ Found {vehicles_count} vehicles")
# Check license plates
license_plates = [v.get("license_plate") for v in vehicles_response if v.get("license_plate")]
print(f" ✅ License plates: {license_plates}")
# Should have exactly 1 car (MNO-345)
if vehicles_count != 1:
print(f" ❌ ERROR: Expected 1 vehicle in personal mode, got {vehicles_count}")
return
if "MNO-345" not in license_plates:
print(f" ❌ ERROR: Expected MNO-345 in personal mode, got {license_plates}")
return
print(" ✅ PERSONAL MODE TEST PASSED: Exactly 1 car (MNO-345)")
# 4. Test corporate mode (organization_id = 15)
print("\n4. TESTING CORPORATE MODE (organization_id = 15)...")
patch_data = {"organization_id": 15}
patch_response = await make_api_request("PATCH", "/users/me/active-organization", token, patch_data)
print(f" ✅ PATCH response: {json.dumps(patch_response, indent=2)}")
# Check if scope_id was updated
if patch_response.get("scope_id") != "15":
print(f" ❌ ERROR: scope_id should be '15', got {patch_response.get('scope_id')}")
return
# 5. Get vehicles in corporate mode
print("\n5. GETTING VEHICLES IN CORPORATE MODE...")
vehicles_response = await make_api_request("GET", "/assets/vehicles", token)
vehicles_count = len(vehicles_response)
print(f" ✅ Found {vehicles_count} vehicles")
# Check license plates
license_plates = [v.get("license_plate") for v in vehicles_response if v.get("license_plate")]
print(f" ✅ License plates: {license_plates}")
# Should have exactly 1 car (PQR-678)
if vehicles_count != 1:
print(f" ❌ ERROR: Expected 1 vehicle in corporate mode, got {vehicles_count}")
return
if "PQR-678" not in license_plates:
print(f" ❌ ERROR: Expected PQR-678 in corporate mode, got {license_plates}")
return
print(" ✅ CORPORATE MODE TEST PASSED: Exactly 1 car (PQR-678)")
# 6. Final verification
print("\n" + "=" * 60)
print("✅ ALL TESTS PASSED!")
print("=" * 60)
print("\nSUMMARY:")
print("- Personal mode (scope_id = null): Shows 1 vehicle (MNO-345)")
print("- Corporate mode (scope_id = 15): Shows 1 vehicle (PQR-678)")
print("- Scope switcher endpoint works correctly")
print("- Vehicle filtering by branch/organization works correctly")
except httpx.HTTPStatusError as e:
print(f"\n❌ HTTP ERROR: {e}")
print(f"Response: {e.response.text}")
return
except Exception as e:
print(f"\n❌ ERROR: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
return
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,209 +0,0 @@
#!/usr/bin/env python3
"""
Billing Engine tesztelő szkript.
Ellenőrzi, hogy a billing_engine.py fájl helyesen működik-e.
"""
import asyncio
import sys
import os
# Add the parent directory to the path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
from app.services.billing_engine import PricingCalculator, SmartDeduction, AtomicTransactionManager
from app.models.identity import UserRole
async def test_pricing_calculator():
"""Árképzési számoló tesztelése."""
print("=== PricingCalculator teszt ===")
# Mock database session (nem használjuk valódi adatbázist)
class MockSession:
pass
db = MockSession()
# Alap teszt
base_amount = 100.0
# 1. Alapár (HU, user)
final_price = await PricingCalculator.calculate_final_price(
db, base_amount, "HU", UserRole.user
)
print(f"HU, user: {base_amount} -> {final_price} (várt: 100.0)")
assert abs(final_price - 100.0) < 0.01
# 2. UK árszorzó
final_price = await PricingCalculator.calculate_final_price(
db, base_amount, "GB", UserRole.user
)
print(f"GB, user: {base_amount} -> {final_price} (várt: 120.0)")
assert abs(final_price - 120.0) < 0.01
# 3. admin kedvezmény (30%)
final_price = await PricingCalculator.calculate_final_price(
db, base_amount, "HU", UserRole.admin
)
print(f"HU, admin: {base_amount} -> {final_price} (várt: 70.0)")
assert abs(final_price - 70.0) < 0.01
# 4. Kombinált (UK + superadmin - 50%)
final_price = await PricingCalculator.calculate_final_price(
db, base_amount, "GB", UserRole.superadmin
)
print(f"GB, superadmin: {base_amount} -> {final_price} (várt: 60.0)")
assert abs(final_price - 60.0) < 0.01
# 5. Egyedi kedvezmények
discounts = [
{"type": "percentage", "value": 10}, # 10% kedvezmény
{"type": "fixed", "value": 5}, # 5 egység kedvezmény
]
final_price = await PricingCalculator.calculate_final_price(
db, base_amount, "HU", UserRole.user, discounts
)
print(f"HU, user + discounts: {base_amount} -> {final_price} (várt: 85.0)")
assert abs(final_price - 85.0) < 0.01
print("✓ PricingCalculator teszt sikeres!\n")
async def test_smart_deduction_logic():
"""Intelligens levonás logikájának tesztelése (mock adatokkal)."""
print("=== SmartDeduction logika teszt ===")
# Mock wallet objektum
class MockWallet:
def __init__(self):
self.earned_balance = 50.0
self.purchased_balance = 30.0
self.service_coins_balance = 20.0
self.id = 1
# Mock database session
class MockSession:
async def commit(self):
pass
async def execute(self, stmt):
class MockResult:
def scalar_one_or_none(self):
return MockWallet()
return MockResult()
db = MockSession()
print("SmartDeduction osztály metódusai:")
print(f"- calculate_final_price: {'van' if hasattr(PricingCalculator, 'calculate_final_price') else 'nincs'}")
print(f"- deduct_from_wallets: {'van' if hasattr(SmartDeduction, 'deduct_from_wallets') else 'nincs'}")
print(f"- process_voucher_expiration: {'van' if hasattr(SmartDeduction, 'process_voucher_expiration') else 'nincs'}")
print("✓ SmartDeduction struktúra ellenőrizve!\n")
async def test_atomic_transaction_manager():
"""Atomikus tranzakciókezelő struktúrájának ellenőrzése."""
print("=== AtomicTransactionManager struktúra teszt ===")
print("AtomicTransactionManager osztály metódusai:")
print(f"- atomic_billing_transaction: {'van' if hasattr(AtomicTransactionManager, 'atomic_billing_transaction') else 'nincs'}")
print(f"- get_transaction_history: {'van' if hasattr(AtomicTransactionManager, 'get_transaction_history') else 'nincs'}")
# Ellenőrizzük, hogy a szükséges importok megvannak-e
try:
from app.models import LedgerEntryType, WalletType
print(f"- LedgerEntryType importálva: {LedgerEntryType}")
print(f"- WalletType importálva: {WalletType}")
except ImportError as e:
print(f"✗ Import hiba: {e}")
print("✓ AtomicTransactionManager struktúra ellenőrizve!\n")
async def test_file_completeness():
"""Fájl teljességének ellenőrzése."""
print("=== billing_engine.py fájl teljesség teszt ===")
file_path = "backend/app/services/billing_engine.py"
if not os.path.exists(file_path):
print(f"✗ A fájl nem létezik: {file_path}")
return
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Ellenőrizzük a kulcsszavakat
checks = [
("class PricingCalculator", "PricingCalculator osztály"),
("class SmartDeduction", "SmartDeduction osztály"),
("class AtomicTransactionManager", "AtomicTransactionManager osztály"),
("calculate_final_price", "calculate_final_price metódus"),
("deduct_from_wallets", "deduct_from_wallets metódus"),
("atomic_billing_transaction", "atomic_billing_transaction metódus"),
("from app.models.identity import", "identity model import"),
("from app.models import", "audit model import"),
]
all_passed = True
for keyword, description in checks:
if keyword in content:
print(f"{description} megtalálva")
else:
print(f"{description} HIÁNYZIK")
all_passed = False
# Ellenőrizzük a fájl végét
lines = content.strip().split('\n')
last_line = lines[-1].strip() if lines else ""
if last_line and not last_line.startswith('#'):
print(f"✓ Fájl vége rendben: '{last_line[:50]}...'")
else:
print(f"✗ Fájl vége lehet hiányos: '{last_line}'")
print(f"✓ Fájl mérete: {len(content)} karakter, {len(lines)} sor")
if all_passed:
print("✓ billing_engine.py fájl teljesség teszt sikeres!\n")
else:
print("✗ billing_engine.py fájl hiányos!\n")
async def main():
"""Fő tesztfolyamat."""
print("🤖 Billing Engine tesztelés indítása...\n")
try:
await test_file_completeness()
await test_pricing_calculator()
await test_smart_deduction_logic()
await test_atomic_transaction_manager()
print("=" * 50)
print("✅ ÖSSZES TESZT SIKERES!")
print("A Billing Engine implementáció alapvetően működőképes.")
print("\nKövetkező lépések:")
print("1. Valódi adatbázis kapcsolattal tesztelés")
print("2. Voucher kezelés tesztelése")
print("3. Atomikus tranzakciók integrációs tesztje")
print("4. API endpoint integráció")
except Exception as e:
print(f"\n❌ TESZT SIKERTELEN: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)

View File

@@ -1,80 +0,0 @@
#!/usr/bin/env python3
"""
Gyors teszt a hierarchikus paraméterekhez.
Futtatás: docker exec sf_api python /app/test_hierarchical.py
"""
import asyncio
import os
import sys
sys.path.insert(0, '/app')
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from app.services.system_service import system_service
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@shared-postgres:5432/service_finder")
async def test():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as db:
# Töröljük a teszt paramétereket
await db.execute(text("DELETE FROM system.system_parameters WHERE key = 'test.hierarchical'"))
await db.commit()
# Beszúrjuk a teszt adatokat
await db.execute(text("""
INSERT INTO system.system_parameters (key, value, scope_level, scope_id, category, is_active)
VALUES
('test.hierarchical', '{"msg": "global"}', 'global', NULL, 'test', true),
('test.hierarchical', '{"msg": "country HU"}', 'country', 'HU', 'test', true),
('test.hierarchical', '{"msg": "region budapest"}', 'region', 'budapest', 'test', true),
('test.hierarchical', '{"msg": "user 123"}', 'user', '123', 'test', true)
"""))
await db.commit()
# Tesztelés
# 1. Global
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', default=None)
print(f"Global: {val}")
assert val['msg'] == 'global'
# 2. Country HU
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', country_code='HU', default=None)
print(f"Country HU: {val}")
assert val['msg'] == 'country HU'
# 3. Region budapest (country is HU)
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', region_id='budapest', country_code='HU', default=None)
print(f"Region budapest: {val}")
assert val['msg'] == 'region budapest'
# 4. User 123 (with region and country)
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', user_id='123', region_id='budapest', country_code='HU', default=None)
print(f"User 123: {val}")
assert val['msg'] == 'user 123'
# 5. Non-existent user, fallback to region
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', user_id='999', region_id='budapest', country_code='HU', default=None)
print(f"Non-existent user -> region: {val}")
assert val['msg'] == 'region budapest'
# 6. Non-existent region, fallback to country
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', region_id='none', country_code='HU', default=None)
print(f"Non-existent region -> country: {val}")
assert val['msg'] == 'country HU'
# 7. Non-existent country, fallback to global
val = await system_service.get_scoped_parameter(db, 'test.hierarchical', country_code='US', default=None)
print(f"Non-existent country -> global: {val}")
assert val['msg'] == 'global'
# Törlés
await db.execute(text("DELETE FROM system.system_parameters WHERE key = 'test.hierarchical'"))
await db.commit()
print("✅ Minden teszt sikeres!")
if __name__ == "__main__":
asyncio.run(test())

View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""
Internal test script for Issues #176 and #177
This script runs inside the sf_api container using only Python standard library.
"""
import urllib.request
import urllib.error
import urllib.parse
import json
import sys
# Configuration - internal Docker network
BASE_URL = "http://sf_api:8000/api/v1"
# Using a test user that should exist in the dev database
TEST_EMAIL = "tester_pro@profibot.hu"
TEST_PASSWORD = "Password123!"
def make_request(method, url, data=None, headers=None, is_form_data=False):
"""Make HTTP request using urllib"""
if headers is None:
headers = {}
req = urllib.request.Request(url, method=method)
for key, value in headers.items():
req.add_header(key, value)
if data is not None:
if is_form_data:
# For form data (application/x-www-form-urlencoded)
data_bytes = urllib.parse.urlencode(data).encode('utf-8')
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
else:
# For JSON data
req.add_header('Content-Type', 'application/json')
data_bytes = json.dumps(data).encode('utf-8')
req.data = data_bytes
try:
with urllib.request.urlopen(req) as response:
response_data = response.read().decode('utf-8')
return response.status, response_data
except urllib.error.HTTPError as e:
return e.code, e.read().decode('utf-8')
except Exception as e:
return 0, str(e)
def login():
"""Login and get JWT token"""
print("🔐 Logging in...")
login_data = {
"username": TEST_EMAIL,
"password": TEST_PASSWORD
}
# Login endpoint expects form data, not JSON
status, response = make_request("POST", f"{BASE_URL}/auth/login", data=login_data, is_form_data=True)
if status == 200:
data = json.loads(response)
token = data.get("access_token")
if token:
print(f"✅ Login successful")
return token
else:
print(f"❌ No access_token in response")
print(f"Response: {response}")
return None
else:
print(f"❌ Login failed with status {status}")
print(f"Response: {response}")
return None
def test_issue_176_organization_switch(token):
"""Test Issue #176: PATCH /api/v1/users/me/active-organization endpoint"""
print("\n" + "="*60)
print("🧪 Testing Issue #176: Organization Switch (500 Error Fix)")
print("="*60)
headers = {"Authorization": f"Bearer {token}"}
# Step 1: Get current user info
print("\n1. Getting current user info...")
status, response = make_request("GET", f"{BASE_URL}/users/me", headers=headers)
if status == 200:
user_data = json.loads(response)
current_scope_id = user_data.get("scope_id")
print(f" Current scope_id: {current_scope_id}")
else:
print(f"❌ Failed to get user info: {status}")
print(f" Response: {response}")
return False
# Step 2: Try to switch to null (personal mode)
print("\n2. Testing switch to null (personal mode)...")
switch_data = {"organization_id": None}
status, response = make_request("PATCH", f"{BASE_URL}/users/me/active-organization",
data=switch_data, headers=headers)
if status == 200:
print(f"✅ Switch to null successful (200 OK)")
result = json.loads(response)
print(f" New scope_id: {result.get('scope_id')}")
else:
print(f"❌ Switch to null failed with status {status}")
print(f" Response: {response}")
return False
# Step 3: Get organizations to test switching
print("\n3. Checking user's organizations...")
status, response = make_request("GET", f"{BASE_URL}/organizations/my", headers=headers)
if status == 200:
orgs = json.loads(response)
if orgs and len(orgs) > 0:
org_id = orgs[0].get("id")
print(f" Found organization with ID: {org_id}")
# Try to switch to this organization
switch_data = {"organization_id": org_id}
status, response = make_request("PATCH", f"{BASE_URL}/users/me/active-organization",
data=switch_data, headers=headers)
if status == 200:
print(f"✅ Switch to organization successful (200 OK)")
result = json.loads(response)
print(f" New scope_id: {result.get('scope_id')}")
elif status == 403:
print(f"⚠️ Switch to organization failed with 403 (not a member)")
print(f" This is expected if user is not a member of this org")
else:
print(f"❌ Switch to organization failed with status {status}")
print(f" Response: {response}")
# Don't fail the test if we can't switch to org (might not be a member)
else:
print(" No organizations found for user, skipping organization switch test")
else:
print(f" Could not fetch organizations: {status}")
# Step 4: Verify final state
print("\n4. Verifying final user state...")
status, response = make_request("GET", f"{BASE_URL}/users/me", headers=headers)
if status == 200:
user_data = json.loads(response)
final_scope_id = user_data.get("scope_id")
print(f" Final scope_id: {final_scope_id}")
print(f"✅ Issue #176 test completed successfully!")
return True
else:
print(f"❌ Failed to verify user info: {status}")
return False
def test_issue_177_garage_dynamic_data(token):
"""Test Issue #177: GET /api/v1/assets/vehicles with dynamic data fields"""
print("\n" + "="*60)
print("🧪 Testing Issue #177: Garage Dynamic Data")
print("="*60)
headers = {"Authorization": f"Bearer {token}"}
# Get user's vehicles
print("\n1. Fetching user's vehicles...")
status, response = make_request("GET", f"{BASE_URL}/assets/vehicles", headers=headers)
if status == 200:
vehicles = json.loads(response)
print(f" Found {len(vehicles)} vehicles")
if len(vehicles) == 0:
print(" No vehicles found, but endpoint works correctly")
print(" ✅ GET /api/v1/assets/vehicles endpoint is working (200 OK)")
return True
# Check first vehicle
first_vehicle = vehicles[0]
print(f"\n2. Checking first vehicle:")
print(f" Vehicle ID: {first_vehicle.get('id')}")
print(f" License plate: {first_vehicle.get('license_plate')}")
# Check for profile_completion_percentage field
profile_completion = first_vehicle.get("profile_completion_percentage")
if profile_completion is not None:
print(f" ✅ profile_completion_percentage field found: {profile_completion}%")
else:
print(f" ❌ profile_completion_percentage field MISSING!")
return False
# Check all fields
print(f"\n3. Available fields in vehicle response:")
for key in first_vehicle.keys():
print(f" - {key}")
# Check for data_status field
if "data_status" in first_vehicle:
print(f" ✅ data_status field found: {first_vehicle['data_status']}")
else:
print(f" ⚠️ data_status field not in response")
# Check if it might be in a nested structure
for key, value in first_vehicle.items():
if isinstance(value, dict) and "data_status" in value:
print(f" ✅ data_status found in nested {key}: {value['data_status']}")
break
# Check required fields
required_fields = ["id", "status", "is_verified", "created_at"]
missing_fields = []
for field in required_fields:
if field not in first_vehicle:
missing_fields.append(field)
if missing_fields:
print(f" ❌ Missing required fields: {missing_fields}")
return False
else:
print(f" ✅ All required fields present")
print(f"\n✅ Issue #177 test completed successfully!")
return True
else:
print(f"❌ Failed to get vehicles: {status}")
print(f" Response: {response}")
return False
def main():
"""Main test function"""
print("🚀 Starting Internal QA Tests for Issues #176 and #177")
print("="*60)
# Login
token = login()
if not token:
print("❌ Cannot proceed without authentication token")
return False
# Test Issue #176
issue_176_passed = test_issue_176_organization_switch(token)
# Test Issue #177
issue_177_passed = test_issue_177_garage_dynamic_data(token)
# Summary
print("\n" + "="*60)
print("📊 TEST SUMMARY")
print("="*60)
print(f"Issue #176 (Organization Switch): {'✅ PASSED' if issue_176_passed else '❌ FAILED'}")
print(f"Issue #177 (Garage Dynamic Data): {'✅ PASSED' if issue_177_passed else '❌ FAILED'}")
overall_passed = issue_176_passed and issue_177_passed
print(f"\nOverall Result: {'✅ ALL TESTS PASSED' if overall_passed else '❌ SOME TESTS FAILED'}")
return overall_passed
if __name__ == "__main__":
try:
success = main()
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\n⚠️ Test interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python3
"""
Egyszerű teszt a ConfigService osztályhoz.
Futtatás: docker compose exec -T sf_api python3 /app/backend/test_config_service.py
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.services.config_service import ConfigService
from app.models.system.system import ParameterScope
async def test_config_service():
# Adatbázis kapcsolat létrehozása (használjuk a teszt adatbázist vagy a dev-et)
# A DATABASE_URL a .env fájlból jön, de itt hardcode-olhatunk egy teszt URL-t
database_url = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@postgres:5432/service_finder")
engine = create_async_engine(database_url, echo=False)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with AsyncSessionLocal() as db:
print("=== ConfigService Teszt ===")
# 1. Teszt: nem létező kulcs, default értékkel
value = await ConfigService.get_int(db, "non_existent_key", 42)
print(f"1. get_int('non_existent_key', 42) = {value} (elvárt: 42)")
assert value == 42, f"Expected 42, got {value}"
# 2. Teszt: string lekérés
value = await ConfigService.get_str(db, "another_key", "hello")
print(f"2. get_str('another_key', 'hello') = {value} (elvárt: hello)")
assert value == "hello"
# 3. Teszt: boolean lekérés
value = await ConfigService.get_bool(db, "bool_key", True)
print(f"3. get_bool('bool_key', True) = {value} (elvárt: True)")
assert value == True
# 4. Teszt: float lekérés
value = await ConfigService.get_float(db, "float_key", 3.14)
print(f"4. get_float('float_key', 3.14) = {value} (elvárt: 3.14)")
assert value == 3.14
# 5. Teszt: JSON lekérés
value = await ConfigService.get_json(db, "json_key", {"foo": "bar"})
print(f"5. get_json('json_key', {{\"foo\": \"bar\"}}) = {value}")
assert value == {"foo": "bar"}
# 6. Teszt: általános get
value = await ConfigService.get(db, "generic_key", "default")
print(f"6. get('generic_key', 'default') = {value}")
assert value == "default"
# 7. Opcionális: beszúrhatunk egy teszt paramétert és lekérjük
# Ehhez szükség van a _insert_default metódusra, de most kihagyjuk
print("\n✅ Minden teszt sikeres!")
await db.commit()
if __name__ == "__main__":
asyncio.run(test_config_service())

View File

@@ -0,0 +1,28 @@
import asyncio
from app.db.session import AsyncSessionLocal
from app.models.identity import User
from app.core.security import create_tokens
from sqlalchemy import select
async def main():
async with AsyncSessionLocal() as db:
result = await db.execute(select(User).where(User.id == 29))
user = result.scalar_one_or_none()
if not user:
print("User 29 not found")
return
print(f"User found: {user.email}, Scope ID: {user.scope_id}")
token_payload = {
"sub": str(user.id),
"role": user.role.value if hasattr(user.role, 'value') else user.role,
"rank": 10,
"scope_level": user.scope_level.value if hasattr(user.scope_level, 'value') else (user.scope_level or "individual"),
"scope_id": str(user.scope_id) if user.scope_id else str(user.id)
}
access_token, refresh_token = create_tokens(data=token_payload)
print(f"TOKEN={access_token}")
if __name__ == "__main__":
asyncio.run(main())

31
backend/raw_data_dump.py Normal file
View File

@@ -0,0 +1,31 @@
import asyncio
from sqlalchemy import text
from app.db.session import AsyncSessionLocal
async def main():
async with AsyncSessionLocal() as session:
print("--- TASK 1: BRANCHES (GARAGES) ---")
try:
result = await session.execute(text("SELECT id, organization_id, name FROM fleet.branches;"))
branches = result.fetchall()
print("| id | organization_id | name |")
print("|---|---|---|")
for b in branches:
print(f"| {b.id} | {b.organization_id} | {b.name} |")
except Exception as e:
print(f"Error querying fleet.branches: {e}")
print("\n--- TASK 2: ASSETS (VEHICLES) ---")
try:
# We will query all assets and print the relevant columns.
result = await session.execute(text("SELECT id, license_plate, owner_person_id, owner_org_id, branch_id FROM vehicle.assets LIMIT 50;"))
assets = result.fetchall()
print("| id | license_plate | owner_person_id | owner_org_id | branch_id |")
print("|---|---|---|---|---|")
for a in assets:
print(f"| {a.id} | {a.license_plate} | {a.owner_person_id} | {a.owner_org_id} | {a.branch_id} |")
except Exception as e:
print(f"Error querying vehicle.assets: {e}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,44 +0,0 @@
#!/usr/bin/env python3
"""Test the AssetCreate schema changes"""
import sys
sys.path.insert(0, 'backend')
from app.schemas.asset import AssetCreate
from pydantic import ValidationError
print("Testing AssetCreate schema...")
# Test 1: Minimal payload with only license_plate
try:
data = {"license_plate": "ABC123"}
asset = AssetCreate(**data)
print(f"✓ Test 1 passed: Minimal payload accepted")
print(f" vin: {asset.vin}, catalog_id: {asset.catalog_id}, organization_id: {asset.organization_id}")
except ValidationError as e:
print(f"✗ Test 1 failed: {e}")
# Test 2: Payload with all optional fields None
try:
data = {"license_plate": "DEF456", "vin": None, "catalog_id": None, "organization_id": None}
asset = AssetCreate(**data)
print(f"✓ Test 2 passed: All optional fields can be None")
except ValidationError as e:
print(f"✗ Test 2 failed: {e}")
# Test 3: Full payload
try:
data = {"license_plate": "GHI789", "vin": "1HGBH41JXMN109186", "catalog_id": 1, "organization_id": 1}
asset = AssetCreate(**data)
print(f"✓ Test 3 passed: Full payload accepted")
except ValidationError as e:
print(f"✗ Test 3 failed: {e}")
# Test 4: Missing required license_plate (should fail)
try:
data = {"vin": "1HGBH41JXMN109186"}
asset = AssetCreate(**data)
print(f"✗ Test 4 failed: Should have required license_plate")
except ValidationError as e:
print(f"✓ Test 4 passed: Missing license_plate correctly rejected")
print("\nSchema validation tests completed.")

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Ellenőrzi, hogy a #179-es issue séma változtatásai sikeresen alkalmazva lettek-e.
- branch_id és relocation_performed oszlopok az assets táblában
- verified_purchase_date oszlop az asset_financials táblában
"""
import asyncio
import sys
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@postgres:5432/service_finder"
async def check_columns():
"""Ellenőrzi a hiányzó oszlopokat."""
engine = create_async_engine(DATABASE_URL, echo=False)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session() as session:
# 1. assets tábla oszlopai
result = await session.execute(text("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'assets'
AND column_name IN ('branch_id', 'relocation_performed')
ORDER BY column_name;
"""))
assets_cols = {row[0]: row for row in result.fetchall()}
# 2. asset_financials tábla oszlopai
result = await session.execute(text("""
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'asset_financials'
AND column_name = 'verified_purchase_date';
"""))
financials_cols = {row[0]: row for row in result.fetchall()}
print("=== #179 SCHEMA ELLENŐRZÉS ===")
print("1. vehicle.assets tábla:")
if 'branch_id' in assets_cols:
col = assets_cols['branch_id']
print(f" ✅ branch_id: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ branch_id oszlop HIÁNYZIK!")
if 'relocation_performed' in assets_cols:
col = assets_cols['relocation_performed']
print(f" ✅ relocation_performed: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ relocation_performed oszlop HIÁNYZIK!")
print("\n2. vehicle.asset_financials tábla:")
if 'verified_purchase_date' in financials_cols:
col = financials_cols['verified_purchase_date']
print(f" ✅ verified_purchase_date: {col[1]} (nullable: {col[2]})")
else:
print(" ❌ verified_purchase_date oszlop HIÁNYZIK!")
# 3. Foreign key ellenőrzés (opcionális)
result = await session.execute(text("""
SELECT tc.constraint_name, tc.constraint_type
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'vehicle' AND tc.table_name = 'assets'
AND kcu.column_name = 'branch_id'
AND tc.constraint_type = 'FOREIGN KEY';
"""))
fk = result.fetchone()
if fk:
print(f"\n3. Foreign key meglétének ellenőrzése: ✅ {fk[0]}")
else:
print("\n3. Foreign key meglétének ellenőrzése: ⚠️ Nincs foreign key constraint (lehet, hogy nem kötelező)")
# 4. Default érték ellenőrzés relocation_performed-re
result = await session.execute(text("""
SELECT column_default
FROM information_schema.columns
WHERE table_schema = 'vehicle' AND table_name = 'assets'
AND column_name = 'relocation_performed';
"""))
default = result.scalar()
if default and 'false' in default.lower():
print(f"4. Default érték relocation_performed: ✅ {default}")
else:
print(f"4. Default érték relocation_performed: ⚠️ {default}")
# Összegzés
missing = []
if 'branch_id' not in assets_cols:
missing.append('branch_id')
if 'relocation_performed' not in assets_cols:
missing.append('relocation_performed')
if 'verified_purchase_date' not in financials_cols:
missing.append('verified_purchase_date')
if not missing:
print("\n🎉 ÖSSZES SÉMA VÁLTOZTATÁS SIKERESEN ALKALMAZVA!")
return True
else:
print(f"\n❌ HIÁNYZÓ OSZLOPOK: {', '.join(missing)}")
return False
async def main():
try:
success = await check_columns()
sys.exit(0 if success else 1)
except Exception as e:
print(f"Hiba történt: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())