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