FEAT: Corporate onboarding implemented with Tax ID validation (HU) and isolated NAS storage
This commit is contained in:
Binary file not shown.
@@ -1,10 +1,16 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.v1.endpoints import auth, catalog # <--- Hozzáadtuk a catalog-ot
|
from app.api.v1.endpoints import auth, catalog, assets, organizations
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
# Autentikáció és KYC végpontok
|
# Felhasználó és Identitás
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||||
|
|
||||||
# Jármű Katalógus és Robot kereső végpontok
|
# Katalógus és Jármű Robotok
|
||||||
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
api_router.include_router(catalog.router, prefix="/catalog", tags=["Vehicle Catalog"])
|
||||||
|
|
||||||
|
# Egyedi Eszközök (Assets)
|
||||||
|
api_router.include_router(assets.router, prefix="/assets", tags=["Assets"])
|
||||||
|
|
||||||
|
# Szervezetek és Onboarding
|
||||||
|
api_router.include_router(organizations.router, prefix="/organizations", tags=["Organizations"])
|
||||||
BIN
backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc
Normal file
BIN
backend/app/api/v1/endpoints/__pycache__/assets.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
104
backend/app/api/v1/endpoints/assets.py
Normal file
104
backend/app/api/v1/endpoints/assets.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.db.session import get_db
|
||||||
|
from app.schemas.asset import AssetCreate, AssetResponse
|
||||||
|
from app.models.vehicle import Asset, VehicleCatalog
|
||||||
|
from app.models.organization import Organization
|
||||||
|
from app.core.validators import VINValidator
|
||||||
|
from app.core.config import settings
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@router.post("/", response_model=AssetResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_asset(
|
||||||
|
asset_in: AssetCreate,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
# Később ide jön: current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Új jármű (Asset) rögzítése a flottába.
|
||||||
|
- Validálja a VIN-t (MOD 11 checksum).
|
||||||
|
- Ellenőrzi a Katalógus elemet.
|
||||||
|
- Létrehozza a NAS mappát a dokumentumoknak.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. VIN Validáció (Szigorú Checksum)
|
||||||
|
# A GEM protokoll szerint kötelező a validátor használata
|
||||||
|
is_valid_vin = VINValidator.validate(asset_in.vin)
|
||||||
|
if not is_valid_vin:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Érvénytelen alvázszám (VIN)! A Checksum ellenőrzés sikertelen."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Katalógus elem ellenőrzése
|
||||||
|
stmt_catalog = select(VehicleCatalog).where(VehicleCatalog.id == asset_in.catalog_id)
|
||||||
|
result_catalog = await db.execute(stmt_catalog)
|
||||||
|
catalog_item = result_catalog.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not catalog_item:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A kiválasztott járműtípus nem található a katalógusban."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Szervezet ellenőrzése (Létezik-e, és van-e joga - jogultságkezelés később)
|
||||||
|
stmt_org = select(Organization).where(Organization.id == asset_in.organization_id)
|
||||||
|
result_org = await db.execute(stmt_org)
|
||||||
|
org_item = result_org.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not org_item:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="A megadott flotta/szervezet nem található."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Asset Duplikáció ellenőrzése (Ugyanaz a VIN nem szerepelhet 2x aktívként)
|
||||||
|
# Megj: A UAI mező tárolja a VIN-t (Unique Asset Identifier)
|
||||||
|
stmt_exist = select(Asset).where(
|
||||||
|
Asset.uai == asset_in.vin.upper(),
|
||||||
|
Asset.status != "deleted"
|
||||||
|
)
|
||||||
|
result_exist = await db.execute(stmt_exist)
|
||||||
|
if result_exist.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Ez a jármű (VIN) már szerepel a rendszerben!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Mentés az adatbázisba
|
||||||
|
new_asset = Asset(
|
||||||
|
uai=asset_in.vin.upper(), # A VIN a fő azonosító
|
||||||
|
catalog_id=asset_in.catalog_id,
|
||||||
|
organization_id=asset_in.organization_id,
|
||||||
|
asset_type=catalog_item.category, # 'car', 'van', 'motorcycle', etc.
|
||||||
|
name=asset_in.name or f"{catalog_item.brand} {catalog_item.model}",
|
||||||
|
current_plate_number=asset_in.license_plate.upper(),
|
||||||
|
status="active",
|
||||||
|
privacy_level="private" # Alapértelmezett
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_asset)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(new_asset)
|
||||||
|
|
||||||
|
# 6. NAS Tároló Létrehozása
|
||||||
|
# Útvonal: /mnt/nas/app_data/assets/{uuid}/
|
||||||
|
# A settings.NAS_STORAGE_PATH-ot használjuk (GEM protokoll)
|
||||||
|
try:
|
||||||
|
# Ha a settings-ben nincs definiálva, fallback a hardcoded path-ra (biztonsági háló)
|
||||||
|
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data/assets")
|
||||||
|
asset_path = os.path.join(base_path, str(new_asset.id))
|
||||||
|
|
||||||
|
os.makedirs(asset_path, exist_ok=True)
|
||||||
|
logger.info(f"NAS mappa létrehozva: {asset_path}")
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"CRITICAL: Nem sikerült létrehozni a NAS mappát: {e}")
|
||||||
|
# Nem dobunk hibát a usernek, mert az adatbázisba bekerült, de riasztunk logban
|
||||||
|
|
||||||
|
return new_asset
|
||||||
70
backend/app/api/v1/endpoints/organizations.py
Normal file
70
backend/app/api/v1/endpoints/organizations.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.db.session import get_db
|
||||||
|
from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse
|
||||||
|
from app.models.organization import Organization, OrgType
|
||||||
|
from app.core.config import settings
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@router.post("/onboard", response_model=CorpOnboardResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def onboard_organization(
|
||||||
|
org_in: CorpOnboardIn,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Új szervezet (cég/szerviz) rögzítése.
|
||||||
|
- Magyar adószám validáció (XXXXXXXX-Y-ZZ).
|
||||||
|
- Duplikáció ellenőrzés adószám alapján.
|
||||||
|
- NAS mappa és DB rekord létrehozása.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. Magyar adószám validáció
|
||||||
|
if org_in.country_code == "HU":
|
||||||
|
if not re.match(r"^\d{8}-\d-\d{2}$", org_in.tax_number):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Érvénytelen magyar adószám formátum! (Példa: 12345678-1-12)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Duplikáció ellenőrzés
|
||||||
|
stmt_exist = select(Organization).where(Organization.tax_number == org_in.tax_number)
|
||||||
|
result_exist = await db.execute(stmt_exist)
|
||||||
|
if result_exist.scalar_one_or_none():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Ezzel az adószámmal már regisztráltak céget!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Mentés (Dinamikus státusszal és kisbetűs Enummal)
|
||||||
|
new_org = Organization(
|
||||||
|
name=org_in.name,
|
||||||
|
tax_number=org_in.tax_number,
|
||||||
|
reg_number=org_in.reg_number,
|
||||||
|
headquarters_address=org_in.headquarters_address,
|
||||||
|
country_code=org_in.country_code,
|
||||||
|
org_type=OrgType.business, # Most már kisbetűs 'business' kerül beküldésre
|
||||||
|
status="pending_verification"
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_org)
|
||||||
|
await db.flush() # ID generálás a NAS-hoz
|
||||||
|
|
||||||
|
# 4. NAS Mappa létrehozása
|
||||||
|
try:
|
||||||
|
base_path = getattr(settings, "NAS_STORAGE_PATH", "/mnt/nas/app_data")
|
||||||
|
org_path = os.path.join(base_path, "organizations", str(new_org.id))
|
||||||
|
os.makedirs(org_path, exist_ok=True)
|
||||||
|
logger.info(f"NAS mappa létrehozva szervezetnek: {org_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"NAS hiba az onboardingnál: {e}")
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(new_org)
|
||||||
|
|
||||||
|
return {"organization_id": new_org.id, "status": new_org.status}
|
||||||
Binary file not shown.
BIN
backend/app/core/__pycache__/validators.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/validators.cpython-312.pyc
Normal file
Binary file not shown.
47
backend/app/core/validators.py
Normal file
47
backend/app/core/validators.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
class VINValidator:
|
||||||
|
@staticmethod
|
||||||
|
def validate(vin: str) -> bool:
|
||||||
|
"""VIN (Vehicle Identification Number) ellenőrzése ISO 3779 szerint."""
|
||||||
|
vin = vin.upper().strip()
|
||||||
|
|
||||||
|
# Alapvető formátum: 17 karakter, tiltott betűk (I, O, Q) nélkül
|
||||||
|
if not re.match(r"^[A-Z0-9]{17}$", vin) or any(c in vin for c in "IOQ"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Karakterértékek táblázata
|
||||||
|
values = {
|
||||||
|
'A':1, 'B':2, 'C':3, 'D':4, 'E':5, 'F':6, 'G':7, 'H':8, 'J':1, 'K':2, 'L':3, 'M':4,
|
||||||
|
'N':5, 'P':7, 'R':9, 'S':2, 'T':3, 'U':4, 'V':5, 'W':6, 'X':7, 'Y':8, 'Z':9,
|
||||||
|
'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9
|
||||||
|
}
|
||||||
|
|
||||||
|
# Súlyozás a pozíciók alapján
|
||||||
|
weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Összegzés: érték * súly
|
||||||
|
total = sum(values[vin[i]] * weights[i] for i in range(17))
|
||||||
|
|
||||||
|
# 2. Maradék számítás 11-el
|
||||||
|
check_digit = total % 11
|
||||||
|
|
||||||
|
# 3. A 10-es maradékot 'X'-nek jelöljük
|
||||||
|
expected = 'X' if check_digit == 10 else str(check_digit)
|
||||||
|
|
||||||
|
# 4. Összevetés a 9. karakterrel (index 8)
|
||||||
|
return vin[8] == expected
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_factory_data(vin: str) -> dict:
|
||||||
|
"""Kinyeri az alapadatokat a VIN-ből (WMI, Évjárat, Gyártó ország)."""
|
||||||
|
# Ez a 'Mágikus Gomb' alapja
|
||||||
|
countries = {"1": "USA", "2": "Kanada", "J": "Japán", "W": "Németország", "S": "Anglia"}
|
||||||
|
return {
|
||||||
|
"country": countries.get(vin[0], "Ismeretlen"),
|
||||||
|
"year_code": vin[9], # Modellév kódja
|
||||||
|
"wmi": vin[0:3] # World Manufacturer Identifier
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,14 +1,18 @@
|
|||||||
import enum
|
import enum
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, Enum, DateTime, ForeignKey
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, JSON
|
||||||
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
|
|
||||||
class OrgType(str, enum.Enum):
|
class OrgType(str, enum.Enum):
|
||||||
INDIVIDUAL = "individual"
|
# A tagok neveit kisbetűre állítjuk, hogy egyezzenek a Postgres Enum értékekkel
|
||||||
SERVICE = "service"
|
individual = "individual"
|
||||||
FLEET_OWNER = "fleet_owner"
|
service = "service"
|
||||||
CLUB = "club"
|
service_provider = "service_provider"
|
||||||
|
fleet_owner = "fleet_owner"
|
||||||
|
club = "club"
|
||||||
|
business = "business"
|
||||||
|
|
||||||
class Organization(Base):
|
class Organization(Base):
|
||||||
__tablename__ = "organizations"
|
__tablename__ = "organizations"
|
||||||
@@ -16,24 +20,38 @@ class Organization(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
# A stabilitás miatt VARCHAR-ként kezeljük a DB-ben
|
|
||||||
org_type = Column(String(50), default="individual")
|
|
||||||
|
|
||||||
# A flotta technikai tulajdonosa (User)
|
# PG_ENUM használata a Python Enum-mal szinkronizálva
|
||||||
|
org_type = Column(
|
||||||
|
PG_ENUM(OrgType, name="orgtype", inherit_schema=True),
|
||||||
|
default=OrgType.individual
|
||||||
|
)
|
||||||
|
|
||||||
|
tax_number = Column(String(20), unique=True, index=True)
|
||||||
|
reg_number = Column(String(50))
|
||||||
|
headquarters_address = Column(String(255))
|
||||||
|
country_code = Column(String(2), default="HU")
|
||||||
|
|
||||||
|
status = Column(String(30), default="pending_verification")
|
||||||
|
is_deleted = Column(Boolean, default=False)
|
||||||
|
|
||||||
|
notification_settings = Column(JSON, default={
|
||||||
|
"notify_owner": True,
|
||||||
|
"notify_manager": True,
|
||||||
|
"notify_contact": True,
|
||||||
|
"alert_days_before": [30, 15, 7, 1]
|
||||||
|
})
|
||||||
|
external_integration_config = Column(JSON, default={})
|
||||||
|
|
||||||
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
owner_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
|
||||||
|
|
||||||
# Üzleti szabályok
|
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_transferable = Column(Boolean, default=True)
|
is_transferable = Column(Boolean, default=True)
|
||||||
|
|
||||||
# Verifikáció
|
|
||||||
is_verified = Column(Boolean, default=False)
|
is_verified = Column(Boolean, default=False)
|
||||||
verification_expires_at = Column(DateTime(timezone=True), nullable=True)
|
verification_expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
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 (Asset-re hivatkozunk a Vehicle helyett)
|
|
||||||
assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan")
|
assets = relationship("Asset", back_populates="organization", cascade="all, delete-orphan")
|
||||||
members = relationship("OrganizationMember", back_populates="organization")
|
members = relationship("OrganizationMember", back_populates="organization")
|
||||||
owner = relationship("User", back_populates="owned_organizations")
|
owner = relationship("User", back_populates="owned_organizations")
|
||||||
@@ -48,5 +66,5 @@ class OrganizationMember(Base):
|
|||||||
|
|
||||||
organization = relationship("Organization", back_populates="members")
|
organization = relationship("Organization", back_populates="members")
|
||||||
|
|
||||||
# --- EZT A SORT TEDD KÍVÜLRE, A MARGÓRA ---
|
# Kompatibilitási réteg a korábbi kódokhoz
|
||||||
Organization.vehicles = Organization.assets
|
Organization.vehicles = Organization.assets
|
||||||
@@ -20,15 +20,17 @@ class VehicleCatalog(Base):
|
|||||||
engine_type = Column(String(50))
|
engine_type = Column(String(50))
|
||||||
engine_power_kw = Column(Integer)
|
engine_power_kw = Column(Integer)
|
||||||
|
|
||||||
# --- EZ A SOR HIÁNYZOTT ---
|
# Robot státusz és gyári adatok
|
||||||
verification_status = Column(String(20), default="verified")
|
verification_status = Column(String(20), default="verified")
|
||||||
|
|
||||||
factory_specs = Column(JSON, default={})
|
factory_specs = Column(JSON, default={})
|
||||||
maintenance_plan = Column(JSON, default={})
|
maintenance_plan = Column(JSON, default={})
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
|
# Kapcsolat az egyedi példányok felé
|
||||||
|
assets = relationship("Asset", back_populates="catalog_entry")
|
||||||
|
|
||||||
# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona
|
# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona
|
||||||
class Asset(Base):
|
class Asset(Base):
|
||||||
__tablename__ = "assets"
|
__tablename__ = "assets"
|
||||||
@@ -52,13 +54,16 @@ class Asset(Base):
|
|||||||
factory_config = Column(JSON, default={})
|
factory_config = Column(JSON, default={})
|
||||||
aftermarket_mods = Column(JSON, default={})
|
aftermarket_mods = Column(JSON, default={})
|
||||||
|
|
||||||
|
# Állapot és láthatóság (EZ HIÁNYZOTT)
|
||||||
status = Column(String(50), default="active")
|
status = Column(String(50), default="active")
|
||||||
|
privacy_level = Column(String(20), default="private")
|
||||||
|
|
||||||
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
|
# Kapcsolatok
|
||||||
organization = relationship("Organization", back_populates="assets")
|
organization = relationship("Organization", back_populates="assets")
|
||||||
catalog_entry = relationship("VehicleCatalog")
|
catalog_entry = relationship("VehicleCatalog", back_populates="assets")
|
||||||
events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan")
|
events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan")
|
||||||
ratings = relationship("AssetRating", back_populates="asset")
|
ratings = relationship("AssetRating", back_populates="asset")
|
||||||
|
|
||||||
|
|||||||
BIN
backend/app/schemas/__pycache__/asset.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/asset.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/organization.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/organization.cpython-312.pyc
Normal file
Binary file not shown.
27
backend/app/schemas/asset.py
Normal file
27
backend/app/schemas/asset.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class AssetCreate(BaseModel):
|
||||||
|
catalog_id: int = Field(..., description="A kiválasztott katalógus elem ID-ja")
|
||||||
|
vin: str = Field(..., min_length=17, max_length=17, description="17 karakteres alvázszám")
|
||||||
|
license_plate: str = Field(..., min_length=1, max_length=20)
|
||||||
|
name: Optional[str] = Field(None, description="Egyedi elnevezés (pl. 'Céges furgon')")
|
||||||
|
organization_id: int = Field(..., description="Melyik flottába kerüljön")
|
||||||
|
|
||||||
|
# Opcionális: Kezdő km óra állás, szín, stb. később bővíthető
|
||||||
|
|
||||||
|
class AssetResponse(BaseModel):
|
||||||
|
id: UUID
|
||||||
|
uai: str
|
||||||
|
catalog_id: int
|
||||||
|
organization_id: int
|
||||||
|
name: str
|
||||||
|
asset_type: str
|
||||||
|
current_plate_number: str
|
||||||
|
status: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
20
backend/app/schemas/organization.py
Normal file
20
backend/app/schemas/organization.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
class ContactCreate(BaseModel):
|
||||||
|
full_name: str
|
||||||
|
email: str
|
||||||
|
phone: Optional[str]
|
||||||
|
contact_type: str = "primary"
|
||||||
|
|
||||||
|
class CorpOnboardIn(BaseModel):
|
||||||
|
name: str
|
||||||
|
tax_number: str
|
||||||
|
country_code: str = "HU"
|
||||||
|
reg_number: Optional[str]
|
||||||
|
headquarters_address: str
|
||||||
|
contacts: Optional[List[ContactCreate]] = []
|
||||||
|
|
||||||
|
class CorpOnboardResponse(BaseModel):
|
||||||
|
organization_id: int
|
||||||
|
status: str = "pending_verification"
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from .harvester_robot import CarHarvester # A korábbi CarHarvester-t nevezzük át így
|
from datetime import datetime
|
||||||
|
# Frissített importok az új fájlnevekhez:
|
||||||
|
from .harvester_cars import CarHarvester
|
||||||
from .harvester_bikes import BikeHarvester
|
from .harvester_bikes import BikeHarvester
|
||||||
from .harvester_trucks import TruckHarvester
|
from .harvester_trucks import TruckHarvester
|
||||||
|
|
||||||
@@ -7,6 +9,8 @@ class RobotManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def run_full_sync(db):
|
async def run_full_sync(db):
|
||||||
"""Sorban lefuttatja az összes robotot."""
|
"""Sorban lefuttatja az összes robotot."""
|
||||||
|
print(f"🕒 Szinkronizáció indítva: {datetime.now()}")
|
||||||
|
|
||||||
robots = [
|
robots = [
|
||||||
CarHarvester(),
|
CarHarvester(),
|
||||||
BikeHarvester(),
|
BikeHarvester(),
|
||||||
@@ -15,8 +19,22 @@ class RobotManager:
|
|||||||
|
|
||||||
for robot in robots:
|
for robot in robots:
|
||||||
try:
|
try:
|
||||||
await robot.run(db)
|
# Itt a robot lekéri az API-tól az ÖSSZES márkát és frissít
|
||||||
# Pihenő a kategóriák között, hogy ne kapjunk IP-tiltást
|
await robot.run(db)
|
||||||
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}")
|
print(f"❌ Hiba a {robot.category} robotnál: {e}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def schedule_nightly_run(db):
|
||||||
|
"""
|
||||||
|
Egyszerű ciklus, ami figyeli az időt.
|
||||||
|
Ha éjjel 2 óra van, elindítja a teljes szinkront.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
now = datetime.now()
|
||||||
|
# Ha hajnali 2 és 2:01 között vagyunk, indítás
|
||||||
|
if now.hour == 2 and now.minute == 0:
|
||||||
|
await RobotManager.run_full_sync(db)
|
||||||
|
await asyncio.sleep(70) # Várunk, hogy ne induljon el többször ugyanabban a percben
|
||||||
|
await asyncio.sleep(30) # 30 másodpercenként ellenőrizzük az időt
|
||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./alembic.ini:/app/alembic.ini
|
- ./alembic.ini:/app/alembic.ini
|
||||||
- ./migrations:/app/migrations
|
- ./migrations:/app/migrations
|
||||||
|
- /mnt/nas/app_data:/mnt/nas/app_data
|
||||||
environment:
|
environment:
|
||||||
PYTHONPATH: /app
|
PYTHONPATH: /app
|
||||||
DATABASE_URL: ${MIGRATION_DATABASE_URL}
|
DATABASE_URL: ${MIGRATION_DATABASE_URL}
|
||||||
|
|||||||
@@ -96,4 +96,14 @@ A rendszer fel van készítve az EU-s piacra:
|
|||||||
- **Person:** Egyéni teljesítmény, megbízhatóság.
|
- **Person:** Egyéni teljesítmény, megbízhatóság.
|
||||||
- **Service (Szerviz):** Szolgáltatási minőség.
|
- **Service (Szerviz):** Szolgáltatási minőség.
|
||||||
- **Vehicle (Jármű):** Műszaki állapot és előélet.
|
- **Vehicle (Jármű):** Műszaki állapot és előélet.
|
||||||
- *Megjegyzés:* A Cég (mint flotta) nem kap önálló értékelést, a hírneve a tagjai és járművei minősítéséből adódik össze.
|
- *Megjegyzés:* A Cég (mint flotta) nem kap önálló értékelést, a hírneve a tagjai és járművei minősítéséből adódik össze.
|
||||||
|
|
||||||
|
## 4. CRM és Szervezeti Kapcsolattartók
|
||||||
|
A `data.organization_contacts` tábla felelős a flottákhoz tartozó humán kapcsolattartók kezeléséért.
|
||||||
|
- **Dinamikus beállítások:** A `data.organizations` tábla `notification_settings` (JSONB) mezője szabályozza, ki és mikor kapjon értesítést.
|
||||||
|
- **Külső szinkron:** Az `external_crm_id` biztosítja a kapcsolatot külső vállalatirányítási rendszerekkel (API-n keresztül).
|
||||||
|
|
||||||
|
## 4.1 Szervezeti és CRM Adatmodell
|
||||||
|
- **data.organizations**: Bővítve `tax_number`, `reg_number`, `headquarters_address` és `is_deleted` mezőkkel.
|
||||||
|
- **data.organization_contacts**: Új tábla a Mini-CRM funkciókhoz (kapcsolattartók típus szerint: billing, primary, operational).
|
||||||
|
- **Audit**: Minden státuszmódosítás és adatváltozás snapshot-olva az `audit_logs` táblába.
|
||||||
@@ -158,4 +158,10 @@ PASSWORD_RESET_TOKEN_EXPIRE_HOURS=1
|
|||||||
EMAIL_PROVIDER=sendgrid
|
EMAIL_PROVIDER=sendgrid
|
||||||
EMAILS_FROM_EMAIL=info@profibot.hu
|
EMAILS_FROM_EMAIL=info@profibot.hu
|
||||||
EMAILS_FROM_NAME='Profibot Service Finder'
|
EMAILS_FROM_NAME='Profibot Service Finder'
|
||||||
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
|
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
## 4. Corporate Onboarding (Céges regisztráció)
|
||||||
|
A folyamat célja, hogy egy már létező Person (vagy új User) saját szervezetet (Organization) alapítson.
|
||||||
|
- **Többszintű validáció:** Kötelező adószám (VAT/Tax ID) ellenőrzés (HU esetén formátum + VIES API).
|
||||||
|
- **Hierarchia:** A regisztráló automatikusan `owner` rangot kap.
|
||||||
|
- **Izoláció:** Minden cég saját mappastruktúrát kap a NAS-on az okmányok izolált kezelése érdekében.
|
||||||
@@ -145,4 +145,11 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
|
|||||||
### ✨ Multi-Robot System
|
### ✨ Multi-Robot System
|
||||||
- **Kategória Robotok:** Elkészült a Car, Bike és Truck harvester moduláris szerkezete.
|
- **Kategória Robotok:** Elkészült a Car, Bike és Truck harvester moduláris szerkezete.
|
||||||
- **Robot Manager:** Központi vezérlő az ütemezett és sorrendi adatgyűjtéshez.
|
- **Robot Manager:** Központi vezérlő az ütemezett és sorrendi adatgyűjtéshez.
|
||||||
- **Katalógus Kereső:** Üzembe helyezve a `/catalog/search` végpont a Swaggerben.
|
- **Katalógus Kereső:** Üzembe helyezve a `/catalog/search` végpont a Swaggerben.
|
||||||
|
|
||||||
|
## [0.4.5] - 2026-02-07
|
||||||
|
### ✨ Asset Management & Infrastructure
|
||||||
|
- **Asset Endpoint:** `POST /api/v1/assets/` élesítve VIN validációval.
|
||||||
|
- **NAS Integration:** Automata mappastruktúra létrehozása az eszközöknek (`/assets/{uuid}`).
|
||||||
|
- **Data Model:** `privacy_level` és `status` mezők hozzáadva az Asset modellhez.
|
||||||
|
- **Bugfix:** SQLAlchemy `TypeError` javítva a modell és a séma szinkronizálásával.
|
||||||
@@ -32,4 +32,35 @@ Minden módosítás előtt a rendszer menti az aktuális rekord állapotát (JSO
|
|||||||
|
|
||||||
## 5. Adminisztrátori Meghívók
|
## 5. Adminisztrátori Meghívók
|
||||||
- Adminisztrátort csak kézi meghívóval lehet felvenni.
|
- Adminisztrátort csak kézi meghívóval lehet felvenni.
|
||||||
- **Lejárati idő:** Minden admin meghívó token 24 óráig érvényes.
|
- **Lejárati idő:** Minden admin meghívó token 24 óráig érvényes.
|
||||||
|
|
||||||
|
## 6. Értesítési Engine és Lejárati Figyelmeztetések
|
||||||
|
|
||||||
|
A rendszer proaktív figyelmeztető rendszert alkalmaz minden előfizetői szinten (Individual és Corporate egyaránt).
|
||||||
|
|
||||||
|
### A) Előfizetés és Pénzügyi Értesítések
|
||||||
|
- **Hatókör:** Minden fizetős csomag (Lite+, VIP, VIP+, Corporate).
|
||||||
|
- **Logika:** Automatikus értesítés küldése 30, 15, 7 és 1 nappal a csomag lejárta előtt.
|
||||||
|
- **Csatornák:** Push notification, Email és a Mini-CRM kontakt személyek értesítése.
|
||||||
|
|
||||||
|
### B) Jármű Okmányok és Technikai Lejáratok
|
||||||
|
A rendszer figyeli az eszközökhöz rögzített metaadatokat:
|
||||||
|
- **Forgalmi engedély:** Műszaki vizsga lejárata.
|
||||||
|
- **Biztosítás:** Kötelező (KGFB) és CASCO fordulónapok.
|
||||||
|
- **Lízing/Szerződés:** Szerződéses futamidő vége.
|
||||||
|
- **Okmányok:** Hajólevél, lajstrom, emelőgép vizsga stb.
|
||||||
|
|
||||||
|
### C) CRM Kontaktok és Kapcsolattartás
|
||||||
|
Minden szervezet (Organization) esetében kötelező megadni legalább egy **Adminisztratív Kontaktot**.
|
||||||
|
- **Több cég kezelése:** Egy Person több szervezetben is betölthet `owner` vagy `fleet_manager` szerepkört.
|
||||||
|
- **CRM Mezők:** Név, beosztás, közvetlen elérhetőség (fizetésért felelős, operatív felelős).
|
||||||
|
|
||||||
|
## 7. Corporate Onboarding és Validációs Szintek
|
||||||
|
A cégek rögzítése háromlépcsős ellenőrzésen esik át:
|
||||||
|
1. **Tier 1 (Automata):** Adószám alapú validáció (HU/VIES API).
|
||||||
|
2. **Tier 2 (AI/OCR):** Feltöltött dokumentumok (Alapító okirat) intelligens elemzése.
|
||||||
|
3. **Tier 3 (Human):** Adminisztrátori jóváhagyás (L2/L3 szint), ha az automata folyamat bizonytalan.
|
||||||
|
|
||||||
|
## 8. B2B Jutalék és MLM Kivételek
|
||||||
|
- **Direct Referral:** Cég által meghívott másik cég esetén csak 1. szintű (L1) jutalék jár.
|
||||||
|
- **MLM Korlát:** Szervezetek nem építhetnek többszintű hálózatot, a kifizetés fix üzleti megállapodás alapú.
|
||||||
Reference in New Issue
Block a user