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 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()
|
||||
|
||||
# Autentikáció és KYC végpontok
|
||||
# Felhasználó és Identitás
|
||||
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"])
|
||||
|
||||
# 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
|
||||
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.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class OrgType(str, enum.Enum):
|
||||
INDIVIDUAL = "individual"
|
||||
SERVICE = "service"
|
||||
FLEET_OWNER = "fleet_owner"
|
||||
CLUB = "club"
|
||||
# A tagok neveit kisbetűre állítjuk, hogy egyezzenek a Postgres Enum értékekkel
|
||||
individual = "individual"
|
||||
service = "service"
|
||||
service_provider = "service_provider"
|
||||
fleet_owner = "fleet_owner"
|
||||
club = "club"
|
||||
business = "business"
|
||||
|
||||
class Organization(Base):
|
||||
__tablename__ = "organizations"
|
||||
@@ -16,24 +20,38 @@ class Organization(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
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)
|
||||
|
||||
# Üzleti szabályok
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_transferable = Column(Boolean, default=True)
|
||||
|
||||
# Verifikáció
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verification_expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=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")
|
||||
members = relationship("OrganizationMember", back_populates="organization")
|
||||
owner = relationship("User", back_populates="owned_organizations")
|
||||
@@ -48,5 +66,5 @@ class OrganizationMember(Base):
|
||||
|
||||
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
|
||||
@@ -20,15 +20,17 @@ class VehicleCatalog(Base):
|
||||
engine_type = Column(String(50))
|
||||
engine_power_kw = Column(Integer)
|
||||
|
||||
# --- EZ A SOR HIÁNYZOTT ---
|
||||
# Robot státusz és gyári adatok
|
||||
verification_status = Column(String(20), default="verified")
|
||||
|
||||
factory_specs = Column(JSON, default={})
|
||||
maintenance_plan = Column(JSON, default={})
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolat az egyedi példányok felé
|
||||
assets = relationship("Asset", back_populates="catalog_entry")
|
||||
|
||||
# 2. EGYEDI ESZKÖZ (Asset) - A felhasználó tulajdona
|
||||
class Asset(Base):
|
||||
__tablename__ = "assets"
|
||||
@@ -52,13 +54,16 @@ class Asset(Base):
|
||||
factory_config = Column(JSON, default={})
|
||||
aftermarket_mods = Column(JSON, default={})
|
||||
|
||||
# Állapot és láthatóság (EZ HIÁNYZOTT)
|
||||
status = Column(String(50), default="active")
|
||||
privacy_level = Column(String(20), default="private")
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
organization = relationship("Organization", back_populates="assets")
|
||||
catalog_entry = relationship("VehicleCatalog")
|
||||
catalog_entry = relationship("VehicleCatalog", back_populates="assets")
|
||||
events = relationship("AssetEvent", back_populates="asset", cascade="all, delete-orphan")
|
||||
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
|
||||
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_trucks import TruckHarvester
|
||||
|
||||
@@ -7,6 +9,8 @@ class RobotManager:
|
||||
@staticmethod
|
||||
async def run_full_sync(db):
|
||||
"""Sorban lefuttatja az összes robotot."""
|
||||
print(f"🕒 Szinkronizáció indítva: {datetime.now()}")
|
||||
|
||||
robots = [
|
||||
CarHarvester(),
|
||||
BikeHarvester(),
|
||||
@@ -15,8 +19,22 @@ class RobotManager:
|
||||
|
||||
for robot in robots:
|
||||
try:
|
||||
# Itt a robot lekéri az API-tól az ÖSSZES márkát és frissít
|
||||
await robot.run(db)
|
||||
# Pihenő a kategóriák között, hogy ne kapjunk IP-tiltást
|
||||
await asyncio.sleep(5)
|
||||
except Exception as 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
|
||||
- ./alembic.ini:/app/alembic.ini
|
||||
- ./migrations:/app/migrations
|
||||
- /mnt/nas/app_data:/mnt/nas/app_data
|
||||
environment:
|
||||
PYTHONPATH: /app
|
||||
DATABASE_URL: ${MIGRATION_DATABASE_URL}
|
||||
|
||||
@@ -97,3 +97,13 @@ A rendszer fel van készítve az EU-s piacra:
|
||||
- **Service (Szerviz):** Szolgáltatási minőség.
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
@@ -159,3 +159,9 @@ EMAIL_PROVIDER=sendgrid
|
||||
EMAILS_FROM_EMAIL=info@profibot.hu
|
||||
EMAILS_FROM_NAME='Profibot Service Finder'
|
||||
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.
|
||||
@@ -146,3 +146,10 @@ A fejlesztések rendben tartásához javaslom a **`17_DEVELOPER_NOTES_AND_PITFAL
|
||||
- **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.
|
||||
- **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.
|
||||
@@ -33,3 +33,34 @@ Minden módosítás előtt a rendszer menti az aktuális rekord állapotát (JSO
|
||||
## 5. Adminisztrátori Meghívók
|
||||
- Adminisztrátort csak kézi meghívóval lehet felvenni.
|
||||
- **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