# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/organizations.py import os import re import uuid import hashlib import logging from typing import List from datetime import datetime, timezone 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.api.deps import get_current_user from app.schemas.organization import CorpOnboardIn, CorpOnboardResponse from app.models.marketplace.organization import Organization, OrgType, OrganizationMember, Branch from app.models.identity import User # JAVÍTVA: Központi Identity modell from app.core.config import settings 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), current_user: User = Depends(get_current_user) ): """ Új szervezet (cég/szerviz) rögzítése. Automatikusan generál slug-ot és létrehozza a NAS mappa-struktúrát. """ # 1. Magyar adószám validáció (XXXXXXXX-Y-ZZ) 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!" ) # 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. KÖTELEZŐ MEZŐ: folder_slug generálása # Mivel az adatbázisban NOT NULL, itt muszáj létrehozni temp_slug = hashlib.md5(f"{org_in.tax_number}-{uuid.uuid4()}".encode()).hexdigest()[:12] # 4. Mentés new_org = Organization( full_name=org_in.full_name, name=org_in.name, display_name=org_in.display_name, tax_number=org_in.tax_number, reg_number=org_in.reg_number, folder_slug=temp_slug, # JAVÍTVA: Kötelező mező beillesztve address_zip=org_in.address_zip, address_city=org_in.address_city, address_street_name=org_in.address_street_name, address_street_type=org_in.address_street_type, address_house_number=org_in.address_house_number, address_hrsz=org_in.address_hrsz, country_code=org_in.country_code, org_type=OrgType.business, status="pending_verification", # --- EXPLICIT IDŐBÉLYEGEK A DB HIBA ELKERÜLÉSÉRE --- first_registered_at=datetime.now(timezone.utc), current_lifecycle_started_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc), subscription_plan="FREE", base_asset_limit=1, purchased_extra_slots=0, notification_settings={}, external_integration_config={}, is_ownership_transferable=True ) db.add(new_org) await db.flush() # 5. ALAPÉRTELMEZETT KÖZPONTI TELEPHELY LÉTREHOZÁSA main_branch = Branch( organization_id=new_org.id, name="Központi Telephely", is_main=True, postal_code=org_in.address_zip, city=org_in.address_city, street_name=org_in.address_street_name, street_type=org_in.address_street_type, house_number=org_in.address_house_number, hrsz=org_in.address_hrsz, status="active" ) db.add(main_branch) # 6. TULAJDONOS RÖGZÍTÉSE owner_member = OrganizationMember( organization_id=new_org.id, user_id=current_user.id, role="OWNER" # JAVÍTVA: Enum kompatibilis nagybetűs forma ) db.add(owner_member) # 7. 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(os.path.join(org_path, "documents"), exist_ok=True) logger.info(f"NAS mappa kész: {org_path}") except Exception as e: logger.error(f"NAS hiba: {e}") await db.commit() await db.refresh(new_org) return {"organization_id": new_org.id, "status": new_org.status} @router.get("/my", response_model=List[dict]) async def get_my_organizations( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ A bejelentkezett felhasználóhoz tartozó összes szervezet listázása. """ stmt = ( select(Organization) .join(OrganizationMember) .where(OrganizationMember.user_id == current_user.id) ) result = await db.execute(stmt) orgs = result.scalars().all() # Return full organization details return [ { "organization_id": o.id, "status": o.status, "name": o.name, "full_name": o.full_name, "display_name": o.display_name, "tax_number": o.tax_number, "country_code": o.country_code, "is_active": o.is_active, "is_deleted": o.is_deleted, "subscription_plan": o.subscription_plan } for o in orgs ] # --- B2B MEGHÍVÓ LOGIKA --- from pydantic import BaseModel, EmailStr from app.models.identity import VerificationToken import uuid from datetime import timedelta class OrgInvitationIn(BaseModel): email: EmailStr role: str = "DRIVER" class OrgInvitationResponse(BaseModel): status: str message: str @router.post("/{org_id}/invitations", response_model=OrgInvitationResponse, status_code=status.HTTP_200_OK) async def invite_to_organization( org_id: int, invite_in: OrgInvitationIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ B2B Meghívó küldése egy szervezetbe. Ha a felhasználó már létezik, egy pending tagot hozunk létre. Ha nem létezik, token készül az email címre. """ # 1. Jogosultság ellenőrzése stmt_member = select(OrganizationMember).where( (OrganizationMember.organization_id == org_id) & (OrganizationMember.user_id == current_user.id) & (OrganizationMember.role.in_(["OWNER", "ADMIN"])) ) member = (await db.execute(stmt_member)).scalar_one_or_none() if not member: raise HTTPException(status_code=403, detail="Nincs jogosultságod meghívót küldeni (csak OWNER/ADMIN).") # 2. Célpont keresése stmt_target = select(User).where(User.email == invite_in.email) target_user = (await db.execute(stmt_target)).scalar_one_or_none() if target_user: # Létező felhasználó, van-e már tagsága? stmt_exist = select(OrganizationMember).where( (OrganizationMember.organization_id == org_id) & (OrganizationMember.user_id == target_user.id) ) if (await db.execute(stmt_exist)).scalar_one_or_none(): raise HTTPException(status_code=400, detail="A felhasználó már tagja a szervezetnek.") new_member = OrganizationMember( organization_id=org_id, user_id=target_user.id, role=invite_in.role.upper(), status="pending" # Válaszolni kell a meghívóra ) db.add(new_member) await db.commit() logger.info(f"Értesítő email küldve a létező felhasználónak: {invite_in.email}") return {"status": "success", "message": "Meghívó elküldve a meglévő felhasználónak."} else: # Új felhasználó -> Token token_val = uuid.uuid4() new_token = VerificationToken( token=token_val, user_id=None, # Mivel még nincs User token_type="org_invite", expires_at=datetime.now(timezone.utc) + timedelta(days=7), extra_data={"org_id": org_id, "role": invite_in.role.upper(), "email": invite_in.email} ) db.add(new_token) await db.commit() logger.info(f"Meghívó email küldve az új felhasználónak: {invite_in.email}, Token: {token_val}") return {"status": "success", "message": "Meghívó email elküldve (új felhasználó)."} @router.post("/invitations/{token}/accept", response_model=OrgInvitationResponse, status_code=status.HTTP_200_OK) async def accept_invitation( token: uuid.UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Meghívó elfogadása token alapján. (Új felhasználó számára, miután regisztrált) """ stmt = select(VerificationToken).where( (VerificationToken.token == token) & (VerificationToken.token_type == "org_invite") & (VerificationToken.is_used == False) ) token_rec = (await db.execute(stmt)).scalar_one_or_none() if not token_rec or token_rec.expires_at < datetime.now(timezone.utc): raise HTTPException(status_code=400, detail="Érvénytelen vagy lejárt meghívó.") extra = token_rec.extra_data if not extra or extra.get("email") != current_user.email: raise HTTPException(status_code=403, detail="A meghívó nem a te e-mail címedre szól.") org_id = extra.get("org_id") role = extra.get("role") # Van-e már tagsága? stmt_exist = select(OrganizationMember).where( (OrganizationMember.organization_id == org_id) & (OrganizationMember.user_id == current_user.id) ) exist_member = (await db.execute(stmt_exist)).scalar_one_or_none() if exist_member: exist_member.status = "active" exist_member.role = role else: new_member = OrganizationMember( organization_id=org_id, user_id=current_user.id, role=role, status="active" ) db.add(new_member) token_rec.is_used = True await db.commit() return {"status": "success", "message": "Meghívó sikeresen elfogadva."}