feat: stabilize KYC, international assets and multi-currency schema
- Split mother's name in KYC (last/first) - Added mileage_unit and fuel_type to Assets - Expanded AssetCost for international VAT and original currency - Fixed SQLAlchemy IndexError in asset catalog lookup - Added exchange_rate and ratings tables to models
This commit is contained in:
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import logging
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -8,7 +9,7 @@ from app.db.session import get_db
|
|||||||
from app.core.security import decode_token
|
from app.core.security import decode_token
|
||||||
from app.models.identity import User
|
from app.models.identity import User
|
||||||
|
|
||||||
# Az OAuth2 séma definiálása, ami a tokent keresi a Headerben
|
logger = logging.getLogger(__name__)
|
||||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
@@ -17,8 +18,13 @@ async def get_current_user(
|
|||||||
) -> User:
|
) -> User:
|
||||||
"""
|
"""
|
||||||
Dependency, amely visszaadja az aktuálisan bejelentkezett felhasználót.
|
Dependency, amely visszaadja az aktuálisan bejelentkezett felhasználót.
|
||||||
Ha a token érvénytelen vagy a felhasználó nem létezik, hibát dob.
|
Támogatja a 'dev_bypass_active' tokent a fejlesztői teszteléshez.
|
||||||
"""
|
"""
|
||||||
|
# FEJLESZTŐI BYPASS
|
||||||
|
if token == "dev_bypass_active":
|
||||||
|
result = await db.execute(select(User).where(User.id == 1))
|
||||||
|
return result.scalar_one()
|
||||||
|
|
||||||
payload = decode_token(token)
|
payload = decode_token(token)
|
||||||
if not payload:
|
if not payload:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -33,7 +39,6 @@ async def get_current_user(
|
|||||||
detail="Token azonosítási hiba."
|
detail="Token azonosítási hiba."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Felhasználó lekérése az adatbázisból
|
|
||||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
@@ -48,8 +53,5 @@ async def get_current_user(
|
|||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="Ez a fiók korábban törlésre került."
|
detail="Ez a fiók korábban törlésre került."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Megjegyzés: is_active ellenőrzést szándékosan nem teszünk itt,
|
|
||||||
# hogy a KYC folyamatot (Step 2) be tudja fejezni a még nem aktív user is.
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,104 +1,136 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, func, and_
|
||||||
from app.db.session import get_db
|
|
||||||
from app.schemas.asset import AssetCreate, AssetResponse
|
|
||||||
from app.models.vehicle import Asset, VehicleCatalog
|
|
||||||
from app.models.organization import Organization
|
|
||||||
from app.core.validators import VINValidator
|
|
||||||
from app.core.config import settings
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from app.db.session import get_db
|
||||||
|
from app.api.deps import get_current_user
|
||||||
|
from app.schemas.asset import AssetCreate, AssetResponse
|
||||||
|
from app.models.asset import Asset, AssetCatalog, AssetAssignment, AssetEvent
|
||||||
|
from app.models.identity import User
|
||||||
|
from app.models.organization import Organization, OrganizationMember, OrgType
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# VIN Validator - Standard 17 karakter, tiltott karakterek (I, O, Q) szűrése
|
||||||
|
class VINValidator:
|
||||||
|
@staticmethod
|
||||||
|
def validate(vin: str) -> bool:
|
||||||
|
vin = vin.upper()
|
||||||
|
if len(vin) != 17:
|
||||||
|
return False
|
||||||
|
if any(c in vin for c in "IOQ"):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_asset(
|
async def create_asset(
|
||||||
asset_in: AssetCreate,
|
asset_in: AssetCreate,
|
||||||
db: AsyncSession = Depends(get_db)
|
target_org_id: int = None,
|
||||||
# Később ide jön: current_user: User = Depends(get_current_active_user)
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
# 1. VIN Validáció
|
||||||
Új jármű (Asset) rögzítése a flottába.
|
if not VINValidator.validate(asset_in.vin):
|
||||||
- Validálja a VIN-t (MOD 11 checksum).
|
raise HTTPException(status_code=400, detail="Érvénytelen alvázszám (VIN) formátum!")
|
||||||
- Ellenőrzi a Katalógus elemet.
|
|
||||||
- Létrehozza a NAS mappát a dokumentumoknak.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. VIN Validáció (Szigorú Checksum)
|
|
||||||
# A GEM protokoll szerint kötelező a validátor használata
|
|
||||||
is_valid_vin = VINValidator.validate(asset_in.vin)
|
|
||||||
if not is_valid_vin:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Érvénytelen alvázszám (VIN)! A Checksum ellenőrzés sikertelen."
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Katalógus elem ellenőrzése
|
# 2. Célflotta ellenőrzése
|
||||||
stmt_catalog = select(VehicleCatalog).where(VehicleCatalog.id == asset_in.catalog_id)
|
if not target_org_id:
|
||||||
result_catalog = await db.execute(stmt_catalog)
|
stmt_org = select(Organization).join(OrganizationMember).where(
|
||||||
catalog_item = result_catalog.scalar_one_or_none()
|
and_(
|
||||||
|
OrganizationMember.user_id == current_user.id,
|
||||||
|
Organization.org_type == OrgType.individual
|
||||||
|
)
|
||||||
|
)
|
||||||
|
org = (await db.execute(stmt_org)).scalar_one_or_none()
|
||||||
|
if not org:
|
||||||
|
raise HTTPException(status_code=404, detail="Privát flotta nem található. KYC szükséges.")
|
||||||
|
final_org_id = org.id
|
||||||
|
else:
|
||||||
|
# Céges jogosultság ellenőrzése
|
||||||
|
stmt_mem = select(OrganizationMember).where(
|
||||||
|
and_(
|
||||||
|
OrganizationMember.organization_id == target_org_id,
|
||||||
|
OrganizationMember.user_id == current_user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
member = (await db.execute(stmt_mem)).scalar_one_or_none()
|
||||||
|
if not member or (member.role != "owner" and not (member.permissions or {}).get("can_add_asset")):
|
||||||
|
raise HTTPException(status_code=403, detail="Nincs jogod ehhez a flottához!")
|
||||||
|
final_org_id = target_org_id
|
||||||
|
|
||||||
|
# 3. Katalógus ellenőrzése
|
||||||
|
stmt_cat = select(AssetCatalog).where(
|
||||||
|
and_(
|
||||||
|
AssetCatalog.make.ilike(asset_in.make), # Simán ilike, nem kell func() köré
|
||||||
|
AssetCatalog.model.ilike(asset_in.model)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
catalog_item = (await db.execute(stmt_cat)).scalar_one_or_none()
|
||||||
|
|
||||||
if not catalog_item:
|
if not catalog_item:
|
||||||
raise HTTPException(
|
catalog_item = AssetCatalog(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
make=asset_in.make,
|
||||||
detail="A kiválasztott járműtípus nem található a katalógusban."
|
model=asset_in.model,
|
||||||
|
vehicle_class=asset_in.vehicle_class,
|
||||||
|
fuel_type=asset_in.fuel_type
|
||||||
)
|
)
|
||||||
|
db.add(catalog_item)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
# 3. Szervezet ellenőrzése (Létezik-e, és van-e joga - jogultságkezelés később)
|
# 4. Asset létrehozása vagy betöltése (Shadow Identity)
|
||||||
stmt_org = select(Organization).where(Organization.id == asset_in.organization_id)
|
stmt_exist = select(Asset).where(Asset.vin == asset_in.vin.upper())
|
||||||
result_org = await db.execute(stmt_org)
|
new_asset = (await db.execute(stmt_exist)).scalar_one_or_none()
|
||||||
org_item = result_org.scalar_one_or_none()
|
|
||||||
|
if not new_asset:
|
||||||
if not org_item:
|
new_asset = Asset(
|
||||||
raise HTTPException(
|
vin=asset_in.vin.upper(),
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
license_plate=asset_in.license_plate,
|
||||||
detail="A megadott flotta/szervezet nem található."
|
name=asset_in.name or f"{asset_in.make} {asset_in.model}",
|
||||||
|
year_of_manufacture=asset_in.year_of_manufacture,
|
||||||
|
fuel_type=asset_in.fuel_type, # JAVÍTVA: Most már átadjuk
|
||||||
|
vehicle_class=asset_in.vehicle_class, # JAVÍTVA: Most már átadjuk
|
||||||
|
mileage_unit=asset_in.reading_unit, # JAVÍTVA: Most már átadjuk
|
||||||
|
catalog_id=catalog_item.id,
|
||||||
|
quality_index=1.00,
|
||||||
|
system_mileage=0
|
||||||
)
|
)
|
||||||
|
db.add(new_asset)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
# 4. Asset Duplikáció ellenőrzése (Ugyanaz a VIN nem szerepelhet 2x aktívként)
|
# 5. Assignment
|
||||||
# Megj: A UAI mező tárolja a VIN-t (Unique Asset Identifier)
|
new_assignment = AssetAssignment(
|
||||||
stmt_exist = select(Asset).where(
|
asset_id=new_asset.id,
|
||||||
Asset.uai == asset_in.vin.upper(),
|
organization_id=final_org_id,
|
||||||
Asset.status != "deleted"
|
status="active"
|
||||||
)
|
)
|
||||||
result_exist = await db.execute(stmt_exist)
|
db.add(new_assignment)
|
||||||
if result_exist.scalar_one_or_none():
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
|
||||||
detail="Ez a jármű (VIN) már szerepel a rendszerben!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Mentés az adatbázisba
|
# 6. Kezdő KM esemény
|
||||||
new_asset = Asset(
|
if asset_in.current_reading:
|
||||||
uai=asset_in.vin.upper(), # A VIN a fő azonosító
|
db.add(AssetEvent(
|
||||||
catalog_id=asset_in.catalog_id,
|
asset_id=new_asset.id,
|
||||||
organization_id=asset_in.organization_id,
|
event_type="initial_reading",
|
||||||
asset_type=catalog_item.category, # 'car', 'van', 'motorcycle', etc.
|
recorded_mileage=asset_in.current_reading,
|
||||||
name=asset_in.name or f"{catalog_item.brand} {catalog_item.model}",
|
description="Kezdeti óraállás rögzítése",
|
||||||
current_plate_number=asset_in.license_plate.upper(),
|
data={"source": "user_registration"}
|
||||||
status="active",
|
))
|
||||||
privacy_level="private" # Alapértelmezett
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(new_asset)
|
|
||||||
await db.commit()
|
|
||||||
await db.refresh(new_asset)
|
|
||||||
|
|
||||||
# 6. NAS Tároló Létrehozása
|
|
||||||
# Útvonal: /mnt/nas/app_data/assets/{uuid}/
|
|
||||||
# A settings.NAS_STORAGE_PATH-ot használjuk (GEM protokoll)
|
|
||||||
try:
|
try:
|
||||||
# Ha a settings-ben nincs definiálva, fallback a hardcoded path-ra (biztonsági háló)
|
await db.commit()
|
||||||
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data/assets")
|
await db.refresh(new_asset)
|
||||||
asset_path = os.path.join(base_path, str(new_asset.id))
|
|
||||||
|
|
||||||
os.makedirs(asset_path, exist_ok=True)
|
# 7. NAS mappa struktúra
|
||||||
logger.info(f"NAS mappa létrehozva: {asset_path}")
|
nas_base = getattr(settings, "NAS_STORAGE_PATH", "/opt/docker/dev/service_finder/nas/assets")
|
||||||
|
asset_path = os.path.join(nas_base, str(new_asset.id))
|
||||||
|
os.makedirs(os.path.join(asset_path, "docs"), exist_ok=True)
|
||||||
|
os.makedirs(os.path.join(asset_path, "photos"), exist_ok=True)
|
||||||
|
|
||||||
except OSError as e:
|
return new_asset
|
||||||
logger.error(f"CRITICAL: Nem sikerült létrehozni a NAS mappát: {e}")
|
except Exception as e:
|
||||||
# Nem dobunk hibát a usernek, mert az adatbázisba bekerült, de riasztunk logban
|
await db.rollback()
|
||||||
|
logger.error(f"Asset Creation Error: {str(e)}")
|
||||||
return new_asset
|
raise HTTPException(status_code=500, detail="Hiba a mentés során.")
|
||||||
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Query
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, or_
|
from sqlalchemy import select, or_
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.vehicle import VehicleCatalog
|
from app.models import AssetCatalog
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|||||||
BIN
backend/app/db/__pycache__/base.cpython-312.pyc
Executable file → Normal file
BIN
backend/app/db/__pycache__/base.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
backend/app/db/__pycache__/base_class.cpython-312.pyc
Normal file
BIN
backend/app/db/__pycache__/base_class.cpython-312.pyc
Normal file
Binary file not shown.
@@ -1,10 +1,6 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
from app.db.base_class import Base # noqa
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from app.models.address import Address, GeoPostalCode, GeoStreet, GeoStreetType # noqa
|
||||||
|
from app.models.identity import User, Person, VerificationToken, Wallet # noqa
|
||||||
|
from app.models.organization import Organization, OrganizationMember # noqa
|
||||||
class Base(AsyncAttrs, DeclarativeBase):
|
from app.models.asset import Asset, AssetCatalog, AssetCost, AssetEvent # noqa
|
||||||
"""
|
from app.models.gamification import UserStats, PointsLedger # noqa
|
||||||
Base class for all SQLAlchemy models.
|
|
||||||
Includes AsyncAttrs to support async attribute access (lazy loading).
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
13
backend/app/db/base_class.py
Normal file
13
backend/app/db/base_class.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import Any
|
||||||
|
from sqlalchemy.ext.declarative import as_declarative, declared_attr
|
||||||
|
|
||||||
|
@as_declarative()
|
||||||
|
class Base:
|
||||||
|
id: Any
|
||||||
|
__name__: str
|
||||||
|
|
||||||
|
# Automatikusan generálja a tábla nevét az osztálynévből,
|
||||||
|
# ha nincs külön megadva (bár mi megadjuk a sémát)
|
||||||
|
@declared_attr
|
||||||
|
def __tablename__(cls) -> str:
|
||||||
|
return cls.__name__.lower()
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
from app.db.base import Base
|
from app.db.base_class import Base
|
||||||
from .identity import User, Person, Wallet, UserRole, VerificationToken
|
from .identity import User, Person, Wallet, UserRole, VerificationToken
|
||||||
from .organization import Organization, OrgType
|
from .organization import Organization, OrganizationMember
|
||||||
from .vehicle import (
|
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent
|
||||||
VehicleCatalog,
|
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
|
||||||
Asset,
|
from .gamification import UserStats, PointsLedger
|
||||||
AssetEvent,
|
|
||||||
AssetRating,
|
|
||||||
ServiceProvider,
|
|
||||||
Vehicle, # Alias az Asset-re a kompatibilitás miatt
|
|
||||||
ServiceRecord # Alias az AssetEvent-re a kompatibilitás miatt
|
|
||||||
)
|
|
||||||
|
|
||||||
# Aliasok a kód többi részének szinkronizálásához
|
# Aliasok a kompatibilitás és a tiszta kód érdekében
|
||||||
UserVehicle = Vehicle
|
Vehicle = Asset
|
||||||
VehicleOwnership = Asset
|
UserVehicle = Asset
|
||||||
|
VehicleCatalog = AssetCatalog
|
||||||
|
ServiceRecord = AssetEvent
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Base",
|
"Base",
|
||||||
@@ -23,14 +19,19 @@ __all__ = [
|
|||||||
"UserRole",
|
"UserRole",
|
||||||
"VerificationToken",
|
"VerificationToken",
|
||||||
"Organization",
|
"Organization",
|
||||||
"OrgType",
|
"OrganizationMember",
|
||||||
"VehicleCatalog",
|
|
||||||
"Asset",
|
"Asset",
|
||||||
"AssetEvent",
|
"AssetCatalog",
|
||||||
"AssetRating",
|
"AssetCost",
|
||||||
"ServiceProvider",
|
"AssetEvent",
|
||||||
|
"Address",
|
||||||
|
"GeoPostalCode",
|
||||||
|
"GeoStreet",
|
||||||
|
"GeoStreetType",
|
||||||
|
"UserStats",
|
||||||
|
"PointsLedger",
|
||||||
"Vehicle",
|
"Vehicle",
|
||||||
"UserVehicle",
|
"UserVehicle",
|
||||||
"ServiceRecord",
|
"VehicleCatalog",
|
||||||
"VehicleOwnership"
|
"ServiceRecord"
|
||||||
]
|
]
|
||||||
Binary file not shown.
BIN
backend/app/models/__pycache__/address.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/address.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/asset.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/asset.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
45
backend/app/models/address.py
Normal file
45
backend/app/models/address.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlalchemy import Column, String, Integer, ForeignKey, Text, DateTime
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
class GeoPostalCode(Base):
|
||||||
|
__tablename__ = "geo_postal_codes"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
country_code = Column(String(5), default="HU")
|
||||||
|
zip_code = Column(String(10), nullable=False)
|
||||||
|
city = Column(String(100), nullable=False)
|
||||||
|
|
||||||
|
class GeoStreet(Base):
|
||||||
|
__tablename__ = "geo_streets"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
|
||||||
|
class GeoStreetType(Base):
|
||||||
|
__tablename__ = "geo_street_types"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(String(50), unique=True, nullable=False)
|
||||||
|
|
||||||
|
class Address(Base):
|
||||||
|
__tablename__ = "addresses"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
postal_code_id = Column(Integer, ForeignKey("data.geo_postal_codes.id"))
|
||||||
|
street_name = Column(String(200), nullable=False)
|
||||||
|
street_type = Column(String(50), nullable=False)
|
||||||
|
house_number = Column(String(50), nullable=False)
|
||||||
|
stairwell = Column(String(20))
|
||||||
|
floor = Column(String(20))
|
||||||
|
door = Column(String(20))
|
||||||
|
parcel_id = Column(String(50)) # HRSZ
|
||||||
|
full_address_text = Column(Text)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
133
backend/app/models/asset.py
Normal file
133
backend/app/models/asset.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import uuid
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
class AssetCatalog(Base):
|
||||||
|
"""Központi jármű katalógus (Admin/Bot által tölthető)"""
|
||||||
|
__tablename__ = "vehicle_catalog"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
make = Column(String, index=True, nullable=False)
|
||||||
|
model = Column(String, index=True, nullable=False)
|
||||||
|
generation = Column(String)
|
||||||
|
year_from = Column(Integer)
|
||||||
|
year_to = Column(Integer)
|
||||||
|
vehicle_class = Column(String) # land, sea, air
|
||||||
|
fuel_type = Column(String)
|
||||||
|
engine_code = Column(String)
|
||||||
|
|
||||||
|
assets = relationship("Asset", back_populates="catalog")
|
||||||
|
|
||||||
|
class Asset(Base):
|
||||||
|
"""A Jármű Identitás (Digital Twin törzsadatok)"""
|
||||||
|
__tablename__ = "assets"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
vin = Column(String(17), unique=True, index=True, nullable=False)
|
||||||
|
license_plate = Column(String(20), index=True)
|
||||||
|
name = Column(String)
|
||||||
|
year_of_manufacture = Column(Integer)
|
||||||
|
|
||||||
|
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"))
|
||||||
|
|
||||||
|
# Nemzetközi mutatók
|
||||||
|
quality_index = Column(Numeric(3, 2), default=1.00)
|
||||||
|
system_mileage = Column(Integer, default=0)
|
||||||
|
mileage_unit = Column(String(10), default="km") # Nemzetközi: km, miles, hours
|
||||||
|
|
||||||
|
is_verified = Column(Boolean, default=False)
|
||||||
|
status = Column(String(20), default="active")
|
||||||
|
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
catalog = relationship("AssetCatalog", back_populates="assets")
|
||||||
|
assignments = relationship("AssetAssignment", back_populates="asset")
|
||||||
|
events = relationship("AssetEvent", back_populates="asset")
|
||||||
|
costs = relationship("AssetCost", back_populates="asset")
|
||||||
|
|
||||||
|
class AssetAssignment(Base):
|
||||||
|
"""Birtoklás követése (Kié a jármű és mettől meddig)"""
|
||||||
|
__tablename__ = "asset_assignments"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
|
|
||||||
|
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
released_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
status = Column(String(30), default="active")
|
||||||
|
notes = Column(String)
|
||||||
|
|
||||||
|
asset = relationship("Asset", back_populates="assignments")
|
||||||
|
organization = relationship("Organization", back_populates="assets")
|
||||||
|
|
||||||
|
class AssetEvent(Base):
|
||||||
|
"""Élettörténeti események (Szerviz, km-óra állások, balesetek)"""
|
||||||
|
__tablename__ = "asset_events"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
|
||||||
|
event_type = Column(String(50), nullable=False)
|
||||||
|
event_date = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
recorded_mileage = Column(Integer)
|
||||||
|
description = Column(String)
|
||||||
|
data = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
|
asset = relationship("Asset", back_populates="events")
|
||||||
|
|
||||||
|
class AssetCost(Base):
|
||||||
|
"""
|
||||||
|
Költségkezelő modell: Bruttó/Nettó/ÁFA és deviza támogatás.
|
||||||
|
"""
|
||||||
|
__tablename__ = "asset_costs"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||||
|
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
|
|
||||||
|
cost_type = Column(String(50), nullable=False) # fuel, service, tax, insurance, toll
|
||||||
|
|
||||||
|
# Pénzügyi adatok
|
||||||
|
amount = Column(Numeric(18, 2), nullable=False) # Bruttó összeg
|
||||||
|
net_amount = Column(Numeric(18, 2)) # Nettó összeg
|
||||||
|
vat_amount = Column(Numeric(18, 2)) # ÁFA érték
|
||||||
|
vat_rate = Column(Numeric(5, 2)) # ÁFA kulcs (pl. 27.00)
|
||||||
|
|
||||||
|
# Nemzetközi deviza kezelés
|
||||||
|
currency = Column(String(3), default="HUF") # Riportálási deviza
|
||||||
|
original_currency = Column(String(3)) # Számla eredeti devizája
|
||||||
|
exchange_rate_at_cost = Column(Numeric(18, 6)) # Rögzítéskori árfolyam
|
||||||
|
|
||||||
|
date = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
description = Column(String)
|
||||||
|
invoice_id = Column(String)
|
||||||
|
mileage_at_cost = Column(Integer)
|
||||||
|
|
||||||
|
data = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
|
asset = relationship("Asset", back_populates="costs")
|
||||||
|
|
||||||
|
class ExchangeRate(Base):
|
||||||
|
"""Napi árfolyamok tárolása (ECB/MNB adatok alapján)"""
|
||||||
|
__tablename__ = "exchange_rates"
|
||||||
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
base_currency = Column(String(3), default="EUR")
|
||||||
|
target_currency = Column(String(3), nullable=False)
|
||||||
|
rate = Column(Numeric(18, 6), nullable=False)
|
||||||
|
rate_date = Column(DateTime(timezone=False), index=True)
|
||||||
|
provider = Column(String(50), default="ECB")
|
||||||
|
updated_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean
|
from sqlalchemy import ForeignKey, String, Integer, DateTime, func, Boolean
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from app.db.base import Base
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
# Típusvizsgálathoz a körkörös import elkerülése érdekében
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.identity import User
|
||||||
|
|
||||||
# Közös beállítás az összes táblához ebben a fájlban
|
# Közös beállítás az összes táblához ebben a fájlban
|
||||||
SCHEMA_ARGS = {"schema": "data"}
|
SCHEMA_ARGS = {"schema": "data"}
|
||||||
@@ -36,21 +42,26 @@ class PointsLedger(Base):
|
|||||||
__tablename__ = "points_ledger"
|
__tablename__ = "points_ledger"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = SCHEMA_ARGS
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
# JAVÍTVA: data.users.id hivatkozás
|
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
||||||
points: Mapped[int] = mapped_column(Integer)
|
points: Mapped[int] = mapped_column(Integer)
|
||||||
reason: Mapped[str] = mapped_column(String)
|
reason: Mapped[str] = mapped_column(String)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
# Kapcsolat a felhasználóhoz
|
||||||
|
user: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
class UserStats(Base):
|
class UserStats(Base):
|
||||||
__tablename__ = "user_stats"
|
__tablename__ = "user_stats"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = SCHEMA_ARGS
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
# user_id a PK, mert 1:1 kapcsolat a User-rel
|
||||||
# JAVÍTVA: data.users.id hivatkozás
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), unique=True)
|
total_xp: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
total_points: Mapped[int] = mapped_column(Integer, default=0)
|
social_points: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
current_level: Mapped[int] = mapped_column(Integer, default=1)
|
current_level: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
last_activity: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
# EZ A JAVÍTÁS: A visszamutató kapcsolat definiálása
|
||||||
|
user: Mapped["User"] = relationship("User", back_populates="stats")
|
||||||
|
|
||||||
class Badge(Base):
|
class Badge(Base):
|
||||||
__tablename__ = "badges"
|
__tablename__ = "badges"
|
||||||
@@ -64,7 +75,18 @@ class UserBadge(Base):
|
|||||||
__tablename__ = "user_badges"
|
__tablename__ = "user_badges"
|
||||||
__table_args__ = SCHEMA_ARGS
|
__table_args__ = SCHEMA_ARGS
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||||
# JAVÍTVA: data.users.id hivatkozás
|
|
||||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
||||||
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
|
badge_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.badges.id"))
|
||||||
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
earned_at: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
user: Mapped["User"] = relationship("User")
|
||||||
|
|
||||||
|
class Rating(Base): # <--- Az új értékelési modell
|
||||||
|
__tablename__ = "ratings"
|
||||||
|
__table_args__ = SCHEMA_ARGS
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
author_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"))
|
||||||
|
target_type: Mapped[str] = mapped_column(String(20))
|
||||||
|
target_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True))
|
||||||
|
score: Mapped[int] = mapped_column(Integer)
|
||||||
|
comment: Mapped[Optional[str]] = mapped_column(String)
|
||||||
@@ -2,9 +2,9 @@ import uuid
|
|||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, Enum, BigInteger
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base import Base
|
from app.db.base_class import Base
|
||||||
|
|
||||||
class UserRole(str, enum.Enum):
|
class UserRole(str, enum.Enum):
|
||||||
admin = "admin"
|
admin = "admin"
|
||||||
@@ -18,23 +18,22 @@ class Person(Base):
|
|||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(BigInteger, primary_key=True, index=True)
|
id = Column(BigInteger, primary_key=True, index=True)
|
||||||
id_uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
id_uuid = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||||
|
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
||||||
|
|
||||||
last_name = Column(String, nullable=False)
|
last_name = Column(String, nullable=False)
|
||||||
first_name = Column(String, nullable=False)
|
first_name = Column(String, nullable=False)
|
||||||
mothers_name = Column(String, nullable=True)
|
mothers_last_name = Column(String, nullable=True)
|
||||||
|
mothers_first_name = Column(String, nullable=True)
|
||||||
birth_place = Column(String, nullable=True)
|
birth_place = Column(String, nullable=True)
|
||||||
birth_date = Column(DateTime, nullable=True)
|
birth_date = Column(DateTime, nullable=True)
|
||||||
phone = Column(String, nullable=True)
|
phone = Column(String, nullable=True)
|
||||||
|
|
||||||
# JSONB mezők az okmányoknak és orvosi adatoknak
|
|
||||||
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
identity_docs = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
|
medical_emergency = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
ice_contact = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
# KYC státusz
|
|
||||||
is_active = Column(Boolean, default=False, nullable=False)
|
is_active = Column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
@@ -47,28 +46,32 @@ class User(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
email = Column(String, unique=True, index=True, nullable=False)
|
email = Column(String, unique=True, index=True, nullable=False)
|
||||||
hashed_password = Column(String, nullable=True)
|
hashed_password = Column(String, nullable=True)
|
||||||
|
|
||||||
role = Column(Enum(UserRole), default=UserRole.user)
|
role = Column(Enum(UserRole), default=UserRole.user)
|
||||||
is_active = Column(Boolean, default=False)
|
is_active = Column(Boolean, default=False)
|
||||||
region_code = Column(String, default="HU")
|
region_code = Column(String, default="HU")
|
||||||
|
|
||||||
is_deleted = Column(Boolean, default=False)
|
is_deleted = Column(Boolean, default=False)
|
||||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
||||||
|
|
||||||
person = relationship("Person", back_populates="users")
|
person = relationship("Person", back_populates="users")
|
||||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||||
owned_organizations = relationship("Organization", back_populates="owner", lazy="select")
|
|
||||||
|
|
||||||
|
# Itt a trükk: csak a string hivatkozás marad, így nincs import hiba,
|
||||||
|
# de a SQLAlchemy tudni fogja, hogy a UserStats-ra gondolunk.
|
||||||
|
stats = relationship("UserStats", back_populates="user", uselist=False)
|
||||||
|
|
||||||
|
owned_organizations = relationship("Organization", back_populates="owner", lazy="select")
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
class Wallet(Base):
|
class Wallet(Base):
|
||||||
|
"""Kétoszlopos pénztárca: Coin (Szerviz) és Kredit (Prémium)."""
|
||||||
__tablename__ = "wallets"
|
__tablename__ = "wallets"
|
||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||||
coin_balance = Column(Numeric(18, 2), default=0.00)
|
coin_balance = Column(Numeric(18, 2), default=0.00)
|
||||||
xp_balance = Column(Integer, default=0)
|
credit_balance = Column(Numeric(18, 2), default=0.00)
|
||||||
|
currency = Column(String(3), default="HUF")
|
||||||
|
|
||||||
user = relationship("User", back_populates="wallet")
|
user = relationship("User", back_populates="wallet")
|
||||||
|
|
||||||
@@ -77,9 +80,9 @@ class VerificationToken(Base):
|
|||||||
__table_args__ = {"schema": "data"}
|
__table_args__ = {"schema": "data"}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
token = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
token = Column(PG_UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="CASCADE"), nullable=False)
|
||||||
token_type = Column(String(20), nullable=False) # 'registration' vagy 'password_reset'
|
token_type = Column(String(20), nullable=False)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
is_used = Column(Boolean, default=False)
|
is_used = Column(Boolean, default=False)
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON
|
import uuid
|
||||||
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, text
|
||||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base import Base
|
from app.db.base_class import Base
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||||
|
|
||||||
class OrgType(str, enum.Enum):
|
class OrgType(str, enum.Enum):
|
||||||
individual = "individual"
|
individual = "individual"
|
||||||
@@ -19,18 +21,21 @@ class Organization(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
|
||||||
|
# ÚJ MEZŐ: Egységes címkezelés (GeoService hibrid)
|
||||||
|
address_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.addresses.id"), nullable=True)
|
||||||
|
|
||||||
# --- NÉVKEZELÉS ---
|
# --- NÉVKEZELÉS ---
|
||||||
full_name = Column(String, nullable=False) # Teljes hivatalos név
|
full_name = Column(String, nullable=False) # Teljes hivatalos név
|
||||||
name = Column(String, nullable=False) # Rövidített cégnév (pl. ProfiBot Kft.)
|
name = Column(String, nullable=False) # Rövidített cégnév
|
||||||
display_name = Column(String(50)) # Alkalmazáson belüli rövidítés (pl. ProfiBot)
|
display_name = Column(String(50)) # Alkalmazáson belüli rövidítés
|
||||||
|
|
||||||
# --- ATOMIZÁLT CÍMKEZELÉS ---
|
# --- ATOMIZÁLT CÍMKEZELÉS (Kompatibilitási réteg) ---
|
||||||
address_zip = Column(String(10))
|
address_zip = Column(String(10))
|
||||||
address_city = Column(String(100))
|
address_city = Column(String(100))
|
||||||
address_street_name = Column(String(150))
|
address_street_name = Column(String(150))
|
||||||
address_street_type = Column(String(50)) # utca, út, tér, dűlő, stb.
|
address_street_type = Column(String(50))
|
||||||
address_house_number = Column(String(20))
|
address_house_number = Column(String(20))
|
||||||
address_hrsz = Column(String(50)) # Helyrajzi szám
|
address_hrsz = Column(String(50))
|
||||||
address_stairwell = Column(String(20))
|
address_stairwell = Column(String(20))
|
||||||
address_floor = Column(String(20))
|
address_floor = Column(String(20))
|
||||||
address_door = Column(String(20))
|
address_door = Column(String(20))
|
||||||
@@ -40,7 +45,6 @@ class Organization(Base):
|
|||||||
tax_number = Column(String(20), unique=True, index=True)
|
tax_number = Column(String(20), unique=True, index=True)
|
||||||
reg_number = Column(String(50))
|
reg_number = Column(String(50))
|
||||||
|
|
||||||
# PG_ENUM használata a Python Enum-mal szinkronizálva
|
|
||||||
org_type = Column(
|
org_type = Column(
|
||||||
PG_ENUM(OrgType, name="orgtype", inherit_schema=True),
|
PG_ENUM(OrgType, name="orgtype", inherit_schema=True),
|
||||||
default=OrgType.individual
|
default=OrgType.individual
|
||||||
@@ -49,13 +53,8 @@ class Organization(Base):
|
|||||||
status = Column(String(30), default="pending_verification")
|
status = Column(String(30), default="pending_verification")
|
||||||
is_deleted = Column(Boolean, default=False)
|
is_deleted = Column(Boolean, default=False)
|
||||||
|
|
||||||
notification_settings = Column(JSON, default={
|
notification_settings = Column(JSON, server_default=text("'{ \"notify_owner\": true, \"alert_days_before\": [30, 15, 7, 1] }'::jsonb"))
|
||||||
"notify_owner": True,
|
external_integration_config = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
"notify_manager": True,
|
|
||||||
"notify_contact": True,
|
|
||||||
"alert_days_before": [30, 15, 7, 1]
|
|
||||||
})
|
|
||||||
external_integration_config = Column(JSON, default={})
|
|
||||||
|
|
||||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
@@ -67,8 +66,8 @@ class Organization(Base):
|
|||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
# Kapcsolatok
|
# Kapcsolatok
|
||||||
assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan")
|
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||||
members = relationship("OrganizationMember", back_populates="organization")
|
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||||
owner = relationship("User", back_populates="owned_organizations")
|
owner = relationship("User", back_populates="owned_organizations")
|
||||||
|
|
||||||
class OrganizationMember(Base):
|
class OrganizationMember(Base):
|
||||||
@@ -77,9 +76,13 @@ class OrganizationMember(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||||
role = Column(String, default="driver")
|
role = Column(String, default="driver") # owner, manager, driver, service_staff
|
||||||
|
|
||||||
|
# JAVÍTVA: Jogosultságok JSONB mezője (can_add_asset, etc.)
|
||||||
|
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||||
|
|
||||||
organization = relationship("Organization", back_populates="members")
|
organization = relationship("Organization", back_populates="members")
|
||||||
|
user = relationship("app.models.identity.User") # Visszamutató kapcsolat a felhasználóra
|
||||||
|
|
||||||
# Kompatibilitási réteg a korábbi kódokhoz
|
# Kompatibilitási réteg
|
||||||
Organization.vehicles = Organization.assets
|
Organization.vehicles = Organization.assets
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Numeric, DateTime, JSON, Date
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
import uuid
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
# 1. GLOBÁLIS KATALÓGUS (A rendszer agya)
|
|
||||||
class VehicleCatalog(Base):
|
|
||||||
__tablename__ = "vehicle_catalog"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
brand = Column(String(100), nullable=False)
|
|
||||||
model = Column(String(100), nullable=False)
|
|
||||||
generation = Column(String(100))
|
|
||||||
year_from = Column(Integer)
|
|
||||||
year_to = Column(Integer)
|
|
||||||
category = Column(String(50))
|
|
||||||
engine_type = Column(String(50))
|
|
||||||
engine_power_kw = Column(Integer)
|
|
||||||
|
|
||||||
# Robot státusz és gyári adatok
|
|
||||||
verification_status = Column(String(20), default="verified")
|
|
||||||
factory_specs = Column(JSON, default={})
|
|
||||||
maintenance_plan = Column(JSON, default={})
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
# Kapcsolat az egyedi példányok felé
|
|
||||||
assets = relationship("Asset", back_populates="catalog_entry")
|
|
||||||
|
|
||||||
# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona
|
|
||||||
class Asset(Base):
|
|
||||||
__tablename__ = "assets"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
||||||
uai = Column(String(100), unique=True, nullable=False) # VIN, HIN vagy Serial Number
|
|
||||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"))
|
|
||||||
catalog_id = Column(Integer, ForeignKey("data.vehicle_catalog.id"), nullable=True)
|
|
||||||
|
|
||||||
asset_type = Column(String(20), nullable=False) # car, boat, plane
|
|
||||||
name = Column(String(255)) # "Kincses E39"
|
|
||||||
|
|
||||||
# Dinamikus állapot
|
|
||||||
current_plate_number = Column(String(20))
|
|
||||||
current_country_code = Column(String(2))
|
|
||||||
odometer_value = Column(Numeric(12, 2), default=0)
|
|
||||||
operating_hours = Column(Numeric(12, 2), default=0)
|
|
||||||
|
|
||||||
# Egyedi DNS (Gyári config + utólagos módosítások)
|
|
||||||
factory_config = Column(JSON, default={})
|
|
||||||
aftermarket_mods = Column(JSON, default={})
|
|
||||||
|
|
||||||
# Állapot és láthatóság (EZ HIÁNYZOTT)
|
|
||||||
status = Column(String(50), default="active")
|
|
||||||
privacy_level = Column(String(20), default="private")
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
||||||
|
|
||||||
# Kapcsolatok
|
|
||||||
organization = relationship("Organization", back_populates="assets")
|
|
||||||
catalog_entry = relationship("VehicleCatalog", back_populates="assets")
|
|
||||||
events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan")
|
|
||||||
ratings = relationship("AssetRating", back_populates="asset")
|
|
||||||
|
|
||||||
# 3. DIGITÁLIS SZERVIZKÖNYV / ESEMÉNYTÁR
|
|
||||||
class AssetEvent(Base):
|
|
||||||
__tablename__ = "asset_events"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
||||||
asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id"))
|
|
||||||
event_type = Column(String(50)) # SERVICE, REPAIR, INSPECTION, ACCIDENT, PLATE_CHANGE
|
|
||||||
|
|
||||||
odometer_at_event = Column(Numeric(12, 2))
|
|
||||||
hours_at_event = Column(Numeric(12, 2))
|
|
||||||
|
|
||||||
description = Column(String)
|
|
||||||
cost = Column(Numeric(12, 2))
|
|
||||||
currency = Column(String(3), default="EUR")
|
|
||||||
|
|
||||||
is_verified = Column(Boolean, default=False)
|
|
||||||
attachments = Column(JSON, default=[]) # Számlák, fotók linkjei
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
asset = relationship("Asset", back_populates="events")
|
|
||||||
|
|
||||||
# 4. EMOCIONÁLIS ÉRTÉKELÉS
|
|
||||||
class AssetRating(Base):
|
|
||||||
__tablename__ = "asset_ratings"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
asset_id = Column(UUID(as_uuid=True), ForeignKey("data.assets.id"))
|
|
||||||
user_id = Column(Integer, ForeignKey("data.users.id"))
|
|
||||||
|
|
||||||
comfort_score = Column(Integer) # 1-10
|
|
||||||
experience_score = Column(Integer) # 1-10
|
|
||||||
practicality_score = Column(Integer) # 1-10
|
|
||||||
comment = Column(String)
|
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
asset = relationship("Asset", back_populates="ratings")
|
|
||||||
|
|
||||||
# 5. SZOLGÁLTATÓK (Szerelők, Partnerek)
|
|
||||||
class ServiceProvider(Base):
|
|
||||||
__tablename__ = "service_providers"
|
|
||||||
__table_args__ = {"schema": "data"}
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
name = Column(String(255), nullable=False)
|
|
||||||
official_brand_partner = Column(Boolean, default=False)
|
|
||||||
technical_rating_pct = Column(Integer, default=80)
|
|
||||||
location_city = Column(String(100))
|
|
||||||
is_active = Column(Boolean, default=True)
|
|
||||||
|
|
||||||
# --- KOMPATIBILITÁSI RÉTEG ---
|
|
||||||
Vehicle = Asset
|
|
||||||
ServiceRecord = AssetEvent
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,27 +1,43 @@
|
|||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
class AssetCreate(BaseModel):
|
class AssetCreate(BaseModel):
|
||||||
catalog_id: int = Field(..., description="A kiválasztott katalógus elem ID-ja")
|
# Alapadatok
|
||||||
vin: str = Field(..., min_length=17, max_length=17, description="17 karakteres alvázszám")
|
make: str = Field(..., example="Ford")
|
||||||
license_plate: str = Field(..., min_length=1, max_length=20)
|
model: str = Field(..., example="Mondeo")
|
||||||
name: Optional[str] = Field(None, description="Egyedi elnevezés (pl. 'Céges furgon')")
|
vin: str = Field(..., min_length=17, max_length=17, description="Alvázszám")
|
||||||
organization_id: int = Field(..., description="Melyik flottába kerüljön")
|
license_plate: Optional[str] = Field(None, max_length=20, example="RRR-555")
|
||||||
|
|
||||||
# Opcionális: Kezdő km óra állás, szín, stb. később bővíthető
|
# Nemzetközi és Admin szempontok
|
||||||
|
vehicle_class: str = Field("land", description="land, sea, air - Admin által bővíthető")
|
||||||
|
fuel_type: str = Field(..., example="Diesel", description="Admin által definiált üzemanyag típusok")
|
||||||
|
|
||||||
|
# Technikai adatok
|
||||||
|
engine_description: Optional[str] = Field(None, example="2.0 TDCI")
|
||||||
|
year_of_manufacture: int = Field(..., ge=1900, le=2100)
|
||||||
|
|
||||||
|
# Kezdő állapot
|
||||||
|
current_reading: int = Field(..., ge=0, description="Kezdő km/üzemóra állás")
|
||||||
|
reading_unit: str = Field("km", description="km, miles, hours - Nemzetközi beállítás")
|
||||||
|
|
||||||
|
# Felhasználói adatok
|
||||||
|
name: Optional[str] = Field(None, description="Egyedi elnevezés")
|
||||||
|
|
||||||
class AssetResponse(BaseModel):
|
class AssetResponse(BaseModel):
|
||||||
id: UUID
|
id: UUID
|
||||||
uai: str
|
catalog_id: Optional[int]
|
||||||
catalog_id: int
|
vin: str
|
||||||
organization_id: int
|
license_plate: Optional[str] = None # JAVÍTVA: Lehet None a válaszban
|
||||||
name: str
|
name: Optional[str] = None
|
||||||
asset_type: str
|
fuel_type: str
|
||||||
current_plate_number: str
|
vehicle_class: str
|
||||||
status: str
|
is_verified: bool
|
||||||
|
year_of_manufacture: int
|
||||||
|
system_mileage: int
|
||||||
|
quality_index: float
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -28,7 +28,15 @@ class UserKYCComplete(BaseModel):
|
|||||||
phone_number: str
|
phone_number: str
|
||||||
birth_place: str
|
birth_place: str
|
||||||
birth_date: date
|
birth_date: date
|
||||||
mothers_name: str
|
mothers_last_name: str
|
||||||
|
mothers_first_name: str
|
||||||
|
# Hibrid Címmezők
|
||||||
|
address_zip: str
|
||||||
|
address_city: str
|
||||||
|
address_street_name: str
|
||||||
|
address_street_type: str
|
||||||
|
address_house_number: str
|
||||||
|
address_hrsz: Optional[str] = None # Helyrajzi szám
|
||||||
identity_docs: Dict[str, DocumentDetail]
|
identity_docs: Dict[str, DocumentDetail]
|
||||||
ice_contact: ICEContact
|
ice_contact: ICEContact
|
||||||
|
|
||||||
@@ -36,7 +44,6 @@ class UserKYCComplete(BaseModel):
|
|||||||
class PasswordResetRequest(BaseModel):
|
class PasswordResetRequest(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
|
|
||||||
# EZ HIÁNYZOTT KORÁBBAN:
|
|
||||||
class PasswordResetConfirm(BaseModel):
|
class PasswordResetConfirm(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
token: str
|
token: str
|
||||||
|
|||||||
Binary file not shown.
@@ -4,28 +4,32 @@ import uuid
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# SQLAlchemy importok
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select, cast, String, func
|
from sqlalchemy import select, and_
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
# Modell és Schema importok - EZ HIÁNYZOTT!
|
|
||||||
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
|
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
|
||||||
from app.models.organization import Organization
|
from app.models.gamification import UserStats # <--- Innen importáljuk mostantól!
|
||||||
from app.schemas.auth import UserLiteRegister, UserKYCComplete # <--- Ez javítja a hibát
|
from app.models.organization import Organization, OrganizationMember, OrgType
|
||||||
|
from app.schemas.auth import UserLiteRegister, UserKYCComplete
|
||||||
from app.core.security import get_password_hash, verify_password
|
from app.core.security import get_password_hash, verify_password
|
||||||
from app.services.email_manager import email_manager
|
from app.services.email_manager import email_manager
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.services.config_service import config # A dinamikus beállításokhoz
|
from app.services.config_service import config
|
||||||
|
from app.services.geo_service import GeoService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
||||||
"""Step 1: Alapszintű regisztráció..."""
|
"""
|
||||||
|
Step 1: Lite Regisztráció (Master Book 1.1)
|
||||||
|
Új User és ideiglenes Person rekord létrehozása.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# 1. Person alap létrehozása
|
# Ideiglenes Person rekord a KYC-ig
|
||||||
new_person = Person(
|
new_person = Person(
|
||||||
first_name=user_in.first_name,
|
first_name=user_in.first_name,
|
||||||
last_name=user_in.last_name,
|
last_name=user_in.last_name,
|
||||||
@@ -34,36 +38,29 @@ class AuthService:
|
|||||||
db.add(new_person)
|
db.add(new_person)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# 2. User fiók
|
|
||||||
new_user = User(
|
new_user = User(
|
||||||
email=user_in.email,
|
email=user_in.email,
|
||||||
hashed_password=get_password_hash(user_in.password),
|
hashed_password=get_password_hash(user_in.password),
|
||||||
person_id=new_person.id,
|
person_id=new_person.id,
|
||||||
role=UserRole.user,
|
role=UserRole.user,
|
||||||
is_active=False,
|
is_active=False,
|
||||||
|
is_deleted=False,
|
||||||
region_code=user_in.region_code
|
region_code=user_in.region_code
|
||||||
)
|
)
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# --- DINAMIKUS TOKEN LEJÁRAT ---
|
# Regisztrációs token generálása
|
||||||
reg_hours = await config.get_setting(
|
reg_hours = await config.get_setting("auth_registration_hours", region_code=user_in.region_code, default=48)
|
||||||
"auth_registration_hours",
|
|
||||||
region_code=user_in.region_code,
|
|
||||||
default=48
|
|
||||||
)
|
|
||||||
|
|
||||||
token_val = uuid.uuid4()
|
token_val = uuid.uuid4()
|
||||||
new_token = VerificationToken(
|
db.add(VerificationToken(
|
||||||
token=token_val,
|
token=token_val,
|
||||||
user_id=new_user.id,
|
user_id=new_user.id,
|
||||||
token_type="registration",
|
token_type="registration",
|
||||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
|
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
|
||||||
)
|
))
|
||||||
db.add(new_token)
|
|
||||||
await db.flush()
|
# Email küldés (Master Book 3.2: Nincs manuális subject)
|
||||||
|
|
||||||
# 4. Email küldés
|
|
||||||
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
||||||
await email_manager.send_email(
|
await email_manager.send_email(
|
||||||
recipient=user_in.email,
|
recipient=user_in.email,
|
||||||
@@ -80,32 +77,139 @@ class AuthService:
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def initiate_password_reset(db: AsyncSession, email: str):
|
async def complete_kyc(db: AsyncSession, user_id: int, kyc_in: UserKYCComplete):
|
||||||
"""Jelszó-visszaállítás indítása dinamikus lejárattal."""
|
"""
|
||||||
stmt = select(User).where(User.email == email, User.is_deleted == False)
|
1.3. Fázis: Atomi Tranzakció & Shadow Identity
|
||||||
res = await db.execute(stmt)
|
Felismeri a visszatérő Person-t, de új User-ként, izolált flottával indít.
|
||||||
user = res.scalar_one_or_none()
|
"""
|
||||||
|
try:
|
||||||
if user:
|
# 1. Aktuális technikai User lekérése
|
||||||
now = datetime.now(timezone.utc)
|
stmt = select(User).options(joinedload(User.person)).where(User.id == user_id)
|
||||||
|
res = await db.execute(stmt)
|
||||||
# --- DINAMIKUS JELSZÓ RESET LEJÁRAT ---
|
user = res.scalar_one_or_none()
|
||||||
reset_hours = await config.get_setting(
|
if not user: return None
|
||||||
"auth_password_reset_hours",
|
|
||||||
region_code=user.region_code,
|
# 2. Shadow Identity Ellenőrzése (Anyja neve + Születési hely + Idő alapján)
|
||||||
default=2
|
# Globális keresés, régiótól függetlenül
|
||||||
|
identity_stmt = select(Person).where(and_(
|
||||||
|
Person.mothers_last_name == kyc_in.mothers_last_name,
|
||||||
|
Person.mothers_first_name == kyc_in.mothers_first_name,
|
||||||
|
Person.birth_place == kyc_in.birth_place,
|
||||||
|
Person.birth_date == kyc_in.birth_date
|
||||||
|
))
|
||||||
|
existing_person = (await db.execute(identity_stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_person:
|
||||||
|
# Visszatérő identitás: A User-t a régi Person-hoz kötjük
|
||||||
|
user.person_id = existing_person.id
|
||||||
|
active_person = existing_person
|
||||||
|
logger.info(f"Shadow Identity linked: User {user_id} -> Person {existing_person.id}")
|
||||||
|
else:
|
||||||
|
active_person = user.person
|
||||||
|
|
||||||
|
# 3. Címkezelés
|
||||||
|
addr_id = await GeoService.get_or_create_full_address(
|
||||||
|
db,
|
||||||
|
zip_code=kyc_in.address_zip,
|
||||||
|
city=kyc_in.address_city,
|
||||||
|
street_name=kyc_in.address_street_name,
|
||||||
|
street_type=kyc_in.address_street_type,
|
||||||
|
house_number=kyc_in.address_house_number,
|
||||||
|
parcel_id=kyc_in.address_hrsz
|
||||||
)
|
)
|
||||||
|
|
||||||
# ... (Rate limit ellenőrzés marad változatlan) ...
|
# 4. Person adatok frissítése (mindig a legfrissebbet tároljuk)
|
||||||
|
active_person.mothers_last_name = kyc_in.mothers_last_name
|
||||||
|
active_person.mothers_first_name = kyc_in.mothers_first_name
|
||||||
|
active_person.birth_place = kyc_in.birth_place
|
||||||
|
active_person.birth_date = kyc_in.birth_date
|
||||||
|
active_person.phone = kyc_in.phone_number
|
||||||
|
active_person.address_id = addr_id
|
||||||
|
active_person.identity_docs = jsonable_encoder(kyc_in.identity_docs)
|
||||||
|
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
|
||||||
|
active_person.is_active = True
|
||||||
|
|
||||||
|
# 5. Új, izolált INDIVIDUAL szervezet (4.2.3)
|
||||||
|
new_org = Organization(
|
||||||
|
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
|
||||||
|
name=f"{active_person.last_name} Flotta",
|
||||||
|
org_type=OrgType.individual,
|
||||||
|
owner_id=user.id,
|
||||||
|
is_transferable=False,
|
||||||
|
is_active=True,
|
||||||
|
status="verified"
|
||||||
|
)
|
||||||
|
db.add(new_org)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
# 6. Tagság és Jogosultságok
|
||||||
|
db.add(OrganizationMember(
|
||||||
|
organization_id=new_org.id,
|
||||||
|
user_id=user.id,
|
||||||
|
role="owner",
|
||||||
|
permissions={"can_add_asset": True, "can_view_costs": True, "is_admin": True}
|
||||||
|
))
|
||||||
|
|
||||||
|
# 7. Wallet & Stats (Friss kezdés 0 ponttal)
|
||||||
|
db.add(Wallet(user_id=user.id, coin_balance=0, credit_balance=0))
|
||||||
|
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
|
||||||
|
|
||||||
|
# 8. Aktiválás
|
||||||
|
user.is_active = True
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
await db.rollback()
|
||||||
|
logger.error(f"KYC Atomi Tranzakció Hiba: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def verify_email(db: AsyncSession, token_str: str):
|
||||||
|
try:
|
||||||
|
token_uuid = uuid.UUID(token_str)
|
||||||
|
stmt = select(VerificationToken).where(
|
||||||
|
and_(
|
||||||
|
VerificationToken.token == token_uuid,
|
||||||
|
VerificationToken.is_used == False,
|
||||||
|
VerificationToken.expires_at > datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = await db.execute(stmt)
|
||||||
|
token = res.scalar_one_or_none()
|
||||||
|
if not token: return False
|
||||||
|
|
||||||
|
token.is_used = True
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def authenticate(db: AsyncSession, email: str, password: str):
|
||||||
|
stmt = select(User).where(and_(User.email == email, User.is_deleted == False))
|
||||||
|
res = await db.execute(stmt)
|
||||||
|
user = res.scalar_one_or_none()
|
||||||
|
if user and verify_password(password, user.hashed_password):
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def initiate_password_reset(db: AsyncSession, email: str):
|
||||||
|
# Csak aktív (nem törölt) felhasználónak engedünk jelszót resetelni
|
||||||
|
stmt = select(User).where(and_(User.email == email, User.is_deleted == False))
|
||||||
|
user = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
reset_hours = await config.get_setting("auth_password_reset_hours", region_code=user.region_code, default=2)
|
||||||
token_val = uuid.uuid4()
|
token_val = uuid.uuid4()
|
||||||
new_token = VerificationToken(
|
db.add(VerificationToken(
|
||||||
token=token_val,
|
token=token_val,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
token_type="password_reset",
|
token_type="password_reset",
|
||||||
expires_at=now + timedelta(hours=int(reset_hours))
|
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours))
|
||||||
)
|
))
|
||||||
db.add(new_token)
|
|
||||||
|
|
||||||
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
|
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
|
||||||
await email_manager.send_email(
|
await email_manager.send_email(
|
||||||
@@ -115,7 +219,30 @@ class AuthService:
|
|||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
return "not_found"
|
return "not_found"
|
||||||
|
|
||||||
# ... (többi metódus: verify_email, complete_kyc, authenticate, reset_password maradnak) ...
|
@staticmethod
|
||||||
|
async def reset_password(db: AsyncSession, email: str, token_str: str, new_password: str):
|
||||||
|
try:
|
||||||
|
token_uuid = uuid.UUID(token_str)
|
||||||
|
stmt = select(VerificationToken).join(User).where(
|
||||||
|
and_(
|
||||||
|
User.email == email,
|
||||||
|
VerificationToken.token == token_uuid,
|
||||||
|
VerificationToken.token_type == "password_reset",
|
||||||
|
VerificationToken.is_used == False,
|
||||||
|
VerificationToken.expires_at > datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
token_rec = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
if not token_rec: return False
|
||||||
|
|
||||||
|
user_stmt = select(User).where(User.id == token_rec.user_id)
|
||||||
|
user = (await db.execute(user_stmt)).scalar_one()
|
||||||
|
user.hashed_password = get_password_hash(new_password)
|
||||||
|
token_rec.is_used = True
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
@@ -183,4 +183,17 @@ A rendszer normalizált címkezelést alkalmaz az adatminőség biztosítása é
|
|||||||
- `data.geo_street_types`: Közterület típusok szótára (út, utca, tér...).
|
- `data.geo_street_types`: Közterület típusok szótára (út, utca, tér...).
|
||||||
|
|
||||||
### 5.2 GIS Adatok
|
### 5.2 GIS Adatok
|
||||||
Minden telephely koordinátája `GEOGRAPHY(POINT, 4326)` típusként van tárolva, amely lehetővé teszi a PostGIS alapú távolságmérést.
|
Minden telephely koordinátája `GEOGRAPHY(POINT, 4326)` típusként van tárolva, amely lehetővé teszi a PostGIS alapú távolságmérést.
|
||||||
|
|
||||||
|
### 5. Hibrid Címkezelési Modell
|
||||||
|
A rendszer az adatintegritás és a sebesség érdekében hibrid modellt használ.
|
||||||
|
|
||||||
|
- **Centralizált adattárolás**: A `data.addresses` tábla tárolja a normalizált címeket (UUID alapú).
|
||||||
|
- **Öntanuló szótárak**: A `data.geo_postal_codes` és `data.geo_streets` táblák automatikusan bővülnek minden új rögzítésnél.
|
||||||
|
- **Denormalizált GPS adatok**: Az `organization_locations` tábla közvetlenül tárolja a koordinátákat a JOIN-nélküli PostGIS lekérdezésekhez.
|
||||||
|
|
||||||
|
| Tábla | Funkció |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `data.addresses` | Konkrét házszám szintű címek (Hibrid hivatkozási pont). |
|
||||||
|
| `data.geo_postal_codes` | Irányítószám és város kapcsolata (HU/EU támogatás). |
|
||||||
|
| `data.user_stats` | Felhasználói XP, szintek és strike-ok tárolása. |
|
||||||
@@ -64,4 +64,15 @@ Az e-mail küldési folyamatokra az alábbi korlátok vonatkoznak:
|
|||||||
- **Retry Cooldown:** Újraigénylés legkorábban 60 másodperc után lehetséges.
|
- **Retry Cooldown:** Újraigénylés legkorábban 60 másodperc után lehetséges.
|
||||||
- **Óránkénti Limit:** Maximum 3 kérelem / e-mail cím.
|
- **Óránkénti Limit:** Maximum 3 kérelem / e-mail cím.
|
||||||
- **Napi Limit:** Maximum 10 kérelem / e-mail cím.
|
- **Napi Limit:** Maximum 10 kérelem / e-mail cím.
|
||||||
- **Zárolás:** A napi limit túllépése esetén a fiók biztonsági okokból 24 órára zárolja a küldési funkciót az adott címre.
|
- **Zárolás:** A napi limit túllépése esetén a fiók biztonsági okokból 24 órára zárolja a küldési funkciót az adott címre.
|
||||||
|
|
||||||
|
### 4. Geo és Kereső Végpontok
|
||||||
|
|
||||||
|
#### GET `/api/v1/services/suggest-street`
|
||||||
|
- **Cél**: Autocomplete támogatás a frontendnek.
|
||||||
|
- **Paraméterek**: `zip_code` (string), `q` (részleges utcanév).
|
||||||
|
|
||||||
|
#### GET `/api/v1/services/search`
|
||||||
|
- **Cél**: Kétlépcsős szervizkereső.
|
||||||
|
- **Free mód**: Légvonalbeli távolságmérés (Radius).
|
||||||
|
- **Premium mód**: Útvonal-idő alapú kalkuláció és forgalmi becslés.
|
||||||
@@ -81,4 +81,10 @@ A fejlődés nem csak dicsőség, hanem gazdasági előny is.
|
|||||||
* **Level 20:** Egyedi avatar keret + állandó 5% kedvezmény a hirdetési árakból (céges esetén).
|
* **Level 20:** Egyedi avatar keret + állandó 5% kedvezmény a hirdetési árakból (céges esetén).
|
||||||
|
|
||||||
### 6.3 Éves/Havi Szezonok
|
### 6.3 Éves/Havi Szezonok
|
||||||
Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, amit a partnereinknél (szervizeknél) válthat be.
|
Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, amit a partnereinknél (szervizeknél) válthat be.
|
||||||
|
|
||||||
|
### 3. Jutalmazási Szabályok (Social Points)
|
||||||
|
- **Célcsoport**: Kizárólag természetes személyek (`role: user`, `driver`).
|
||||||
|
- **Kizárások**: Szervezetek (Organizations) és Adminisztrátorok nem gyűjtenek XP-t.
|
||||||
|
- **Logika**: Minden `PointsLedger` bejegyzés kötelezően hivatkozik egy `user_id`-ra.
|
||||||
|
- **Mezőnevek**: Adatbázis szinten a pontok az `id`, `user_id`, `points`, `reason` mezőkben tárolódnak.
|
||||||
@@ -168,4 +168,45 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
|
|||||||
|
|
||||||
DATA: Atomizált címmezők hozzáadva a data.organizations táblához.
|
DATA: Atomizált címmezők hozzáadva a data.organizations táblához.
|
||||||
|
|
||||||
LOGIC: Háromszintű névkezelés (Hivatalos, Rövid, Display) bevezetve.
|
LOGIC: Háromszintű névkezelés (Hivatalos, Rövid, Display) bevezetve.
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-02-08
|
||||||
|
|
||||||
|
### Hozzáadva (Added)
|
||||||
|
- **Hibrid Címkezelő Rendszer**: Új `data.addresses` központi tábla, amely UUID alapú azonosítással köti össze a személyeket, cégeket és szervizeket.
|
||||||
|
- **Öntanuló GeoService**: A rendszer rögzítéskor automatikusan bővíti a ZIP-kód, város és utcanév szótárakat.
|
||||||
|
- **Autocomplete API**: `/api/v1/services/suggest-street` végpont a frontend gépelés közbeni támogatásához.
|
||||||
|
- **Kétlépcsős Keresés**: `/api/v1/services/search` végpont, amely megkülönbözteti a légvonalbeli (Free) és útvonal/idő alapú (Premium) kalkulációt.
|
||||||
|
- **PostGIS Integráció**: Tűpontos GPS távolságmérés `geography` casting használatával.
|
||||||
|
|
||||||
|
### Javítva (Fixed)
|
||||||
|
- **Gamifikációs hiba**: A `PointsLedger` modellben javítva a `points` mezőnév (TypeError: points_change fix).
|
||||||
|
- **Adatbázis Inkonzisztencia**: Létrehozva a hiányzó `data.user_stats` tábla.
|
||||||
|
- **Import hibák**: `Optional`, `UploadFile` és `File` hiányzó importok pótolva a szolgáltatás végpontokon.
|
||||||
|
|
||||||
|
### Változtatva (Changed)
|
||||||
|
- **Címkezelési Logika**: A cégek és magánszemélyek mostantól egységes, bontott címstruktúrát (zip, city, street, street_type, house_number, parcel_id) használnak.
|
||||||
|
- **XP Szűrés**: A szervizek és cégek rögzítésekor a rendszer mostantól csak a természetes személyeknek (User/Driver) oszt social pontokat.
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-02-08
|
||||||
|
- **Feat**: Hibrid címkezelő rendszer bevezetése (UUID alapú `data.addresses`).
|
||||||
|
- **Feat**: Öntanuló Geo-logika (Auto-create ZIP/Street).
|
||||||
|
- **Feat**: Kétlépcsős (Free/Premium) szervizkereső API.
|
||||||
|
- **Fix**: `points_ledger` mezőnév szinkronizáció.
|
||||||
|
- **Fix**: `data.user_stats` tábla inicializálása.
|
||||||
|
|
||||||
|
## [2026-02-08] - Nemzetközi Asset és KYC Stabilizáció
|
||||||
|
|
||||||
|
### Hozzáadva
|
||||||
|
- **Nemzetközi paraméterek**: `mileage_unit` (km/miles/hours) és `reading_unit` támogatása az Asset modellben.
|
||||||
|
- **Pénzügyi alapok**: `AssetCost` modell bővítése nettó/bruttó összeggel, ÁFA kulccsal és deviza-kezeléssel.
|
||||||
|
- **Árfolyamkezelés**: `exchange_rates` tábla implementálása MNB/ECB alapú napi frissítéshez.
|
||||||
|
- **Értékelési rendszer**: `ratings` tábla létrehozva (user, asset, service_provider szinten).
|
||||||
|
|
||||||
|
### Javítva
|
||||||
|
- **KYC Folyamat**: Anyja neve bontása vezetéknévre és keresztnévre a magyar/nemzetközi szabványok szerint.
|
||||||
|
- **Database Schema**: Számos hiányzó oszlop pótolva (`asset_events.data`, `asset_events.event_date`, `user_stats.total_xp`).
|
||||||
|
- **SQLAlchemy hiba**: Javítva az `IndexError` a katalógus lekérdezésnél és az import hiba a `PG_UUID` kapcsán.
|
||||||
|
|
||||||
|
### Megjegyzés
|
||||||
|
A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasználót, létrehozni az egyéni flottáját, inicializálni a pénztárcáját (Kredit/Coin) és rögzíteni az első járművét kezdő km-óra állással.
|
||||||
@@ -173,4 +173,17 @@ Minden telephely rögzítésekor az alábbi bontott címadatok kötelezőek:
|
|||||||
- Opcionális: Helyrajzi szám (parcel_id) külterületi vagy HRSZ alapú azonosításhoz.
|
- Opcionális: Helyrajzi szám (parcel_id) külterületi vagy HRSZ alapú azonosításhoz.
|
||||||
|
|
||||||
### 4.2 Validációs Folyamat
|
### 4.2 Validációs Folyamat
|
||||||
A rögzített címek automatikusan bekerülnek a Master Geo adatbázisba, építve a rendszer globális címjegyzékét.
|
A rögzített címek automatikusan bekerülnek a Master Geo adatbázisba, építve a rendszer globális címjegyzékét.
|
||||||
|
|
||||||
|
## 5. Járművek és Költségek (MVP)
|
||||||
|
A járműadatok kezelése hibrid módon történik.
|
||||||
|
|
||||||
|
### 5.1 Jármű Katalógus
|
||||||
|
- A rendszer egy központi katalógust (`asset_catalog`) épít.
|
||||||
|
- Új rögzítéskor a rendszer először a katalógusból kínál fel opciókat (Dropdown).
|
||||||
|
- Ha a modell nem létezik, a rendszer automatikusan felveszi (Self-learning catalog).
|
||||||
|
|
||||||
|
### 5.2 Költségkövetés (TCO)
|
||||||
|
- Minden Asset-hez rögzíthető költség (`asset_costs`).
|
||||||
|
- Kötelező adatok: Kategória, Összeg, Dátum.
|
||||||
|
- Opcionális: Km óra állása (az amortizáció és szervizintervallum számításához).
|
||||||
Reference in New Issue
Block a user