diff --git a/alembic.ini b/alembic.ini deleted file mode 100755 index f07ee97..0000000 --- a/alembic.ini +++ /dev/null @@ -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 /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 diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..1caad0d --- /dev/null +++ b/backend/.env @@ -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 \ No newline at end of file diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc index 4eb33d6..b571516 100644 Binary files a/backend/app/api/__pycache__/deps.cpython-312.pyc and b/backend/app/api/__pycache__/deps.cpython-312.pyc differ diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 36ced24..2550451 100755 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -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." ) - - user_id: str = payload.get("sub") + return payload + +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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc index b1a5b8d..0f8d077 100644 Binary files a/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc and b/backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc differ diff --git a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc index 2324bfa..0338035 100644 Binary files a/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc and b/backend/app/api/v1/endpoints/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc deleted file mode 100755 index 8985571..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc deleted file mode 100755 index 89a9077..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/expenses.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc deleted file mode 100755 index 77c4845..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/fleet.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc deleted file mode 100755 index 99b5547..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/reports.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc deleted file mode 100755 index 7e8c21a..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/users.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc b/backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc deleted file mode 100755 index 9804fe5..0000000 Binary files a/backend/app/api/v1/endpoints/__pycache__/vehicles.cpython-312.pyc and /dev/null differ diff --git a/backend/app/api/v1/endpoints/assets.py b/backend/app/api/v1/endpoints/assets.py index 235433f..3a98065 100644 --- a/backend/app/api/v1/endpoints/assets.py +++ b/backend/app/api/v1/endpoints/assets.py @@ -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!") - - # 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 - - # 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() + """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() - 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() + if not asset: + raise HTTPException(status_code=404, detail="Jármű nem található") + + 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', {}) + } + } - # 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() - - 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() - - # 5. Assignment - new_assignment = AssetAssignment( - asset_id=new_asset.id, - organization_id=final_org_id, - status="active" - ) - db.add(new_assignment) - - # 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"} - )) +# --- 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() + + summary_local = {} + summary_eur = {} + history = [] + + 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 + + summary_local[cat] = summary_local.get(cat, 0) + amt_local + summary_eur[cat] = summary_eur.get(cat, 0) + amt_eur + + 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.") \ No newline at end of file + 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 + } \ No newline at end of file diff --git a/backend/app/api/v1/endpoints/auth.py b/backend/app/api/v1/endpoints/auth.py index 279322b..9109092 100644 --- a/backend/app/api/v1/endpoints/auth.py +++ b/backend/app/api/v1/endpoints/auth.py @@ -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."} \ No newline at end of file + raise HTTPException(status_code=400, detail="Hiba a jelszó frissítésekor.") + return {"message": "A jelszó sikeresen frissítve!"} \ No newline at end of file diff --git a/backend/app/core/__pycache__/__init__.cpython-312.pyc b/backend/app/core/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 2754663..277aaa3 100644 Binary files a/backend/app/core/__pycache__/config.cpython-312.pyc and b/backend/app/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/i18n.cpython-312.pyc b/backend/app/core/__pycache__/i18n.cpython-312.pyc index 18b715d..a615e0d 100644 Binary files a/backend/app/core/__pycache__/i18n.cpython-312.pyc and b/backend/app/core/__pycache__/i18n.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc index 027241a..45a8a51 100644 Binary files a/backend/app/core/__pycache__/security.cpython-312.pyc and b/backend/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/backend/app/core/__pycache__/validators.cpython-312.pyc b/backend/app/core/__pycache__/validators.cpython-312.pyc deleted file mode 100644 index 57a7936..0000000 Binary files a/backend/app/core/__pycache__/validators.cpython-312.pyc and /dev/null differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d08760b..cb6770c 100755 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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() \ No newline at end of file diff --git a/backend/app/core/i18n.py b/backend/app/core/i18n.py index b1fc0ef..3fcfe2d 100644 --- a/backend/app/core/i18n.py +++ b/backend/app/core/i18n.py @@ -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() \ No newline at end of file +locale_manager = LocaleManager() +# Rövid alias a könnyebb használathoz +t = locale_manager.get \ No newline at end of file diff --git a/backend/app/core/rbac.py b/backend/app/core/rbac.py new file mode 100644 index 0000000..3400e54 --- /dev/null +++ b/backend/app/core/rbac.py @@ -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 \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 92a3208..755cd18 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -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 diff --git a/backend/app/db/__pycache__/__init__.cpython-312.pyc b/backend/app/db/__pycache__/__init__.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/db/__pycache__/base.cpython-312.pyc b/backend/app/db/__pycache__/base.cpython-312.pyc index dcf3c65..ac879e9 100644 Binary files a/backend/app/db/__pycache__/base.cpython-312.pyc and b/backend/app/db/__pycache__/base.cpython-312.pyc differ diff --git a/backend/app/db/__pycache__/session.cpython-312.pyc b/backend/app/db/__pycache__/session.cpython-312.pyc old mode 100755 new mode 100644 diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 306d75c..219f02c 100755 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -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 \ No newline at end of file +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 +) \ No newline at end of file diff --git a/backend/app/diagnose_system.py b/backend/app/diagnose_system.py new file mode 100644 index 0000000..6a7bef6 --- /dev/null +++ b/backend/app/diagnose_system.py @@ -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()) \ No newline at end of file diff --git a/backend/app/locales/hu.json b/backend/app/locales/hu.json index b1d1eee..c0fc27f 100644 --- a/backend/app/locales/hu.json +++ b/backend/app/locales/hu.json @@ -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" } } \ No newline at end of file diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 63d30ad..3b19ade 100755 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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" ] \ No newline at end of file diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc index 42da96a..b41676a 100644 Binary files a/backend/app/models/__pycache__/__init__.cpython-312.pyc and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/asset.cpython-312.pyc b/backend/app/models/__pycache__/asset.cpython-312.pyc index de3e6fb..e7435c1 100644 Binary files a/backend/app/models/__pycache__/asset.cpython-312.pyc and b/backend/app/models/__pycache__/asset.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/company.cpython-312.pyc b/backend/app/models/__pycache__/company.cpython-312.pyc deleted file mode 100755 index 3bdb19b..0000000 Binary files a/backend/app/models/__pycache__/company.cpython-312.pyc and /dev/null differ diff --git a/backend/app/models/__pycache__/core_logic.cpython-312.pyc b/backend/app/models/__pycache__/core_logic.cpython-312.pyc new file mode 100644 index 0000000..f892bc3 Binary files /dev/null and b/backend/app/models/__pycache__/core_logic.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/document.cpython-312.pyc b/backend/app/models/__pycache__/document.cpython-312.pyc index ca8f2c8..1b73f8c 100644 Binary files a/backend/app/models/__pycache__/document.cpython-312.pyc and b/backend/app/models/__pycache__/document.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/history.cpython-312.pyc b/backend/app/models/__pycache__/history.cpython-312.pyc new file mode 100644 index 0000000..db83aa0 Binary files /dev/null and b/backend/app/models/__pycache__/history.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/identity.cpython-312.pyc b/backend/app/models/__pycache__/identity.cpython-312.pyc index 14d73ba..c791879 100644 Binary files a/backend/app/models/__pycache__/identity.cpython-312.pyc and b/backend/app/models/__pycache__/identity.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/organization.cpython-312.pyc b/backend/app/models/__pycache__/organization.cpython-312.pyc index 1b52a98..e92395d 100644 Binary files a/backend/app/models/__pycache__/organization.cpython-312.pyc and b/backend/app/models/__pycache__/organization.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/system_config.cpython-312.pyc b/backend/app/models/__pycache__/system_config.cpython-312.pyc new file mode 100644 index 0000000..b868ff4 Binary files /dev/null and b/backend/app/models/__pycache__/system_config.cpython-312.pyc differ diff --git a/backend/app/models/__pycache__/vehicle.cpython-312.pyc b/backend/app/models/__pycache__/vehicle.cpython-312.pyc deleted file mode 100644 index 775573c..0000000 Binary files a/backend/app/models/__pycache__/vehicle.cpython-312.pyc and /dev/null differ diff --git a/backend/app/models/asset.py b/backend/app/models/asset.py index 7d7d83c..783dd76 100644 --- a/backend/app/models/asset.py +++ b/backend/app/models/asset.py @@ -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()) - + event_type = Column(String(50), nullable=False) 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()) \ No newline at end of file + rate_date = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/backend/app/models/company.py b/backend/app/models/company.py deleted file mode 100755 index 9bdb0ee..0000000 --- a/backend/app/models/company.py +++ /dev/null @@ -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]) \ No newline at end of file diff --git a/backend/app/models/core_logic.py b/backend/app/models/core_logic.py index 9c80bc2..30291b4 100755 --- a/backend/app/models/core_logic.py +++ b/backend/app/models/core_logic.py @@ -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" @@ -39,4 +40,4 @@ class ServiceSpecialty(Base): name = Column(String, nullable=False) slug = Column(String, unique=True) - parent = relationship("ServiceSpecialty", remote_side=[id], backref="children") + parent = relationship("ServiceSpecialty", remote_side=[id], backref="children") \ No newline at end of file diff --git a/backend/app/models/document.py b/backend/app/models/document.py index ce7942e..4f9731a 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -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" diff --git a/backend/app/models/email_log.py b/backend/app/models/email_log.py deleted file mode 100755 index 75f50bd..0000000 --- a/backend/app/models/email_log.py +++ /dev/null @@ -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()) diff --git a/backend/app/models/email_provider.py b/backend/app/models/email_provider.py deleted file mode 100755 index 42f9a09..0000000 --- a/backend/app/models/email_provider.py +++ /dev/null @@ -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 diff --git a/backend/app/models/email_system.py b/backend/app/models/email_system.py deleted file mode 100755 index 9a0b13a..0000000 --- a/backend/app/models/email_system.py +++ /dev/null @@ -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) diff --git a/backend/app/models/email_template.py b/backend/app/models/email_template.py deleted file mode 100755 index 67ed983..0000000 --- a/backend/app/models/email_template.py +++ /dev/null @@ -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 diff --git a/backend/app/models/expense.py b/backend/app/models/expense.py deleted file mode 100755 index 86168c8..0000000 --- a/backend/app/models/expense.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/backend/app/models/history.py b/backend/app/models/history.py index 1df733e..8d4d21d 100755 --- a/backend/app/models/history.py +++ b/backend/app/models/history.py @@ -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") \ No newline at end of file diff --git a/backend/app/models/identity.py b/backend/app/models/identity.py index 33d5f61..9d53ffc 100644 --- a/backend/app/models/identity.py +++ b/backend/app/models/identity.py @@ -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) - - # 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") + # 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")) + 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) diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index c931de6..6279661 100755 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -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)) - # --- ATOMIZÁLT CÍMKEZELÉS (Kompatibilitási réteg) --- + default_currency = Column(String(3), default="HUF") + country_code = Column(String(2), default="HU") + language = Column(String(5), default="hu") + 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 \ No newline at end of file + user = relationship("User") # Egyszerűsített string hivatkozás \ No newline at end of file diff --git a/backend/app/models/system.py b/backend/app/models/system.py new file mode 100644 index 0000000..d202a64 --- /dev/null +++ b/backend/app/models/system.py @@ -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()) \ No newline at end of file diff --git a/backend/app/models/system_config.py b/backend/app/models/system_config.py new file mode 100644 index 0000000..9a1be7c --- /dev/null +++ b/backend/app/models/system_config.py @@ -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()) \ No newline at end of file diff --git a/backend/app/models/vehicle_catalog.py b/backend/app/models/vehicle_catalog.py deleted file mode 100755 index 587e9a1..0000000 --- a/backend/app/models/vehicle_catalog.py +++ /dev/null @@ -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' \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/asset.cpython-312.pyc b/backend/app/schemas/__pycache__/asset.cpython-312.pyc deleted file mode 100644 index a65dd0f..0000000 Binary files a/backend/app/schemas/__pycache__/asset.cpython-312.pyc and /dev/null differ diff --git a/backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc b/backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc new file mode 100644 index 0000000..7bef383 Binary files /dev/null and b/backend/app/schemas/__pycache__/asset_cost.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc index 99171c4..ed6517b 100644 Binary files a/backend/app/schemas/__pycache__/auth.cpython-312.pyc and b/backend/app/schemas/__pycache__/auth.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/organization.cpython-312.pyc b/backend/app/schemas/__pycache__/organization.cpython-312.pyc index e564d1e..f057bd1 100644 Binary files a/backend/app/schemas/__pycache__/organization.cpython-312.pyc and b/backend/app/schemas/__pycache__/organization.cpython-312.pyc differ diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc deleted file mode 100755 index b1706c6..0000000 Binary files a/backend/app/schemas/__pycache__/user.cpython-312.pyc and /dev/null differ diff --git a/backend/app/schemas/asset.py b/backend/app/schemas/asset.py index fd661d7..c5ab1e5 100644 --- a/backend/app/schemas/asset.py +++ b/backend/app/schemas/asset.py @@ -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") - - # Nemzetközi és Admin szempontok - vehicle_class: str = Field("land", description="land, sea, air - Admin által bővíthető") - fuel_type: str = Field(..., example="Diesel", description="Admin által definiált üzemanyag típusok") - - # Technikai adatok - engine_description: Optional[str] = Field(None, example="2.0 TDCI") - year_of_manufacture: int = Field(..., ge=1900, le=2100) - - # Kezdő állapot - current_reading: int = Field(..., ge=0, description="Kezdő km/üzemóra állás") - reading_unit: str = Field("km", description="km, miles, hours - Nemzetközi beállítás") - - # Felhasználói adatok - name: Optional[str] = Field(None, description="Egyedi elnevezés") +# --- 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 -class AssetResponse(BaseModel): - id: UUID - catalog_id: Optional[int] - vin: str - license_plate: Optional[str] = None # JAVÍTVA: Lehet None a válaszban +class AssetCatalogResponse(AssetCatalogBase): + id: int + factory_data: Optional[Dict[str, Any]] = None # A robot által gyűjtött adatok + + model_config = ConfigDict(from_attributes=True) + +# --- 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 \ No newline at end of file +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) \ No newline at end of file diff --git a/backend/app/schemas/asset_cost.py b/backend/app/schemas/asset_cost.py new file mode 100644 index 0000000..815aba9 --- /dev/null +++ b/backend/app/schemas/asset_cost.py @@ -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 \ No newline at end of file diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 37689f0..ac72049 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -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): @@ -53,4 +55,13 @@ class PasswordResetConfirm(BaseModel): class Token(BaseModel): access_token: str token_type: str - is_active: bool \ No newline at end of file + 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 \ No newline at end of file diff --git a/backend/app/schemas/auth_old.py b/backend/app/schemas/auth_old.py deleted file mode 100755 index 9f541c4..0000000 --- a/backend/app/schemas/auth_old.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/backend/app/schemas/organization.py b/backend/app/schemas/organization.py index a32ab77..8ded139 100644 --- a/backend/app/schemas/organization.py +++ b/backend/app/schemas/organization.py @@ -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 diff --git a/backend/app/scripts/seed_system_params.py b/backend/app/scripts/seed_system_params.py new file mode 100644 index 0000000..58d7e8e --- /dev/null +++ b/backend/app/scripts/seed_system_params.py @@ -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()) \ No newline at end of file diff --git a/backend/app/seed_system.py b/backend/app/seed_system.py index 75a3b05..6ff8e3d 100755 --- a/backend/app/seed_system.py +++ b/backend/app/seed_system.py @@ -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") ] - - # 2. Email Sablon (Regisztráció) - reg_template = EmailTemplate( - type=EmailType.REGISTRATION, - subject="Üdvözöljük a Service Finderben!", - body_html=""" -

Kedves {{ name }}!

-

Köszönjük a regisztrációt! Az aktiváláshoz kattints ide:

- Fiók aktiválása -

A link 24 óráig érvényes.

- """ - ) + 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)) - # 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 - ) + # --- 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") + ]) + + # --- 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()) + asyncio.run(seed_data()) \ No newline at end of file diff --git a/backend/app/seed_test_scenario.py b/backend/app/seed_test_scenario.py new file mode 100644 index 0000000..ab6db8e --- /dev/null +++ b/backend/app/seed_test_scenario.py @@ -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()) \ No newline at end of file diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc index 8de3f8a..091f5b5 100644 Binary files a/backend/app/services/__pycache__/auth_service.cpython-312.pyc and b/backend/app/services/__pycache__/auth_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/cost_service.cpython-312.pyc b/backend/app/services/__pycache__/cost_service.cpython-312.pyc new file mode 100644 index 0000000..cc10c9d Binary files /dev/null and b/backend/app/services/__pycache__/cost_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/email_manager.cpython-312.pyc b/backend/app/services/__pycache__/email_manager.cpython-312.pyc index 05517c9..2345992 100644 Binary files a/backend/app/services/__pycache__/email_manager.cpython-312.pyc and b/backend/app/services/__pycache__/email_manager.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc index 4f49e9b..04264e8 100644 Binary files a/backend/app/services/__pycache__/gamification_service.cpython-312.pyc and b/backend/app/services/__pycache__/gamification_service.cpython-312.pyc differ diff --git a/backend/app/services/__pycache__/harvester_robot.cpython-312.pyc b/backend/app/services/__pycache__/harvester_robot.cpython-312.pyc deleted file mode 100644 index df0ec4b..0000000 Binary files a/backend/app/services/__pycache__/harvester_robot.cpython-312.pyc and /dev/null differ diff --git a/backend/app/services/__pycache__/media_service.cpython-312.pyc b/backend/app/services/__pycache__/media_service.cpython-312.pyc deleted file mode 100644 index b4d73ea..0000000 Binary files a/backend/app/services/__pycache__/media_service.cpython-312.pyc and /dev/null differ diff --git a/backend/app/services/asset_service.py b/backend/app/services/asset_service.py new file mode 100644 index 0000000..ac1b2af --- /dev/null +++ b/backend/app/services/asset_service.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 0f5a127..78717fc 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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" diff --git a/backend/app/services/cost_service.py b/backend/app/services/cost_service.py new file mode 100644 index 0000000..7bc1d64 --- /dev/null +++ b/backend/app/services/cost_service.py @@ -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() \ No newline at end of file diff --git a/backend/app/services/email_manager.py b/backend/app/services/email_manager.py index 8a73ed9..a698414 100755 --- a/backend/app/services/email_manager.py +++ b/backend/app/services/email_manager.py @@ -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""" @@ -30,8 +33,8 @@ class EmailManager:

- Ha a gomb nem működik, másolja be ezt a linket a böngészőjébe:
- {variables.get('link')} + {link_fallback_text}
+ {variables.get('link')}


{footer}

@@ -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}>" diff --git a/backend/app/services/gamification_service.py b/backend/app/services/gamification_service.py index 87c8caa..1789d59 100755 --- a/backend/app/services/gamification_service.py +++ b/backend/app/services/gamification_service.py @@ -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) + + # 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 - stats.total_points += points - await db.flush() - return stats.total_points \ No newline at end of file + # 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 \ No newline at end of file diff --git a/backend/app/services/harvester_base.py b/backend/app/services/harvester_base.py index ebed6a8..ff23818 100644 --- a/backend/app/services/harvester_base.py +++ b/backend/app/services/harvester_base.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/recon_bot.py b/backend/app/services/recon_bot.py new file mode 100644 index 0000000..1af6d7b --- /dev/null +++ b/backend/app/services/recon_bot.py @@ -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 \ No newline at end of file diff --git a/backend/app/services/robot_manager.py b/backend/app/services/robot_manager.py index d2dbe7d..92875e1 100644 --- a/backend/app/services/robot_manager.py +++ b/backend/app/services/robot_manager.py @@ -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 \ No newline at end of file + await asyncio.sleep(70) + await asyncio.sleep(30) \ No newline at end of file diff --git a/backend/migrations/__pycache__/env.cpython-312.pyc b/backend/migrations/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000..952453c Binary files /dev/null and b/backend/migrations/__pycache__/env.cpython-312.pyc differ diff --git a/backend/migrations/env.py b/backend/migrations/env.py index 777ef41..5e06b95 100755 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -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 + literal_binds=True, + include_schemas=True, + include_object=include_object ) with context.begin_transaction(): context.run_migrations() diff --git a/backend/migrations/versions/0adbe75a0b3f_complete_sync.py b/backend/migrations/versions/0adbe75a0b3f_complete_sync.py new file mode 100644 index 0000000..68163e6 --- /dev/null +++ b/backend/migrations/versions/0adbe75a0b3f_complete_sync.py @@ -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 ### diff --git a/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py b/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py new file mode 100644 index 0000000..49204fa --- /dev/null +++ b/backend/migrations/versions/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.py @@ -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 ### diff --git a/backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc b/backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc new file mode 100644 index 0000000..c5e1118 Binary files /dev/null and b/backend/migrations/versions/__pycache__/0adbe75a0b3f_complete_sync.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc b/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc new file mode 100644 index 0000000..5933b88 Binary files /dev/null and b/backend/migrations/versions/__pycache__/16d09fe82f82_fix_all_circular_imports_and_finalize_.cpython-312.pyc differ diff --git a/backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc b/backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc new file mode 100644 index 0000000..83cdc7a Binary files /dev/null and b/backend/migrations/versions/__pycache__/2cfe9285eb9d_fix_identity_scope_and_finalize_asset_.cpython-312.pyc differ diff --git a/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py b/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py deleted file mode 100755 index 82ee655..0000000 --- a/backend/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py +++ /dev/null @@ -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 ### diff --git a/docker-compose.yml b/docker-compose.yml index 79dcbf9..1bb0ab4 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/docs/V01_gemini/11_Gamification_Social.md b/docs/V01_gemini/11_Gamification_Social.md index b74e38c..93bd6fc 100644 --- a/docs/V01_gemini/11_Gamification_Social.md +++ b/docs/V01_gemini/11_Gamification_Social.md @@ -87,4 +87,18 @@ Minden hónap végén az első 3 helyezett extra Kreditet vagy "Voucher"-t kap, - **Célcsoport**: Kizárólag természetes személyek (`role: user`, `driver`). - **Kizárások**: Szervezetek (Organizations) és Adminisztrátorok nem gyűjtenek XP-t. - **Logika**: Minden `PointsLedger` bejegyzés kötelezően hivatkozik egy `user_id`-ra. -- **Mezőnevek**: Adatbázis szinten a pontok az `id`, `user_id`, `points`, `reason` mezőkben tárolódnak. \ No newline at end of file +- **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. \ No newline at end of file diff --git a/docs/V01_gemini/15_Changelog.md b/docs/V01_gemini/15_Changelog.md index 3732cd0..c03cedb 100644 --- a/docs/V01_gemini/15_Changelog.md +++ b/docs/V01_gemini/15_Changelog.md @@ -209,4 +209,44 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL - **SQLAlchemy hiba**: Javítva az `IndexError` a katalógus lekérdezésnél és az import hiba a `PG_UUID` kapcsán. ### Megjegyzés -A rendszer most már képes egyetlen KYC folyamat alatt aktiválni a felhasználót, létrehozni az egyéni flottáját, inicializálni a pénztárcáját (Kredit/Coin) és rögzíteni az első járművét kezdő km-óra állással. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md index 7495c58..7850f81 100644 --- a/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md +++ b/docs/V01_gemini/18_ASSET_AND_FLEET_SPECIFICATION.md @@ -186,4 +186,19 @@ A járműadatok kezelése hibrid módon történik. ### 5.2 Költségkövetés (TCO) - Minden Asset-hez rögzíthető költség (`asset_costs`). - Kötelező adatok: Kategória, Összeg, Dátum. -- Opcionális: Km óra állása (az amortizáció és szervizintervallum számításához). \ No newline at end of file +- 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). \ No newline at end of file diff --git a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md index af6d7df..029e2fe 100644 --- a/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md +++ b/docs/V01_gemini/19_ADMIN_AND_PERMISSIONS_SPEC.md @@ -86,4 +86,20 @@ A rendszer minden fontos paramétere a `data.system_settings` táblában tárolt ### 6.2 Paraméterezhető modulok * **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. \ No newline at end of file +* **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. \ No newline at end of file diff --git a/docs/V01_gemini/_00_gemini_gem_kód b/docs/V01_gemini/_00_gemini_gem_kód index c224ea0..2b64f77 100644 --- a/docs/V01_gemini/_00_gemini_gem_kód +++ b/docs/V01_gemini/_00_gemini_gem_kód @@ -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). diff --git a/migrations/README b/migrations/README deleted file mode 100755 index 98e4f9c..0000000 --- a/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/migrations/__pycache__/env.cpython-312.pyc b/migrations/__pycache__/env.cpython-312.pyc deleted file mode 100755 index fa1c160..0000000 Binary files a/migrations/__pycache__/env.cpython-312.pyc and /dev/null differ diff --git a/migrations/env.py b/migrations/env.py deleted file mode 100755 index 777ef41..0000000 --- a/migrations/env.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/migrations/script.py.mako b/migrations/script.py.mako deleted file mode 100755 index 1101630..0000000 --- a/migrations/script.py.mako +++ /dev/null @@ -1,28 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - """Upgrade schema.""" - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - """Downgrade schema.""" - ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py b/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py deleted file mode 100755 index 60bc499..0000000 --- a/migrations/versions/10b73fee8967_fix_roles_and_universal_vehicles.py +++ /dev/null @@ -1,38 +0,0 @@ -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -revision = "10b73fee8967" -down_revision = "13d050e8cf6d" -branch_labels = None -depends_on = None - -def upgrade() -> None: - # 1. Régi táblák eltávolítása kényszerítve - tables_to_drop = [ - "user_badges", "badges", "points_ledger", "votes", "user_scores", - "user_stats", "competitions", "level_configs", "point_rules", - "regional_settings", "translations", "system_settings", "vehicles" - ] - for table in tables_to_drop: - op.execute(f"DROP TABLE IF EXISTS data.{table} CASCADE") - - # 2. Szerepkör oszlop típusának módosítása - op.execute("ALTER TABLE data.organization_members ALTER COLUMN role TYPE orguserrole USING role::text::orguserrole") - - # 3. Új oszlopok hozzáadása (Szigorúan Sima Idézőjellel a DEFAULT-nál!) - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS hashed_password VARCHAR") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS role VARCHAR DEFAULT \u0027user\u0027") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS is_superuser BOOLEAN DEFAULT FALSE") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS is_company BOOLEAN DEFAULT FALSE") - op.execute("ALTER TABLE data.users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE") - - op.execute("ALTER TABLE data.organizations ADD COLUMN IF NOT EXISTS theme VARCHAR DEFAULT \u0027system\u0027") - op.execute("ALTER TABLE data.organizations ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE") - - # 4. Takarítás - op.execute("ALTER TABLE data.users DROP COLUMN IF EXISTS password_hash") - op.execute("ALTER TABLE data.users DROP COLUMN IF EXISTS is_email_verified") - -def downgrade() -> None: - pass diff --git a/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py b/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py deleted file mode 100755 index ff91dee..0000000 --- a/migrations/versions/13bd03551ebf_add_verification_tokens_and_legal_tables.py +++ /dev/null @@ -1,607 +0,0 @@ -"""Add verification tokens and legal tables - -Revision ID: 13bd03551ebf -Revises: 8d450e9dc77f -Create Date: 2026-01-26 09:34:26.147993 - -""" -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 = '13bd03551ebf' -down_revision: Union[str, Sequence[str], None] = '8d450e9dc77f' -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('email_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=True), - sa.Column('type', sa.String(), nullable=True), - sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False, schema='data') - op.create_table('email_provider_configs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=True), - sa.Column('provider_type', sa.String(length=20), nullable=True), - sa.Column('priority', sa.Integer(), nullable=True), - sa.Column('settings', sa.JSON(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('fail_count', sa.Integer(), nullable=True), - sa.Column('max_fail_threshold', sa.Integer(), nullable=True), - sa.Column('success_rate', sa.Float(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - schema='data' - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False, schema='data') - op.create_table('email_templates', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), nullable=True), - sa.Column('subject', sa.String(length=255), nullable=False), - sa.Column('body_html', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True, schema='data') - op.create_table('legal_documents', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('version', sa.String(length=20), nullable=False), - sa.Column('region_code', sa.String(length=5), nullable=True), - sa.Column('language', sa.String(length=5), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False, schema='data') - op.create_table('legal_acceptances', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('document_id', sa.Integer(), nullable=True), - sa.Column('accepted_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['data.legal_documents.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False, schema='data') - op.create_table('organizations', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('org_type', sa.Enum('PRIVATE', 'COMPANY', name='orgtype'), nullable=True), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('founded_at', sa.Date(), nullable=True), - sa.Column('validation_status', sa.Enum('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=True), - sa.Column('ui_theme', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - 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=False, schema='data') - op.create_table('verification_tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('token', sa.String(length=255), nullable=True), - sa.Column('token_type', sa.Enum('EMAIL_VERIFY', 'PASSWORD_RESET', name='tokentype'), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True, schema='data') - op.create_table('organization_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('org_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', name='userrole'), nullable=True), - sa.Column('is_permanent', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data') - op.create_table('vehicles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vin', sa.String(), nullable=False), - sa.Column('license_plate', sa.String(), nullable=False), - sa.Column('make', sa.String(), nullable=True), - sa.Column('model', sa.String(), nullable=True), - sa.Column('year', sa.Integer(), nullable=True), - sa.Column('is_deleted', sa.Boolean(), nullable=True), - sa.Column('current_org_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['data.organizations.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicles_id'), 'vehicles', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_license_plate'), 'vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_vin'), 'vehicles', ['vin'], unique=True, 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_company_members_id'), table_name='company_members', schema='data') - op.drop_table('company_members', schema='data') - op.drop_table('vehicle_options_catalog', 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_table('alembic_version') - op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules', schema='data') - op.drop_index(op.f('ix_data_point_rules_id'), 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('vehicle_engines', schema='data') - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings', schema='data') - op.drop_table('system_settings', schema='data') - op.drop_table('vehicle_models', schema='data') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data') - op.drop_index(op.f('ix_data_points_ledger_user_id'), table_name='points_ledger', schema='data') - op.drop_table('points_ledger', schema='data') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data') - op.drop_table('badges', schema='data') - op.drop_table('vehicle_categories', schema='data') - op.drop_table('competitions', schema='data') - op.drop_index(op.f('ix_data_service_providers_id'), table_name='service_providers', schema='data') - op.drop_table('service_providers', schema='data') - op.drop_table('vehicle_makes', schema='data') - op.drop_table('votes', schema='data') - op.drop_index(op.f('ix_data_vehicle_events_id'), table_name='vehicle_events', schema='data') - op.drop_table('vehicle_events', schema='data') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') - op.drop_table('translations', schema='data') - op.drop_table('staged_vehicle_data', schema='data') - op.drop_index(op.f('ix_data_locations_id'), table_name='locations', schema='data') - op.drop_table('locations', 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_table('user_badges', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_id'), table_name='user_vehicles', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_license_plate'), table_name='user_vehicles', schema='data') - op.drop_index(op.f('ix_data_user_vehicles_vin'), table_name='user_vehicles', schema='data') - op.drop_table('user_vehicles', schema='data') - op.drop_index(op.f('ix_data_audit_logs_id'), 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_target_type'), table_name='audit_logs', schema='data') - op.drop_table('audit_logs', schema='data') - op.drop_table('user_scores', schema='data') - op.drop_table('user_vehicle_modifications', 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.add_column('users', sa.Column('first_name', sa.String(), nullable=True), schema='data') - op.add_column('users', sa.Column('last_name', sa.String(), nullable=True), schema='data') - op.add_column('users', sa.Column('birthday', sa.Date(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_email_verified', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_banned', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('is_gdpr_deleted', sa.Boolean(), nullable=True), schema='data') - op.add_column('users', sa.Column('previous_login_count', sa.Integer(), nullable=True), schema='data') - op.add_column('users', sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True), schema='data') - op.drop_index(op.f('ix_data_users_region_code'), table_name='users', schema='data') - op.drop_column('users', 'is_superuser', schema='data') - op.drop_column('users', 'full_name', schema='data') - op.drop_column('users', 'reputation_score', schema='data') - op.drop_column('users', 'updated_at', schema='data') - op.drop_column('users', 'license_expiry_date', schema='data') - op.drop_column('users', 'role', schema='data') - op.drop_column('users', 'region_code', schema='data') - op.drop_column('users', 'id_card_expiry_date', schema='data') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('users', sa.Column('id_card_expiry_date', sa.DATE(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'HU'::character varying"), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('role', postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), server_default=sa.text("'USER'::userrole"), autoincrement=False, nullable=False), schema='data') - op.add_column('users', sa.Column('license_expiry_date', sa.DATE(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('reputation_score', sa.INTEGER(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('full_name', sa.VARCHAR(), autoincrement=False, nullable=True), schema='data') - op.add_column('users', sa.Column('is_superuser', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='data') - op.create_index(op.f('ix_data_users_region_code'), 'users', ['region_code'], unique=False, schema='data') - op.drop_column('users', 'verified_at', schema='data') - op.drop_column('users', 'previous_login_count', schema='data') - op.drop_column('users', 'is_gdpr_deleted', schema='data') - op.drop_column('users', 'is_banned', schema='data') - op.drop_column('users', 'is_email_verified', schema='data') - op.drop_column('users', 'birthday', schema='data') - op.drop_column('users', 'last_name', schema='data') - op.drop_column('users', 'first_name', schema='data') - op.create_table('vehicle_assignments', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('company_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('driver_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('start_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('end_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('notes', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], name=op.f('vehicle_assignments_company_id_fkey')), - sa.ForeignKeyConstraint(['driver_id'], ['data.users.id'], name=op.f('vehicle_assignments_driver_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_assignments_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_assignments_id'), 'vehicle_assignments', ['id'], unique=False, schema='data') - op.create_table('user_vehicle_modifications', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('mod_type', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_validated', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('installed_at_km', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('cost', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicle_modifications_pkey')), - schema='data' - ) - op.create_table('user_scores', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('competition_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('last_updated', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['competition_id'], ['data.competitions.id'], name=op.f('user_scores_competition_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_scores_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_scores_pkey')), - sa.UniqueConstraint('user_id', 'competition_id', name=op.f('uq_user_competition_score'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('audit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('target_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('target_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('action', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('changes', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('timestamp', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('audit_logs_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('audit_logs_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_audit_logs_target_type'), 'audit_logs', ['target_type'], 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_id'), 'audit_logs', ['id'], unique=False, schema='data') - op.create_table('user_vehicles', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('nickname', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('make', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('model', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('year', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('fuel_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('license_plate', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('vin', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('color_code', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('manufacture_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('registration_region', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('initial_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('current_odometer', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('mot_expiry_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('insurance_expiry_date', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('factory_options', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['engine_id'], ['data.vehicle_engines.id'], name=op.f('user_vehicles_engine_id_fkey')), - sa.ForeignKeyConstraint(['model_id'], ['data.vehicle_models.id'], name=op.f('user_vehicles_model_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_vehicles_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicles_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_user_vehicles_vin'), 'user_vehicles', ['vin'], unique=True, schema='data') - op.create_index(op.f('ix_data_user_vehicles_license_plate'), 'user_vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_user_vehicles_id'), 'user_vehicles', ['id'], unique=False, schema='data') - op.create_table('user_badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('badge_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('earned_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['badge_id'], ['data.badges.id'], name=op.f('user_badges_badge_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_badges_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_badges_pkey')), - schema='data' - ) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('currency', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('regional_settings_pkey')), - sa.UniqueConstraint('country_code', name=op.f('regional_settings_country_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False, schema='data') - op.create_table('locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('type', postgresql.ENUM('stop', 'warehouse', 'client', name='location_type_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('coordinates', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('address_full', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('capacity', sa.INTEGER(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('locations_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_locations_id'), 'locations', ['id'], unique=False, schema='data') - op.create_table('staged_vehicle_data', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('source_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('raw_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('status', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('error_log', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('staged_vehicle_data_pkey')), - schema='data' - ) - op.create_table('translations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('lang_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('translations_pkey')), - sa.UniqueConstraint('key', 'lang_code', name=op.f('uq_translation_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') - op.create_table('vehicle_events', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('event_type', postgresql.ENUM('PURCHASE_PRICE', 'TRANSFER_TAX', 'ADMIN_FEE', 'VEHICLE_TAX', 'INSURANCE', 'REFUELING', 'SERVICE', 'PARKING', 'TOLL', 'FINE', 'TUNING_ACCESSORIES', 'OTHER', name='expense_category_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('odometer_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('odometer_anomaly', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('cost_amount', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('image_paths', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('is_diy', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('service_provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['service_provider_id'], ['data.service_providers.id'], name=op.f('vehicle_events_service_provider_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_events_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_events_id'), 'vehicle_events', ['id'], unique=False, schema='data') - op.create_table('votes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vote_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['provider_id'], ['data.service_providers.id'], name=op.f('votes_provider_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('votes_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('votes_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', name=op.f('uq_user_provider_vote'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('vehicle_makes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('logo_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_makes_pkey')), - sa.UniqueConstraint('name', name=op.f('vehicle_makes_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_table('service_providers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('address', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('status', postgresql.ENUM('pending', 'approved', 'rejected', name='moderation_status_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('source', postgresql.ENUM('manual', 'ocr', 'api_import', name='source_type_enum', schema='data'), autoincrement=False, nullable=False), - sa.Column('validation_score', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('evidence_image_path', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('added_by_user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['added_by_user_id'], ['data.users.id'], name=op.f('service_providers_added_by_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_providers_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_service_providers_id'), 'service_providers', ['id'], unique=False, schema='data') - op.create_table('competitions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('start_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('end_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('competitions_pkey')), - schema='data' - ) - op.create_table('vehicle_categories', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_categories_pkey')), - schema='data' - ) - op.create_table('badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('icon_url', sa.VARCHAR(length=500), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('badges_pkey')), - sa.UniqueConstraint('name', name=op.f('badges_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - schema='data' - ) - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data') - op.create_table('points_ledger', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('points_change', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('timestamp', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('points_ledger_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('points_ledger_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_points_ledger_user_id'), 'points_ledger', ['user_id'], unique=False, schema='data') - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data') - op.create_table('vehicle_models', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('make_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('generation_name', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('production_start_year', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('production_end_year', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['data.vehicle_categories.id'], name=op.f('vehicle_models_category_id_fkey')), - sa.ForeignKeyConstraint(['make_id'], ['data.vehicle_makes.id'], name=op.f('vehicle_models_make_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_models_pkey')), - schema='data' - ) - op.create_table('system_settings', - sa.Column('key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('value', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('key', name=op.f('system_settings_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key'], unique=False, schema='data') - op.create_table('vehicle_engines', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_code', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('fuel_type', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('displacement_ccm', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('power_kw', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('torque_nm', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('transmission_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('gears_count', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('drive_type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['model_id'], ['data.vehicle_models.id'], name=op.f('vehicle_engines_model_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_engines_pkey')), - schema='data' - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('rank_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('level_configs_pkey')), - sa.UniqueConstraint('level_number', name=op.f('level_configs_level_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False), - 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(), autoincrement=True, nullable=False), - sa.Column('action_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('point_rules_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True, 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('user_stats', - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('total_points', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False), - sa.Column('current_level', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=False), - sa.Column('last_updated', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('user_stats_user_id_fkey')), - sa.PrimaryKeyConstraint('user_id', name=op.f('user_stats_pkey')), - schema='data' - ) - op.create_table('vehicle_ownerships', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), 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('notes', sa.TEXT(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('vehicle_ownerships_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_ownerships_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_vehicle_ownerships_id'), 'vehicle_ownerships', ['id'], unique=False, schema='data') - op.create_table('vehicle_options_catalog', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('category', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('name_key', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_options_catalog_pkey')), - schema='data' - ) - op.create_table('company_members', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('company_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', postgresql.ENUM('OWNER', 'MANAGER', 'DRIVER', name='companyrole'), autoincrement=False, nullable=False), - sa.Column('can_edit_service', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('can_see_costs', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['company_id'], ['data.companies.id'], name=op.f('company_members_company_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], name=op.f('company_members_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('company_members_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.create_table('companies', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('tax_number', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('subscription_tier', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('owner_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['owner_id'], ['data.users.id'], name=op.f('companies_owner_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('companies_pkey')), - schema='data' - ) - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.drop_index(op.f('ix_data_vehicles_vin'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_license_plate'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_id'), table_name='vehicles', schema='data') - op.drop_table('vehicles', schema='data') - 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_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens', 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_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_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances', schema='data') - op.drop_table('legal_acceptances', schema='data') - op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents', schema='data') - op.drop_table('legal_documents', schema='data') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates', schema='data') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates', schema='data') - op.drop_table('email_templates', schema='data') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs', schema='data') - op.drop_table('email_provider_configs', schema='data') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs', schema='data') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs', schema='data') - op.drop_table('email_logs', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/13d050e8cf6d_initial_baseline_v2.py b/migrations/versions/13d050e8cf6d_initial_baseline_v2.py deleted file mode 100755 index 9aba351..0000000 --- a/migrations/versions/13d050e8cf6d_initial_baseline_v2.py +++ /dev/null @@ -1,200 +0,0 @@ -"""Initial baseline v2 - -Revision ID: 13d050e8cf6d -Revises: 13bd03551ebf -Create Date: 2026-01-26 09:53:50.248698 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '13d050e8cf6d' -down_revision: Union[str, Sequence[str], None] = '13bd03551ebf' -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('email_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=True), - sa.Column('type', sa.String(), nullable=True), - sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False, schema='data') - op.create_table('email_provider_configs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=True), - sa.Column('provider_type', sa.String(length=20), nullable=True), - sa.Column('priority', sa.Integer(), nullable=True), - sa.Column('settings', sa.JSON(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('fail_count', sa.Integer(), nullable=True), - sa.Column('max_fail_threshold', sa.Integer(), nullable=True), - sa.Column('success_rate', sa.Float(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name'), - schema='data' - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False, schema='data') - op.create_table('email_templates', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), nullable=True), - sa.Column('subject', sa.String(length=255), nullable=False), - sa.Column('body_html', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True, schema='data') - op.create_table('legal_documents', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(length=255), nullable=True), - sa.Column('content', sa.Text(), nullable=False), - sa.Column('version', sa.String(length=20), nullable=False), - sa.Column('region_code', sa.String(length=5), nullable=True), - sa.Column('language', sa.String(length=5), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['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('password_hash', sa.Text(), nullable=False), - sa.Column('first_name', sa.String(), nullable=True), - sa.Column('last_name', sa.String(), nullable=True), - sa.Column('birthday', sa.Date(), nullable=True), - sa.Column('is_email_verified', sa.Boolean(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_banned', sa.Boolean(), nullable=True), - sa.Column('is_gdpr_deleted', sa.Boolean(), nullable=True), - sa.Column('previous_login_count', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True), - 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('legal_acceptances', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('document_id', sa.Integer(), nullable=True), - sa.Column('accepted_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['data.legal_documents.id'], ), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False, schema='data') - op.create_table('organizations', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('org_type', sa.Enum('PRIVATE', 'COMPANY', name='orgtype'), nullable=True), - sa.Column('tax_number', sa.String(), nullable=True), - sa.Column('founded_at', sa.Date(), nullable=True), - sa.Column('validation_status', sa.Enum('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=True), - sa.Column('ui_theme', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - 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=False, schema='data') - op.create_table('verification_tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('token', sa.String(length=255), nullable=True), - sa.Column('token_type', sa.Enum('EMAIL_VERIFY', 'PASSWORD_RESET', name='tokentype'), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True, schema='data') - op.create_table('organization_members', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('org_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', name='userrole'), nullable=True), - sa.Column('is_permanent', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['data.organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['data.users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False, schema='data') - op.create_table('vehicles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('vin', sa.String(), nullable=False), - sa.Column('license_plate', sa.String(), nullable=False), - sa.Column('make', sa.String(), nullable=True), - sa.Column('model', sa.String(), nullable=True), - sa.Column('year', sa.Integer(), nullable=True), - sa.Column('is_deleted', sa.Boolean(), nullable=True), - sa.Column('current_org_id', sa.Integer(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['data.organizations.id'], ), - sa.PrimaryKeyConstraint('id'), - schema='data' - ) - op.create_index(op.f('ix_data_vehicles_id'), 'vehicles', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_license_plate'), 'vehicles', ['license_plate'], unique=False, schema='data') - op.create_index(op.f('ix_data_vehicles_vin'), 'vehicles', ['vin'], unique=True, schema='data') - op.drop_table('alembic_version') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - 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.drop_index(op.f('ix_data_vehicles_vin'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_license_plate'), table_name='vehicles', schema='data') - op.drop_index(op.f('ix_data_vehicles_id'), table_name='vehicles', schema='data') - op.drop_table('vehicles', schema='data') - 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_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens', 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_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_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances', schema='data') - op.drop_table('legal_acceptances', 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_legal_documents_id'), table_name='legal_documents', schema='data') - op.drop_table('legal_documents', schema='data') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates', schema='data') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates', schema='data') - op.drop_table('email_templates', schema='data') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs', schema='data') - op.drop_table('email_provider_configs', schema='data') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs', schema='data') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs', schema='data') - op.drop_table('email_logs', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/553ef1388276_rebuild_schema_v2.py b/migrations/versions/553ef1388276_rebuild_schema_v2.py deleted file mode 100755 index 7455c01..0000000 --- a/migrations/versions/553ef1388276_rebuild_schema_v2.py +++ /dev/null @@ -1,263 +0,0 @@ -"""rebuild_schema_v2 - -Revision ID: 553ef1388276 -Revises: c21c2c7e70d4 -Create Date: 2026-01-25 11:32:10.692756 - -""" -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 = '553ef1388276' -down_revision: Union[str, Sequence[str], None] = 'c21c2c7e70d4' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('system_settings', - sa.Column('key', sa.String(), nullable=False), - sa.Column('value', sa.JSON(), nullable=False), - sa.Column('description', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('key'), - schema='data' - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key'], unique=False, schema='data') - op.drop_table('costs', schema='data') - op.drop_table('point_rules', schema='data') - op.drop_table('regional_settings', schema='data') - op.drop_table('level_configs', schema='data') - op.drop_table('vehicle_history', schema='data') - op.alter_column('badges', 'description', - existing_type=sa.TEXT(), - type_=sa.String(length=255), - nullable=False, - schema='data') - op.alter_column('badges', 'icon_url', - existing_type=sa.VARCHAR(length=255), - type_=sa.String(length=500), - existing_nullable=True, - schema='data') - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'badges', ['name'], schema='data') - op.drop_column('badges', 'created_at', schema='data') - op.drop_column('badges', 'criteria_json', schema='data') - op.alter_column('points_ledger', 'user_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('points_ledger', 'timestamp', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_points_ledger_user_id'), 'points_ledger', ['user_id'], unique=False, schema='data') - op.add_column('translations', sa.Column('is_published', sa.Boolean(), nullable=True), schema='data') - op.drop_constraint('translations_key_lang_code_key', 'translations', schema='data', type_='unique') - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False, schema='data') - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False, schema='data') - op.create_unique_constraint('uq_translation_key_lang', 'translations', ['key', 'lang_code'], schema='data') - op.alter_column('user_badges', 'user_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('user_badges', 'badge_id', - existing_type=sa.INTEGER(), - nullable=False, - schema='data') - op.alter_column('user_badges', 'earned_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - op.drop_constraint('user_badges_user_id_badge_id_key', 'user_badges', schema='data', type_='unique') - op.alter_column('user_stats', 'total_points', - existing_type=sa.INTEGER(), - nullable=False, - existing_server_default=sa.text('0'), - schema='data') - op.alter_column('user_stats', 'current_level', - existing_type=sa.INTEGER(), - nullable=False, - existing_server_default=sa.text('1'), - schema='data') - op.alter_column('user_stats', 'last_updated', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=False, - existing_server_default=sa.text('now()'), - schema='data') - - # --- JAVÍTOTT RÉSZ KEZDETE (Role konverzió default kezeléssel) --- - # 1. Meglévő default eldobása - op.execute('ALTER TABLE data.users ALTER COLUMN role DROP DEFAULT') - - # 2. Típusmódosítás - op.alter_column('users', 'role', - existing_type=postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='user_role', schema='data'), - type_=sa.Enum('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), - nullable=False, - schema='data', - postgresql_using='role::text::userrole') - - # 3. Új default beállítása az új típussal - op.execute("ALTER TABLE data.users ALTER COLUMN role SET DEFAULT 'USER'::userrole") - # --- JAVÍTOTT RÉSZ VÉGE --- - - op.drop_index('idx_users_role', table_name='users', schema='data') - op.create_index(op.f('ix_data_users_region_code'), 'users', ['region_code'], unique=False, schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'user_vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index('ix_data_vehicle_events_vehicle_id', table_name='vehicle_events', schema='data') - op.create_foreign_key(None, 'vehicle_events', 'user_vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_ownerships', 'user_vehicles', ['vehicle_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, 'vehicle_ownerships', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_events', schema='data', type_='foreignkey') - op.create_index('ix_data_vehicle_events_vehicle_id', 'vehicle_events', ['vehicle_id'], unique=False, schema='data') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_index(op.f('ix_data_users_region_code'), table_name='users', schema='data') - op.create_index('idx_users_role', 'users', ['role'], unique=False, schema='data') - op.alter_column('users', 'role', - existing_type=sa.Enum('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='userrole'), - type_=postgresql.ENUM('SUPERUSER', 'REGIONAL_ADMIN', 'MODERATOR', 'BUSINESS_PARTNER', 'USER', name='user_role', schema='data'), - nullable=True, - existing_server_default=sa.text("'USER'::data.user_role"), - schema='data') - op.alter_column('user_stats', 'last_updated', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('user_stats', 'current_level', - existing_type=sa.INTEGER(), - nullable=True, - existing_server_default=sa.text('1'), - schema='data') - op.alter_column('user_stats', 'total_points', - existing_type=sa.INTEGER(), - nullable=True, - existing_server_default=sa.text('0'), - schema='data') - op.create_unique_constraint('user_badges_user_id_badge_id_key', 'user_badges', ['user_id', 'badge_id'], schema='data') - op.alter_column('user_badges', 'earned_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('user_badges', 'badge_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.alter_column('user_badges', 'user_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.drop_constraint('uq_translation_key_lang', 'translations', schema='data', type_='unique') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations', schema='data') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations', schema='data') - op.create_unique_constraint('translations_key_lang_code_key', 'translations', ['key', 'lang_code'], schema='data') - op.drop_column('translations', 'is_published', schema='data') - op.drop_index(op.f('ix_data_points_ledger_user_id'), table_name='points_ledger', schema='data') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger', schema='data') - op.alter_column('points_ledger', 'timestamp', - existing_type=postgresql.TIMESTAMP(timezone=True), - nullable=True, - existing_server_default=sa.text('now()'), - schema='data') - op.alter_column('points_ledger', 'user_id', - existing_type=sa.INTEGER(), - nullable=True, - schema='data') - op.add_column('badges', sa.Column('criteria_json', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), schema='data') - op.add_column('badges', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), schema='data') - op.drop_constraint(None, 'badges', schema='data', type_='unique') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges', schema='data') - op.alter_column('badges', 'icon_url', - existing_type=sa.String(length=500), - type_=sa.VARCHAR(length=255), - existing_nullable=True, - schema='data') - op.alter_column('badges', 'description', - existing_type=sa.String(length=255), - type_=sa.TEXT(), - nullable=True, - schema='data') - op.create_table('vehicle_history', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.vehicle_history_id_seq'::regclass)"), 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='vehicle_history_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='vehicle_history_pkey'), - schema='data' - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.level_configs_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('name_translation_key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'GLOBAL'::character varying"), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='level_configs_pkey'), - sa.UniqueConstraint('level_number', name='level_configs_level_number_key'), - schema='data' - ) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.regional_settings_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('setting_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('value', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='regional_settings_pkey'), - schema='data' - ) - op.create_table('point_rules', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.point_rules_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('rule_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('start_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('end_date', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), server_default=sa.text("'GLOBAL'::character varying"), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='point_rules_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='alembic_version_pkc') - ) - op.create_table('costs', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('data.costs_id_seq'::regclass)"), 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='costs_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='costs_pkey'), - schema='data' - ) - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings', schema='data') - op.drop_table('system_settings', schema='data') - # ### end Alembic commands ### diff --git a/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py b/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py deleted file mode 100755 index 694df0e..0000000 --- a/migrations/versions/5aed26900f0b_add_persons_and_owner_person_id.py +++ /dev/null @@ -1,28 +0,0 @@ -"""add persons and owner_person_id - -Revision ID: 5aed26900f0b -Revises: 10b73fee8967 -Create Date: 2026-02-01 22:22:21.486469 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '5aed26900f0b' -down_revision: Union[str, Sequence[str], None] = '10b73fee8967' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - pass - - -def downgrade() -> None: - """Downgrade schema.""" - pass diff --git a/migrations/versions/8d450e9dc77f_add_vehicle_staging.py b/migrations/versions/8d450e9dc77f_add_vehicle_staging.py deleted file mode 100755 index 3ddd055..0000000 --- a/migrations/versions/8d450e9dc77f_add_vehicle_staging.py +++ /dev/null @@ -1,35 +0,0 @@ -"""add_vehicle_staging - -Revision ID: 8d450e9dc77f -Revises: 553ef1388276 -Create Date: 2026-01-25 12:08:48.056502 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8d450e9dc77f' -down_revision: Union[str, Sequence[str], None] = '553ef1388276' -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_table('alembic_version') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('alembic_version', - sa.Column('version_num', sa.VARCHAR(length=32), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('version_num', name='alembic_version_pkc') - ) - # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc b/migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc deleted file mode 100755 index 73d9b2b..0000000 Binary files a/migrations/versions/__pycache__/10b73fee8967_fix_roles_and_universal_vehicles.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/13bd03551ebf_add_verification_tokens_and_legal_tables.cpython-312.pyc b/migrations/versions/__pycache__/13bd03551ebf_add_verification_tokens_and_legal_tables.cpython-312.pyc deleted file mode 100755 index 14002f6..0000000 Binary files a/migrations/versions/__pycache__/13bd03551ebf_add_verification_tokens_and_legal_tables.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc b/migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc deleted file mode 100755 index 215700c..0000000 Binary files a/migrations/versions/__pycache__/13d050e8cf6d_initial_baseline_v2.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/553ef1388276_rebuild_schema_v2.cpython-312.pyc b/migrations/versions/__pycache__/553ef1388276_rebuild_schema_v2.cpython-312.pyc deleted file mode 100755 index c49d61e..0000000 Binary files a/migrations/versions/__pycache__/553ef1388276_rebuild_schema_v2.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc b/migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc deleted file mode 100755 index 502ae1b..0000000 Binary files a/migrations/versions/__pycache__/5aed26900f0b_add_persons_and_owner_person_id.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc b/migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc deleted file mode 100755 index aa3516e..0000000 Binary files a/migrations/versions/__pycache__/8d450e9dc77f_add_vehicle_staging.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc b/migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc deleted file mode 100755 index 686d445..0000000 Binary files a/migrations/versions/__pycache__/c21c2c7e70d4_clean_gamification_setup.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc b/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc deleted file mode 100644 index c5bfef7..0000000 Binary files a/migrations/versions/__pycache__/fba92ed020b1_merge_identity_v1.cpython-312.pyc and /dev/null differ diff --git a/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py b/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py deleted file mode 100755 index 82ee655..0000000 --- a/migrations/versions/c21c2c7e70d4_clean_gamification_setup.py +++ /dev/null @@ -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 ### diff --git a/migrations/versions/fba92ed020b1_merge_identity_v1.py b/migrations/versions/fba92ed020b1_merge_identity_v1.py deleted file mode 100644 index af8356f..0000000 --- a/migrations/versions/fba92ed020b1_merge_identity_v1.py +++ /dev/null @@ -1,945 +0,0 @@ -"""merge_identity_v1 - -Revision ID: fba92ed020b1 -Revises: 5aed26900f0b -Create Date: 2026-02-04 21:31:43.854642 - -""" -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 = 'fba92ed020b1' -down_revision: Union[str, Sequence[str], None] = '5aed26900f0b' -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_table('user_vehicle_equipment') - op.drop_table('credit_logs') - op.drop_table('votes') - op.drop_table('audit_logs') - op.drop_index(op.f('ix_data_level_configs_id'), table_name='level_configs') - op.drop_table('level_configs') - op.drop_table('vouchers') - op.drop_index(op.f('ix_data_verification_tokens_id'), table_name='verification_tokens') - op.drop_index(op.f('ix_data_verification_tokens_token'), table_name='verification_tokens') - op.drop_index(op.f('ix_verification_tokens_lookup'), table_name='verification_tokens') - op.drop_index(op.f('ix_verification_tokens_user'), table_name='verification_tokens') - op.drop_index(op.f('uq_verification_tokens_token_hash'), table_name='verification_tokens', postgresql_where='(token_hash IS NOT NULL)') - op.drop_table('verification_tokens') - op.drop_index(op.f('ix_data_regional_settings_id'), table_name='regional_settings') - op.drop_table('regional_settings') - op.drop_index(op.f('ix_data_vehicle_ownership_id'), table_name='vehicle_ownership') - op.drop_table('vehicle_ownership') - op.drop_table('user_scores') - op.drop_index(op.f('idx_vm_slug'), table_name='vehicle_models') - op.drop_index(op.f('ix_data_vehicle_models_id'), table_name='vehicle_models') - op.drop_table('vehicle_models') - op.drop_index(op.f('ix_data_email_templates_id'), table_name='email_templates') - op.drop_index(op.f('ix_data_email_templates_type'), table_name='email_templates') - op.drop_table('email_templates') - op.drop_index(op.f('ix_data_points_ledger_id'), table_name='points_ledger') - op.drop_table('points_ledger') - op.drop_table('bot_discovery_logs') - op.drop_table('equipment_items') - op.drop_index(op.f('ix_data_organization_members_id'), table_name='organization_members') - op.drop_table('organization_members') - op.drop_index(op.f('idx_settings_lookup'), table_name='system_settings') - op.drop_index(op.f('ix_data_system_settings_key'), table_name='system_settings') - op.drop_table('system_settings') - op.drop_table('user_credits') - op.drop_table('referrals') - op.drop_index(op.f('ix_data_vehicle_variants_id'), table_name='vehicle_variants') - op.drop_table('vehicle_variants') - op.drop_table('subscription_notification_rules') - op.drop_index(op.f('ix_data_badges_id'), table_name='badges') - op.drop_table('badges') - op.drop_index(op.f('ix_data_legal_acceptances_id'), table_name='legal_acceptances') - op.drop_table('legal_acceptances') - op.drop_table('service_specialties') - op.drop_table('competitions') - op.drop_table('credit_transactions') - op.drop_table('locations') - op.drop_index(op.f('ix_data_legal_documents_id'), table_name='legal_documents') - op.drop_table('legal_documents') - op.drop_table('email_providers') - op.drop_table('subscription_tiers') - op.drop_index(op.f('ix_data_email_logs_email'), table_name='email_logs') - op.drop_index(op.f('ix_data_email_logs_id'), table_name='email_logs') - op.drop_table('email_logs') - op.drop_table('organization_locations') - op.drop_table('vehicle_events') - op.drop_table('vehicle_expenses') - op.drop_table('credit_rules') - op.drop_index(op.f('ix_data_email_provider_configs_id'), table_name='email_provider_configs') - op.drop_table('email_provider_configs') - op.drop_table('org_subscriptions') - op.drop_index(op.f('ix_data_user_badges_id'), table_name='user_badges') - op.drop_table('user_badges') - op.drop_index(op.f('idx_vc_slug'), table_name='vehicle_categories') - op.drop_index(op.f('ix_data_vehicle_categories_id'), table_name='vehicle_categories') - op.drop_table('vehicle_categories') - op.drop_index(op.f('ix_data_user_vehicles_id'), table_name='user_vehicles') - op.drop_index(op.f('ix_data_user_vehicles_license_plate'), table_name='user_vehicles') - op.drop_index(op.f('ix_data_user_vehicles_vin'), table_name='user_vehicles') - op.drop_table('user_vehicles') - op.drop_table('fuel_stations') - op.drop_table('alembic_version') - op.drop_index(op.f('ix_data_translations_id'), table_name='translations') - op.drop_index(op.f('ix_data_translations_key'), table_name='translations') - op.drop_index(op.f('ix_data_translations_lang_code'), table_name='translations') - op.drop_table('translations') - op.drop_table('service_reviews') - op.drop_index(op.f('ix_data_user_stats_id'), table_name='user_stats') - op.drop_table('user_stats') - op.drop_index(op.f('ix_data_point_rules_action_key'), table_name='point_rules') - op.drop_index(op.f('ix_data_point_rules_id'), table_name='point_rules') - op.drop_table('point_rules') - op.drop_index(op.f('ix_companies_owner_person_id'), table_name='companies') - op.create_index(op.f('ix_data_companies_id'), 'companies', ['id'], unique=False, schema='data') - op.drop_constraint(op.f('fk_companies_owner_person'), 'companies', type_='foreignkey') - op.drop_constraint(op.f('companies_owner_id_fkey'), 'companies', type_='foreignkey') - op.create_foreign_key(None, 'companies', 'users', ['owner_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('companies', 'owner_person_id') - op.alter_column('company_members', 'role', - existing_type=sa.VARCHAR(length=50), - type_=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), - nullable=False, - existing_server_default=sa.text("'driver'::companyrole")) - op.create_index(op.f('ix_data_company_members_id'), 'company_members', ['id'], unique=False, schema='data') - op.drop_constraint(op.f('company_members_company_id_fkey'), 'company_members', type_='foreignkey') - op.drop_constraint(op.f('company_members_user_id_fkey'), 'company_members', type_='foreignkey') - op.create_foreign_key(None, 'company_members', 'users', ['user_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'company_members', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index(op.f('idx_engine_code'), table_name='engine_specs') - op.create_index(op.f('ix_data_engine_specs_id'), 'engine_specs', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'engine_specs', ['engine_code'], schema='data') - op.drop_column('engine_specs', 'emissions_class') - op.drop_column('engine_specs', 'phases') - op.drop_column('engine_specs', 'default_service_interval_hours') - op.drop_column('engine_specs', 'onboard_charger_kw') - op.drop_column('engine_specs', 'battery_capacity_kwh') - op.drop_index(op.f('idx_org_slug'), table_name='organizations') - op.drop_index(op.f('ix_data_organizations_tax_number'), table_name='organizations') - 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.drop_column('organizations', 'theme') - op.drop_column('organizations', 'validation_status') - op.drop_column('organizations', 'founded_at') - op.drop_column('organizations', 'ui_theme') - op.drop_column('organizations', 'tax_number') - op.drop_column('organizations', 'slug') - op.drop_column('organizations', 'country_code') - op.add_column('persons', sa.Column('id_uuid', sa.UUID(), nullable=False)) - op.add_column('persons', sa.Column('last_name', sa.String(), nullable=False)) - op.add_column('persons', sa.Column('first_name', sa.String(), nullable=False)) - op.add_column('persons', sa.Column('mothers_name', sa.String(), nullable=True)) - op.add_column('persons', sa.Column('birth_place', sa.String(), nullable=True)) - op.add_column('persons', sa.Column('birth_date', sa.DateTime(), nullable=True)) - op.add_column('persons', sa.Column('identity_docs', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.add_column('persons', sa.Column('medical_emergency', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.add_column('persons', sa.Column('ice_contact', sa.JSON(), server_default=sa.text("'{}'::jsonb"), nullable=True)) - op.alter_column('persons', 'id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=False, - autoincrement=True) - op.create_index(op.f('ix_data_persons_id'), 'persons', ['id'], unique=False, schema='data') - op.create_unique_constraint(None, 'persons', ['id_uuid'], schema='data') - op.drop_column('persons', 'updated_at') - op.drop_column('persons', 'is_active') - op.drop_column('persons', 'reputation_score') - op.drop_column('persons', 'created_at') - op.drop_column('persons', 'risk_level') - op.alter_column('service_providers', 'search_tags', - existing_type=sa.TEXT(), - type_=sa.String(), - existing_nullable=True) - op.create_index(op.f('ix_data_service_providers_id'), 'service_providers', ['id'], unique=False, schema='data') - op.drop_column('service_providers', 'handled_vehicle_types') - op.drop_column('service_providers', 'verification_status') - op.drop_column('service_providers', 'specialized_brands') - op.drop_constraint(op.f('service_records_provider_id_fkey'), 'service_records', type_='foreignkey') - op.drop_constraint(op.f('service_records_vehicle_id_fkey'), 'service_records', type_='foreignkey') - op.create_foreign_key(None, 'service_records', 'service_providers', ['provider_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'service_records', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('service_records', 'invoice_path') - op.drop_column('service_records', 'parts_quality_index') - op.drop_column('service_records', 'description') - op.drop_column('service_records', 'is_accident_repair') - op.drop_column('service_records', 'rating_impact_score') - op.alter_column('users', 'hashed_password', - existing_type=sa.VARCHAR(), - nullable=False) - op.alter_column('users', 'role', - existing_type=sa.VARCHAR(), - type_=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), - existing_nullable=True, - existing_server_default=sa.text("'user'::character varying")) - op.alter_column('users', 'is_deleted', - existing_type=sa.BOOLEAN(), - nullable=True, - existing_server_default=sa.text('false')) - op.alter_column('users', 'deleted_at', - existing_type=postgresql.TIMESTAMP(timezone=True), - type_=sa.DateTime(), - existing_nullable=True) - op.alter_column('users', 'person_id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=True) - op.drop_index(op.f('idx_user_email_active_only'), table_name='users', postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') - op.drop_index(op.f('ix_users_is_deleted'), table_name='users') - op.drop_index(op.f('ix_users_person_id'), table_name='users') - op.create_index(op.f('ix_data_users_email'), 'users', ['email'], unique=True, schema='data') - op.drop_constraint(op.f('fk_users_person'), 'users', type_='foreignkey') - op.create_foreign_key(None, 'users', 'persons', ['person_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('users', 'previous_login_count') - op.drop_column('users', 'first_name') - op.drop_column('users', 'is_gdpr_deleted') - op.drop_column('users', 'verified_at') - op.drop_column('users', 'is_banned') - op.drop_column('users', 'birthday') - op.drop_column('users', 'last_name') - op.alter_column('vehicle_assignments', 'vehicle_id', - existing_type=sa.INTEGER(), - type_=sa.UUID(), - existing_nullable=False) - op.drop_constraint(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.drop_constraint(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.drop_constraint(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', type_='foreignkey') - op.create_foreign_key(None, 'vehicle_assignments', 'companies', ['company_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'vehicles', ['vehicle_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicle_assignments', 'users', ['driver_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_index(op.f('idx_vb_slug'), table_name='vehicle_brands') - op.drop_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', type_='unique') - op.create_unique_constraint(None, 'vehicle_brands', ['slug'], schema='data') - op.drop_constraint(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', type_='foreignkey') - op.drop_column('vehicle_brands', 'country_code') - op.drop_column('vehicle_brands', 'category_id') - op.drop_column('vehicle_brands', 'origin_country') - op.drop_index(op.f('idx_vehicle_company'), table_name='vehicles') - op.drop_index(op.f('idx_vehicle_plate'), table_name='vehicles') - op.drop_index(op.f('idx_vehicle_vin'), table_name='vehicles') - op.drop_constraint(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', type_='foreignkey') - op.drop_constraint(op.f('vehicles_current_company_id_fkey'), 'vehicles', type_='foreignkey') - op.drop_constraint(op.f('fk_vehicle_brand'), 'vehicles', type_='foreignkey') - op.create_foreign_key(None, 'vehicles', 'engine_specs', ['engine_spec_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicles', 'vehicle_brands', ['brand_id'], ['id'], source_schema='data', referent_schema='data') - op.create_foreign_key(None, 'vehicles', 'companies', ['current_company_id'], ['id'], source_schema='data', referent_schema='data') - op.drop_column('vehicles', 'custom_specs') - op.drop_column('vehicles', 'odometer_at_last_check') - op.drop_column('vehicles', 'factory_snapshot') - op.alter_column('wallets', 'id', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=False, - autoincrement=True) - op.alter_column('wallets', 'user_id', - existing_type=sa.INTEGER(), - nullable=True) - op.alter_column('wallets', 'xp_balance', - existing_type=sa.BIGINT(), - type_=sa.Integer(), - existing_nullable=True, - existing_server_default=sa.text('0')) - op.create_index(op.f('ix_data_wallets_id'), 'wallets', ['id'], unique=False, schema='data') - 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') - op.drop_column('wallets', 'updated_at') - op.drop_column('wallets', 'created_at') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('wallets', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) - op.add_column('wallets', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True)) - 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_index(op.f('ix_data_wallets_id'), table_name='wallets', schema='data') - op.alter_column('wallets', 'xp_balance', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=True, - existing_server_default=sa.text('0')) - op.alter_column('wallets', 'user_id', - existing_type=sa.INTEGER(), - nullable=False) - op.alter_column('wallets', 'id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=False, - autoincrement=True) - op.add_column('vehicles', sa.Column('factory_snapshot', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True)) - op.add_column('vehicles', sa.Column('odometer_at_last_check', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True)) - op.add_column('vehicles', sa.Column('custom_specs', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicles', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('fk_vehicle_brand'), 'vehicles', 'vehicle_brands', ['brand_id'], ['id']) - op.create_foreign_key(op.f('vehicles_current_company_id_fkey'), 'vehicles', 'companies', ['current_company_id'], ['id']) - op.create_foreign_key(op.f('vehicles_engine_spec_id_fkey'), 'vehicles', 'engine_specs', ['engine_spec_id'], ['id']) - op.create_index(op.f('idx_vehicle_vin'), 'vehicles', ['identification_number'], unique=False) - op.create_index(op.f('idx_vehicle_plate'), 'vehicles', ['license_plate'], unique=False) - op.create_index(op.f('idx_vehicle_company'), 'vehicles', ['current_company_id'], unique=False) - op.add_column('vehicle_brands', sa.Column('origin_country', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('vehicle_brands', sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.add_column('vehicle_brands', sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.create_foreign_key(op.f('vehicle_brands_category_id_fkey'), 'vehicle_brands', 'vehicle_categories', ['category_id'], ['id']) - op.drop_constraint(None, 'vehicle_brands', schema='data', type_='unique') - op.create_unique_constraint(op.f('vehicle_brands_cat_name_key'), 'vehicle_brands', ['category_id', 'name'], postgresql_nulls_not_distinct=False) - op.create_index(op.f('idx_vb_slug'), 'vehicle_brands', ['slug'], unique=True) - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.drop_constraint(None, 'vehicle_assignments', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('vehicle_assignments_driver_id_fkey'), 'vehicle_assignments', 'users', ['driver_id'], ['id']) - op.create_foreign_key(op.f('vehicle_assignments_vehicle_id_fkey'), 'vehicle_assignments', 'user_vehicles', ['vehicle_id'], ['id']) - op.create_foreign_key(op.f('vehicle_assignments_company_id_fkey'), 'vehicle_assignments', 'companies', ['company_id'], ['id']) - op.alter_column('vehicle_assignments', 'vehicle_id', - existing_type=sa.UUID(), - type_=sa.INTEGER(), - existing_nullable=False) - op.add_column('users', sa.Column('last_name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('birthday', sa.DATE(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('is_banned', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('verified_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('is_gdpr_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('first_name', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('users', sa.Column('previous_login_count', sa.INTEGER(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'users', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('fk_users_person'), 'users', 'persons', ['person_id'], ['id']) - op.drop_index(op.f('ix_data_users_email'), table_name='users', schema='data') - op.create_index(op.f('ix_users_person_id'), 'users', ['person_id'], unique=False) - op.create_index(op.f('ix_users_is_deleted'), 'users', ['is_deleted', 'deleted_at'], unique=False) - op.create_index(op.f('idx_user_email_active_only'), 'users', ['email'], unique=True, postgresql_where='((is_deleted IS FALSE) AND (deleted_at IS NULL))') - op.alter_column('users', 'person_id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=True) - op.alter_column('users', 'deleted_at', - existing_type=sa.DateTime(), - type_=postgresql.TIMESTAMP(timezone=True), - existing_nullable=True) - op.alter_column('users', 'is_deleted', - existing_type=sa.BOOLEAN(), - nullable=False, - existing_server_default=sa.text('false')) - op.alter_column('users', 'role', - existing_type=sa.Enum('ADMIN', 'USER', 'SERVICE', 'FLEET_MANAGER', name='userrole'), - type_=sa.VARCHAR(), - existing_nullable=True, - existing_server_default=sa.text("'user'::character varying")) - op.alter_column('users', 'hashed_password', - existing_type=sa.VARCHAR(), - nullable=True) - op.add_column('service_records', sa.Column('rating_impact_score', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('is_accident_repair', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('parts_quality_index', sa.NUMERIC(precision=3, scale=2), server_default=sa.text('1.0'), autoincrement=False, nullable=True)) - op.add_column('service_records', sa.Column('invoice_path', sa.TEXT(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') - op.drop_constraint(None, 'service_records', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('service_records_vehicle_id_fkey'), 'service_records', 'vehicles', ['vehicle_id'], ['id']) - op.create_foreign_key(op.f('service_records_provider_id_fkey'), 'service_records', 'service_providers', ['provider_id'], ['id']) - op.add_column('service_providers', sa.Column('specialized_brands', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'[]'::jsonb"), autoincrement=False, nullable=True)) - op.add_column('service_providers', sa.Column('verification_status', sa.VARCHAR(length=20), server_default=sa.text("'pending'::character varying"), autoincrement=False, nullable=True)) - op.add_column('service_providers', sa.Column('handled_vehicle_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'["passenger_car"]\'::jsonb'), autoincrement=False, nullable=True)) - op.drop_index(op.f('ix_data_service_providers_id'), table_name='service_providers', schema='data') - op.alter_column('service_providers', 'search_tags', - existing_type=sa.String(), - type_=sa.TEXT(), - existing_nullable=True) - op.add_column('persons', sa.Column('risk_level', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('reputation_score', sa.NUMERIC(precision=10, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=False)) - op.add_column('persons', sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'persons', schema='data', type_='unique') - op.drop_index(op.f('ix_data_persons_id'), table_name='persons', schema='data') - op.alter_column('persons', 'id', - existing_type=sa.Integer(), - type_=sa.BIGINT(), - existing_nullable=False, - autoincrement=True) - op.drop_column('persons', 'ice_contact') - op.drop_column('persons', 'medical_emergency') - op.drop_column('persons', 'identity_docs') - op.drop_column('persons', 'birth_date') - op.drop_column('persons', 'birth_place') - op.drop_column('persons', 'mothers_name') - op.drop_column('persons', 'first_name') - op.drop_column('persons', 'last_name') - op.drop_column('persons', 'id_uuid') - op.add_column('organizations', sa.Column('country_code', sa.CHAR(length=2), server_default=sa.text("'HU'::bpchar"), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('tax_number', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('ui_theme', sa.VARCHAR(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('founded_at', sa.DATE(), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('validation_status', postgresql.ENUM('NOT_VALIDATED', 'PENDING', 'VALIDATED', 'REJECTED', name='validationstatus'), autoincrement=False, nullable=True)) - op.add_column('organizations', sa.Column('theme', sa.VARCHAR(), server_default=sa.text("'system'::character varying"), autoincrement=False, nullable=True)) - 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_index(op.f('ix_data_organizations_tax_number'), 'organizations', ['tax_number'], unique=False) - op.create_index(op.f('idx_org_slug'), 'organizations', ['slug'], unique=True) - op.add_column('engine_specs', sa.Column('battery_capacity_kwh', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('onboard_charger_kw', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('default_service_interval_hours', sa.INTEGER(), server_default=sa.text('500'), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('phases', sa.INTEGER(), server_default=sa.text('3'), autoincrement=False, nullable=True)) - op.add_column('engine_specs', sa.Column('emissions_class', sa.VARCHAR(length=20), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'engine_specs', schema='data', type_='unique') - op.drop_index(op.f('ix_data_engine_specs_id'), table_name='engine_specs', schema='data') - op.create_index(op.f('idx_engine_code'), 'engine_specs', ['engine_code'], unique=False) - op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') - op.drop_constraint(None, 'company_members', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('company_members_user_id_fkey'), 'company_members', 'users', ['user_id'], ['id'], ondelete='CASCADE') - op.create_foreign_key(op.f('company_members_company_id_fkey'), 'company_members', 'companies', ['company_id'], ['id'], ondelete='CASCADE') - op.drop_index(op.f('ix_data_company_members_id'), table_name='company_members', schema='data') - op.alter_column('company_members', 'role', - existing_type=postgresql.ENUM('owner', 'manager', 'driver', name='companyrole', schema='data'), - type_=sa.VARCHAR(length=50), - nullable=True, - existing_server_default=sa.text("'driver'::companyrole")) - op.add_column('companies', sa.Column('owner_person_id', sa.BIGINT(), autoincrement=False, nullable=True)) - op.drop_constraint(None, 'companies', schema='data', type_='foreignkey') - op.create_foreign_key(op.f('companies_owner_id_fkey'), 'companies', 'users', ['owner_id'], ['id']) - op.create_foreign_key(op.f('fk_companies_owner_person'), 'companies', 'persons', ['owner_person_id'], ['id']) - op.drop_index(op.f('ix_data_companies_id'), table_name='companies', schema='data') - op.create_index(op.f('ix_companies_owner_person_id'), 'companies', ['owner_person_id'], unique=False) - op.create_table('point_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('action_key', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('point_rules_pkey')) - ) - op.create_index(op.f('ix_data_point_rules_id'), 'point_rules', ['id'], unique=False) - op.create_index(op.f('ix_data_point_rules_action_key'), 'point_rules', ['action_key'], unique=True) - op.create_table('user_stats', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('total_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('current_level', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('last_activity', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_stats_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_stats_pkey')), - sa.UniqueConstraint('user_id', name=op.f('user_stats_user_id_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_user_stats_id'), 'user_stats', ['id'], unique=False) - op.create_table('service_reviews', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('service_record_id', sa.UUID(), autoincrement=False, nullable=True), - sa.Column('is_anonymous', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('overall_stars', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('detailed_ratings', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"comm": 0, "tech": 0, "clean": 0, "price": 0}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('comment', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.CheckConstraint('overall_stars >= 1 AND overall_stars <= 5', name=op.f('service_reviews_overall_stars_check')), - sa.ForeignKeyConstraint(['provider_id'], ['service_providers.id'], name=op.f('service_reviews_provider_id_fkey')), - sa.ForeignKeyConstraint(['service_record_id'], ['service_records.id'], name=op.f('service_reviews_service_record_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('service_reviews_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_reviews_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', 'created_at', name=op.f('service_reviews_user_id_provider_id_created_at_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('translations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('lang_code', sa.VARCHAR(length=5), autoincrement=False, nullable=False), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('translations_pkey')), - sa.UniqueConstraint('key', 'lang_code', name=op.f('uq_translation_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_translations_lang_code'), 'translations', ['lang_code'], unique=False) - op.create_index(op.f('ix_data_translations_key'), 'translations', ['key'], unique=False) - op.create_index(op.f('ix_data_translations_id'), 'translations', ['id'], unique=False) - 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('fuel_stations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('location_city', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), - sa.Column('amenities', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"food": false, "shop": false, "car_wash": "none"}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('fuel_types', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text('\'{"diesel": true, "petrol_95": true, "ev_fast_charge": false}\'::jsonb'), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('fuel_stations_pkey')) - ) - op.create_table('user_vehicles', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vin', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('license_plate', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('variant_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('color', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('purchase_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.Column('purchase_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('current_odometer', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('extras', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('current_org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('is_deleted', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('tire_size_front', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('tire_size_rear', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('tire_dot_code', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('custom_service_interval_km', sa.INTEGER(), server_default=sa.text('20000'), autoincrement=False, nullable=True), - sa.Column('last_service_km', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.Column('vin_verified', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('vin_deadline', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['current_org_id'], ['organizations.id'], name=op.f('user_vehicles_current_org_id_fkey')), - sa.ForeignKeyConstraint(['variant_id'], ['vehicle_variants.id'], name=op.f('user_vehicles_variant_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicles_pkey')) - ) - op.create_index(op.f('ix_data_user_vehicles_vin'), 'user_vehicles', ['vin'], unique=True) - op.create_index(op.f('ix_data_user_vehicles_license_plate'), 'user_vehicles', ['license_plate'], unique=False) - op.create_index(op.f('ix_data_user_vehicles_id'), 'user_vehicles', ['id'], unique=False) - op.create_table('vehicle_categories', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_categories_pkey')), - sa.UniqueConstraint('name', name=op.f('vehicle_categories_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_vehicle_categories_id'), 'vehicle_categories', ['id'], unique=False) - op.create_index(op.f('idx_vc_slug'), 'vehicle_categories', ['slug'], unique=True) - op.create_table('user_badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('badge_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('earned_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['badge_id'], ['badges.id'], name=op.f('user_badges_badge_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_badges_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_badges_pkey')) - ) - op.create_index(op.f('ix_data_user_badges_id'), 'user_badges', ['id'], unique=False) - op.create_table('org_subscriptions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('valid_from', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.Column('valid_until', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('auto_renew', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('trial_ends_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('org_subscriptions_org_id_fkey')), - sa.ForeignKeyConstraint(['tier_id'], ['subscription_tiers.id'], name=op.f('org_subscriptions_tier_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('org_subscriptions_pkey')) - ) - op.create_table('email_provider_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('provider_type', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('priority', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('settings', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('fail_count', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('max_fail_threshold', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('success_rate', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_provider_configs_pkey')), - sa.UniqueConstraint('name', name=op.f('email_provider_configs_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_email_provider_configs_id'), 'email_provider_configs', ['id'], unique=False) - op.create_table('credit_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('rule_key', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('credit_rules_pkey')), - sa.UniqueConstraint('rule_key', name=op.f('credit_rules_rule_key_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('vehicle_expenses', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('vehicle_id', sa.UUID(), autoincrement=False, nullable=False), - sa.Column('category', postgresql.ENUM('PURCHASE_PRICE', 'TRANSFER_TAX', 'ADMIN_FEE', 'VEHICLE_TAX', 'INSURANCE', 'REFUELING', 'SERVICE', 'PARKING', 'TOLL', 'FINE', 'TUNING_ACCESSORIES', 'OTHER', name='expense_category_enum'), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), - sa.Column('odometer_value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], name=op.f('vehicle_expenses_vehicle_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_expenses_pkey')) - ) - op.create_table('vehicle_events', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('service_provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('event_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('odometer_reading', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('event_date', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['vehicle_id'], ['user_vehicles.id'], name=op.f('vehicle_events_vehicle_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_events_pkey')) - ) - op.create_table('organization_locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('label', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=10, scale=8), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=11, scale=8), autoincrement=False, nullable=True), - sa.Column('is_main_location', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_locations_organization_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('organization_locations_pkey')) - ) - op.create_table('email_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('sent_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('recipient', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('status', sa.VARCHAR(length=50), server_default=sa.text("'sent'::character varying"), autoincrement=False, nullable=True), - sa.Column('email_type', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_logs_pkey')) - ) - op.create_index(op.f('ix_data_email_logs_id'), 'email_logs', ['id'], unique=False) - op.create_index(op.f('ix_data_email_logs_email'), 'email_logs', ['email'], unique=False) - op.create_table('subscription_tiers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('rules', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('is_custom', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('subscription_tiers_pkey')), - sa.UniqueConstraint('name', name=op.f('subscription_tiers_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('email_providers', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column('priority', sa.INTEGER(), server_default=sa.text('1'), autoincrement=False, nullable=True), - sa.Column('provider_type', sa.VARCHAR(length=10), server_default=sa.text("'SMTP'::character varying"), autoincrement=False, nullable=True), - sa.Column('host', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('port', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('username', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('password_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.Column('daily_limit', sa.INTEGER(), server_default=sa.text('300'), autoincrement=False, nullable=True), - sa.Column('current_daily_usage', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_providers_pkey')), - sa.UniqueConstraint('name', name=op.f('unique_provider_name'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('legal_documents', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('title', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('version', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('language', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('legal_documents_pkey')) - ) - op.create_index(op.f('ix_data_legal_documents_id'), 'legal_documents', ['id'], unique=False) - op.create_table('locations', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('address', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('latitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), - sa.Column('longitude', sa.NUMERIC(precision=9, scale=6), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('locations_pkey')) - ) - op.create_table('credit_transactions', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('amount', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('credit_transactions_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('credit_transactions_pkey')) - ) - op.create_table('competitions', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('start_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('end_date', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('competitions_pkey')) - ) - op.create_table('service_specialties', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('parent_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('slug', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['parent_id'], ['service_specialties.id'], name=op.f('service_specialties_parent_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('service_specialties_pkey')), - sa.UniqueConstraint('slug', name=op.f('service_specialties_slug_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('legal_acceptances', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('document_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('accepted_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['document_id'], ['legal_documents.id'], name=op.f('legal_acceptances_document_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('legal_acceptances_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('legal_acceptances_pkey')) - ) - op.create_index(op.f('ix_data_legal_acceptances_id'), 'legal_acceptances', ['id'], unique=False) - op.create_table('badges', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('icon_url', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('badges_pkey')), - sa.UniqueConstraint('name', name=op.f('badges_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_badges_id'), 'badges', ['id'], unique=False) - op.create_table('subscription_notification_rules', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('subscription_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('days_before', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('template_key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('subscription_notification_rules_pkey')) - ) - op.create_table('vehicle_variants', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('engine_size', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('power_kw', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('spec_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('fuel_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('engine_code', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('cylinder_capacity', sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['model_id'], ['vehicle_models.id'], name=op.f('vehicle_variants_model_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_variants_pkey')) - ) - op.create_index(op.f('ix_data_vehicle_variants_id'), 'vehicle_variants', ['id'], unique=False) - op.create_table('referrals', - sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), - sa.Column('referrer_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('referee_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('commission_level', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('commission_percentage', sa.NUMERIC(precision=5, scale=2), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.CheckConstraint('commission_level = ANY (ARRAY[1, 2, 3])', name=op.f('referrals_commission_level_check')), - sa.ForeignKeyConstraint(['referee_id'], ['users.id'], name=op.f('referrals_referee_id_fkey')), - sa.ForeignKeyConstraint(['referrer_id'], ['users.id'], name=op.f('referrals_referrer_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('referrals_pkey')) - ) - op.create_table('user_credits', - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('balance', sa.NUMERIC(precision=15, scale=2), server_default=sa.text('0'), autoincrement=False, nullable=True), - sa.Column('updated_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_credits_user_id_fkey')), - sa.PrimaryKeyConstraint('user_id', name=op.f('user_credits_pkey')) - ) - op.create_table('system_settings', - sa.Column('key_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('value_json', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=False), - sa.Column('description', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('region_code', sa.VARCHAR(length=5), autoincrement=False, nullable=True), - sa.Column('tier_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('value', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('key_name', name=op.f('system_settings_pkey')) - ) - op.create_index(op.f('ix_data_system_settings_key'), 'system_settings', ['key_name'], unique=False) - op.create_index(op.f('idx_settings_lookup'), 'system_settings', ['key_name', sa.literal_column("COALESCE(region_code, ''::character varying)"), sa.literal_column('COALESCE(tier_id, 0)'), sa.literal_column('COALESCE(org_id, 0)')], unique=True) - op.create_table('organization_members', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('organization_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', postgresql.ENUM('OWNER', 'ADMIN', 'FLEET_MANAGER', 'DRIVER', 'owner', 'manager', 'driver', 'service', name='orguserrole'), autoincrement=False, nullable=True), - sa.Column('is_permanent', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], name=op.f('organization_members_org_id_fkey'), ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('organization_members_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('organization_members_pkey')), - sa.UniqueConstraint('organization_id', 'user_id', name=op.f('unique_user_org'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_organization_members_id'), 'organization_members', ['id'], unique=False) - op.create_table('equipment_items', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('category', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('equipment_items_pkey')) - ) - op.create_table('bot_discovery_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('category', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('brand_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('model_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('action_taken', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('discovered_at', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('bot_discovery_logs_pkey')) - ) - op.create_table('points_ledger', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('reason', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('points_ledger_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('points_ledger_pkey')) - ) - op.create_index(op.f('ix_data_points_ledger_id'), 'points_ledger', ['id'], unique=False) - op.create_table('email_templates', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('type', postgresql.ENUM('REGISTRATION', 'PASSWORD_RESET', 'GDPR_NOTICE', name='emailtype'), autoincrement=False, nullable=True), - sa.Column('subject', sa.VARCHAR(length=255), autoincrement=False, nullable=False), - sa.Column('body_html', sa.TEXT(), autoincrement=False, nullable=False), - sa.Column('key', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('lang', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('email_templates_pkey')), - sa.UniqueConstraint('key', 'lang', name=op.f('unique_email_key_lang'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_email_templates_type'), 'email_templates', ['type'], unique=True) - op.create_index(op.f('ix_data_email_templates_id'), 'email_templates', ['id'], unique=False) - op.create_table('vehicle_models', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('brand_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('year_start', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('year_end', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('slug', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['brand_id'], ['vehicle_brands.id'], name=op.f('vehicle_models_brand_id_fkey')), - sa.ForeignKeyConstraint(['category_id'], ['vehicle_categories.id'], name=op.f('vehicle_models_category_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_models_pkey')), - sa.UniqueConstraint('brand_id', 'name', name=op.f('vehicle_models_brand_name_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_vehicle_models_id'), 'vehicle_models', ['id'], unique=False) - op.create_index(op.f('idx_vm_slug'), 'vehicle_models', ['brand_id', 'slug'], unique=True) - op.create_table('user_scores', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('competition_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('points', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('last_updated', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['competition_id'], ['competitions.id'], name=op.f('user_scores_competition_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('user_scores_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('user_scores_pkey')), - sa.UniqueConstraint('user_id', 'competition_id', name=op.f('uq_user_competition_score'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('vehicle_ownership', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('role', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('license_plate', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('start_date', sa.DATE(), server_default=sa.text('CURRENT_DATE'), autoincrement=False, nullable=True), - sa.Column('end_date', sa.DATE(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('vehicle_ownership_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('vehicle_ownership_pkey')) - ) - op.create_index(op.f('ix_data_vehicle_ownership_id'), 'vehicle_ownership', ['id'], unique=False) - op.create_table('regional_settings', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('country_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('currency_code', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column('language_code', sa.CHAR(length=2), server_default=sa.text("'hu'::bpchar"), autoincrement=False, nullable=True), - sa.Column('is_eu_member', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name=op.f('regional_settings_pkey')), - sa.UniqueConstraint('country_code', name=op.f('regional_settings_country_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_regional_settings_id'), 'regional_settings', ['id'], unique=False) - op.create_table('verification_tokens', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('token', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('token_type', postgresql.ENUM('email_verify', 'password_reset', 'api_key', name='tokentype'), autoincrement=False, nullable=True), - sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), - sa.Column('token_hash', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('verification_tokens_user_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('verification_tokens_pkey')) - ) - op.create_index(op.f('uq_verification_tokens_token_hash'), 'verification_tokens', ['token_hash'], unique=True, postgresql_where='(token_hash IS NOT NULL)') - op.create_index(op.f('ix_verification_tokens_user'), 'verification_tokens', ['user_id', 'token_type', sa.literal_column('created_at DESC')], unique=False) - op.create_index(op.f('ix_verification_tokens_lookup'), 'verification_tokens', ['token_type', 'is_used', 'expires_at'], unique=False) - op.create_index(op.f('ix_data_verification_tokens_token'), 'verification_tokens', ['token'], unique=True) - op.create_index(op.f('ix_data_verification_tokens_id'), 'verification_tokens', ['id'], unique=False) - op.create_table('vouchers', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False), - sa.Column('code', sa.VARCHAR(length=20), autoincrement=False, nullable=False), - sa.Column('value', sa.NUMERIC(precision=15, scale=2), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.Column('batch_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True), - sa.Column('is_used', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=True), - sa.Column('used_by', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('used_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['used_by'], ['users.id'], name=op.f('vouchers_used_by_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('vouchers_pkey')), - sa.UniqueConstraint('code', name=op.f('vouchers_code_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('level_configs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('level_number', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('min_points', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('rank_name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('level_configs_pkey')), - sa.UniqueConstraint('level_number', name=op.f('level_configs_level_number_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_index(op.f('ix_data_level_configs_id'), 'level_configs', ['id'], unique=False) - op.create_table('audit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('action', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('endpoint', sa.VARCHAR(length=255), autoincrement=False, nullable=True), - sa.Column('method', sa.VARCHAR(length=10), autoincrement=False, nullable=True), - sa.Column('payload', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(length=45), autoincrement=False, nullable=True), - sa.Column('user_agent', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('audit_logs_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('audit_logs_pkey')) - ) - op.create_table('votes', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('provider_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('vote_value', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('votes_user_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('votes_pkey')), - sa.UniqueConstraint('user_id', 'provider_id', name=op.f('uq_user_provider_vote'), postgresql_include=[], postgresql_nulls_not_distinct=False) - ) - op.create_table('credit_logs', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('org_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('amount', sa.NUMERIC(precision=10, scale=2), autoincrement=False, nullable=True), - sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], name=op.f('credit_logs_org_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('credit_logs_pkey')) - ) - op.create_table('user_vehicle_equipment', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_vehicle_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('equipment_item_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('source', postgresql.ENUM('factory', 'aftermarket', name='equipment_source'), server_default=sa.text("'factory'::equipment_source"), autoincrement=False, nullable=True), - sa.Column('installed_at', sa.DATE(), autoincrement=False, nullable=True), - sa.Column('notes', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['equipment_item_id'], ['equipment_items.id'], name=op.f('user_vehicle_equipment_equipment_item_id_fkey')), - sa.ForeignKeyConstraint(['user_vehicle_id'], ['user_vehicles.id'], name=op.f('user_vehicle_equipment_user_vehicle_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('user_vehicle_equipment_pkey')) - ) - # ### end Alembic commands ###