feat: implement pivot-currency model, rbac smart tokens & fix circular imports
This commit is contained in:
150
alembic.ini
150
alembic.ini
@@ -1,150 +0,0 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/migrations
|
||||
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = postgresql+asyncpg://user:pass@postgres-db:5432/service_finder
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
13
backend/.env
Normal file
13
backend/.env
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql+asyncpg://service_finder_app:JELSZAVAD@db:5432/service_finder
|
||||
|
||||
# Security
|
||||
SECRET_KEY=ide_generálj_egy_hosszú_véletlen_karaktersort
|
||||
|
||||
# Initial Admin (Ezt fogja a seed script használni)
|
||||
INITIAL_ADMIN_EMAIL=kincses@valami.hu
|
||||
INITIAL_ADMIN_PASSWORD=Kincs€s74
|
||||
|
||||
# Debug mód (opcionális)
|
||||
DEBUG=True
|
||||
0
backend/app/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
0
backend/app/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
@@ -6,24 +6,27 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import decode_token
|
||||
from app.core.security import decode_token, RANK_MAP
|
||||
from app.models.identity import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
token: str = Depends(reusable_oauth2),
|
||||
) -> User:
|
||||
async def get_current_token_payload(
|
||||
token: str = Depends(reusable_oauth2)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Dependency, amely visszaadja az aktuálisan bejelentkezett felhasználót.
|
||||
Támogatja a 'dev_bypass_active' tokent a fejlesztői teszteléshez.
|
||||
Kinyeri a token payload-ot DB hívás nélkül.
|
||||
Ez teszi lehetővé a gyors jogosultság-ellenőrzést.
|
||||
"""
|
||||
# FEJLESZTŐI BYPASS
|
||||
if token == "dev_bypass_active":
|
||||
result = await db.execute(select(User).where(User.id == 1))
|
||||
return result.scalar_one()
|
||||
return {
|
||||
"sub": "1",
|
||||
"role": "superadmin",
|
||||
"rank": 100,
|
||||
"scope_level": "global",
|
||||
"scope_id": "all"
|
||||
}
|
||||
|
||||
payload = decode_token(token)
|
||||
if not payload:
|
||||
@@ -31,27 +34,38 @@ async def get_current_user(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Érvénytelen vagy lejárt munkamenet."
|
||||
)
|
||||
return payload
|
||||
|
||||
user_id: str = payload.get("sub")
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
payload: Dict[str, Any] = Depends(get_current_token_payload),
|
||||
) -> User:
|
||||
"""
|
||||
Visszaadja a teljes User modellt. Akkor használjuk, ha módosítani kell az usert.
|
||||
"""
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token azonosítási hiba."
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token azonosítási hiba.")
|
||||
|
||||
result = await db.execute(select(User).where(User.id == int(user_id)))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A felhasználó nem található."
|
||||
)
|
||||
|
||||
if user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Ez a fiók korábban törlésre került."
|
||||
)
|
||||
if not user or user.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="A felhasználó nem található.")
|
||||
|
||||
return user
|
||||
|
||||
def check_min_rank(required_rank: int):
|
||||
"""
|
||||
Függőség-gyár: Ellenőrzi, hogy a felhasználó rangja eléri-e a minimumot.
|
||||
Használat: Depends(check_min_rank(60)) -> RegionAdmin+
|
||||
"""
|
||||
def rank_checker(payload: Dict[str, Any] = Depends(get_current_token_payload)):
|
||||
user_rank = payload.get("rank", 0)
|
||||
if user_rank < required_rank:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Nincs elegendő jogosultsága a művelethez. (Szükséges szint: {required_rank})"
|
||||
)
|
||||
return True
|
||||
return rank_checker
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,136 +1,129 @@
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, and_
|
||||
import os
|
||||
import logging
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
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.asset import Asset, AssetCost, AssetTelemetry
|
||||
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
|
||||
from app.services.cost_service import cost_service
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_asset(
|
||||
asset_in: AssetCreate,
|
||||
target_org_id: int = None,
|
||||
# --- 1. MODUL: IDENTITÁS (Alapadatok) ---
|
||||
@router.get("/{asset_id}", response_model=Dict[str, Any])
|
||||
async def get_asset_identity(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
# 1. VIN Validáció
|
||||
if not VINValidator.validate(asset_in.vin):
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen alvázszám (VIN) formátum!")
|
||||
"""Csak a jármű alapadatai és katalógus információi."""
|
||||
stmt = select(Asset).where(Asset.id == asset_id).options(selectinload(Asset.catalog))
|
||||
asset = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
# 2. Célflotta ellenőrzése
|
||||
if not target_org_id:
|
||||
stmt_org = select(Organization).join(OrganizationMember).where(
|
||||
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
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Jármű nem található")
|
||||
|
||||
# 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()
|
||||
return {
|
||||
"id": asset.id,
|
||||
"vin": asset.vin,
|
||||
"license_plate": asset.license_plate,
|
||||
"name": asset.name,
|
||||
"catalog": {
|
||||
"make": asset.catalog.make,
|
||||
"model": asset.catalog.model,
|
||||
"type": asset.catalog.vehicle_class,
|
||||
"factory_data": getattr(asset.catalog, 'factory_data', {})
|
||||
}
|
||||
}
|
||||
|
||||
if not catalog_item:
|
||||
catalog_item = AssetCatalog(
|
||||
make=asset_in.make,
|
||||
model=asset_in.model,
|
||||
vehicle_class=asset_in.vehicle_class,
|
||||
fuel_type=asset_in.fuel_type
|
||||
)
|
||||
db.add(catalog_item)
|
||||
await db.flush()
|
||||
# --- 2. MODUL: PÉNZÜGY (Költségek) ---
|
||||
@router.get("/{asset_id}/costs", response_model=Dict[str, Any])
|
||||
async def get_asset_costs(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Pénzügyi modul: Helyi és EUR alapú összesítő, tételes lista."""
|
||||
stmt = select(AssetCost).where(AssetCost.asset_id == asset_id)
|
||||
costs = (await db.execute(stmt)).scalars().all()
|
||||
|
||||
# 4. Asset létrehozása vagy betöltése (Shadow Identity)
|
||||
stmt_exist = select(Asset).where(Asset.vin == asset_in.vin.upper())
|
||||
new_asset = (await db.execute(stmt_exist)).scalar_one_or_none()
|
||||
summary_local = {}
|
||||
summary_eur = {}
|
||||
history = []
|
||||
|
||||
if not new_asset:
|
||||
new_asset = Asset(
|
||||
vin=asset_in.vin.upper(),
|
||||
license_plate=asset_in.license_plate,
|
||||
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()
|
||||
for c in costs:
|
||||
cat = c.cost_type or "OTHER"
|
||||
amt_local = float(c.amount_local)
|
||||
amt_eur = float(c.amount_eur) if c.amount_eur else 0.0
|
||||
|
||||
# 5. Assignment
|
||||
new_assignment = AssetAssignment(
|
||||
asset_id=new_asset.id,
|
||||
organization_id=final_org_id,
|
||||
status="active"
|
||||
)
|
||||
db.add(new_assignment)
|
||||
summary_local[cat] = summary_local.get(cat, 0) + amt_local
|
||||
summary_eur[cat] = summary_eur.get(cat, 0) + amt_eur
|
||||
|
||||
# 6. Kezdő KM esemény
|
||||
if asset_in.current_reading:
|
||||
db.add(AssetEvent(
|
||||
asset_id=new_asset.id,
|
||||
event_type="initial_reading",
|
||||
recorded_mileage=asset_in.current_reading,
|
||||
description="Kezdeti óraállás rögzítése",
|
||||
data={"source": "user_registration"}
|
||||
))
|
||||
history.append({
|
||||
"id": c.id,
|
||||
"category": cat,
|
||||
"amount_local": amt_local,
|
||||
"currency_local": c.currency_local,
|
||||
"amount_eur": amt_eur,
|
||||
"exchange_rate": float(c.exchange_rate_used) if c.exchange_rate_used else 1.0,
|
||||
"date": c.date
|
||||
})
|
||||
|
||||
return {
|
||||
"total_gross_local": sum(summary_local.values()),
|
||||
"total_gross_eur": sum(summary_eur.values()),
|
||||
"summary_local": summary_local,
|
||||
"summary_eur": summary_eur,
|
||||
"history": history
|
||||
}
|
||||
|
||||
@router.post("/{asset_id}/costs", response_model=AssetCostResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_asset_cost(
|
||||
asset_id: uuid.UUID,
|
||||
cost_in: AssetCostCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új költség rögzítése.
|
||||
Automatikus: EUR konverzió, Telemetria frissítés, XP jóváírás.
|
||||
"""
|
||||
# Validáció: az asset_id-nak egyeznie kell a path-szal
|
||||
if cost_in.asset_id != asset_id:
|
||||
raise HTTPException(status_code=400, detail="Asset ID mismatch")
|
||||
|
||||
try:
|
||||
await db.commit()
|
||||
await db.refresh(new_asset)
|
||||
|
||||
# 7. NAS mappa struktúra
|
||||
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)
|
||||
|
||||
return new_asset
|
||||
new_cost = await cost_service.record_cost(
|
||||
db=db,
|
||||
cost_in=cost_in,
|
||||
user_id=current_user.id
|
||||
)
|
||||
return new_cost
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Asset Creation Error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Hiba a mentés során.")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# --- 3. MODUL: TELEMETRIA (Állapot) ---
|
||||
@router.get("/{asset_id}/telemetry", response_model=Dict[str, Any])
|
||||
async def get_asset_telemetry(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Műszaki állapot: KM óra, VQI (Quality) és DBS (Driving) pontszámok."""
|
||||
stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
|
||||
tel = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not tel:
|
||||
return {"current_mileage": 0, "vqi_score": 100.0, "dbs_score": 100.0}
|
||||
|
||||
return {
|
||||
"current_mileage": tel.current_mileage,
|
||||
"vqi_score": float(tel.vqi_score),
|
||||
"dbs_score": float(tel.dbs_score),
|
||||
"last_update": tel.updated_at if hasattr(tel, 'updated_at') else None
|
||||
}
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy import select
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.security import create_access_token
|
||||
from app.core.security import create_access_token, RANK_MAP
|
||||
from app.schemas.auth import (
|
||||
UserLiteRegister, Token, PasswordResetRequest,
|
||||
UserKYCComplete, PasswordResetConfirm
|
||||
@@ -17,7 +17,7 @@ router = APIRouter()
|
||||
|
||||
@router.post("/register-lite", response_model=Token, status_code=status.HTTP_201_CREATED)
|
||||
async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(get_db)):
|
||||
"""Step 1: Alapszintű regisztráció (Email + Jelszó)."""
|
||||
"""Step 1: Alapszintű regisztráció. Az új felhasználó alapértelmezetten 'user' (Rank 10)."""
|
||||
stmt = select(User).where(User.email == user_in.email)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
@@ -28,7 +28,17 @@ async def register_lite(user_in: UserLiteRegister, db: AsyncSession = Depends(ge
|
||||
|
||||
try:
|
||||
user = await AuthService.register_lite(db, user_in)
|
||||
token = create_access_token(data={"sub": str(user.id)})
|
||||
|
||||
# Kezdeti token generálása
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": "user",
|
||||
"rank": 10,
|
||||
"scope_level": "individual",
|
||||
"scope_id": str(user.id)
|
||||
}
|
||||
|
||||
token = create_access_token(data=token_data)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -45,7 +55,7 @@ async def login(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
"""Bejelentkezés és Access Token generálása."""
|
||||
"""Bejelentkezés és okos JWT generálása RBAC adatokkal."""
|
||||
user = await AuthService.authenticate(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
@@ -53,7 +63,20 @@ async def login(
|
||||
detail="Hibás e-mail cím vagy jelszó."
|
||||
)
|
||||
|
||||
token = create_access_token(data={"sub": str(user.id)})
|
||||
# Szerepkör string kinyerése és rang meghatározása a RANK_MAP-ből
|
||||
role_name = user.role.value if hasattr(user.role, 'value') else str(user.role)
|
||||
user_rank = RANK_MAP.get(role_name, 10)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"role": role_name,
|
||||
"rank": user_rank,
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": user.scope_id or str(user.id),
|
||||
"region": user.region_code
|
||||
}
|
||||
|
||||
token = create_access_token(data=token_data)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -62,14 +85,11 @@ async def login(
|
||||
|
||||
@router.get("/verify-email")
|
||||
async def verify_email(token: str, db: AsyncSession = Depends(get_db)):
|
||||
"""E-mail megerősítése a kiküldött link alapján."""
|
||||
"""E-mail megerősítése."""
|
||||
success = await AuthService.verify_email(db, token)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Érvénytelen vagy lejárt megerősítő token."
|
||||
)
|
||||
return {"message": "Email sikeresen megerősítve! Jöhet a profil kitöltése (KYC)."}
|
||||
raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt token.")
|
||||
return {"message": "Email sikeresen megerősítve!"}
|
||||
|
||||
@router.post("/complete-kyc")
|
||||
async def complete_kyc(
|
||||
@@ -77,38 +97,27 @@ async def complete_kyc(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Step 2: Személyes adatok és okmányok rögzítése."""
|
||||
"""Step 2: KYC adatok rögzítése és aktiválás."""
|
||||
user = await AuthService.complete_kyc(db, current_user.id, kyc_in)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Felhasználó nem található.")
|
||||
return {"status": "success", "message": "A profil adatok rögzítve, a rendszer aktiválva."}
|
||||
raise HTTPException(status_code=404, detail="Felhasználó nem található.")
|
||||
return {"status": "success", "message": "A profil aktiválva."}
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(req: PasswordResetRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Elfelejtett jelszó folyamat indítása biztonsági korlátokkal."""
|
||||
"""Elfelejtett jelszó folyamat."""
|
||||
result = await AuthService.initiate_password_reset(db, req.email)
|
||||
|
||||
if result == "cooldown":
|
||||
raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet az újabb kérés előtt.")
|
||||
if result in ["hourly_limit", "daily_limit"]:
|
||||
raise HTTPException(status_code=429, detail="Túllépte a napi/óránkénti próbálkozások számát.")
|
||||
|
||||
return {"message": "Amennyiben a megadott e-mail cím szerepel a rendszerünkben, kiküldtük a linket."}
|
||||
raise HTTPException(status_code=429, detail="Kérjük várjon 2 percet.")
|
||||
return {"message": "Amennyiben a cím létezik, a linket kiküldtük."}
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(req: PasswordResetConfirm, db: AsyncSession = Depends(get_db)):
|
||||
"""Új jelszó beállítása. Backend ellenőrzi az egyezőséget és a tokent."""
|
||||
"""Új jelszó beállítása."""
|
||||
if req.password != req.password_confirm:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="A két jelszó nem egyezik meg."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="A jelszavak nem egyeznek.")
|
||||
|
||||
success = await AuthService.reset_password(db, req.email, req.token, req.password)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Érvénytelen adatok vagy lejárt token."
|
||||
)
|
||||
|
||||
return {"message": "A jelszó sikeresen frissítve! Most már bejelentkezhet."}
|
||||
raise HTTPException(status_code=400, detail="Hiba a jelszó frissítésekor.")
|
||||
return {"message": "A jelszó sikeresen frissítve!"}
|
||||
0
backend/app/core/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
0
backend/app/core/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Any, Optional, List
|
||||
from typing import Any, Optional
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -10,41 +9,38 @@ class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "Traffic Ecosystem SuperApp"
|
||||
VERSION: str = "1.0.0"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
DEBUG: bool = False
|
||||
|
||||
# --- Security / JWT ---
|
||||
# Szigorúan .env-ből!
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "NOT_SET_DANGER")
|
||||
SECRET_KEY: str = "NOT_SET_DANGER"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 nap
|
||||
|
||||
# --- Database & Cache ---
|
||||
DATABASE_URL: str = os.getenv("DATABASE_URL")
|
||||
REDIS_URL: str = os.getenv("REDIS_URL", "redis://service_finder_redis:6379/0")
|
||||
# --- Initial Admin (ÚJ SZEKCIÓ) ---
|
||||
# Ezeket a .env-ből fogja venni
|
||||
INITIAL_ADMIN_EMAIL: str = "admin@servicefinder.hu"
|
||||
INITIAL_ADMIN_PASSWORD: str = "Admin123!"
|
||||
|
||||
# --- Email (Auto Provider) ---
|
||||
EMAIL_PROVIDER: str = os.getenv("EMAIL_PROVIDER", "auto")
|
||||
EMAILS_FROM_EMAIL: str = os.getenv("EMAILS_FROM_EMAIL", "info@profibot.hu")
|
||||
# --- Database & Cache ---
|
||||
DATABASE_URL: str
|
||||
REDIS_URL: str = "redis://service_finder_redis:6379/0"
|
||||
|
||||
# --- Email ---
|
||||
EMAIL_PROVIDER: str = "auto"
|
||||
EMAILS_FROM_EMAIL: str = "info@profibot.hu"
|
||||
EMAILS_FROM_NAME: str = "Profibot"
|
||||
|
||||
# SMTP & SendGrid (Szigorúan .env-ből)
|
||||
SENDGRID_API_KEY: Optional[str] = os.getenv("SENDGRID_API_KEY")
|
||||
SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
|
||||
SMTP_PORT: int = int(os.getenv("SMTP_PORT", 587))
|
||||
SMTP_USER: Optional[str] = os.getenv("SMTP_USER")
|
||||
SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
|
||||
SENDGRID_API_KEY: Optional[str] = None
|
||||
SMTP_HOST: Optional[str] = None
|
||||
SMTP_PORT: int = 587
|
||||
SMTP_USER: Optional[str] = None
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
|
||||
# --- External URLs ---
|
||||
# .env-ben legyen átírva a .10-es IP-re!
|
||||
FRONTEND_BASE_URL: str = os.getenv("FRONTEND_BASE_URL", "http://localhost:3000")
|
||||
FRONTEND_BASE_URL: str = "http://localhost:3000"
|
||||
|
||||
# --- Dinamikus Admin Motor ---
|
||||
async def get_db_setting(self, db: AsyncSession, key_name: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Lekéri a paramétert a data.system_settings táblából.
|
||||
Ezzel érjük el, hogy a kód újraírása nélkül, adminból lehessen
|
||||
állítani a jutalom napokat, százalékokat, stb.
|
||||
"""
|
||||
try:
|
||||
query = text("SELECT value_json FROM data.system_settings WHERE key_name = :key")
|
||||
result = await db.execute(query, {"key": key_name})
|
||||
@@ -63,5 +59,4 @@ class Settings(BaseSettings):
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -1,3 +1,4 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/core/i18n.py
|
||||
import json
|
||||
import os
|
||||
|
||||
@@ -9,21 +10,44 @@ class LocaleManager:
|
||||
self._load()
|
||||
|
||||
data = self._locales.get(lang, self._locales.get("hu", {}))
|
||||
# Biztonságos bejárás a pontokkal elválasztott kulcsokhoz
|
||||
for k in key.split("."):
|
||||
data = data.get(k, {})
|
||||
if isinstance(data, dict):
|
||||
data = data.get(k, {})
|
||||
else:
|
||||
return key # Ha elakadunk, adjuk vissza magát a kulcsot
|
||||
|
||||
if isinstance(data, str):
|
||||
return data.format(**kwargs)
|
||||
return key
|
||||
|
||||
def _load(self):
|
||||
path = "backend/app/locales" # Konténeren belül: "/app/app/locales"
|
||||
if not os.path.exists(path): path = "app/locales"
|
||||
# A konténeren belül ez a biztos útvonal
|
||||
possible_paths = [
|
||||
"/app/app/locales",
|
||||
"app/locales",
|
||||
"backend/app/locales"
|
||||
]
|
||||
|
||||
path = ""
|
||||
for p in possible_paths:
|
||||
if os.path.exists(p):
|
||||
path = p
|
||||
break
|
||||
|
||||
if not path:
|
||||
print("FIGYELEM: Nem található a locales könyvtár!")
|
||||
return
|
||||
|
||||
for file in os.listdir(path):
|
||||
if file.endswith(".json"):
|
||||
lang = file.split(".")[0]
|
||||
with open(os.path.join(path, file), "r", encoding="utf-8") as f:
|
||||
self._locales[lang] = json.load(f)
|
||||
try:
|
||||
with open(os.path.join(path, file), "r", encoding="utf-8") as f:
|
||||
self._locales[lang] = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Hiba a {file} betöltésekor: {e}")
|
||||
|
||||
locale_manager = LocaleManager()
|
||||
# Rövid alias a könnyebb használathoz
|
||||
t = locale_manager.get
|
||||
40
backend/app/core/rbac.py
Normal file
40
backend/app/core/rbac.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/core/rbac.py
|
||||
from fastapi import HTTPException, Depends, status
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.identity import User
|
||||
|
||||
class RBAC:
|
||||
def __init__(self, required_perm: str = None, min_rank: int = 0):
|
||||
self.required_perm = required_perm
|
||||
self.min_rank = min_rank
|
||||
|
||||
async def __call__(self, current_user: User = Depends(get_current_user)):
|
||||
# 1. Szuperadmin (Rank 100) mindent visz
|
||||
if current_user.role == "SUPERADMIN":
|
||||
return True
|
||||
|
||||
# 2. Rang ellenőrzés (Hierarchia)
|
||||
# Itt feltételezzük, hogy a role-okhoz rendelt rank-okat egy configból vesszük
|
||||
user_rank = self.get_role_rank(current_user.role)
|
||||
if user_rank < self.min_rank:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Ezen a hierarchia szinten ez a művelet nem engedélyezett."
|
||||
)
|
||||
|
||||
# 3. Egyedi képesség ellenőrzés (Capabilities)
|
||||
user_perms = current_user.custom_permissions.get("capabilities", [])
|
||||
if self.required_perm and self.required_perm not in user_perms:
|
||||
# Ha a sablonban sincs benne, akkor tiltás
|
||||
if not self.check_role_template(current_user.role, self.required_perm):
|
||||
raise HTTPException(status_code=403, detail="Nincs meg a specifikus jogosultságod.")
|
||||
|
||||
return True
|
||||
|
||||
def get_role_rank(self, role: str):
|
||||
ranks = {"COUNTRY_ADMIN": 80, "REGION_ADMIN": 60, "MODERATOR": 40, "SALES": 20, "USER": 10}
|
||||
return ranks.get(role, 0)
|
||||
|
||||
def check_role_template(self, role: str, perm: str):
|
||||
# Ide jön majd az RBAC_MASTER_CONFIG JSON betöltése
|
||||
return False
|
||||
@@ -4,6 +4,20 @@ import bcrypt
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
|
||||
# Master Book 5.0: RBAC Rank Definition Matrix
|
||||
# Ezek a szintek határozzák meg a hozzáférést a Middleware szintjén.
|
||||
RANK_MAP = {
|
||||
"superadmin": 100,
|
||||
"country_admin": 80,
|
||||
"region_admin": 60,
|
||||
"moderator": 40,
|
||||
"sales": 20,
|
||||
"user": 10,
|
||||
"service": 15,
|
||||
"fleet_manager": 25,
|
||||
"driver": 5
|
||||
}
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Összehasonlítja a sima szöveges jelszót a hash-elt változattal."""
|
||||
if not hashed_password:
|
||||
@@ -22,14 +36,23 @@ def get_password_hash(password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""Létrehozza a JWT access tokent."""
|
||||
"""
|
||||
Létrehozza a JWT access tokent bővített RBAC adatokkal.
|
||||
Várt kulcsok: sub (user_id), role, rank, scope_level, scope_id
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire})
|
||||
# Rendszer szintű metaadatok hozzáadása
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"iss": "service-finder-auth"
|
||||
})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
0
backend/app/db/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
0
backend/app/db/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
0
backend/app/db/__pycache__/session.cpython-312.pyc
Executable file → Normal file
0
backend/app/db/__pycache__/session.cpython-312.pyc
Executable file → Normal file
@@ -1,6 +1,20 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/db/base.py
|
||||
from app.db.base_class import Base # noqa
|
||||
|
||||
# Közvetlen importok a fájlokból (Circular Import elkerülése)
|
||||
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
|
||||
from app.models.asset import Asset, AssetCatalog, AssetCost, AssetEvent # noqa
|
||||
from app.models.gamification import UserStats, PointsLedger # noqa
|
||||
from app.models.asset import ( # noqa
|
||||
Asset, AssetCatalog, AssetCost, AssetEvent,
|
||||
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
|
||||
)
|
||||
from app.models.gamification import ( # noqa
|
||||
PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
|
||||
)
|
||||
from app.models.system_config import SystemParameter # noqa
|
||||
from app.models.history import AuditLog, VehicleOwnership # noqa
|
||||
from app.models.document import Document # noqa
|
||||
from app.models.core_logic import ( # noqa
|
||||
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
)
|
||||
91
backend/app/diagnose_system.py
Normal file
91
backend/app/diagnose_system.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy import text, select
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Importáljuk a rendszermodulokat az ellenőrzéshez
|
||||
try:
|
||||
from app.core.config import settings
|
||||
from app.core.i18n import t
|
||||
from app.models.system_config import SystemParameter
|
||||
except ImportError as e:
|
||||
print(f"❌ Import hiba: {e}")
|
||||
print("Ellenőrizd, hogy a PYTHONPATH be van-e állítva!")
|
||||
exit(1)
|
||||
|
||||
async def diagnose():
|
||||
print("\n" + "="*40)
|
||||
print("🔍 SZERVIZ KERESŐ - RENDSZER DIAGNOSZTIKA")
|
||||
print("="*40 + "\n")
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# --- 1. SÉMA ELLENŐRZÉSE ---
|
||||
print("1️⃣ Adatbázis séma ellenőrzése...")
|
||||
try:
|
||||
# Organizations tábla oszlopai
|
||||
org_res = await session.execute(text(
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_schema = 'data' AND table_name = 'organizations';"
|
||||
))
|
||||
org_cols = [row[0] for row in org_res.fetchall()]
|
||||
|
||||
# Users tábla oszlopai
|
||||
user_res = await session.execute(text(
|
||||
"SELECT column_name FROM information_schema.columns "
|
||||
"WHERE table_schema = 'data' AND table_name = 'users';"
|
||||
))
|
||||
user_cols = [row[0] for row in user_res.fetchall()]
|
||||
|
||||
checks = [
|
||||
("organizations.language", "language" in org_cols),
|
||||
("organizations.default_currency", "default_currency" in org_cols),
|
||||
("users.preferred_language", "preferred_language" in user_cols),
|
||||
("system_parameters tábla létezik", True) # Ha idáig eljut, a SystemParameter import sikerült
|
||||
]
|
||||
|
||||
for label, success in checks:
|
||||
status = "✅ OK" if success else "❌ HIÁNYZIK"
|
||||
print(f" [{status}] {label}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Hiba a séma lekérdezésekor: {e}")
|
||||
|
||||
# --- 2. ADATOK ELLENŐRZÉSE ---
|
||||
print("\n2️⃣ System Parameters (Alapadatok) ellenőrzése...")
|
||||
try:
|
||||
result = await session.execute(select(SystemParameter))
|
||||
params = result.scalars().all()
|
||||
if params:
|
||||
print(f" ✅ Talált paraméterek: {len(params)} db")
|
||||
for p in params:
|
||||
print(f" - {p.key}: {p.value[:2]}... (+{len(p.value)-2} elem)")
|
||||
else:
|
||||
print(" ⚠️ Figyelem: A system_parameters tábla üres!")
|
||||
except Exception as e:
|
||||
print(f" ❌ Hiba az adatok lekérésekor: {e}")
|
||||
|
||||
# --- 3. NYELVI MOTOR ELLENŐRZÉSE ---
|
||||
print("\n3️⃣ Nyelvi motor (i18n) és hu.json ellenőrzése...")
|
||||
try:
|
||||
test_save = t("COMMON.SAVE")
|
||||
test_email = t("email.reg_greeting", first_name="Admin")
|
||||
|
||||
if test_save != "COMMON.SAVE":
|
||||
print(f" ✅ Fordítás sikeres: COMMON.SAVE -> '{test_save}'")
|
||||
print(f" ✅ Paraméteres fordítás: '{test_email}'")
|
||||
else:
|
||||
print(" ❌ A fordítás NEM működik (csak a kulcsot adta vissza).")
|
||||
print(f" Ellenőrizd a /app/app/locales/hu.json elérhetőségét!")
|
||||
except Exception as e:
|
||||
print(f" ❌ Hiba a nyelvi motor futtatásakor: {e}")
|
||||
|
||||
print("\n" + "="*40)
|
||||
print("✅ DIAGNOSZTIKA KÉSZ")
|
||||
print("="*40 + "\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(diagnose())
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"email": {
|
||||
"registration_subject": "Regisztráció - Service Finder",
|
||||
"password_reset_subject": "Jelszó visszaállítás - Service Finder",
|
||||
"reg_subject": "Regisztráció - Service Finder",
|
||||
"pwd_reset_subject": "Jelszó visszaállítás - Service Finder",
|
||||
"reg_greeting": "Szia {first_name}!",
|
||||
"reg_body": "A regisztrációd befejezéséhez és a 'Privát Széfed' aktiválásához kattints az alábbi gombra:",
|
||||
"reg_button": "Fiók Aktiválása",
|
||||
@@ -9,6 +9,23 @@
|
||||
"pwd_reset_greeting": "Szia!",
|
||||
"pwd_reset_body": "Jelszó-visszaállítási kérelem érkezett. Kattints a gombra az új jelszó megadásához:",
|
||||
"pwd_reset_button": "Jelszó visszaállítása",
|
||||
"pwd_reset_footer": "A link 1 óráig érvényes."
|
||||
"pwd_reset_footer": "A link 1 óráig érvényes.",
|
||||
"link_fallback": "Ha a gomb nem működik, másolja be ezt a linket a böngészőjébe:"
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE": "Mentés",
|
||||
"CANCEL": "Mégse",
|
||||
"DELETE": "Törlés"
|
||||
},
|
||||
"VEHICLE": {
|
||||
"LICENSE_PLATE": "Rendszám",
|
||||
"VIN": "Alvázszám",
|
||||
"ADD_SUCCESS": "Jármű sikeresen hozzáadva: {name}",
|
||||
"NOT_FOUND": "A jármű nem található."
|
||||
},
|
||||
"COST": {
|
||||
"AMOUNT": "Összeg",
|
||||
"CURRENCY": "Pénznem",
|
||||
"VAT": "ÁFA"
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,32 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/__init__.py
|
||||
from app.db.base_class import Base
|
||||
|
||||
from .identity import User, Person, Wallet, UserRole, VerificationToken
|
||||
from .organization import Organization, OrganizationMember
|
||||
from .asset import Asset, AssetCatalog, AssetCost, AssetEvent
|
||||
from .asset import (
|
||||
Asset, AssetCatalog, AssetCost, AssetEvent,
|
||||
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
|
||||
)
|
||||
from .address import Address, GeoPostalCode, GeoStreet, GeoStreetType
|
||||
from .gamification import UserStats, PointsLedger
|
||||
from .gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, Rating, PointsLedger
|
||||
from .system_config import SystemParameter
|
||||
from .document import Document
|
||||
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
from .history import AuditLog, VehicleOwnership
|
||||
|
||||
# Aliasok a kompatibilitás és a tiszta kód érdekében
|
||||
# Aliasok
|
||||
Vehicle = Asset
|
||||
UserVehicle = Asset
|
||||
VehicleCatalog = AssetCatalog
|
||||
ServiceRecord = AssetEvent
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
"User",
|
||||
"Person",
|
||||
"Wallet",
|
||||
"UserRole",
|
||||
"VerificationToken",
|
||||
"Organization",
|
||||
"OrganizationMember",
|
||||
"Asset",
|
||||
"AssetCatalog",
|
||||
"AssetCost",
|
||||
"AssetEvent",
|
||||
"Address",
|
||||
"GeoPostalCode",
|
||||
"GeoStreet",
|
||||
"GeoStreetType",
|
||||
"UserStats",
|
||||
"PointsLedger",
|
||||
"Vehicle",
|
||||
"UserVehicle",
|
||||
"VehicleCatalog",
|
||||
"ServiceRecord"
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken",
|
||||
"Organization", "OrganizationMember", "Asset", "AssetCatalog", "AssetCost",
|
||||
"AssetEvent", "AssetFinancials", "AssetTelemetry", "AssetReview", "ExchangeRate",
|
||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "PointRule",
|
||||
"LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
|
||||
"SystemParameter", "Document", "SubscriptionTier", "OrganizationSubscription",
|
||||
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
|
||||
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/app/models/__pycache__/core_logic.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/core_logic.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
backend/app/models/__pycache__/history.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/history.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/app/models/__pycache__/system_config.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/system_config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,133 +1,128 @@
|
||||
import uuid
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON, Numeric, text, 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
|
||||
vehicle_class = Column(String)
|
||||
fuel_type = Column(String)
|
||||
engine_code = Column(String)
|
||||
|
||||
factory_data = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
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")
|
||||
financials = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||
telemetry = relationship("AssetTelemetry", back_populates="asset", uselist=False)
|
||||
assignments = relationship("AssetAssignment", back_populates="asset")
|
||||
events = relationship("AssetEvent", back_populates="asset")
|
||||
costs = relationship("AssetCost", back_populates="asset")
|
||||
reviews = relationship("AssetReview", back_populates="asset")
|
||||
ownership_history = relationship("VehicleOwnership", back_populates="vehicle")
|
||||
|
||||
class AssetFinancials(Base):
|
||||
__tablename__ = "asset_financials"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||
acquisition_price = Column(Numeric(18, 2))
|
||||
acquisition_date = Column(DateTime)
|
||||
financing_type = Column(String)
|
||||
residual_value_estimate = Column(Numeric(18, 2))
|
||||
asset = relationship("Asset", back_populates="financials")
|
||||
|
||||
class AssetTelemetry(Base):
|
||||
__tablename__ = "asset_telemetry"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), unique=True)
|
||||
current_mileage = Column(Integer, default=0)
|
||||
mileage_unit = Column(String(10), default="km")
|
||||
vqi_score = Column(Numeric(5, 2), default=100.00)
|
||||
dbs_score = Column(Numeric(5, 2), default=100.00)
|
||||
asset = relationship("Asset", back_populates="telemetry")
|
||||
|
||||
class AssetReview(Base):
|
||||
__tablename__ = "asset_reviews"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
asset_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
overall_rating = Column(Integer)
|
||||
criteria_scores = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
comment = Column(Text)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
asset = relationship("Asset", back_populates="reviews")
|
||||
|
||||
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
|
||||
|
||||
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
cost_type = Column(String(50), nullable=False)
|
||||
amount_local = Column(Numeric(18, 2), nullable=False)
|
||||
currency_local = Column(String(3), nullable=False)
|
||||
amount_eur = Column(Numeric(18, 2), nullable=True)
|
||||
net_amount_local = Column(Numeric(18, 2))
|
||||
vat_rate = Column(Numeric(5, 2))
|
||||
exchange_rate_used = Column(Numeric(18, 6))
|
||||
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)
|
||||
id = Column(Integer, primary_key=True)
|
||||
base_currency = Column(String(3), default="EUR")
|
||||
target_currency = Column(String(3), nullable=False)
|
||||
target_currency = Column(String(3), unique=True)
|
||||
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())
|
||||
rate_date = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -1,63 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM, UUID
|
||||
from app.db.base import Base
|
||||
import enum
|
||||
|
||||
# A Python enum marad, de a Column definíciónál pontosítunk
|
||||
class CompanyRole(str, enum.Enum):
|
||||
OWNER = "owner"
|
||||
MANAGER = "manager"
|
||||
DRIVER = "driver"
|
||||
|
||||
class Company(Base):
|
||||
__tablename__ = "companies"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
tax_number = Column(String, nullable=True)
|
||||
subscription_tier = Column(String, default="free")
|
||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
members = relationship("CompanyMember", back_populates="company", cascade="all, delete-orphan")
|
||||
assignments = relationship("VehicleAssignment", back_populates="company")
|
||||
|
||||
class CompanyMember(Base):
|
||||
__tablename__ = "company_members"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
# JAVÍTÁS: Kifejezetten megadjuk a natív Postgres típust
|
||||
role = Column(
|
||||
PG_ENUM('owner', 'manager', 'driver', name='companyrole', schema='data', create_type=False),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
can_edit_service = Column(Boolean, default=False)
|
||||
can_see_costs = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
company = relationship("Company", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
class VehicleAssignment(Base):
|
||||
__tablename__ = "vehicle_assignments"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
company_id = Column(Integer, ForeignKey("data.companies.id"), nullable=False)
|
||||
vehicle_id = Column(UUID(as_uuid=True), ForeignKey("data.vehicles.id"), nullable=False)
|
||||
driver_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
start_date = Column(DateTime(timezone=True), server_default=func.now())
|
||||
end_date = Column(DateTime(timezone=True), nullable=True)
|
||||
notes = Column(String, nullable=True)
|
||||
|
||||
company = relationship("Company", back_populates="assignments")
|
||||
vehicle = relationship("Vehicle") # Itt már a Vehicle-re hivatkozunk
|
||||
driver = relationship("User", foreign_keys=[driver_id])
|
||||
@@ -1,7 +1,8 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, JSON, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
# JAVÍTVA: Import közvetlenül a base_class-ból
|
||||
from app.db.base_class import Base
|
||||
|
||||
class SubscriptionTier(Base):
|
||||
__tablename__ = "subscription_tiers"
|
||||
|
||||
@@ -2,7 +2,8 @@ from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
from app.db.base import Base
|
||||
# JAVÍTVA: Közvetlenül a base_class-ból importálunk, nem a base-ből!
|
||||
from app.db.base_class import Base
|
||||
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailLog(Base):
|
||||
__tablename__ = "email_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, nullable=True) # Hozzáadva
|
||||
recipient = Column(String, index=True) # Hozzáadva
|
||||
email = Column(String, index=True)
|
||||
email_type = Column(String) # Frissítve a kódhoz
|
||||
type = Column(String) # Megtartva a kompatibilitás miatt
|
||||
provider_id = Column(Integer) # Hozzáadva
|
||||
status = Column(String) # Hozzáadva
|
||||
sent_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -1,21 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, JSON, Float
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailProviderConfig(Base):
|
||||
__tablename__ = "email_provider_configs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(50), unique=True) # Pl: SendGrid_Main, Office365_Backup
|
||||
provider_type = Column(String(20)) # SENDGRID, SMTP, MAILGUN
|
||||
priority = Column(Integer, default=1) # 1 = legfontosabb
|
||||
|
||||
# JSON-ban tároljuk a paramétereket (host, port, api_key, user, stb.)
|
||||
settings = Column(JSON, nullable=False)
|
||||
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Failover figyelés
|
||||
fail_count = Column(Integer, default=0)
|
||||
max_fail_threshold = Column(Integer, default=3) # Hány hiba után kapcsoljon le?
|
||||
success_rate = Column(Float, default=100.0) # Statisztika az adminnak
|
||||
@@ -1,30 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailProvider(Base):
|
||||
__tablename__ = 'email_providers'
|
||||
__table_args__ = {'schema': 'data'}
|
||||
id = Column(Integer, PRIMARY KEY=True)
|
||||
name = Column(String(50), nullable=False)
|
||||
priority = Column(Integer, default=1)
|
||||
provider_type = Column(String(10), default='SMTP')
|
||||
host = Column(String(255))
|
||||
port = Column(Integer)
|
||||
username = Column(String(255))
|
||||
password_hash = Column(String(255))
|
||||
is_active = Column(Boolean, default=True)
|
||||
daily_limit = Column(Integer, default=300)
|
||||
current_daily_usage = Column(Integer, default=0)
|
||||
|
||||
class EmailLog(Base):
|
||||
__tablename__ = 'email_logs'
|
||||
__table_args__ = {'schema': 'data'}
|
||||
id = Column(Integer, PRIMARY KEY=True)
|
||||
user_id = Column(Integer, ForeignKey('data.users.id'), nullable=True)
|
||||
email_type = Column(String(50))
|
||||
recipient = Column(String(255))
|
||||
provider_id = Column(Integer, ForeignKey('data.email_providers.id'))
|
||||
status = Column(String(20))
|
||||
sent_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
error_message = Column(Text)
|
||||
@@ -1,17 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Enum
|
||||
import enum
|
||||
from app.db.base import Base
|
||||
|
||||
class EmailType(str, enum.Enum):
|
||||
REGISTRATION = "REGISTRATION"
|
||||
PASSWORD_RESET = "PASSWORD_RESET"
|
||||
GDPR_NOTICE = "GDPR_NOTICE"
|
||||
|
||||
class EmailTemplate(Base):
|
||||
__tablename__ = "email_templates"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
type = Column(Enum(EmailType), unique=True, index=True)
|
||||
subject = Column(String(255), nullable=False)
|
||||
body_html = Column(Text, nullable=False) # Adminról szerkeszthető HTML tartalom
|
||||
@@ -1,50 +0,0 @@
|
||||
import enum
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Enum, DateTime, Boolean, Date, JSON
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
# Költség Kategóriák
|
||||
class ExpenseCategory(str, enum.Enum):
|
||||
PURCHASE_PRICE = "PURCHASE_PRICE" # Vételár
|
||||
TRANSFER_TAX = "TRANSFER_TAX" # Vagyonszerzési illeték
|
||||
ADMIN_FEE = "ADMIN_FEE" # Eredetiség, forgalmi, törzskönyv
|
||||
VEHICLE_TAX = "VEHICLE_TAX" # Gépjárműadó
|
||||
INSURANCE = "INSURANCE" # Biztosítás
|
||||
REFUELING = "REFUELING" # Tankolás
|
||||
SERVICE = "SERVICE" # Szerviz / Javítás
|
||||
PARKING = "PARKING" # Parkolás
|
||||
TOLL = "TOLL" # Autópálya matrica
|
||||
FINE = "FINE" # Bírság
|
||||
TUNING_ACCESSORIES = "TUNING_ACCESSORIES" # Extrák
|
||||
OTHER = "OTHER" # Egyéb
|
||||
|
||||
class VehicleEvent(Base):
|
||||
__tablename__ = "vehicle_events"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False)
|
||||
|
||||
# Esemény típusa
|
||||
event_type = Column(Enum(ExpenseCategory, schema="data", name="expense_category_enum"), nullable=False)
|
||||
|
||||
date = Column(Date, nullable=False)
|
||||
|
||||
# Kilométeróra (KÖTELEZŐ!)
|
||||
odometer_value = Column(Integer, nullable=False)
|
||||
odometer_anomaly = Column(Boolean, default=False) # Ha csökkenést észlelünk, True lesz
|
||||
|
||||
# Pénzügyek
|
||||
cost_amount = Column(Integer, nullable=False, default=0) # HUF
|
||||
|
||||
# Leírás és Képek
|
||||
description = Column(String, nullable=True)
|
||||
image_paths = Column(JSON, nullable=True) # Lista a feltöltött képek (számla, fotó) útvonalairól
|
||||
|
||||
# Kapcsolat a szolgáltatóval
|
||||
# Ha is_diy=True, akkor a user maga csinálta.
|
||||
# Ha is_diy=False és service_provider_id=None, akkor ismeretlen helyen készült.
|
||||
is_diy = Column(Boolean, default=False)
|
||||
service_provider_id = Column(Integer, ForeignKey("data.service_providers.id"), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -1,63 +1,30 @@
|
||||
# /opt/service_finder/backend/app/models/history.py
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Date, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
|
||||
from app.db.base_class import Base
|
||||
|
||||
# --- 1. Jármű Birtoklási Előzmények (Ownership History) ---
|
||||
# Ez a tábla mondja meg, kié volt az autó egy adott időpillanatban.
|
||||
# Így biztosítjuk, hogy a régi tulajdonos adatai védve legyenek az újtól.
|
||||
class VehicleOwnership(Base):
|
||||
__tablename__ = "vehicle_ownerships"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Kapcsolatok
|
||||
vehicle_id = Column(Integer, ForeignKey("data.user_vehicles.id"), nullable=False)
|
||||
vehicle_id = Column(PG_UUID(as_uuid=True), ForeignKey("data.assets.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
|
||||
# Időszak
|
||||
start_date = Column(Date, nullable=False, default=func.current_date()) # Mikor került hozzá
|
||||
end_date = Column(Date, nullable=True) # Ha NULL, akkor ő a jelenlegi tulajdonos!
|
||||
|
||||
# Jegyzet (pl. adásvételi szerződés száma)
|
||||
start_date = Column(Date, nullable=False, default=func.current_date())
|
||||
end_date = Column(Date, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
|
||||
# SQLAlchemy kapcsolatok (visszahivatkozások a fő modellekben kellenek majd)
|
||||
vehicle = relationship("UserVehicle", back_populates="ownership_history")
|
||||
user = relationship("User", back_populates="owned_vehicles")
|
||||
vehicle = relationship("Asset", back_populates="ownership_history")
|
||||
user = relationship("User", back_populates="ownership_history")
|
||||
|
||||
|
||||
# --- 2. Audit Log (A "Fekete Doboz") ---
|
||||
# Minden kritikus módosítást itt tárolunk. Ez a rendszer "igazságügyi naplója".
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# KI? (A felhasználó, aki a műveletet végezte)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||
|
||||
# MIT? (Milyen objektumot érintett?)
|
||||
target_type = Column(String, index=True) # pl. "vehicle", "cost", "user_profile"
|
||||
target_id = Column(Integer, index=True) # pl. az autó ID-ja
|
||||
|
||||
# HOGYAN?
|
||||
action = Column(String, nullable=False) # CREATE, UPDATE, DELETE, LOGIN_FAILED, EXPORT_DATA
|
||||
|
||||
# RÉSZLETEK (Mi változott?)
|
||||
# Pl: {"field": "odometer", "old_value": 150000, "new_value": 120000} <- Visszatekerés gyanú!
|
||||
target_type = Column(String, index=True)
|
||||
target_id = Column(String, index=True)
|
||||
action = Column(String, nullable=False)
|
||||
changes = Column(JSON, nullable=True)
|
||||
|
||||
# BIZTONSÁG
|
||||
ip_address = Column(String, nullable=True) # Honnan jött a kérés?
|
||||
user_agent = Column(String, nullable=True) # Milyen böngészőből?
|
||||
|
||||
# MIKOR?
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Kapcsolat (Opcionális, csak ha le akarjuk kérdezni a user adatait a logból)
|
||||
user = relationship("User")
|
||||
@@ -12,6 +12,7 @@ class UserRole(str, enum.Enum):
|
||||
service = "service"
|
||||
fleet_manager = "fleet_manager"
|
||||
driver = "driver"
|
||||
superadmin = "superadmin" # Hozzáadva a biztonság kedvéért
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = "persons"
|
||||
@@ -51,34 +52,37 @@ class User(Base):
|
||||
region_code = Column(String, default="HU")
|
||||
is_deleted = Column(Boolean, default=False)
|
||||
person_id = Column(BigInteger, ForeignKey("data.persons.id"), nullable=True)
|
||||
preferred_language = Column(String(5), default="hu")
|
||||
preferred_currency = Column(String(3), default="HUF")
|
||||
timezone = Column(String(50), default="Europe/Budapest")
|
||||
|
||||
person = relationship("Person", back_populates="users")
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
# RBAC & SCOPE mezők (Visszaállítva a DB sémához)
|
||||
scope_level = Column(String(30), server_default="individual")
|
||||
scope_id = Column(String(50))
|
||||
custom_permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# 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())
|
||||
|
||||
# Kapcsolatok
|
||||
person = relationship("Person", back_populates="users")
|
||||
wallet = relationship("Wallet", back_populates="user", uselist=False)
|
||||
stats = relationship("UserStats", back_populates="user", uselist=False)
|
||||
ownership_history = relationship("VehicleOwnership", back_populates="user")
|
||||
owned_organizations = relationship("Organization", back_populates="owner")
|
||||
|
||||
class Wallet(Base):
|
||||
"""Kétoszlopos pénztárca: Coin (Szerviz) és Kredit (Prémium)."""
|
||||
__tablename__ = "wallets"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), unique=True)
|
||||
coin_balance = Column(Numeric(18, 2), default=0.00)
|
||||
credit_balance = Column(Numeric(18, 2), default=0.00)
|
||||
currency = Column(String(3), default="HUF")
|
||||
|
||||
user = relationship("User", back_populates="wallet")
|
||||
|
||||
class VerificationToken(Base):
|
||||
__tablename__ = "verification_tokens"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
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)
|
||||
|
||||
@@ -20,16 +20,16 @@ class Organization(Base):
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
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 ---
|
||||
full_name = Column(String, nullable=False) # Teljes hivatalos név
|
||||
name = Column(String, nullable=False) # Rövidített cégnév
|
||||
display_name = Column(String(50)) # Alkalmazáson belüli rövidítés
|
||||
full_name = Column(String, nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
display_name = Column(String(50))
|
||||
|
||||
default_currency = Column(String(3), default="HUF")
|
||||
country_code = Column(String(2), default="HU")
|
||||
language = Column(String(5), default="hu")
|
||||
|
||||
# --- ATOMIZÁLT CÍMKEZELÉS (Kompatibilitási réteg) ---
|
||||
address_zip = Column(String(10))
|
||||
address_city = Column(String(100))
|
||||
address_street_name = Column(String(150))
|
||||
@@ -39,9 +39,7 @@ class Organization(Base):
|
||||
address_stairwell = Column(String(20))
|
||||
address_floor = Column(String(20))
|
||||
address_door = Column(String(20))
|
||||
country_code = Column(String(2), default="HU")
|
||||
|
||||
# --- ÜZLETI ADATOK ---
|
||||
tax_number = Column(String(20), unique=True, index=True)
|
||||
reg_number = Column(String(50))
|
||||
|
||||
@@ -65,7 +63,7 @@ class Organization(Base):
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
# String alapú hivatkozás a körkörös import ellen
|
||||
assets = relationship("AssetAssignment", back_populates="organization", cascade="all, delete-orphan")
|
||||
members = relationship("OrganizationMember", back_populates="organization", cascade="all, delete-orphan")
|
||||
owner = relationship("User", back_populates="owned_organizations")
|
||||
@@ -76,13 +74,9 @@ class OrganizationMember(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
organization_id = Column(Integer, ForeignKey("data.organizations.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("data.users.id"), nullable=False)
|
||||
role = Column(String, default="driver") # owner, manager, driver, service_staff
|
||||
role = Column(String, default="driver")
|
||||
|
||||
# JAVÍTVA: Jogosultságok JSONB mezője (can_add_asset, etc.)
|
||||
permissions = Column(JSON, server_default=text("'{}'::jsonb"))
|
||||
|
||||
organization = relationship("Organization", back_populates="members")
|
||||
user = relationship("app.models.identity.User") # Visszamutató kapcsolat a felhasználóra
|
||||
|
||||
# Kompatibilitási réteg
|
||||
Organization.vehicles = Organization.assets
|
||||
user = relationship("User") # Egyszerűsített string hivatkozás
|
||||
13
backend/app/models/system.py
Normal file
13
backend/app/models/system.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import Column, String, JSON, DateTime, Boolean
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class SystemParameter(Base):
|
||||
__tablename__ = "system_parameters"
|
||||
__table_args__ = {"schema": "data", "extend_existing": True}
|
||||
|
||||
key = Column(String, primary_key=True, index=True, nullable=False)
|
||||
value = Column(JSON, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
description = Column(String)
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
16
backend/app/models/system_config.py
Normal file
16
backend/app/models/system_config.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, String, JSON, Integer, Boolean, DateTime, func
|
||||
from app.db.base_class import Base
|
||||
|
||||
class SystemParameter(Base):
|
||||
"""
|
||||
Globális rendszerbeállítások (A meglévő data.system_parameters tábla alapján).
|
||||
"""
|
||||
__tablename__ = "system_parameters"
|
||||
__table_args__ = {"schema": "data"}
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(50), unique=True, index=True, nullable=False)
|
||||
value = Column(JSON, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
description = Column(String, nullable=True)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
@@ -1,54 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, Float, JSON, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
# 1. Kategória (Autó, Motor, Kisteher...)
|
||||
class VehicleCategory(Base):
|
||||
__tablename__ = "vehicle_categories"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
name_key = Column(String, nullable=False) # i18n kulcs: 'CAR', 'MOTORCYCLE'
|
||||
|
||||
# 2. Márka (Audi, Honda, BMW...)
|
||||
class VehicleMake(Base):
|
||||
__tablename__ = "vehicle_makes"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
logo_url = Column(String, nullable=True)
|
||||
|
||||
# 3. Modell és Generáció (pl. Audi A3 -> A3 8V)
|
||||
class VehicleModel(Base):
|
||||
__tablename__ = "vehicle_models"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
make_id = Column(Integer, ForeignKey("data.vehicle_makes.id"))
|
||||
category_id = Column(Integer, ForeignKey("data.vehicle_categories.id"))
|
||||
name = Column(String, nullable=False)
|
||||
generation_name = Column(String, nullable=True) # pl: "8V Facelift"
|
||||
production_start_year = Column(Integer, nullable=True)
|
||||
production_end_year = Column(Integer, nullable=True)
|
||||
|
||||
# 4. Motor és Hajtáslánc (Technikai specifikációk)
|
||||
class VehicleEngine(Base):
|
||||
__tablename__ = "vehicle_engines"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
model_id = Column(Integer, ForeignKey("data.vehicle_models.id"))
|
||||
|
||||
engine_code = Column(String, nullable=True)
|
||||
fuel_type = Column(String, nullable=False) # 'Petrol', 'Diesel', 'Hybrid', 'EV'
|
||||
displacement_ccm = Column(Integer, nullable=True)
|
||||
power_kw = Column(Integer, nullable=True)
|
||||
torque_nm = Column(Integer, nullable=True)
|
||||
transmission_type = Column(String, nullable=True) # 'Manual', 'Automatic'
|
||||
gears_count = Column(Integer, nullable=True)
|
||||
drive_type = Column(String, nullable=True) # 'FWD', 'RWD', 'AWD'
|
||||
|
||||
# 5. Opciók Katalógusa (Gyári extrák listája)
|
||||
class VehicleOptionCatalog(Base):
|
||||
__tablename__ = "vehicle_options_catalog"
|
||||
__table_args__ = {"schema": "data"}
|
||||
id = Column(Integer, primary_key=True)
|
||||
category = Column(String) # 'Security', 'Comfort', 'Multimedia'
|
||||
name_key = Column(String) # 'MATRIX_LED'
|
||||
Binary file not shown.
BIN
backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,43 +1,54 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
class AssetCreate(BaseModel):
|
||||
# Alapadatok
|
||||
make: str = Field(..., example="Ford")
|
||||
model: str = Field(..., example="Mondeo")
|
||||
vin: str = Field(..., min_length=17, max_length=17, description="Alvázszám")
|
||||
license_plate: Optional[str] = Field(None, max_length=20, example="RRR-555")
|
||||
# --- KATALÓGUS SÉMÁK (Gyári adatok) ---
|
||||
class AssetCatalogBase(BaseModel):
|
||||
make: str
|
||||
model: str
|
||||
generation: Optional[str] = None
|
||||
year_from: Optional[int] = None
|
||||
year_to: Optional[int] = None
|
||||
vehicle_class: Optional[str] = None
|
||||
fuel_type: Optional[str] = None
|
||||
engine_code: Optional[str] = None
|
||||
|
||||
# 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")
|
||||
class AssetCatalogResponse(AssetCatalogBase):
|
||||
id: int
|
||||
factory_data: Optional[Dict[str, Any]] = None # A robot által gyűjtött adatok
|
||||
|
||||
# Technikai adatok
|
||||
engine_description: Optional[str] = Field(None, example="2.0 TDCI")
|
||||
year_of_manufacture: int = Field(..., ge=1900, le=2100)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# 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):
|
||||
id: UUID
|
||||
catalog_id: Optional[int]
|
||||
vin: str
|
||||
license_plate: Optional[str] = None # JAVÍTVA: Lehet None a válaszban
|
||||
# --- JÁRMŰ SÉMÁK (Asset) ---
|
||||
class AssetBase(BaseModel):
|
||||
vin: str = Field(..., min_length=17, max_length=17)
|
||||
license_plate: str
|
||||
name: Optional[str] = None
|
||||
fuel_type: str
|
||||
vehicle_class: str
|
||||
is_verified: bool
|
||||
year_of_manufacture: int
|
||||
system_mileage: int
|
||||
quality_index: float
|
||||
created_at: datetime
|
||||
year_of_manufacture: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
class AssetCreate(AssetBase):
|
||||
# A létrehozáshoz kellenek a katalógus infók is
|
||||
make: str
|
||||
model: str
|
||||
vehicle_class: Optional[str] = "land"
|
||||
fuel_type: Optional[str] = None
|
||||
current_reading: Optional[int] = 0
|
||||
|
||||
class AssetResponse(AssetBase):
|
||||
id: UUID
|
||||
catalog_id: int
|
||||
is_verified: bool
|
||||
status: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# --- DIGITÁLIS IKER (Full Profile) ---
|
||||
# Ez a séma felel a 9 pontos költség és a mélységi szerviz adatok átadásáért
|
||||
class AssetFullProfile(BaseModel):
|
||||
identity: Dict[str, Any]
|
||||
telemetry: Dict[str, Any]
|
||||
financial_summary: Dict[str, Any]
|
||||
service_history: List[Dict[str, Any]]
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
35
backend/app/schemas/asset_cost.py
Normal file
35
backend/app/schemas/asset_cost.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
class AssetCostBase(BaseModel):
|
||||
"""Alap költség adatok (Frontendről érkező bevitel)."""
|
||||
cost_type: str = Field(..., description="fuel, service, fine, insurance, toll, etc.")
|
||||
amount_local: Decimal = Field(..., description="A fizetett bruttó összeg helyi devizában")
|
||||
currency_local: str = Field("HUF", min_length=3, max_length=3)
|
||||
date: datetime = Field(default_factory=datetime.now)
|
||||
mileage_at_cost: Optional[int] = Field(None, description="Kilométeróra állása a költség rögzítésekor")
|
||||
description: Optional[str] = None
|
||||
net_amount_local: Optional[Decimal] = None
|
||||
vat_rate: Optional[Decimal] = Field(27.0, description="ÁFA kulcs (pl. 27.0)")
|
||||
data: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Extra adatok (pl. helyszín, számlaszám)")
|
||||
|
||||
class AssetCostCreate(AssetCostBase):
|
||||
"""Költség rögzítésekor használt séma."""
|
||||
asset_id: UUID
|
||||
organization_id: int
|
||||
|
||||
class AssetCostResponse(AssetCostBase):
|
||||
"""Visszatérő adat modell a frontend felé."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
organization_id: int
|
||||
driver_id: Optional[int]
|
||||
amount_eur: Decimal
|
||||
exchange_rate_used: Decimal
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import date
|
||||
|
||||
# --- STEP 1: LITE REGISTRATION ---
|
||||
@@ -9,6 +9,8 @@ class UserLiteRegister(BaseModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
region_code: str = "HU"
|
||||
lang: str = Field("hu", description="Választott nyelv kódja")
|
||||
timezone: str = Field("Europe/Budapest", description="Felhasználó időzónája")
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
@@ -30,15 +32,15 @@ class UserKYCComplete(BaseModel):
|
||||
birth_date: date
|
||||
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
|
||||
address_hrsz: Optional[str] = None
|
||||
identity_docs: Dict[str, DocumentDetail]
|
||||
ice_contact: ICEContact
|
||||
preferred_currency: Optional[str] = Field("HUF", max_length=3)
|
||||
|
||||
# --- COMMON & SECURITY ---
|
||||
class PasswordResetRequest(BaseModel):
|
||||
@@ -54,3 +56,12 @@ class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
is_active: bool
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
"""JWT Token payload struktúrája validációhoz."""
|
||||
sub: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
rank: Optional[int] = 0
|
||||
scope_level: Optional[str] = None
|
||||
scope_id: Optional[str] = None
|
||||
region: Optional[str] = None
|
||||
@@ -1,46 +0,0 @@
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
# --- AUTH ---
|
||||
email: EmailStr = Field(..., example="teszt.user@profibot.hu")
|
||||
password: Optional[str] = Field(None, min_length=8, description="Social login esetén üres maradhat")
|
||||
|
||||
# --- IDENTITY (KYC Step 2) ---
|
||||
last_name: str = Field(..., min_length=2)
|
||||
first_name: str = Field(..., min_length=2)
|
||||
mothers_name: str = Field(..., description="Anyja születési neve")
|
||||
birth_place: Optional[str] = None
|
||||
birth_date: Optional[date] = None
|
||||
|
||||
# --- OKMÁNYOK (Banki szint) ---
|
||||
id_card_number: Optional[str] = None
|
||||
id_card_expiry: Optional[date] = None
|
||||
|
||||
driver_license_number: Optional[str] = None
|
||||
driver_license_expiry: Optional[date] = None
|
||||
driver_license_categories: List[str] = Field(default_factory=list, example=["B", "A"])
|
||||
|
||||
# --- SPECIÁLIS ENGEDÉLYEK ---
|
||||
boat_license_number: Optional[str] = None
|
||||
pilot_license_number: Optional[str] = None
|
||||
|
||||
# --- SYSTEM ---
|
||||
region_code: str = Field(default="HU")
|
||||
invite_token: Optional[str] = None
|
||||
social_provider: Optional[str] = None
|
||||
social_id: Optional[str] = None
|
||||
|
||||
@field_validator('region_code')
|
||||
@classmethod
|
||||
def validate_region(cls, v: str) -> str:
|
||||
return v.upper() if v else "HU"
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
@@ -15,6 +15,8 @@ class CorpOnboardIn(BaseModel):
|
||||
|
||||
tax_number: str
|
||||
country_code: str = "HU"
|
||||
language: str = Field("hu", description="A szervezet alapértelmezett nyelve")
|
||||
default_currency: str = Field("HUF", description="A szervezet alapértelmezett pénzneme")
|
||||
reg_number: Optional[str] = None
|
||||
|
||||
# Atomizált Címkezelés
|
||||
|
||||
43
backend/app/scripts/seed_system_params.py
Normal file
43
backend/app/scripts/seed_system_params.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
import json
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.models.system_config import SystemParameter
|
||||
from app.core.config import settings
|
||||
|
||||
async def seed_system():
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
params = [
|
||||
{
|
||||
"key": "fuel_types",
|
||||
"value": ["Benzin (95)", "Benzin (100)", "Dízel", "Prémium Dízel", "LPG", "Elektromos", "Hibrid"],
|
||||
"description": "Rendszerben használható üzemanyag típusok"
|
||||
},
|
||||
{
|
||||
"key": "currencies",
|
||||
"value": ["HUF", "EUR", "USD", "GBP"],
|
||||
"description": "Támogatott pénznemek"
|
||||
},
|
||||
{
|
||||
"key": "expense_categories",
|
||||
"value": ["Üzemanyag", "Szerviz", "Biztosítás", "Autópálya matrica", "Parkolás", "Adó", "Egyéb"],
|
||||
"description": "Költség kategóriák"
|
||||
}
|
||||
]
|
||||
|
||||
for p in params:
|
||||
# Megnézzük, létezik-e már
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(select(SystemParameter).where(SystemParameter.key == p["key"]))
|
||||
if not result.scalar_one_or_none():
|
||||
new_param = SystemParameter(**p)
|
||||
session.add(new_param)
|
||||
|
||||
await session.commit()
|
||||
print("✅ Rendszer paraméterek sikeresen feltöltve!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_system())
|
||||
@@ -1,58 +1,97 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import uuid
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.legal import LegalDocument
|
||||
from app.models.email_template import EmailTemplate, EmailType
|
||||
from app.models.email_provider import EmailProviderConfig
|
||||
from app.models import (
|
||||
User, Person, UserRole, SystemParameter,
|
||||
PointRule, LevelConfig, SubscriptionTier, UserStats
|
||||
)
|
||||
from app.core.security import get_password_hash
|
||||
from app.core.config import settings
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def seed_data():
|
||||
async with SessionLocal() as db:
|
||||
# 1. Jogi dokumentumok (HU)
|
||||
legal_docs = [
|
||||
LegalDocument(
|
||||
title="Általános Szerződési Feltételek",
|
||||
content="Ide jön az ÁSZF szövege... Kérjük görgessen az aljáig.",
|
||||
version="v1.0",
|
||||
region_code="HU",
|
||||
language="hu"
|
||||
),
|
||||
LegalDocument(
|
||||
title="Adatkezelési Tájékoztató (GDPR)",
|
||||
content="Ide jön a GDPR szövege... Kérjük görgessen az aljáig.",
|
||||
version="v1.0",
|
||||
region_code="HU",
|
||||
language="hu"
|
||||
logger.info("🚀 Alapadatok feltöltése biztonságos módban...")
|
||||
|
||||
admin_email = settings.INITIAL_ADMIN_EMAIL
|
||||
admin_password = settings.INITIAL_ADMIN_PASSWORD
|
||||
|
||||
if not admin_email or not admin_password:
|
||||
logger.error("❌ HIBA: INITIAL_ADMIN_EMAIL vagy PASSWORD nincs beállítva!")
|
||||
return
|
||||
|
||||
stmt = select(User).where(User.email == admin_email)
|
||||
admin_exists = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not admin_exists:
|
||||
new_person = Person(
|
||||
first_name="Rendszer",
|
||||
last_name="Adminisztrátor",
|
||||
id_uuid=uuid.uuid4()
|
||||
)
|
||||
db.add(new_person)
|
||||
await db.flush()
|
||||
|
||||
new_admin = User(
|
||||
email=admin_email,
|
||||
hashed_password=get_password_hash(admin_password),
|
||||
role=UserRole.admin,
|
||||
is_active=True,
|
||||
# JAVÍTÁS: is_verified eltávolítva, mert nincs ilyen mező a modellben
|
||||
person_id=new_person.id
|
||||
)
|
||||
db.add(new_admin)
|
||||
await db.flush()
|
||||
|
||||
db.add(UserStats(user_id=new_admin.id, total_xp=0, current_level=1))
|
||||
logger.info(f"✅ Admin létrehozva: {admin_email}")
|
||||
|
||||
# --- 1. Értékelési szempontok (Admin Motor) ---
|
||||
criteria_key = "ASSET_REVIEW_CRITERIA"
|
||||
stmt_crit = select(SystemParameter).where(SystemParameter.key == criteria_key)
|
||||
if not (await db.execute(stmt_crit)).scalar_one_or_none():
|
||||
db.add(SystemParameter(
|
||||
key=criteria_key,
|
||||
value=["Kényelem", "Fogyasztás", "Megbízhatóság", "Vezetési élmény", "Szervizigény"],
|
||||
description="Járműértékelési szempontok"
|
||||
))
|
||||
|
||||
# --- 2. Gamification Pontszabályok ---
|
||||
rules = [
|
||||
("ASSET_REGISTER", 100, "Új jármű felvétele"),
|
||||
("ASSET_REVIEW", 75, "Jármű értékelése"),
|
||||
("COST_RECORD", 50, "Költség/Tankolás rögzítése")
|
||||
]
|
||||
for key, pts, desc in rules:
|
||||
stmt_rule = select(PointRule).where(PointRule.action_key == key)
|
||||
if not (await db.execute(stmt_rule)).scalar_one_or_none():
|
||||
db.add(PointRule(action_key=key, points=pts, description=desc))
|
||||
|
||||
# 2. Email Sablon (Regisztráció)
|
||||
reg_template = EmailTemplate(
|
||||
type=EmailType.REGISTRATION,
|
||||
subject="Üdvözöljük a Service Finderben!",
|
||||
body_html="""
|
||||
<h3>Kedves {{ name }}!</h3>
|
||||
<p>Köszönjük a regisztrációt! Az aktiváláshoz kattints ide:</p>
|
||||
<a href="{{ link }}">Fiók aktiválása</a>
|
||||
<p>A link 24 óráig érvényes.</p>
|
||||
"""
|
||||
)
|
||||
# --- 3. Gamification Szintek ---
|
||||
stmt_level = select(LevelConfig)
|
||||
if not (await db.execute(stmt_level)).first():
|
||||
db.add_all([
|
||||
LevelConfig(level_number=1, min_points=0, rank_name="Kezdő Sofőr"),
|
||||
LevelConfig(level_number=2, min_points=500, rank_name="Tapasztalt Vezető"),
|
||||
LevelConfig(level_number=3, min_points=2000, rank_name="Flotta Mester")
|
||||
])
|
||||
|
||||
# 3. Email Szolgáltató (SendGrid)
|
||||
sendgrid_provider = EmailProviderConfig(
|
||||
name="SendGrid_Primary",
|
||||
provider_type="SENDGRID",
|
||||
priority=1,
|
||||
settings={"api_key": "YOUR_SENDGRID_KEY_HERE"}, # Ezt majd az adminon írjuk át
|
||||
max_fail_threshold=3
|
||||
)
|
||||
# --- 4. Előfizetési csomagok (MVP korlátok) ---
|
||||
stmt_tier = select(SubscriptionTier)
|
||||
if not (await db.execute(stmt_tier)).first():
|
||||
db.add_all([
|
||||
SubscriptionTier(name="Ingyenes", rules={"max_assets": 1, "reports": False}),
|
||||
SubscriptionTier(name="Prémium", rules={"max_assets": 5, "reports": True}),
|
||||
SubscriptionTier(name="Flotta", rules={"max_assets": 100, "reports": True})
|
||||
])
|
||||
|
||||
db.add_all(legal_docs)
|
||||
db.add(reg_template)
|
||||
db.add(sendgrid_provider)
|
||||
|
||||
await db.commit()
|
||||
print("🌱 Alapadatok sikeresen feltöltve!")
|
||||
logger.info("✨ A rendszer alapadatai és a Gamification motor készen áll!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_data())
|
||||
107
backend/app/seed_test_scenario.py
Normal file
107
backend/app/seed_test_scenario.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import select
|
||||
from app.db.session import SessionLocal
|
||||
from app.models import (
|
||||
User, Organization, OrganizationMember, Asset, AssetCatalog,
|
||||
AssetTelemetry, AssetFinancials, AssetCost, AssetEvent
|
||||
)
|
||||
from app.models.organization import OrgType
|
||||
|
||||
async def seed_test_scenario():
|
||||
async with SessionLocal() as db:
|
||||
print("🚀 Teszt ökoszisztéma felépítése a meglévő modellek alapján...")
|
||||
|
||||
# 1. Admin lekérése
|
||||
admin = (await db.execute(select(User))).scalars().first()
|
||||
if not admin:
|
||||
print("❌ Hiba: Nincs admin az adatbázisban!")
|
||||
return
|
||||
|
||||
# 2. SZERVEZETEK (A te OrgType enumod alapján)
|
||||
# Privát flotta
|
||||
private_org = Organization(
|
||||
name="Kincses Privát",
|
||||
full_name="Kincses Magánflotta és Garázs",
|
||||
org_type=OrgType.individual,
|
||||
owner_id=admin.id
|
||||
)
|
||||
# Céges flotta (OrgType.business-t használunk!)
|
||||
company_org = Organization(
|
||||
name="ProfiBot Fleet",
|
||||
full_name="ProfiBot Software Solutions Kft.",
|
||||
org_type=OrgType.business,
|
||||
owner_id=admin.id
|
||||
)
|
||||
# Szolgáltatók
|
||||
service_org = Organization(
|
||||
name="Mester Szerviz",
|
||||
full_name="Mester Autójavító és Vizsgabázis Kft.",
|
||||
org_type=OrgType.service,
|
||||
owner_id=admin.id
|
||||
)
|
||||
gas_station = Organization(
|
||||
name="MOL Digit",
|
||||
full_name="MOL Digitális Töltőállomás 001",
|
||||
org_type=OrgType.service_provider, # OrgType.service_provider-t használunk!
|
||||
owner_id=admin.id
|
||||
)
|
||||
|
||||
db.add_all([private_org, company_org, service_org, gas_station])
|
||||
await db.flush()
|
||||
|
||||
# Tagságok rögzítése
|
||||
db.add(OrganizationMember(user_id=admin.id, organization_id=private_org.id, role="owner"))
|
||||
db.add(OrganizationMember(user_id=admin.id, organization_id=company_org.id, role="owner"))
|
||||
|
||||
# 3. RÉTESZLETES JÁRMŰ ADAT (Tesla Model 3)
|
||||
catalog = AssetCatalog(
|
||||
make="Tesla", model="Model 3", generation="Long Range",
|
||||
year_from=2021, fuel_type="Electric",
|
||||
factory_data={
|
||||
"battery": "75 kWh", "power": "366 kW", "torque": "493 Nm",
|
||||
"tire_size": "235/45 R18", "oil_type": "None (EV)"
|
||||
}
|
||||
)
|
||||
db.add(catalog)
|
||||
await db.flush()
|
||||
|
||||
vehicle = Asset(
|
||||
vin="5YJ3E1EB8LF000000", license_plate="TES-777-EV",
|
||||
name="Főnök Teslája", year_of_manufacture=2021,
|
||||
catalog_id=catalog.id, status="active"
|
||||
)
|
||||
db.add(vehicle)
|
||||
await db.flush()
|
||||
|
||||
# Telemetria és Pénzügyi modulok
|
||||
db.add(AssetTelemetry(asset_id=vehicle.id, current_mileage=45200, vqi_score=100.0, dbs_score=100.0))
|
||||
db.add(AssetFinancials(asset_id=vehicle.id, acquisition_price=18500000))
|
||||
|
||||
# 4. KÖLTSÉGEK (9 kategória szimulálása)
|
||||
costs_data = [
|
||||
("FUEL", 15000, "Szupertöltés MOL", gas_station.id),
|
||||
("MAINTENANCE", 120000, "Éves szerviz + fékfolyadék", service_org.id),
|
||||
("TIRES", 240000, "Michelin Pilot Sport szett", None),
|
||||
("INSURANCE", 45000, "Allianz Casco", None),
|
||||
("TAX", 0, "Zöld rendszám kedvezmény", None),
|
||||
("TOLL", 5500, "Pest megyei e-matrica", None),
|
||||
("CLEANING", 8500, "Nano bevonat + Mosás", None),
|
||||
("PARKING", 2400, "Airport Parking", None),
|
||||
("FINE", 0, "Nincs aktív bírság", None)
|
||||
]
|
||||
|
||||
for c_type, amount, desc, vendor_id in costs_data:
|
||||
db.add(AssetCost(
|
||||
asset_id=vehicle.id, organization_id=company_org.id,
|
||||
cost_type=c_type, amount=amount, currency="HUF",
|
||||
data={"description": desc, "vendor_id": vendor_id},
|
||||
date=datetime.now() - timedelta(days=2)
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
print("✅ Siker! Flották, Tesla és a 9 költségtípus rögzítve.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_test_scenario())
|
||||
Binary file not shown.
BIN
backend/app/services/__pycache__/cost_service.cpython-312.pyc
Normal file
BIN
backend/app/services/__pycache__/cost_service.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35
backend/app/services/asset_service.py
Normal file
35
backend/app/services/asset_service.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.asset import Asset, AssetTelemetry, AssetFinancials
|
||||
from app.models.gamification import UserStats, PointRule
|
||||
import uuid
|
||||
|
||||
async def create_new_vehicle(db: AsyncSession, user_id: int, vin: str, license_plate: str):
|
||||
# 1. Alap Asset létrehozása
|
||||
new_asset = Asset(
|
||||
vin=vin,
|
||||
license_plate=license_plate,
|
||||
name=f"Teszt Autó ({license_plate})"
|
||||
)
|
||||
db.add(new_asset)
|
||||
await db.flush() # Hogy legyen ID-ja
|
||||
|
||||
# 2. Modulok inicializálása (Digital Twin alapozás)
|
||||
db.add(AssetTelemetry(asset_id=new_asset.id, current_mileage=0))
|
||||
db.add(AssetFinancials(asset_id=new_asset.id))
|
||||
|
||||
# 3. GAMIFICATION: Pontszerzés (ASSET_REGISTER = 100 XP)
|
||||
# Megkeressük a szabályt
|
||||
rule_stmt = select(PointRule).where(PointRule.action_key == "ASSET_REGISTER")
|
||||
rule = (await db.execute(rule_stmt)).scalar_one_or_none()
|
||||
|
||||
if rule:
|
||||
# Frissítjük a felhasználó XP-jét
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
|
||||
stats = (await db.execute(stats_stmt)).scalar_one_or_none()
|
||||
if stats:
|
||||
stats.total_xp += rule.points
|
||||
# Itt később jöhet a szintlépés ellenőrzése is!
|
||||
|
||||
await db.commit()
|
||||
return new_asset
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy.orm import joinedload
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from app.models.identity import User, Person, UserRole, VerificationToken, Wallet
|
||||
from app.models.gamification import UserStats # <--- Innen importáljuk mostantól!
|
||||
from app.models.gamification import UserStats
|
||||
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
|
||||
@@ -26,7 +26,7 @@ class AuthService:
|
||||
async def register_lite(db: AsyncSession, user_in: UserLiteRegister):
|
||||
"""
|
||||
Step 1: Lite Regisztráció (Master Book 1.1)
|
||||
Új User és ideiglenes Person rekord létrehozása.
|
||||
Új User és ideiglenes Person rekord létrehozása nyelvi és időzóna adatokkal.
|
||||
"""
|
||||
try:
|
||||
# Ideiglenes Person rekord a KYC-ig
|
||||
@@ -45,7 +45,10 @@ class AuthService:
|
||||
role=UserRole.user,
|
||||
is_active=False,
|
||||
is_deleted=False,
|
||||
region_code=user_in.region_code
|
||||
region_code=user_in.region_code,
|
||||
# --- NYELVI ÉS ADMIN BEÁLLÍTÁSOK MENTÉSE ---
|
||||
preferred_language=user_in.lang,
|
||||
timezone=user_in.timezone
|
||||
)
|
||||
db.add(new_user)
|
||||
await db.flush()
|
||||
@@ -60,12 +63,14 @@ class AuthService:
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reg_hours))
|
||||
))
|
||||
|
||||
# Email küldés (Master Book 3.2: Nincs manuális subject)
|
||||
# --- EMAIL KÜLDÉSE A VÁLASZTOTT NYELVEN ---
|
||||
# Master Book 3.2: Nincs manuális subject, a nyelvi kulcs alapján töltődik be
|
||||
verification_link = f"{settings.FRONTEND_BASE_URL}/verify?token={token_val}"
|
||||
await email_manager.send_email(
|
||||
recipient=user_in.email,
|
||||
template_key="registration",
|
||||
variables={"first_name": user_in.first_name, "link": verification_link}
|
||||
template_key="reg", # hu.json: email.reg_subject, reg_greeting stb.
|
||||
variables={"first_name": user_in.first_name, "link": verification_link},
|
||||
lang=user_in.lang # Dinamikus nyelvválasztás
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
@@ -81,6 +86,7 @@ class AuthService:
|
||||
"""
|
||||
1.3. Fázis: Atomi Tranzakció & Shadow Identity
|
||||
Felismeri a visszatérő Person-t, de új User-ként, izolált flottával indít.
|
||||
Frissíti a nyelvi és pénzügyi beállításokat.
|
||||
"""
|
||||
try:
|
||||
# 1. Aktuális technikai User lekérése
|
||||
@@ -89,8 +95,11 @@ class AuthService:
|
||||
user = res.scalar_one_or_none()
|
||||
if not user: return None
|
||||
|
||||
# 2. Shadow Identity Ellenőrzése (Anyja neve + Születési hely + Idő alapján)
|
||||
# Globális keresés, régiótól függetlenül
|
||||
# --- PÉNZNEM PREFERENCIA FRISSÍTÉSE ---
|
||||
if hasattr(kyc_in, 'preferred_currency') and kyc_in.preferred_currency:
|
||||
user.preferred_currency = kyc_in.preferred_currency
|
||||
|
||||
# 2. Shadow Identity Ellenőrzése
|
||||
identity_stmt = select(Person).where(and_(
|
||||
Person.mothers_last_name == kyc_in.mothers_last_name,
|
||||
Person.mothers_first_name == kyc_in.mothers_first_name,
|
||||
@@ -100,7 +109,6 @@ class AuthService:
|
||||
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}")
|
||||
@@ -118,7 +126,7 @@ class AuthService:
|
||||
parcel_id=kyc_in.address_hrsz
|
||||
)
|
||||
|
||||
# 4. Person adatok frissítése (mindig a legfrissebbet tároljuk)
|
||||
# 4. Person adatok frissítése
|
||||
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
|
||||
@@ -129,7 +137,7 @@ class AuthService:
|
||||
active_person.ice_contact = jsonable_encoder(kyc_in.ice_contact)
|
||||
active_person.is_active = True
|
||||
|
||||
# 5. Új, izolált INDIVIDUAL szervezet (4.2.3)
|
||||
# 5. Új, izolált INDIVIDUAL szervezet (4.2.3) i18n beállításokkal
|
||||
new_org = Organization(
|
||||
full_name=f"{active_person.last_name} {active_person.first_name} Egyéni Flotta",
|
||||
name=f"{active_person.last_name} Flotta",
|
||||
@@ -137,7 +145,11 @@ class AuthService:
|
||||
owner_id=user.id,
|
||||
is_transferable=False,
|
||||
is_active=True,
|
||||
status="verified"
|
||||
status="verified",
|
||||
# Megörökölt adminisztrációs adatok
|
||||
language=user.preferred_language,
|
||||
default_currency=user.preferred_currency,
|
||||
country_code=user.region_code
|
||||
)
|
||||
db.add(new_org)
|
||||
await db.flush()
|
||||
@@ -150,8 +162,13 @@ class AuthService:
|
||||
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))
|
||||
# 7. Wallet & Stats
|
||||
db.add(Wallet(
|
||||
user_id=user.id,
|
||||
coin_balance=0,
|
||||
credit_balance=0,
|
||||
currency=user.preferred_currency
|
||||
))
|
||||
db.add(UserStats(user_id=user.id, total_xp=0, current_level=1))
|
||||
|
||||
# 8. Aktiválás
|
||||
@@ -197,7 +214,6 @@ class AuthService:
|
||||
|
||||
@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()
|
||||
|
||||
@@ -211,11 +227,13 @@ class AuthService:
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=int(reset_hours))
|
||||
))
|
||||
|
||||
# --- EMAIL KÜLDÉSE A FELHASZNÁLÓ SAJÁT NYELVÉN ---
|
||||
reset_link = f"{settings.FRONTEND_BASE_URL}/reset-password?token={token_val}"
|
||||
await email_manager.send_email(
|
||||
recipient=email,
|
||||
template_key="password_reset",
|
||||
variables={"link": reset_link}
|
||||
template_key="pwd_reset", # hu.json: email.pwd_reset_subject stb.
|
||||
variables={"link": reset_link},
|
||||
lang=user.preferred_language # Adatbázisból kinyert nyelv
|
||||
)
|
||||
await db.commit()
|
||||
return "success"
|
||||
|
||||
97
backend/app/services/cost_service.py
Normal file
97
backend/app/services/cost_service.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc
|
||||
from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate
|
||||
from app.models.gamification import UserStats
|
||||
from app.models.system_config import SystemParameter
|
||||
from app.schemas.asset_cost import AssetCostCreate
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CostService:
|
||||
@staticmethod
|
||||
async def get_param(db: AsyncSession, key: str, default: any) -> any:
|
||||
"""Rendszerparaméter lekérése (pl. XP szorzó)."""
|
||||
stmt = select(SystemParameter).where(SystemParameter.key == key)
|
||||
res = await db.execute(stmt)
|
||||
param = res.scalar_one_or_none()
|
||||
return param.value if param else default
|
||||
|
||||
async def record_cost(self, db: AsyncSession, cost_in: AssetCostCreate, user_id: int):
|
||||
"""
|
||||
Költség rögzítése: EUR konverzió + Telemetria + XP.
|
||||
"""
|
||||
try:
|
||||
# 1. Árfolyam lekérése (EUR alapú pivot)
|
||||
# Megkeressük a legfrissebb rögzített árfolyamot a megadott devizához
|
||||
rate_stmt = select(ExchangeRate).where(
|
||||
ExchangeRate.target_currency == cost_in.currency_local
|
||||
).order_by(desc(ExchangeRate.updated_at)).limit(1)
|
||||
|
||||
rate_res = await db.execute(rate_stmt)
|
||||
rate_obj = rate_res.scalar_one_or_none()
|
||||
|
||||
# Ha nincs rögzített árfolyam, 1.0-val számolunk (vagy hibát dobhatunk a konfigurációtól függően)
|
||||
exchange_rate = rate_obj.rate if rate_obj else Decimal("1.0")
|
||||
|
||||
# EUR kalkuláció: Helyi összeg / Árfolyam (Pl. 40000 HUF / 400 = 100 EUR)
|
||||
amt_eur = Decimal(str(cost_in.amount_local)) / exchange_rate if exchange_rate > 0 else Decimal("0")
|
||||
|
||||
# 2. Költség rekord létrehozása
|
||||
new_cost = AssetCost(
|
||||
asset_id=cost_in.asset_id,
|
||||
organization_id=cost_in.organization_id,
|
||||
driver_id=user_id,
|
||||
cost_type=cost_in.cost_type,
|
||||
amount_local=cost_in.amount_local,
|
||||
currency_local=cost_in.currency_local,
|
||||
amount_eur=amt_eur,
|
||||
net_amount_local=cost_in.net_amount_local,
|
||||
vat_rate=cost_in.vat_rate,
|
||||
exchange_rate_used=exchange_rate,
|
||||
mileage_at_cost=cost_in.mileage_at_cost,
|
||||
date=cost_in.date or datetime.now(),
|
||||
data=cost_in.data or {}
|
||||
)
|
||||
db.add(new_cost)
|
||||
|
||||
# 3. Telemetria frissítése (Ha érkezett kilométeróra állás)
|
||||
if cost_in.mileage_at_cost:
|
||||
tel_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == cost_in.asset_id)
|
||||
res = await db.execute(tel_stmt)
|
||||
telemetry = res.scalar_one_or_none()
|
||||
|
||||
if telemetry:
|
||||
# Megakadályozzuk a "visszatekerést"
|
||||
if cost_in.mileage_at_cost > (telemetry.current_mileage or 0):
|
||||
telemetry.current_mileage = cost_in.mileage_at_cost
|
||||
else:
|
||||
# Ha még nem volt telemetria adat, létrehozzuk
|
||||
new_telemetry = AssetTelemetry(
|
||||
asset_id=cost_in.asset_id,
|
||||
current_mileage=cost_in.mileage_at_cost
|
||||
)
|
||||
db.add(new_telemetry)
|
||||
|
||||
# 4. Gamification XP jóváírás
|
||||
xp_reward = await self.get_param(db, "XP_PER_COST_LOG", 50)
|
||||
stats_stmt = select(UserStats).where(UserStats.user_id == user_id)
|
||||
stats_res = await db.execute(stats_stmt)
|
||||
user_stats = stats_res.scalar_one_or_none()
|
||||
|
||||
if user_stats:
|
||||
user_stats.total_xp += int(xp_reward)
|
||||
logger.info(f"User {user_id} earned {xp_reward} XP for cost logging.")
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_cost)
|
||||
return new_cost
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error in record_cost: {str(e)}")
|
||||
raise e
|
||||
|
||||
cost_service = CostService()
|
||||
@@ -17,6 +17,9 @@ class EmailManager:
|
||||
button_text = locale_manager.get(f"email.{template_key}_button", lang=lang)
|
||||
footer = locale_manager.get(f"email.{template_key}_footer", lang=lang)
|
||||
|
||||
# ÚJ: A link fallback szöveg is a nyelvi fájlból jön
|
||||
link_fallback_text = locale_manager.get("email.link_fallback", lang=lang)
|
||||
|
||||
return f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
|
||||
@@ -30,8 +33,8 @@ class EmailManager:
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #777; word-break: break-all;">
|
||||
Ha a gomb nem működik, másolja be ezt a linket a böngészőjébe:<br>
|
||||
{variables.get('link')}
|
||||
{link_fallback_text}<br>
|
||||
<a href="{variables.get('link')}" style="color: #3498db;">{variables.get('link')}</a>
|
||||
</p>
|
||||
<hr style="border: 0; border-top: 1px solid #eee; margin: 30px 0;">
|
||||
<p style="font-size: 0.8em; color: #999; text-align: center;">{footer}</p>
|
||||
@@ -66,18 +69,11 @@ class EmailManager:
|
||||
response = sg.send(message)
|
||||
|
||||
logger.info(f"SendGrid Status: {response.status_code} for {recipient}")
|
||||
if response.status_code >= 400:
|
||||
logger.error(f"SendGrid Hibaüzenet: {response.body}")
|
||||
|
||||
return {"status": "success", "provider": "sendgrid", "code": response.status_code}
|
||||
except Exception as e:
|
||||
logger.error(f"SendGrid Kritikus Hiba: {str(e)}")
|
||||
|
||||
# 2. SMTP Fallback
|
||||
if not settings.SMTP_HOST or not settings.SMTP_USER or not settings.SMTP_PASSWORD:
|
||||
logger.warning("SMTP nincs konfigurálva a fallback-hez.")
|
||||
return {"status": "error", "message": "Nincs elérhető szolgáltató."}
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = f"{settings.EMAILS_FROM_NAME} <{settings.EMAILS_FROM_EMAIL}>"
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import select
|
||||
from app.models.gamification import UserStats, PointsLedger
|
||||
from app.models.identity import User
|
||||
import math
|
||||
|
||||
class GamificationService:
|
||||
@staticmethod
|
||||
async def award_points(db: AsyncSession, user_id: int, points: int, reason: str):
|
||||
"""Pontok jóváírása (SQL szinkronizált points mezővel)."""
|
||||
new_entry = PointsLedger(
|
||||
user_id=user_id,
|
||||
points=points, # Javítva: points_change helyett points
|
||||
reason=reason
|
||||
)
|
||||
db.add(new_entry)
|
||||
|
||||
result = await db.execute(select(UserStats).where(UserStats.user_id == user_id))
|
||||
stats = result.scalar_one_or_none()
|
||||
async def process_activity(db: AsyncSession, user_id: int, xp_amount: int, social_amount: int, reason: str):
|
||||
"""
|
||||
XP növelés, Szintlépés csekk és Automata Kredit váltás.
|
||||
"""
|
||||
# 1. User statisztika lekérése
|
||||
stmt = select(UserStats).where(UserStats.user_id == user_id)
|
||||
stats = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if not stats:
|
||||
stats = UserStats(user_id=user_id, total_points=0, current_level=1)
|
||||
stats = UserStats(user_id=user_id, total_xp=0, social_points=0, current_level=1, credits=0)
|
||||
db.add(stats)
|
||||
|
||||
stats.total_points += points
|
||||
await db.flush()
|
||||
return stats.total_points
|
||||
# 2. Részletes Logolás (PointsLedger) - A visszakövethetőség miatt
|
||||
db.add(PointsLedger(
|
||||
user_id=user_id,
|
||||
xp_gain=xp_amount,
|
||||
social_gain=social_amount,
|
||||
reason=reason
|
||||
))
|
||||
|
||||
# 3. XP és Szintlépés (Nehezedő görbe)
|
||||
stats.total_xp += xp_amount
|
||||
# Képlet: Level = (XP / 500)^(1/1.5)
|
||||
new_level = int((stats.total_xp / 500) ** (1/1.5)) + 1
|
||||
if new_level > stats.current_level:
|
||||
stats.current_level = new_level
|
||||
|
||||
# 4. Automata Kredit váltás
|
||||
# Példa: Minden 100 Social pont automatikusan 1 Kredit lesz
|
||||
stats.social_points += social_amount
|
||||
if stats.social_points >= 100:
|
||||
new_credits = stats.social_points // 100
|
||||
stats.credits += new_credits
|
||||
stats.social_points %= 100 # A maradék megmarad a következő váltáshoz
|
||||
|
||||
# Külön log a váltásról
|
||||
db.add(PointsLedger(user_id=user_id, reason=f"Auto-conversion: {new_credits} Credits", credits_change=new_credits))
|
||||
|
||||
await db.commit()
|
||||
return stats
|
||||
@@ -1,34 +1,45 @@
|
||||
# /app/services/harvester_base.py
|
||||
import httpx
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.vehicle import VehicleCatalog
|
||||
from app.models.asset import AssetCatalog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseHarvester:
|
||||
def __init__(self, category: str):
|
||||
self.category = category
|
||||
self.category = category # car, bike, truck
|
||||
self.headers = {"User-Agent": "ServiceFinder-Harvester-Bot/2.0"}
|
||||
|
||||
async def check_exists(self, db: AsyncSession, brand: str, model: str):
|
||||
"""Ellenőrzi, hogy az adott modell létezik-e már."""
|
||||
stmt = select(VehicleCatalog).where(
|
||||
VehicleCatalog.brand == brand,
|
||||
VehicleCatalog.model == model,
|
||||
VehicleCatalog.category == self.category
|
||||
async def check_exists(self, db: AsyncSession, brand: str, model: str, gen: str = None):
|
||||
"""Ellenőrzi a katalógusban való létezést."""
|
||||
stmt = select(AssetCatalog).where(
|
||||
AssetCatalog.make == brand,
|
||||
AssetCatalog.model == model,
|
||||
AssetCatalog.vehicle_class == self.category
|
||||
)
|
||||
if gen:
|
||||
stmt = stmt.where(AssetCatalog.generation == gen)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict = None):
|
||||
"""Létrehoz vagy frissít egy katalógus bejegyzést."""
|
||||
existing = await self.check_exists(db, brand, model)
|
||||
async def log_entry(self, db: AsyncSession, brand: str, model: str, specs: dict):
|
||||
"""Létrehoz vagy frissít egy bejegyzést az AssetCatalog-ban."""
|
||||
existing = await self.check_exists(db, brand, model, specs.get("generation"))
|
||||
if not existing:
|
||||
new_v = VehicleCatalog(
|
||||
brand=brand,
|
||||
new_v = AssetCatalog(
|
||||
make=brand,
|
||||
model=model,
|
||||
category=self.category,
|
||||
factory_specs=specs or {},
|
||||
verification_status="incomplete" if not specs else "verified"
|
||||
generation=specs.get("generation"),
|
||||
year_from=specs.get("year_from"),
|
||||
year_to=specs.get("year_to"),
|
||||
vehicle_class=self.category,
|
||||
fuel_type=specs.get("fuel_type"),
|
||||
engine_code=specs.get("engine_code")
|
||||
)
|
||||
db.add(new_v)
|
||||
logger.info(f"🆕 Új katalógus elem: {brand} {model}")
|
||||
return True
|
||||
return False
|
||||
51
backend/app/services/recon_bot.py
Normal file
51
backend/app/services/recon_bot.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.asset import Asset, AssetCatalog, AssetTelemetry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def run_vehicle_recon(db: AsyncSession, asset_id: str):
|
||||
"""
|
||||
VIN alapján megkeresi a mélységi adatokat és frissíti a Digitális Ikert.
|
||||
"""
|
||||
# 1. Lekérjük a járművet és a katalógusát
|
||||
stmt = select(Asset).where(Asset.id == asset_id)
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
|
||||
if not asset or not asset.catalog_id:
|
||||
return False
|
||||
|
||||
logger.info(f"🤖 Robot indul: {asset.vin} felderítése...")
|
||||
|
||||
# 2. SZIMULÁLT ADATGYŰJTÉS (Itt hívnánk meg az API-kat: NHTSA, autodna stb.)
|
||||
await asyncio.sleep(2) # Időigényes keresés szimulálása
|
||||
|
||||
deep_data = {
|
||||
"assembly_plant": "Fremont, California",
|
||||
"drive_unit": "Dual Motor - Raven type",
|
||||
"onboard_charger": "11 kW",
|
||||
"supercharging_max": "250 kW",
|
||||
"safety_rating": "5-star EuroNCAP"
|
||||
}
|
||||
|
||||
# 3. Katalógus frissítése
|
||||
catalog_stmt = select(AssetCatalog).where(AssetCatalog.id == asset.catalog_id)
|
||||
catalog = (await db.execute(catalog_stmt)).scalar_one_or_none()
|
||||
|
||||
if catalog:
|
||||
current_data = catalog.factory_data or {}
|
||||
current_data.update(deep_data)
|
||||
catalog.factory_data = current_data
|
||||
|
||||
# 4. Telemetria frissítése (A robot talált egy visszahívást, VQI csökken kicsit)
|
||||
telemetry_stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
|
||||
telemetry = (await db.execute(telemetry_stmt)).scalar_one_or_none()
|
||||
if telemetry:
|
||||
telemetry.vqi_score = 99.2 # Robot frissített állapota
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✨ Robot végzett: {asset.license_plate} felokosítva.")
|
||||
return True
|
||||
@@ -1,40 +1,37 @@
|
||||
# /app/services/robot_manager.py
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
# Frissített importok az új fájlnevekhez:
|
||||
from .harvester_cars import CarHarvester
|
||||
from .harvester_bikes import BikeHarvester
|
||||
from .harvester_trucks import TruckHarvester
|
||||
# Megjegyzés: Ellenőrizd, hogy a harvester_bikes/trucks fájlokban is BaseHarvester az alap!
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RobotManager:
|
||||
@staticmethod
|
||||
async def run_full_sync(db):
|
||||
"""Sorban lefuttatja az összes robotot."""
|
||||
print(f"🕒 Szinkronizáció indítva: {datetime.now()}")
|
||||
"""Sorban lefuttatja a robotokat az új AssetCatalog struktúrához."""
|
||||
logger.info(f"🕒 Teljes szinkronizáció indítva: {datetime.now()}")
|
||||
|
||||
robots = [
|
||||
CarHarvester(),
|
||||
BikeHarvester(),
|
||||
TruckHarvester()
|
||||
# BikeHarvester(),
|
||||
# TruckHarvester()
|
||||
]
|
||||
|
||||
for robot in robots:
|
||||
try:
|
||||
# Itt a robot lekéri az API-tól az ÖSSZES márkát és frissít
|
||||
await robot.run(db)
|
||||
logger.info(f"✅ {robot.category} robot sikeresen lefutott.")
|
||||
await asyncio.sleep(5)
|
||||
except Exception as e:
|
||||
print(f"❌ Hiba a {robot.category} robotnál: {e}")
|
||||
logger.error(f"❌ Kritikus hiba a {robot.category} robotnál: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def schedule_nightly_run(db):
|
||||
"""
|
||||
Egyszerű ciklus, ami figyeli az időt.
|
||||
Ha éjjel 2 óra van, elindítja a teljes szinkront.
|
||||
"""
|
||||
while True:
|
||||
now = datetime.now()
|
||||
# Ha hajnali 2 és 2:01 között vagyunk, indítás
|
||||
if now.hour == 2 and now.minute == 0:
|
||||
await RobotManager.run_full_sync(db)
|
||||
await asyncio.sleep(70) # Várunk, hogy ne induljon el többször ugyanabban a percben
|
||||
await asyncio.sleep(30) # 30 másodpercenként ellenőrizzük az időt
|
||||
await asyncio.sleep(70)
|
||||
await asyncio.sleep(30)
|
||||
BIN
backend/migrations/__pycache__/env.cpython-312.pyc
Normal file
BIN
backend/migrations/__pycache__/env.cpython-312.pyc
Normal file
Binary file not shown.
@@ -8,28 +8,18 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
from alembic import context
|
||||
|
||||
# --- ÚTVONAL JAVÍTÁS ---
|
||||
# Az aktuális fájl (env.py) helyéből kiindulva meghatározzuk a könyvtárakat
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
project_root = os.path.realpath(os.path.join(current_dir, '..'))
|
||||
backend_dir = os.path.join(project_root, 'backend')
|
||||
sys.path.insert(0, "/app")
|
||||
|
||||
# Mindkét útvonalat betesszük a keresőbe, hogy a 'backend.app' és a sima 'app' is működjön
|
||||
sys.path.insert(0, project_root)
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
# Most már az Alembic megtalálja a konfigurációt és a modelleket
|
||||
try:
|
||||
from app.core.config import settings
|
||||
from app.db.base import Base
|
||||
from app.models import * # Fontos, hogy minden modell be legyen importálva!
|
||||
# Minden modellt importálunk a szinkronhoz
|
||||
import app.models
|
||||
except ImportError as e:
|
||||
print(f"Hiba az importálásnál: {e}")
|
||||
print(f"Próbált útvonalak: {sys.path}")
|
||||
raise
|
||||
|
||||
config = context.config
|
||||
|
||||
# Dinamikus adatbázis URL a .env alapján (App User jelszavával)
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
@@ -37,37 +27,40 @@ if config.config_file_name is not None:
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# CSAK a 'data' sémával foglalkozunk!
|
||||
def include_object(object, name, type_, reflected, compare_to):
|
||||
if type_ == "table":
|
||||
return object.schema == "data"
|
||||
return True
|
||||
|
||||
def do_run_migrations(connection):
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
include_schemas=True, # Adatbázis sémák (pl. 'data') támogatása
|
||||
version_table_schema='public' # Alembic tábla a public-ban marad
|
||||
include_schemas=True,
|
||||
include_object=include_object,
|
||||
version_table_schema='public'
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
"""Aszinkron kapcsolat felépítése és migráció futtatása"""
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
if context.is_offline_mode():
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
url=config.get_main_option("sqlalchemy.url"),
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
include_schemas=True
|
||||
include_schemas=True,
|
||||
include_object=include_object
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
554
backend/migrations/versions/0adbe75a0b3f_complete_sync.py
Normal file
554
backend/migrations/versions/0adbe75a0b3f_complete_sync.py
Normal file
@@ -0,0 +1,554 @@
|
||||
"""complete_sync
|
||||
|
||||
Revision ID: 0adbe75a0b3f
|
||||
Revises:
|
||||
Create Date: 2026-02-09 17:49:12.955967
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '0adbe75a0b3f'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('badges',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=False),
|
||||
sa.Column('icon_url', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data')
|
||||
op.create_table('exchange_rates',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('base_currency', sa.String(length=3), nullable=True),
|
||||
sa.Column('target_currency', sa.String(length=3), nullable=True),
|
||||
sa.Column('rate', sa.Numeric(precision=18, scale=6), nullable=True),
|
||||
sa.Column('rate_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('geo_postal_codes',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('country_code', sa.String(length=5), nullable=True),
|
||||
sa.Column('zip_code', sa.String(length=10), nullable=False),
|
||||
sa.Column('city', sa.String(length=100), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('geo_street_types',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('level_configs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('level_number', sa.Integer(), nullable=False),
|
||||
sa.Column('min_points', sa.Integer(), nullable=False),
|
||||
sa.Column('rank_name', sa.String(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('level_number'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False, schema='data')
|
||||
op.create_table('point_rules',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('action_key', sa.String(), nullable=False),
|
||||
sa.Column('points', sa.Integer(), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True, schema='data')
|
||||
op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False, schema='data')
|
||||
op.create_table('regional_settings',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('country_code', sa.String(), nullable=False),
|
||||
sa.Column('currency', sa.String(), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('country_code'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False, schema='data')
|
||||
op.create_table('service_specialties',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('slug', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['data.service_specialties.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('slug'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('subscription_tiers',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('rules', sa.JSON(), nullable=True),
|
||||
sa.Column('is_custom', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('system_parameters',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('key', sa.String(length=50), nullable=True),
|
||||
sa.Column('value', sa.JSON(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_system_parameters_id'), 'system_parameters', ['id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_system_parameters_key'), 'system_parameters', ['key'], unique=True, schema='data')
|
||||
op.create_table('vehicle_catalog',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('make', sa.String(), nullable=False),
|
||||
sa.Column('model', sa.String(), nullable=False),
|
||||
sa.Column('generation', sa.String(), nullable=True),
|
||||
sa.Column('year_from', sa.Integer(), nullable=True),
|
||||
sa.Column('year_to', sa.Integer(), nullable=True),
|
||||
sa.Column('vehicle_class', sa.String(), nullable=True),
|
||||
sa.Column('fuel_type', sa.String(), nullable=True),
|
||||
sa.Column('engine_code', sa.String(), nullable=True),
|
||||
sa.Column('factory_data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_vehicle_catalog_id'), 'vehicle_catalog', ['id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_vehicle_catalog_make'), 'vehicle_catalog', ['make'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_vehicle_catalog_model'), 'vehicle_catalog', ['model'], unique=False, schema='data')
|
||||
op.create_table('addresses',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('postal_code_id', sa.Integer(), nullable=True),
|
||||
sa.Column('street_name', sa.String(length=200), nullable=False),
|
||||
sa.Column('street_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('house_number', sa.String(length=50), nullable=False),
|
||||
sa.Column('stairwell', sa.String(length=20), nullable=True),
|
||||
sa.Column('floor', sa.String(length=20), nullable=True),
|
||||
sa.Column('door', sa.String(length=20), nullable=True),
|
||||
sa.Column('parcel_id', sa.String(length=50), nullable=True),
|
||||
sa.Column('full_address_text', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['postal_code_id'], ['data.geo_postal_codes.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('assets',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('vin', sa.String(length=17), nullable=False),
|
||||
sa.Column('license_plate', sa.String(length=20), nullable=True),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('year_of_manufacture', sa.Integer(), nullable=True),
|
||||
sa.Column('catalog_id', sa.Integer(), nullable=True),
|
||||
sa.Column('is_verified', sa.Boolean(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['catalog_id'], ['data.vehicle_catalog.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_assets_license_plate'), 'assets', ['license_plate'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_assets_vin'), 'assets', ['vin'], unique=True, schema='data')
|
||||
op.create_table('geo_streets',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('postal_code_id', sa.Integer(), nullable=True),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.ForeignKeyConstraint(['postal_code_id'], ['data.geo_postal_codes.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('asset_events',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=False),
|
||||
sa.Column('event_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('recorded_mileage', sa.Integer(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('asset_financials',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=True),
|
||||
sa.Column('acquisition_price', sa.Numeric(precision=18, scale=2), nullable=True),
|
||||
sa.Column('acquisition_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('financing_type', sa.String(), nullable=True),
|
||||
sa.Column('residual_value_estimate', sa.Numeric(precision=18, scale=2), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('asset_id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('asset_telemetry',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=True),
|
||||
sa.Column('current_mileage', sa.Integer(), nullable=True),
|
||||
sa.Column('mileage_unit', sa.String(length=10), nullable=True),
|
||||
sa.Column('vqi_score', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('dbs_score', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('asset_id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('persons',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('id_uuid', sa.UUID(), nullable=False),
|
||||
sa.Column('address_id', sa.UUID(), nullable=True),
|
||||
sa.Column('last_name', sa.String(), nullable=False),
|
||||
sa.Column('first_name', sa.String(), nullable=False),
|
||||
sa.Column('mothers_last_name', sa.String(), nullable=True),
|
||||
sa.Column('mothers_first_name', sa.String(), nullable=True),
|
||||
sa.Column('birth_place', sa.String(), nullable=True),
|
||||
sa.Column('birth_date', sa.DateTime(), nullable=True),
|
||||
sa.Column('phone', sa.String(), nullable=True),
|
||||
sa.Column('identity_docs', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.Column('medical_emergency', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['address_id'], ['data.addresses.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('id_uuid'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_persons_id'), 'persons', ['id'], unique=False, schema='data')
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('email', sa.String(), nullable=False),
|
||||
sa.Column('hashed_password', sa.String(), nullable=True),
|
||||
sa.Column('role', sa.Enum('admin', 'user', 'service', 'fleet_manager', 'driver', name='userrole'), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('region_code', sa.String(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.Column('person_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('preferred_language', sa.String(length=5), nullable=True),
|
||||
sa.Column('preferred_currency', sa.String(length=3), nullable=True),
|
||||
sa.Column('timezone', sa.String(length=50), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['person_id'], ['data.persons.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data')
|
||||
op.create_index(op.f('ix_data_users_id'), 'users', ['id'], unique=False, schema='data')
|
||||
op.create_table('asset_reviews',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('overall_rating', sa.Integer(), nullable=True),
|
||||
sa.Column('criteria_scores', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.Column('comment', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('target_type', sa.String(), nullable=True),
|
||||
sa.Column('target_id', sa.String(), nullable=True),
|
||||
sa.Column('action', sa.String(), nullable=False),
|
||||
sa.Column('changes', sa.JSON(), nullable=True),
|
||||
sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data')
|
||||
op.create_table('documents',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('parent_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('parent_id', sa.String(length=50), nullable=False),
|
||||
sa.Column('doc_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('original_name', sa.String(length=255), nullable=False),
|
||||
sa.Column('file_hash', sa.String(length=64), nullable=False),
|
||||
sa.Column('file_ext', sa.String(length=10), nullable=True),
|
||||
sa.Column('mime_type', sa.String(length=100), nullable=True),
|
||||
sa.Column('file_size', sa.Integer(), nullable=True),
|
||||
sa.Column('has_thumbnail', sa.Boolean(), nullable=True),
|
||||
sa.Column('thumbnail_path', sa.String(length=255), nullable=True),
|
||||
sa.Column('uploaded_by', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['uploaded_by'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('organizations',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('address_id', sa.UUID(), nullable=True),
|
||||
sa.Column('full_name', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('display_name', sa.String(length=50), nullable=True),
|
||||
sa.Column('default_currency', sa.String(length=3), nullable=True),
|
||||
sa.Column('country_code', sa.String(length=2), nullable=True),
|
||||
sa.Column('language', sa.String(length=5), nullable=True),
|
||||
sa.Column('address_zip', sa.String(length=10), nullable=True),
|
||||
sa.Column('address_city', sa.String(length=100), nullable=True),
|
||||
sa.Column('address_street_name', sa.String(length=150), nullable=True),
|
||||
sa.Column('address_street_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('address_house_number', sa.String(length=20), nullable=True),
|
||||
sa.Column('address_hrsz', sa.String(length=50), nullable=True),
|
||||
sa.Column('address_stairwell', sa.String(length=20), nullable=True),
|
||||
sa.Column('address_floor', sa.String(length=20), nullable=True),
|
||||
sa.Column('address_door', sa.String(length=20), nullable=True),
|
||||
sa.Column('tax_number', sa.String(length=20), nullable=True),
|
||||
sa.Column('reg_number', sa.String(length=50), nullable=True),
|
||||
sa.Column('org_type', postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True), nullable=True),
|
||||
sa.Column('status', sa.String(length=30), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.Column('notification_settings', sa.JSON(), server_default=sa.text('\'{ "notify_owner": true, "alert_days_before": [30, 15, 7, 1] }\'::jsonb'), nullable=True),
|
||||
sa.Column('external_integration_config', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_transferable', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_verified', sa.Boolean(), nullable=True),
|
||||
sa.Column('verification_expires_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['address_id'], ['data.addresses.id'], ),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_organizations_id'), 'organizations', ['id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=True, schema='data')
|
||||
op.create_table('points_ledger',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('points', sa.Integer(), nullable=False),
|
||||
sa.Column('reason', sa.String(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data')
|
||||
op.create_table('ratings',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||
sa.Column('target_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('target_id', sa.UUID(), nullable=False),
|
||||
sa.Column('score', sa.Integer(), nullable=False),
|
||||
sa.Column('comment', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['author_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('user_badges',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('badge_id', sa.Integer(), nullable=False),
|
||||
sa.Column('earned_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['badge_id'], ['data.badges.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_user_badges_id'), 'user_badges', ['id'], unique=False, schema='data')
|
||||
op.create_table('user_stats',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('total_xp', sa.Integer(), nullable=False),
|
||||
sa.Column('social_points', sa.Integer(), nullable=False),
|
||||
sa.Column('current_level', sa.Integer(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('user_id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('vehicle_ownerships',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vehicle_id', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('start_date', sa.Date(), nullable=False),
|
||||
sa.Column('end_date', sa.Date(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.ForeignKeyConstraint(['vehicle_id'], ['data.assets.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data')
|
||||
op.create_table('verification_tokens',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('token', sa.UUID(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('token_type', sa.String(length=20), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('is_used', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('token'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data')
|
||||
op.create_table('wallets',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('coin_balance', sa.Numeric(precision=18, scale=2), nullable=True),
|
||||
sa.Column('credit_balance', sa.Numeric(precision=18, scale=2), nullable=True),
|
||||
sa.Column('currency', sa.String(length=3), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_wallets_id'), 'wallets', ['id'], unique=False, schema='data')
|
||||
op.create_table('asset_assignments',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=False),
|
||||
sa.Column('organization_id', sa.Integer(), nullable=False),
|
||||
sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('released_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('status', sa.String(length=30), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('asset_costs',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('asset_id', sa.UUID(), nullable=False),
|
||||
sa.Column('organization_id', sa.Integer(), nullable=False),
|
||||
sa.Column('driver_id', sa.Integer(), nullable=True),
|
||||
sa.Column('cost_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('amount', sa.Numeric(precision=18, scale=2), nullable=False),
|
||||
sa.Column('currency', sa.String(length=3), nullable=True),
|
||||
sa.Column('net_amount', sa.Numeric(precision=18, scale=2), nullable=True),
|
||||
sa.Column('vat_rate', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('exchange_rate_at_cost', sa.Numeric(precision=18, scale=6), nullable=True),
|
||||
sa.Column('date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('mileage_at_cost', sa.Integer(), nullable=True),
|
||||
sa.Column('data', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.ForeignKeyConstraint(['asset_id'], ['data.assets.id'], ),
|
||||
sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], ),
|
||||
sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('credit_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('org_id', sa.Integer(), nullable=True),
|
||||
sa.Column('amount', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('org_subscriptions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('org_id', sa.Integer(), nullable=True),
|
||||
sa.Column('tier_id', sa.Integer(), nullable=True),
|
||||
sa.Column('valid_from', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('valid_until', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ),
|
||||
sa.ForeignKeyConstraint(['tier_id'], ['data.subscription_tiers.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('organization_members',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('organization_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role', sa.String(), nullable=True),
|
||||
sa.Column('permissions', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True),
|
||||
sa.ForeignKeyConstraint(['organization_id'], ['data.organizations.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members', schema='data')
|
||||
op.drop_table('organization_members', schema='data')
|
||||
op.drop_table('org_subscriptions', schema='data')
|
||||
op.drop_table('credit_logs', schema='data')
|
||||
op.drop_table('asset_costs', schema='data')
|
||||
op.drop_table('asset_assignments', schema='data')
|
||||
op.drop_index(op.f('ix_data_wallets_id'), table_name='wallets', schema='data')
|
||||
op.drop_table('wallets', schema='data')
|
||||
op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens', schema='data')
|
||||
op.drop_table('verification_tokens', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data')
|
||||
op.drop_table('vehicle_ownerships', schema='data')
|
||||
op.drop_table('user_stats', schema='data')
|
||||
op.drop_index(op.f('ix_data_user_badges_id'), table_name='user_badges', schema='data')
|
||||
op.drop_table('user_badges', schema='data')
|
||||
op.drop_table('ratings', schema='data')
|
||||
op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data')
|
||||
op.drop_table('points_ledger', schema='data')
|
||||
op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations', schema='data')
|
||||
op.drop_index(op.f('ix_data_organizations_id'), table_name='organizations', schema='data')
|
||||
op.drop_table('organizations', schema='data')
|
||||
op.drop_table('documents', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data')
|
||||
op.drop_table('audit_logs', schema='data')
|
||||
op.drop_table('asset_reviews', schema='data')
|
||||
op.drop_index(op.f('ix_data_users_id'), table_name='users', schema='data')
|
||||
op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data')
|
||||
op.drop_table('users', schema='data')
|
||||
op.drop_index(op.f('ix_data_persons_id'), table_name='persons', schema='data')
|
||||
op.drop_table('persons', schema='data')
|
||||
op.drop_table('asset_telemetry', schema='data')
|
||||
op.drop_table('asset_financials', schema='data')
|
||||
op.drop_table('asset_events', schema='data')
|
||||
op.drop_table('geo_streets', schema='data')
|
||||
op.drop_index(op.f('ix_data_assets_vin'), table_name='assets', schema='data')
|
||||
op.drop_index(op.f('ix_data_assets_license_plate'), table_name='assets', schema='data')
|
||||
op.drop_table('assets', schema='data')
|
||||
op.drop_table('addresses', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_catalog_model'), table_name='vehicle_catalog', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_catalog_make'), table_name='vehicle_catalog', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_catalog_id'), table_name='vehicle_catalog', schema='data')
|
||||
op.drop_table('vehicle_catalog', schema='data')
|
||||
op.drop_index(op.f('ix_data_system_parameters_key'), table_name='system_parameters', schema='data')
|
||||
op.drop_index(op.f('ix_data_system_parameters_id'), table_name='system_parameters', schema='data')
|
||||
op.drop_table('system_parameters', schema='data')
|
||||
op.drop_table('subscription_tiers', schema='data')
|
||||
op.drop_table('service_specialties', schema='data')
|
||||
op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings', schema='data')
|
||||
op.drop_table('regional_settings', schema='data')
|
||||
op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules', schema='data')
|
||||
op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules', schema='data')
|
||||
op.drop_table('point_rules', schema='data')
|
||||
op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs', schema='data')
|
||||
op.drop_table('level_configs', schema='data')
|
||||
op.drop_table('geo_street_types', schema='data')
|
||||
op.drop_table('geo_postal_codes', schema='data')
|
||||
op.drop_table('exchange_rates', schema='data')
|
||||
op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data')
|
||||
op.drop_table('badges', schema='data')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,222 @@
|
||||
"""fix_identity_scope_and_finalize_asset_costs
|
||||
|
||||
Revision ID: 2cfe9285eb9d
|
||||
Revises: 0adbe75a0b3f
|
||||
Create Date: 2026-02-10 09:47:16.879385
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '2cfe9285eb9d'
|
||||
down_revision: Union[str, Sequence[str], None] = '0adbe75a0b3f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('addresses_postal_code_id_fkey'), 'addresses', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_assignments', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_assignments', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.add_column('asset_costs', sa.Column('amount_local', sa.Numeric(precision=18, scale=2), nullable=False))
|
||||
op.add_column('asset_costs', sa.Column('currency_local', sa.String(length=3), nullable=False))
|
||||
op.add_column('asset_costs', sa.Column('amount_eur', sa.Numeric(precision=18, scale=2), nullable=True))
|
||||
op.add_column('asset_costs', sa.Column('net_amount_local', sa.Numeric(precision=18, scale=2), nullable=True))
|
||||
op.add_column('asset_costs', sa.Column('exchange_rate_used', sa.Numeric(precision=18, scale=6), nullable=True))
|
||||
op.drop_constraint(op.f('asset_costs_asset_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_costs_organization_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_costs_driver_id_fkey'), 'asset_costs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_costs', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_costs', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_costs', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_column('asset_costs', 'currency')
|
||||
op.drop_column('asset_costs', 'amount')
|
||||
op.drop_column('asset_costs', 'exchange_rate_at_cost')
|
||||
op.drop_column('asset_costs', 'net_amount')
|
||||
op.drop_constraint(op.f('asset_events_asset_id_fkey'), 'asset_events', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_events', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_financials_asset_id_fkey'), 'asset_financials', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_financials', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', type_='foreignkey')
|
||||
op.drop_constraint(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_reviews', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'asset_reviews', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'asset_telemetry', 'assets', ['asset_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('assets_catalog_id_fkey'), 'assets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'assets', 'vehicle_catalog', ['catalog_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('audit_logs_user_id_fkey'), 'audit_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'audit_logs', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('credit_logs_org_id_fkey'), 'credit_logs', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'credit_logs', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('documents_uploaded_by_fkey'), 'documents', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'documents', 'users', ['uploaded_by'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('exchange_rates', 'rate',
|
||||
existing_type=sa.NUMERIC(precision=18, scale=6),
|
||||
nullable=False)
|
||||
op.create_unique_constraint(None, 'exchange_rates', ['target_currency'], schema='data')
|
||||
op.drop_constraint(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', type_='foreignkey')
|
||||
op.drop_constraint(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'org_subscriptions', 'organizations', ['org_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('organization_members_organization_id_fkey'), 'organization_members', type_='foreignkey')
|
||||
op.drop_constraint(op.f('organization_members_user_id_fkey'), 'organization_members', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'organization_members', 'organizations', ['organization_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'organization_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('organizations', 'org_type',
|
||||
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
|
||||
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(op.f('organizations_address_id_fkey'), 'organizations', type_='foreignkey')
|
||||
op.drop_constraint(op.f('organizations_owner_id_fkey'), 'organizations', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'organizations', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'organizations', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('persons_address_id_fkey'), 'persons', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'persons', 'addresses', ['address_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('points_ledger_user_id_fkey'), 'points_ledger', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'points_ledger', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('ratings_author_id_fkey'), 'ratings', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'ratings', 'users', ['author_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('service_specialties_parent_id_fkey'), 'service_specialties', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'service_specialties', 'service_specialties', ['parent_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('system_parameters', 'key',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
nullable=False)
|
||||
op.alter_column('system_parameters', 'value',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
nullable=False)
|
||||
op.drop_constraint(op.f('user_badges_badge_id_fkey'), 'user_badges', type_='foreignkey')
|
||||
op.drop_constraint(op.f('user_badges_user_id_fkey'), 'user_badges', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'user_badges', 'badges', ['badge_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'user_badges', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('user_stats_user_id_fkey'), 'user_stats', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'user_stats', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.alter_column('users', 'custom_permissions',
|
||||
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
||||
type_=sa.JSON(),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text("'{}'::jsonb"))
|
||||
op.drop_constraint(op.f('users_person_id_fkey'), 'users', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
|
||||
op.drop_constraint(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'vehicle_ownerships', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.create_foreign_key(None, 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
op.drop_constraint(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'verification_tokens', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data', ondelete='CASCADE')
|
||||
op.drop_constraint(op.f('wallets_user_id_fkey'), 'wallets', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'wallets', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'wallets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('wallets_user_id_fkey'), 'wallets', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'verification_tokens', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('verification_tokens_user_id_fkey'), 'verification_tokens', 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
||||
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'vehicle_ownerships', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('vehicle_ownerships_vehicle_id_fkey'), 'vehicle_ownerships', 'assets', ['vehicle_id'], ['id'])
|
||||
op.create_foreign_key(op.f('vehicle_ownerships_user_id_fkey'), 'vehicle_ownerships', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'users', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('users_person_id_fkey'), 'users', 'persons', ['person_id'], ['id'])
|
||||
op.alter_column('users', 'custom_permissions',
|
||||
existing_type=sa.JSON(),
|
||||
type_=postgresql.JSONB(astext_type=sa.Text()),
|
||||
existing_nullable=True,
|
||||
existing_server_default=sa.text("'{}'::jsonb"))
|
||||
op.drop_constraint(None, 'user_stats', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('user_stats_user_id_fkey'), 'user_stats', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'user_badges', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('user_badges_user_id_fkey'), 'user_badges', 'users', ['user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('user_badges_badge_id_fkey'), 'user_badges', 'badges', ['badge_id'], ['id'])
|
||||
op.alter_column('system_parameters', 'value',
|
||||
existing_type=postgresql.JSON(astext_type=sa.Text()),
|
||||
nullable=True)
|
||||
op.alter_column('system_parameters', 'key',
|
||||
existing_type=sa.VARCHAR(length=50),
|
||||
nullable=True)
|
||||
op.drop_constraint(None, 'service_specialties', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('service_specialties_parent_id_fkey'), 'service_specialties', 'service_specialties', ['parent_id'], ['id'])
|
||||
op.drop_constraint(None, 'ratings', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('ratings_author_id_fkey'), 'ratings', 'users', ['author_id'], ['id'])
|
||||
op.drop_constraint(None, 'points_ledger', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('points_ledger_user_id_fkey'), 'points_ledger', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'persons', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('persons_address_id_fkey'), 'persons', 'addresses', ['address_id'], ['id'])
|
||||
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'organizations', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('organizations_owner_id_fkey'), 'organizations', 'users', ['owner_id'], ['id'])
|
||||
op.create_foreign_key(op.f('organizations_address_id_fkey'), 'organizations', 'addresses', ['address_id'], ['id'])
|
||||
op.alter_column('organizations', 'org_type',
|
||||
existing_type=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype', schema='data', inherit_schema=True),
|
||||
type_=postgresql.ENUM('individual', 'service', 'service_provider', 'fleet_owner', 'club', 'business', name='orgtype'),
|
||||
existing_nullable=True)
|
||||
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'organization_members', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('organization_members_user_id_fkey'), 'organization_members', 'users', ['user_id'], ['id'])
|
||||
op.create_foreign_key(op.f('organization_members_organization_id_fkey'), 'organization_members', 'organizations', ['organization_id'], ['id'])
|
||||
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'org_subscriptions', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('org_subscriptions_tier_id_fkey'), 'org_subscriptions', 'subscription_tiers', ['tier_id'], ['id'])
|
||||
op.create_foreign_key(op.f('org_subscriptions_org_id_fkey'), 'org_subscriptions', 'organizations', ['org_id'], ['id'])
|
||||
op.drop_constraint(None, 'geo_streets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('geo_streets_postal_code_id_fkey'), 'geo_streets', 'geo_postal_codes', ['postal_code_id'], ['id'])
|
||||
op.drop_constraint(None, 'exchange_rates', schema='data', type_='unique')
|
||||
op.alter_column('exchange_rates', 'rate',
|
||||
existing_type=sa.NUMERIC(precision=18, scale=6),
|
||||
nullable=True)
|
||||
op.drop_constraint(None, 'documents', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('documents_uploaded_by_fkey'), 'documents', 'users', ['uploaded_by'], ['id'])
|
||||
op.drop_constraint(None, 'credit_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('credit_logs_org_id_fkey'), 'credit_logs', 'organizations', ['org_id'], ['id'])
|
||||
op.drop_constraint(None, 'audit_logs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('audit_logs_user_id_fkey'), 'audit_logs', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'assets', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('assets_catalog_id_fkey'), 'assets', 'vehicle_catalog', ['catalog_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_telemetry', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_telemetry_asset_id_fkey'), 'asset_telemetry', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_reviews', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_reviews_asset_id_fkey'), 'asset_reviews', 'assets', ['asset_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_reviews_user_id_fkey'), 'asset_reviews', 'users', ['user_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_financials', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_financials_asset_id_fkey'), 'asset_financials', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'asset_events', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_events_asset_id_fkey'), 'asset_events', 'assets', ['asset_id'], ['id'])
|
||||
op.add_column('asset_costs', sa.Column('net_amount', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=True))
|
||||
op.add_column('asset_costs', sa.Column('exchange_rate_at_cost', sa.NUMERIC(precision=18, scale=6), autoincrement=False, nullable=True))
|
||||
op.add_column('asset_costs', sa.Column('amount', sa.NUMERIC(precision=18, scale=2), autoincrement=False, nullable=False))
|
||||
op.add_column('asset_costs', sa.Column('currency', sa.VARCHAR(length=3), autoincrement=False, nullable=True))
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_costs', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_costs_driver_id_fkey'), 'asset_costs', 'users', ['driver_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_costs_organization_id_fkey'), 'asset_costs', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_costs_asset_id_fkey'), 'asset_costs', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_column('asset_costs', 'exchange_rate_used')
|
||||
op.drop_column('asset_costs', 'net_amount_local')
|
||||
op.drop_column('asset_costs', 'amount_eur')
|
||||
op.drop_column('asset_costs', 'currency_local')
|
||||
op.drop_column('asset_costs', 'amount_local')
|
||||
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
|
||||
op.drop_constraint(None, 'asset_assignments', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('asset_assignments_organization_id_fkey'), 'asset_assignments', 'organizations', ['organization_id'], ['id'])
|
||||
op.create_foreign_key(op.f('asset_assignments_asset_id_fkey'), 'asset_assignments', 'assets', ['asset_id'], ['id'])
|
||||
op.drop_constraint(None, 'addresses', schema='data', type_='foreignkey')
|
||||
op.create_foreign_key(op.f('addresses_postal_code_id_fkey'), 'addresses', 'geo_postal_codes', ['postal_code_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,152 +0,0 @@
|
||||
"""Clean gamification setup
|
||||
|
||||
Revision ID: c21c2c7e70d4
|
||||
Revises:
|
||||
Create Date: 2026-01-24 11:19:10.464212
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c21c2c7e70d4'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('target_type', sa.String(), nullable=True),
|
||||
sa.Column('target_id', sa.Integer(), nullable=True),
|
||||
sa.Column('action', sa.String(), nullable=False),
|
||||
sa.Column('changes', sa.JSON(), nullable=True),
|
||||
sa.Column('ip_address', sa.String(), nullable=True),
|
||||
sa.Column('user_agent', sa.String(), nullable=True),
|
||||
sa.Column('timestamp', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_audit_logs_id'), 'audit_logs', ['id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_audit_logs_target_id'), 'audit_logs', ['target_id'], unique=False, schema='data')
|
||||
op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], unique=False, schema='data')
|
||||
op.create_table('companies',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('tax_number', sa.String(), nullable=True),
|
||||
sa.Column('subscription_tier', sa.String(), nullable=True),
|
||||
sa.Column('owner_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data')
|
||||
op.create_table('company_members',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('role', sa.Enum('OWNER', 'MANAGER', 'DRIVER', name='companyrole'), nullable=False),
|
||||
sa.Column('can_edit_service', sa.Boolean(), nullable=True),
|
||||
sa.Column('can_see_costs', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data')
|
||||
op.create_table('vehicle_assignments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_id', sa.Integer(), nullable=False),
|
||||
sa.Column('vehicle_id', sa.Integer(), nullable=False),
|
||||
sa.Column('driver_id', sa.Integer(), nullable=False),
|
||||
sa.Column('start_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('start_odometer', sa.Integer(), nullable=True),
|
||||
sa.Column('end_odometer', sa.Integer(), nullable=True),
|
||||
sa.Column('notes', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], ),
|
||||
sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], ),
|
||||
sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_vehicle_assignments_id'), 'vehicle_assignments', ['id'], unique=False, schema='data')
|
||||
op.create_table('vehicle_ownerships',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('vehicle_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('start_date', sa.Date(), nullable=False),
|
||||
sa.Column('end_date', sa.Date(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ),
|
||||
sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
schema='data'
|
||||
)
|
||||
op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data')
|
||||
# op.drop_table('costs', schema='data')
|
||||
# op.drop_table('alembic_version')
|
||||
# op.drop_table('vehicle_history', schema='data')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('vehicle_history',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=False),
|
||||
sa.Column('start_date', sa.DATE(), autoincrement=False, nullable=False),
|
||||
sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True),
|
||||
sa.Column('start_mileage', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('end_mileage', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('is_active', sa.BOOLEAN(), sa.Computed('(end_date IS NULL)', persisted=True), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('vehicle_history_user_id_fkey')),
|
||||
sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('vehicle_history_vehicle_id_fkey')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('vehicle_history_pkey')),
|
||||
schema='data'
|
||||
)
|
||||
op.create_table('alembic_version',
|
||||
sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('version_num', name=op.f('alembic_version_pkc'))
|
||||
)
|
||||
op.create_table('costs',
|
||||
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
||||
sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('cost_type', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
||||
sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=False),
|
||||
sa.Column('date', sa.DATE(), autoincrement=False, nullable=False),
|
||||
sa.Column('mileage_at_cost', sa.INTEGER(), autoincrement=False, nullable=True),
|
||||
sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True),
|
||||
sa.Column('document_url', sa.VARCHAR(length=255), autoincrement=False, nullable=True),
|
||||
sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('costs_user_id_fkey')),
|
||||
sa.ForeignKeyConstraint(['vehicle_id'], ['data.vehicles.id'], name=op.f('costs_vehicle_id_fkey')),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('costs_pkey')),
|
||||
schema='data'
|
||||
)
|
||||
op.drop_index(op.f('ix_data_vehicle_ownerships_id'), table_name='vehicle_ownerships', schema='data')
|
||||
op.drop_table('vehicle_ownerships', schema='data')
|
||||
op.drop_index(op.f('ix_data_vehicle_assignments_id'), table_name='vehicle_assignments', schema='data')
|
||||
op.drop_table('vehicle_assignments', schema='data')
|
||||
op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data')
|
||||
op.drop_table('company_members', schema='data')
|
||||
op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data')
|
||||
op.drop_table('companies', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_target_type'), table_name='audit_logs', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_target_id'), table_name='audit_logs', schema='data')
|
||||
op.drop_index(op.f('ix_data_audit_logs_id'), table_name='audit_logs', schema='data')
|
||||
op.drop_table('audit_logs', schema='data')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,3 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 1. MIGRÁCIÓ (Adatbázis szerkezet frissítése)
|
||||
migrate:
|
||||
@@ -7,9 +9,7 @@ services:
|
||||
container_name: service_finder_migrate
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./alembic.ini:/app/alembic.ini
|
||||
- ./migrations:/app/migrations
|
||||
- ./backend:/app # Ez tartalmazza az alembic.ini-t és a migrations mappát is!
|
||||
environment:
|
||||
PYTHONPATH: /app
|
||||
DATABASE_URL: ${MIGRATION_DATABASE_URL}
|
||||
@@ -28,10 +28,8 @@ services:
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- ./alembic.ini:/app/alembic.ini
|
||||
- ./migrations:/app/migrations
|
||||
- /mnt/nas/app_data:/mnt/nas/app_data # Központi NAS elérés
|
||||
- ./static_previews:/app/static/previews # Lokális SSD gyorsítótár a miniképeknek
|
||||
- ./static_previews:/app/static/previews # Lokális SSD gyorsítótár
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="*"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
@@ -72,7 +70,7 @@ services:
|
||||
- default
|
||||
restart: unless-stopped
|
||||
|
||||
# 4. REDIS (Lokális cache, NAS perzisztencia)
|
||||
# 4. REDIS (Lokális cache)
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: service_finder_redis
|
||||
|
||||
@@ -88,3 +88,17 @@ Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap,
|
||||
- **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.
|
||||
|
||||
## 2026.02.10 FRISSÍTÉS - GAMIFICATION ÖKOSZISZTÉMA
|
||||
|
||||
### 1. Pontrendszer Logika
|
||||
A rendszer különválasztja a tekintélyt és a jutalmat:
|
||||
- **XP (Tapasztalat):** Végleges szintlépéshez. Képlet: $BaseXP \times Level^{1.5}$. (Nehezedő görbe).
|
||||
- **Social Points (Szezonális):** Időszakos versenyekhez (pl. Hónap Vadásza).
|
||||
- **Kredit:** Fizetőeszköz, amit Social Pontokból lehet váltani (pl. 1000 pont = 100 Kredit).
|
||||
|
||||
### 2. Konfiguráció
|
||||
Minden érték (szorzók, határok) a \`GAMIFICATION_MASTER_CONFIG\` JSON paraméterben állítható Admin felületről, kódmódosítás nélkül.
|
||||
|
||||
### 3. Audit
|
||||
Minden pontmozgás a \`PointsLedger\` táblába kerül rögzítésre a visszakövethetőség érdekében.
|
||||
@@ -210,3 +210,43 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
|
||||
|
||||
### 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.
|
||||
|
||||
## [Unreleased] - 2026-02-10
|
||||
|
||||
### 🚀 Added (Új funkciók)
|
||||
- **RBAC System:**
|
||||
- \`User\` tábla bővítése: \`scope_level\`, \`scope_id\`, \`custom_permissions\`.
|
||||
- \`system_parameters\` tábla létrehozása a globális JSON konfigurációkhoz.
|
||||
- Master RBAC JSON konfiguráció seedelése.
|
||||
- **Gamification:**
|
||||
- \`GamificationService\` létrehozása (XP, Level, Credit logika).
|
||||
- Automata pontszámítás és nehezedő szintek logikája.
|
||||
- **API Modularitás:**
|
||||
- \`assets.py\` szétbontása 3 végpontra (Identity, Costs, Telemetry).
|
||||
|
||||
### 🛠 Changed (Módosítások)
|
||||
- **Asset Model:** Az \`Asset\` entitás mostantól lazy loading helyett \`selectinload\` stratégiát használ a teljesítmény érdekében.
|
||||
- **Error Handling:** Javítottuk az \`asyncpg\` többszörös parancs-futtatási hibáját a setup scriptekben.
|
||||
- **Configuration:** A rendszerbeállítások mostantól adatbázis-alapúak (JSONB) a hardcoded konstansok helyett.
|
||||
|
||||
### 🐛 Fixed (Javítások)
|
||||
- **Schema Mismatch:** SQLAlchemy modellek és Pydantic sémák szétválasztása (`models/asset.py` vs `schemas/asset.py`).
|
||||
- **Data Integrity:** \`updated_at\` és \`is_active\` oszlopok pótlása a \`system_parameters\` táblában.
|
||||
- **API Stability:** \`getattr\` használata a hiányzó opcionális mezőknél (pl. \`net_amount\`), hogy ne szálljon el az API 500-as hibával.
|
||||
|
||||
## [1.2.0] - 2026-02-10
|
||||
|
||||
### Added
|
||||
- **Asset Financials 2.0**: Pivot-Currency modell implementálva (helyi deviza + EUR párhuzamos tárolás).
|
||||
- **Smart Auth Token**: JWT token mostantól tartalmazza a `rank`, `scope_level` és `scope_id` mezőket a gyors jogosultságkezeléshez.
|
||||
- **CostService**: Automatikus árfolyam-kalkuláció, telemetria-szinkron és XP jóváírás költségrögzítéskor.
|
||||
- **ExchangeRate**: Új árfolyamtábla és modell az EUR alapú váltásokhoz.
|
||||
|
||||
### Fixed
|
||||
- **Circular Import Resolution**: Megszüntetve a `db.base` és a `models` közötti körkörös függőség az import lánc modularizálásával.
|
||||
- **Alembic Identity Sync**: Visszaállítva a `User` modell hiányzó `scope_level` és `custom_permissions` mezői, megakadályozva az adatvesztést migrációkor.
|
||||
- **NotNullViolationError**: Fixálva az `asset_costs` tábla migrációja (amount_local NOT NULL kényszer).
|
||||
|
||||
### Changed
|
||||
- `AssetCost` modell mezőnevek szinkronizálva a pénzügyi standardokhoz (`amount_local`, `amount_eur`).
|
||||
- `SystemParameter` modell elnevezés igazítva a meglévő adatbázis sémához.
|
||||
@@ -187,3 +187,18 @@ A járműadatok kezelése hibrid módon történik.
|
||||
- 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).
|
||||
|
||||
## 2026.02.10 FRISSÍTÉS - ATOMIZÁLT ADATMODELL ÉS MODULÁRIS API
|
||||
|
||||
### 1. Adatbázis Szerkezet (A 4 Pillér)
|
||||
A járművek kezelése "Single Responsibility" elv alapján 4 modulra bomlott:
|
||||
1. **Identity (Asset):** Alapadatok (VIN, Rendszám, Tulajdonos).
|
||||
2. **Catalog (AssetCatalog):** Gyári statikus adatok (Típus, Motor, Akku). Ezt a Robotok töltik.
|
||||
3. **Telemetry (AssetTelemetry):** Változó állapot (KM óra, VQI minőség index, DBS vezetési stílus).
|
||||
4. **Financials (AssetCost):** Pénzügyi tranzakciók 9 kategóriába sorolva (Fuel, Service, Tax, stb.).
|
||||
|
||||
### 2. Moduláris API Végpontok
|
||||
A teljesítmény optimalizálása érdekében a \`Full Profile\` helyett 3 dedikált végpontot használunk:
|
||||
- \`GET /api/v1/assets/{id}\`: Csak identitás és katalógus (Gyors nézet).
|
||||
- \`GET /api/v1/assets/{id}/costs\`: Csak pénzügyi történet és grafikonok.
|
||||
- \`GET /api/v1/assets/{id}/telemetry\`: Csak élő adatok (Dashboard).
|
||||
@@ -87,3 +87,19 @@ A rendszer minden fontos paramétere a `data.system_settings` táblában tárolt
|
||||
* **Service Hunt:** Távolságok, XP/Kredit szorzók.
|
||||
* **Fraud Protection:** Strike limitek, kitiltási idők.
|
||||
* **Billing:** EUR/HUF/USD váltószámok, csomagárak, jármű-slot árak.
|
||||
|
||||
## 2026.02.10 FRISSÍTÉS - HIERARCHIKUS RBAC RENDSZER
|
||||
|
||||
### 1. Rang-alapú Jogosultság (Rank System)
|
||||
A rendszer a \`system_parameters\` táblában tárolt \`RBAC_MASTER_CONFIG\` JSON alapján működik.
|
||||
- **SUPERADMIN (Rank 100):** Globális hatókör, mindent lát.
|
||||
- **COUNTRY_ADMIN (Rank 80):** Országos felelős.
|
||||
- **REGION_ADMIN (Rank 60):** Területi vezető (Manage Moderators).
|
||||
- **MODERATOR (Rank 40):** Adatvalidátor.
|
||||
- **SALES (Rank 20):** Üzletkötő (Csak saját partnerek).
|
||||
- **USER (Rank 10):** Végfelhasználó.
|
||||
|
||||
### 2. Scope (Hatókör) Védelem
|
||||
Minden műveletnél ellenőrizzük a \`scope_id\` egyezését:
|
||||
- Ha a felhasználó \`scope_level = 'region'\`, akkor csak olyan adatot szerkeszthet, ami ugyanahhoz a régióhoz tartozik.
|
||||
- Kivétel: Impersonation (Megszemélyesítés) - Audit loggal védve.
|
||||
@@ -1,3 +1,135 @@
|
||||
🧬 SERVICE FINDER - UNIVERSAL SYSTEM PROMPT (v1.2)
|
||||
|
||||
ROLE: Senior Technical Product Manager & Lead System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp SOURCE OF TRUTH: Grand Master Book (00–19) + 2026.02.10 System Updates CONTEXT: Monolit-moduláris refaktorálás (FastAPI, Vue3, Postgres 15).
|
||||
1. VÍZIÓ ÉS KONTEXTUS (00, 01)
|
||||
|
||||
Nem egy egyszerű nyilvántartó rendszert építünk, hanem egy Digital Twin (Digitális Iker) alapú ökoszisztémát.
|
||||
|
||||
Core Philosophy: "A jármű örök, a tulajdonos vándor." (Vehicle-Centric Architecture).
|
||||
|
||||
Pillére:
|
||||
|
||||
Core Fleet: Életút és TCO követés.
|
||||
|
||||
Marketplace: Szervizkereső és időpontfoglalás.
|
||||
|
||||
Trust Engine: Bizonyíték alapú előélet (OCR, Fotó).
|
||||
|
||||
Economy: Kredit és Gamification.
|
||||
|
||||
2. TECHNOLÓGIAI STACK ÉS INFRA (02, 03, 04, 08)
|
||||
|
||||
Frontend: Vue 3 (Composition API) + Vite + Tailwind CSS + Pinia. Dumb Frontend elv.
|
||||
|
||||
Backend: Python 3.12 + FastAPI. Szigorú Pydantic validáció.
|
||||
|
||||
Adatbázis: PostgreSQL 15. Két séma: data (üzleti), public (rendszer).
|
||||
|
||||
Storage: MinIO (S3 kompatibilis) titkosított dokumentumokhoz.
|
||||
|
||||
Hálózat: Internal Net (shared_db_net) zárt. Public Net: csak 80/443 (NPM Proxy).
|
||||
|
||||
Config: Minden konfiguráció .env fájlból vagy data.system_parameters táblából jön. Hardkódolás TILOS.
|
||||
|
||||
3. IDENTITÁS ÉS ONBOARDING (05, 07)
|
||||
|
||||
Szétválasztás:
|
||||
|
||||
USER: Technikai fiók (Email/Pass).
|
||||
|
||||
PERSON: Valós jogi személy (Okmányok, KYC). Nem törölhető.
|
||||
|
||||
Folyamat: Kétlépcsős (2-Step) Onboarding.
|
||||
|
||||
Lite: Csak User létrehozása (is_active=False).
|
||||
|
||||
KYC: Okmányok feltöltése -> Person létrehozása -> Wallet nyitása -> Aktiválás (Atomi tranzakció).
|
||||
|
||||
4. ATOMIZÁLT ASSET MODELL (18) [FRISSÍTVE 2026.02.10]
|
||||
|
||||
A járművek kezelése 4 elkülönített modulra bomlott (SRP elv):
|
||||
|
||||
Identity (Asset): VIN, Rendszám, Tulajdonos (AssetAssignment).
|
||||
|
||||
Catalog (AssetCatalog): Gyári statikus adatok. Robot Scout tölti.
|
||||
|
||||
Telemetry (AssetTelemetry): Változó állapot (KM, VQI, DBS).
|
||||
|
||||
Financials (AssetCost): Pénzügyi tranzakciók 9 kategóriában.
|
||||
|
||||
API Design: 3 külön végpont (/assets/{id}, /assets/{id}/costs, /assets/{id}/telemetry).
|
||||
|
||||
5. HIERARCHIKUS JOGOSULTSÁG (RBAC & SCOPE) (09, 19) [FRISSÍTVE 2026.02.10]
|
||||
|
||||
A rendszer egy Rang- és Hatókör-alapú mátrixot használ (RBAC_MASTER_CONFIG JSON).
|
||||
|
||||
Szintek (Rank):
|
||||
|
||||
SUPERADMIN (100): Globális (L0).
|
||||
|
||||
COUNTRY_ADMIN (80): Országos (L1).
|
||||
|
||||
REGION_ADMIN (60): Területi (L1/B).
|
||||
|
||||
MODERATOR (40): Adatvalidátor (L2).
|
||||
|
||||
SALES (20): Üzletkötő (L3).
|
||||
|
||||
USER (10): Végfelhasználó.
|
||||
|
||||
Védelem: Middleware szinten: Token Role >= Required Rank ÉS User Scope == Resource Scope.
|
||||
|
||||
Adattábla: User tábla új mezői: scope_level, scope_id, custom_permissions.
|
||||
|
||||
6. GAMIFICATION ÉS ECONOMY (10, 11) [FRISSÍTVE 2026.02.10]
|
||||
|
||||
XP (Tapasztalat): Végleges szintlépés (BaseXP×Level1.5). Nem csökken.
|
||||
|
||||
Social Points: Szezonális, resetelhető pontok.
|
||||
|
||||
Kredit: Valuta, Social pontokból váltható.
|
||||
|
||||
Service: GamificationService és PointsLedger (auditált naplózás).
|
||||
|
||||
Billing: Többvalutás rendszer (HUF/EUR tárolás).
|
||||
|
||||
7. ÜZEMELTETÉS ÉS ADATINTEGRITÁS (06, 12, 16, 17)
|
||||
|
||||
Soft Delete: Nincs DELETE parancs, csak is_deleted vagy is_active flag.
|
||||
|
||||
Audit: Kritikus műveletek (pl. Impersonation) előtt/után állapotmentés az audit_logs táblába.
|
||||
|
||||
Enum: Postgres Enum típusok mindig kisbetűsek (pl. role='user').
|
||||
|
||||
Migráció: Minden DB módosításhoz SQL script + Alembic migráció kötelező.
|
||||
|
||||
🚀 KÖVETKEZŐ LÉPÉSEK (ACTION PLAN - 2026.02.11)
|
||||
|
||||
A rendszer magja (Asset, RBAC, Gamification) stabil. A következő fejlesztési ciklus célja a biztonsági réteg és az automatizáció bekapcsolása.
|
||||
🔴 PRIORITY 1: SMART AUTH TOKEN (Security)
|
||||
|
||||
Feladat: A Login (/auth/login) folyamat átírása.
|
||||
|
||||
Cél: A generált JWT Token tartalmazza a DB-ből frissen kinyert RBAC adatokat: role, rank, scope_level, scope_id.
|
||||
|
||||
Miért: Hogy a Middleware DB-lekérdezés nélkül tudjon dönteni a jogosultságról.
|
||||
|
||||
File: backend/app/core/security.py, backend/app/api/v1/endpoints/auth.py.
|
||||
|
||||
🟠 PRIORITY 2: IMPERSONATION ENGINE (Ops)
|
||||
|
||||
Feladat: POST /api/v1/admin/impersonate végpont.
|
||||
|
||||
Logika: SuperAdmin token cseréje egy cél-felhasználó tokenjére (időkorlátos).
|
||||
|
||||
Biztonság: Szigorú naplózás az audit_logs táblába (reason kötelező).
|
||||
|
||||
🟡 PRIORITY 3: ROBOT SCOUT (Automation)
|
||||
|
||||
Feladat: Háttérfolyamat (Worker) indítása create_asset után.
|
||||
|
||||
Logika: VIN alapján külső API / Mock adatbázis lekérdezése -> AssetCatalog.factory_data feltöltése.
|
||||
|
||||
# 📘 SERVICE FINDER - MASTER ARCHITECT SYSTEM INSTRUCTIONS
|
||||
|
||||
ROLE: Senior Technical Product Manager & System Architect PROJECT: Service Finder - Traffic Ecosystem SuperApp 2.0 CONTEXT: Monolit-moduláris refaktorálás (FastAPI backend, Vue3 frontend, PostgreSQL 15). SSoT: Grand Master Book (v1.4).
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Generic single-database configuration.
|
||||
Binary file not shown.
@@ -1,75 +0,0 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
from alembic import context
|
||||
|
||||
# --- ÚTVONAL JAVÍTÁS ---
|
||||
# Az aktuális fájl (env.py) helyéből kiindulva meghatározzuk a könyvtárakat
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
project_root = os.path.realpath(os.path.join(current_dir, '..'))
|
||||
backend_dir = os.path.join(project_root, 'backend')
|
||||
|
||||
# Mindkét útvonalat betesszük a keresőbe, hogy a 'backend.app' és a sima 'app' is működjön
|
||||
sys.path.insert(0, project_root)
|
||||
sys.path.insert(0, backend_dir)
|
||||
|
||||
# Most már az Alembic megtalálja a konfigurációt és a modelleket
|
||||
try:
|
||||
from app.core.config import settings
|
||||
from app.db.base import Base
|
||||
from app.models import * # Fontos, hogy minden modell be legyen importálva!
|
||||
except ImportError as e:
|
||||
print(f"Hiba az importálásnál: {e}")
|
||||
print(f"Próbált útvonalak: {sys.path}")
|
||||
raise
|
||||
|
||||
config = context.config
|
||||
|
||||
# Dinamikus adatbázis URL a .env alapján (App User jelszavával)
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
def do_run_migrations(connection):
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
include_schemas=True, # Adatbázis sémák (pl. 'data') támogatása
|
||||
version_table_schema='public' # Alembic tábla a public-ban marad
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
"""Aszinkron kapcsolat felépítése és migráció futtatása"""
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
if context.is_offline_mode():
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
include_schemas=True
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user