Files
service-finder/backend/app/api/v1/endpoints/admin_organizations.py

865 lines
30 KiB
Python

"""
🏢 Admin Garages (Organizations) CRM API
Végpontok:
GET /admin/organizations — Garázsok listázása előfizetési adatokkal
GET /admin/organizations/{org_id}/details — Garázs részletes adatai (General Tab)
PATCH /admin/organizations/{org_id}/subscription — Előfizetés módosítása
POST /admin/organizations/{org_id}/members — Új tag hozzáadása a garázshoz
PUT /admin/organizations/{org_id}/members/{member_id} — Tag adatainak módosítása
DELETE /admin/organizations/{org_id}/members/{member_id} — Tag eltávolítása
"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select, func, update as sa_update, delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload
from pydantic import BaseModel, Field
from app.api import deps
from app.db.session import get_db
from app.models.core_logic import SubscriptionTier, OrganizationSubscription
from app.models.marketplace.organization import Organization, OrganizationMember
from app.models.fleet.organization import ContactPerson
from app.models.identity import User, Person
from app.schemas.organization import (
OrganizationMemberCreate,
OrganizationMemberUpdate,
OrganizationMemberResponse,
PersonBrief,
)
logger = logging.getLogger("admin-organizations")
router = APIRouter()
# =============================================================================
# Pydantic Schemas
# =============================================================================
class OrgSubscriptionUpdate(BaseModel):
"""Előfizetés módosításának kérése."""
tier_id: int = Field(..., description="Az új SubscriptionTier ID-ja")
expires_at: Optional[datetime] = Field(
default=None,
description="Egyedi lejárati dátum (pl. 2-5 éves B2B deal esetén). "
"Ha None, a csomag duration.days alapján számolódik."
)
class OrgSubscriptionResponse(BaseModel):
"""Előfizetés adatainak válasza."""
id: int
org_id: int
tier_id: int
tier_name: str = ""
valid_from: Optional[str] = None
valid_until: Optional[str] = None
is_active: bool = True
extra_allowances: dict = {}
class GarageListItem(BaseModel):
"""Egy garázs adatai a listában."""
id: int
name: str
full_name: str
display_name: Optional[str] = None
email: Optional[str] = None
status: str
is_active: bool
is_verified: bool
is_deleted: bool
org_type: str
city: Optional[str] = None
region: Optional[str] = None
segment: Optional[str] = None
member_count: int = 0
created_at: Optional[str] = None
# Előfizetési adatok
subscription: Optional[OrgSubscriptionResponse] = None
subscription_tier_name: str = "Free/Fallback"
subscription_expires_at: Optional[str] = None
class GarageListResponse(BaseModel):
"""Garázsok listájának válasza."""
total: int
skip: int
limit: int
garages: List[GarageListItem]
# =============================================================================
# Pydantic Schemas — Garage Details
# =============================================================================
class ContactPersonInfo(BaseModel):
"""Kapcsolattartó személy adatai."""
id: int
full_name: str = ""
role: str = ""
department: Optional[str] = None
phone: Optional[str] = None
email: Optional[str] = None
is_primary: bool = False
class SubscriptionSummary(BaseModel):
"""Előfizetés összefoglaló."""
tier_name: str = "Free/Fallback"
tier_level: int = 0
valid_from: Optional[str] = None
expires_at: Optional[str] = None
is_active: bool = True
asset_count: int = 0
asset_limit: int = 1
class GarageDetailsResponse(BaseModel):
"""Garázs részletes adatai (General Tab)."""
# Szervezet adatok
id: int
name: str
full_name: str
display_name: Optional[str] = None
status: str
is_active: bool
is_verified: bool
org_type: str
# Cím adatok
address_city: Optional[str] = None
address_zip: Optional[str] = None
address_street_name: Optional[str] = None
address_street_type: Optional[str] = None
address_house_number: Optional[str] = None
tax_number: Optional[str] = None
reg_number: Optional[str] = None
# Előfizetés
subscription: Optional[SubscriptionSummary] = None
# Kapcsolattartó
primary_contact: Optional[ContactPersonInfo] = None
# Meta
created_at: Optional[str] = None
member_count: int = 0
# P0: Full member list with nested person data
members: List[OrganizationMemberResponse] = []
# =============================================================================
# GET / — Garázsok listázása
# =============================================================================
@router.get(
"",
response_model=GarageListResponse,
summary="Garázsok listázása",
description="Visszaadja az összes szervezetet (garázst) előfizetési adatokkal és taglétszámmal.",
)
async def list_organizations(
skip: int = Query(0, ge=0, description="Hány rekordot hagyjunk ki (lapozás)"),
limit: int = Query(50, ge=1, le=200, description="Maximum visszaadott rekordok száma"),
search_term: Optional[str] = Query(None, description="Keresés név vagy e-mail alapján"),
status_filter: Optional[str] = Query(None, description="Szűrés státusz szerint (active/inactive/suspended/pending_verification)"),
org_type_filter: Optional[str] = Query(None, description="Szűrés szervezet típus szerint"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:view")),
) -> GarageListResponse:
"""
Garázsok / Szervezetek listázása lapozással és kereséssel.
Minden garázshoz visszaadja:
- Alapadatokat (név, státusz, típus, város)
- Taglétszámot (OrganizationMember-ek száma)
- Aktív előfizetés adatait (tier név, lejárati dátum)
"""
# Alap lekérdezés
query = select(Organization).options(
selectinload(Organization.subscription_tier),
)
# Szűrés
if search_term:
like_pattern = f"%{search_term}%"
query = query.where(
Organization.full_name.ilike(like_pattern) |
Organization.name.ilike(like_pattern) |
Organization.display_name.ilike(like_pattern)
)
if status_filter:
query = query.where(Organization.status == status_filter)
if org_type_filter:
query = query.where(Organization.org_type == org_type_filter)
# Teljes számolás
count_stmt = select(func.count()).select_from(query.subquery())
total_result = await db.execute(count_stmt)
total = total_result.scalar() or 0
# Lapozás
query = query.order_by(Organization.id.desc()).offset(skip).limit(limit)
result = await db.execute(query)
organizations = result.scalars().all()
# Összes org_id begyűjtése a tömeges subscription lekérdezéshez
org_ids = [org.id for org in organizations]
# Tömeges subscription lekérdezés
subscriptions_map: Dict[int, OrganizationSubscription] = {}
if org_ids:
sub_stmt = (
select(OrganizationSubscription)
.options(selectinload(OrganizationSubscription.tier))
.where(
OrganizationSubscription.org_id.in_(org_ids),
OrganizationSubscription.is_active == True,
)
)
sub_result = await db.execute(sub_stmt)
for sub in sub_result.scalars().all():
# Ha több subscription is van, a legutolsót vegyük
if sub.org_id not in subscriptions_map:
subscriptions_map[sub.org_id] = sub
# Tömeges taglétszám lekérdezés
member_counts: Dict[int, int] = {}
if org_ids:
from sqlalchemy import func as sa_func
member_stmt = (
select(
OrganizationMember.organization_id,
sa_func.count(OrganizationMember.id)
)
.where(
OrganizationMember.organization_id.in_(org_ids),
OrganizationMember.status == "active",
)
.group_by(OrganizationMember.organization_id)
)
member_result = await db.execute(member_stmt)
for row in member_result.all():
member_counts[row[0]] = row[1]
# Válasz összeállítása
garage_list = []
for org in organizations:
sub = subscriptions_map.get(org.id)
sub_response = None
tier_name = "Free/Fallback"
expires_at = None
if sub and sub.tier:
tier_name = sub.tier.name
expires_at = sub.valid_until.isoformat() if sub.valid_until else None
sub_response = OrgSubscriptionResponse(
id=sub.id,
org_id=sub.org_id,
tier_id=sub.tier_id,
tier_name=sub.tier.name,
valid_from=sub.valid_from.isoformat() if sub.valid_from else None,
valid_until=expires_at,
is_active=sub.is_active,
extra_allowances=sub.extra_allowances or {},
)
elif org.subscription_tier:
# Fallback: Organization.subscription_tier_id kapcsolat
tier_name = org.subscription_tier.name
expires_at = org.subscription_expires_at.isoformat() if org.subscription_expires_at else None
garage_list.append(GarageListItem(
id=org.id,
name=org.name,
full_name=org.full_name,
display_name=org.display_name,
email=None, # Organization-nak nincs közvetlen email mezője
status=org.status,
is_active=org.is_active,
is_verified=org.is_verified,
is_deleted=org.is_deleted,
org_type=org.org_type.value if hasattr(org.org_type, "value") else str(org.org_type),
city=org.address_city,
region=org.region,
segment=org.segment,
member_count=member_counts.get(org.id, 0),
created_at=org.created_at.isoformat() if org.created_at else None,
subscription=sub_response,
subscription_tier_name=tier_name,
subscription_expires_at=expires_at,
))
return GarageListResponse(
total=total,
skip=skip,
limit=limit,
garages=garage_list,
)
# =============================================================================
# GET /{org_id}/details — Garázs részletes adatai (General Tab)
# =============================================================================
@router.get(
"/{org_id}/details",
response_model=GarageDetailsResponse,
summary="Garázs részletes adatai",
description="Visszaadja egy garázs teljes adatait, előfizetési összefoglalóját, "
"elsődleges kapcsolattartóját és taglistáját a General Tab számára.",
)
async def get_organization_details(
org_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:view")),
) -> GarageDetailsResponse:
"""
Garázs részletes adatainak lekérése.
Visszaadja:
- Teljes szervezet adatokat (név, cím, adószám, státusz)
- Előfizetés összefoglalót (tier név, lejárat, járműszám/korlát)
- Elsődleges kapcsolattartó adatait (fleet.contact_persons)
- Tagok teljes listáját (OrganizationMember) nested Person adatokkal
"""
# 1. Szervezet lekérése subscription_tier kapcsolattal
org_stmt = (
select(Organization)
.options(
selectinload(Organization.subscription_tier),
)
.where(Organization.id == org_id)
)
org_result = await db.execute(org_stmt)
org = org_result.scalar_one_or_none()
if not org:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Szervezet nem található ID-vel: {org_id}",
)
# 2. Aktív előfizetés lekérése
sub_stmt = (
select(OrganizationSubscription)
.options(selectinload(OrganizationSubscription.tier))
.where(
OrganizationSubscription.org_id == org_id,
OrganizationSubscription.is_active == True,
)
.order_by(OrganizationSubscription.id.desc())
.limit(1)
)
sub_result = await db.execute(sub_stmt)
active_sub = sub_result.scalar_one_or_none()
# 3. Tagok lekérése eager loading-gal (User + Person)
members_stmt = (
select(OrganizationMember)
.options(
selectinload(OrganizationMember.user).selectinload(User.person),
selectinload(OrganizationMember.person),
)
.where(
OrganizationMember.organization_id == org_id,
)
.order_by(OrganizationMember.id)
)
members_result = await db.execute(members_stmt)
member_rows = members_result.scalars().all()
# Taglétszám (aktív tagok száma)
member_count = sum(1 for m in member_rows if m.status == "active")
# 4. Elsődleges kapcsolattartó lekérése
contact_stmt = (
select(ContactPerson)
.options(selectinload(ContactPerson.person))
.where(
ContactPerson.organization_id == org_id,
ContactPerson.is_primary == True,
)
.limit(1)
)
contact_result = await db.execute(contact_stmt)
contact = contact_result.scalar_one_or_none()
# 5. Subscription összefoglaló
subscription_summary = None
if active_sub and active_sub.tier:
rules = active_sub.tier.rules or {}
asset_limit = (
rules.get("max_vehicles")
or active_sub.tier.feature_capabilities.get("max_vehicles")
or 1
)
subscription_summary = SubscriptionSummary(
tier_name=active_sub.tier.name,
tier_level=active_sub.tier.tier_level,
valid_from=active_sub.valid_from.isoformat() if active_sub.valid_from else None,
expires_at=active_sub.valid_until.isoformat() if active_sub.valid_until else None,
is_active=active_sub.is_active,
asset_count=0,
asset_limit=asset_limit,
)
elif org.subscription_tier:
rules = org.subscription_tier.rules or {}
asset_limit = (
rules.get("max_vehicles")
or org.subscription_tier.feature_capabilities.get("max_vehicles")
or 1
)
subscription_summary = SubscriptionSummary(
tier_name=org.subscription_tier.name,
tier_level=org.subscription_tier.tier_level,
expires_at=org.subscription_expires_at.isoformat() if org.subscription_expires_at else None,
asset_limit=asset_limit,
)
# 6. Kapcsolattartó adatok
primary_contact = None
if contact and contact.person:
primary_contact = ContactPersonInfo(
id=contact.id,
full_name=f"{contact.person.last_name} {contact.person.first_name}",
role=contact.role,
department=contact.department,
phone=contact.person.phone,
is_primary=contact.is_primary,
)
# 7. Tagok összeállítása nested Person adatokkal
members_list: List[OrganizationMemberResponse] = []
for m in member_rows:
person_data = None
# Try to get Person from the member's direct person relationship first
person_obj = m.person
# Fallback: get Person through the User relationship
if not person_obj and m.user and m.user.person:
person_obj = m.user.person
if person_obj:
# Get email from the user record
email = m.user.email if m.user else None
person_data = PersonBrief(
first_name=person_obj.first_name or "",
last_name=person_obj.last_name or "",
email=email,
phone=person_obj.phone,
)
members_list.append(OrganizationMemberResponse(
id=m.id,
user_id=m.user_id,
organization_id=m.organization_id,
role=m.role,
status=m.status or "active",
is_verified=m.is_verified,
person=person_data,
created_at=m.created_at.isoformat() if m.created_at else None,
updated_at=m.updated_at.isoformat() if m.updated_at else None,
))
logger.info(
f"Admin {current_user.id} ({current_user.email}) fetched details for org {org_id} "
f"({org.full_name}) — {len(members_list)} members"
)
return GarageDetailsResponse(
id=org.id,
name=org.name,
full_name=org.full_name,
display_name=org.display_name,
status=org.status,
is_active=org.is_active,
is_verified=org.is_verified,
org_type=org.org_type.value if hasattr(org.org_type, "value") else str(org.org_type),
address_city=org.address_city,
address_zip=org.address_zip,
address_street_name=org.address_street_name,
address_street_type=org.address_street_type,
address_house_number=org.address_house_number,
tax_number=org.tax_number,
reg_number=org.reg_number,
subscription=subscription_summary,
primary_contact=primary_contact,
created_at=org.created_at.isoformat() if org.created_at else None,
member_count=member_count,
members=members_list,
)
# =============================================================================
# PATCH /{org_id}/subscription — Előfizetés módosítása
# =============================================================================
@router.patch(
"/{org_id}/subscription",
summary="Garázs előfizetésének módosítása",
description="Frissíti vagy létrehozza egy garázs OrganizationSubscription rekordját. "
"Lehetőség van egyedi lejárati dátum megadására (pl. 2-5 éves B2B deal).",
)
async def update_org_subscription(
org_id: int,
payload: OrgSubscriptionUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:edit")),
) -> Dict[str, Any]:
"""
Garázs előfizetésének módosítása.
- Ellenőrzi, hogy a szervezet létezik-e.
- Ellenőrzi, hogy a SubscriptionTier létezik-e.
- Ha van aktív OrganizationSubscription, inaktiválja (is_active=False).
- Létrehoz egy új OrganizationSubscription rekordot a megadott tier_id-val
és opcionális expires_at dátummal.
- Ha expires_at nincs megadva, a csomag rules.duration.days alapján számol.
"""
# 1. Ellenőrizzük a szervezetet
org_stmt = select(Organization).where(Organization.id == org_id)
org_result = await db.execute(org_stmt)
org = org_result.scalar_one_or_none()
if not org:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Szervezet nem található ID-vel: {org_id}",
)
# 2. Ellenőrizzük a SubscriptionTier-t
tier_stmt = select(SubscriptionTier).where(SubscriptionTier.id == payload.tier_id)
tier_result = await db.execute(tier_stmt)
tier = tier_result.scalar_one_or_none()
if not tier:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Előfizetési csomag nem található ID-vel: {payload.tier_id}",
)
# 3. Inaktiváljuk a meglévő aktív subscription-öket
await db.execute(
sa_update(OrganizationSubscription)
.where(
OrganizationSubscription.org_id == org_id,
OrganizationSubscription.is_active == True,
)
.values(is_active=False)
)
# 4. Számoljuk a lejárati dátumot
now = datetime.utcnow()
if payload.expires_at:
valid_until = payload.expires_at
else:
# Ha nincs megadva, a csomag duration.days alapján számolunk
rules = tier.rules or {}
duration = rules.get("duration", {})
days = duration.get("days", 30) if isinstance(duration, dict) else 30
from datetime import timedelta
valid_until = now + timedelta(days=days)
# 5. Létrehozzuk az új subscription rekordot
new_sub = OrganizationSubscription(
org_id=org_id,
tier_id=payload.tier_id,
valid_from=now,
valid_until=valid_until,
is_active=True,
)
db.add(new_sub)
# 6. Frissítjük a Organization denormalizált mezőit is
org.subscription_tier_id = payload.tier_id
org.subscription_expires_at = valid_until
org.subscription_plan = tier.name
await db.commit()
await db.refresh(new_sub)
logger.info(
f"Admin {current_user.id} ({current_user.email}) updated subscription for org {org_id}: "
f"tier_id={payload.tier_id}, tier_name={tier.name}, "
f"valid_until={valid_until.isoformat()}"
)
return {
"status": "success",
"message": f"Előfizetés frissítve a '{org.full_name}' garázs számára.",
"subscription": {
"id": new_sub.id,
"org_id": new_sub.org_id,
"tier_id": new_sub.tier_id,
"tier_name": tier.name,
"valid_from": new_sub.valid_from.isoformat() if new_sub.valid_from else None,
"valid_until": new_sub.valid_until.isoformat() if new_sub.valid_until else None,
"is_active": new_sub.is_active,
},
}
# =============================================================================
# POST /{org_id}/members — Új tag hozzáadása a garázshoz
# =============================================================================
@router.post(
"/{org_id}/members",
response_model=OrganizationMemberResponse,
status_code=status.HTTP_201_CREATED,
summary="Új tag hozzáadása",
description="Hozzáad egy felhasználót a garázshoz a megadott szerepkörrel.",
)
async def add_organization_member(
org_id: int,
payload: OrganizationMemberCreate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:manage-members")),
) -> OrganizationMemberResponse:
"""
Új tag hozzáadása egy garázshoz.
- Ellenőrzi, hogy a szervezet létezik-e.
- Ellenőrzi, hogy a user_id létezik-e az identity.users táblában.
- Ellenőrzi, hogy a felhasználó még nem tagja a szervezetnek.
- Létrehozza az OrganizationMember rekordot.
"""
# 1. Ellenőrizzük a szervezetet
org_stmt = select(Organization).where(Organization.id == org_id)
org_result = await db.execute(org_stmt)
org = org_result.scalar_one_or_none()
if not org:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Szervezet nem található ID-vel: {org_id}",
)
# 2. Ellenőrizzük a felhasználót
user_stmt = select(User).where(User.id == payload.user_id)
user_result = await db.execute(user_stmt)
user = user_result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Felhasználó nem található ID-vel: {payload.user_id}",
)
# 3. Ellenőrizzük, hogy a felhasználó még nem tagja a szervezetnek
existing_stmt = select(OrganizationMember).where(
OrganizationMember.organization_id == org_id,
OrganizationMember.user_id == payload.user_id,
OrganizationMember.status != "archived",
)
existing_result = await db.execute(existing_stmt)
existing = existing_result.scalar_one_or_none()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"A felhasználó (ID: {payload.user_id}) már tagja ennek a szervezetnek.",
)
# 4. Létrehozzuk az új tag rekordot
person_id = user.person_id
new_member = OrganizationMember(
organization_id=org_id,
user_id=payload.user_id,
person_id=person_id,
role=payload.role,
status=payload.status,
is_verified=False,
)
db.add(new_member)
await db.commit()
await db.refresh(new_member)
# 5. Visszatöltjük a kapcsolódó adatokat a válaszhoz
await db.refresh(new_member, attribute_names=["user", "person"])
# Person adatok összeállítása
person_data = None
person_obj = new_member.person
if not person_obj and new_member.user and new_member.user.person:
person_obj = new_member.user.person
if person_obj:
person_data = PersonBrief(
first_name=person_obj.first_name or "",
last_name=person_obj.last_name or "",
email=user.email,
phone=person_obj.phone,
)
logger.info(
f"Admin {current_user.id} ({current_user.email}) added member user_id={payload.user_id} "
f"to org {org_id} ({org.full_name}) with role={payload.role}"
)
return OrganizationMemberResponse(
id=new_member.id,
user_id=new_member.user_id,
organization_id=new_member.organization_id,
role=new_member.role,
status=new_member.status or "active",
is_verified=new_member.is_verified,
person=person_data,
created_at=new_member.created_at.isoformat() if new_member.created_at else None,
updated_at=new_member.updated_at.isoformat() if new_member.updated_at else None,
)
# =============================================================================
# PUT /{org_id}/members/{member_id} — Tag adatainak módosítása
# =============================================================================
@router.put(
"/{org_id}/members/{member_id}",
response_model=OrganizationMemberResponse,
summary="Tag adatainak módosítása",
description="Frissíti egy meglévő tag szerepkörét és/vagy státuszát.",
)
async def update_organization_member(
org_id: int,
member_id: int,
payload: OrganizationMemberUpdate,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:manage-members")),
) -> OrganizationMemberResponse:
"""
Meglévő tag adatainak módosítása.
- Ellenőrzi, hogy a tag rekord létezik és a megadott szervezethez tartozik.
- Frissíti a role és/vagy status mezőket.
"""
# 1. Ellenőrizzük a tag rekordot
member_stmt = (
select(OrganizationMember)
.options(
selectinload(OrganizationMember.user).selectinload(User.person),
selectinload(OrganizationMember.person),
)
.where(
OrganizationMember.id == member_id,
OrganizationMember.organization_id == org_id,
)
)
member_result = await db.execute(member_stmt)
member = member_result.scalar_one_or_none()
if not member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tag nem található (ID: {member_id}) a szervezetben (ID: {org_id}).",
)
# 2. Frissítjük a mezőket
if payload.role is not None:
member.role = payload.role
if payload.status is not None:
member.status = payload.status
await db.commit()
await db.refresh(member)
# 3. Person adatok összeállítása
person_data = None
person_obj = member.person
if not person_obj and member.user and member.user.person:
person_obj = member.user.person
if person_obj:
email = member.user.email if member.user else None
person_data = PersonBrief(
first_name=person_obj.first_name or "",
last_name=person_obj.last_name or "",
email=email,
phone=person_obj.phone,
)
logger.info(
f"Admin {current_user.id} ({current_user.email}) updated member {member_id} in org {org_id}: "
f"role={payload.role}, status={payload.status}"
)
return OrganizationMemberResponse(
id=member.id,
user_id=member.user_id,
organization_id=member.organization_id,
role=member.role,
status=member.status or "active",
is_verified=member.is_verified,
person=person_data,
created_at=member.created_at.isoformat() if member.created_at else None,
updated_at=member.updated_at.isoformat() if member.updated_at else None,
)
# =============================================================================
# DELETE /{org_id}/members/{member_id} — Tag eltávolítása
# =============================================================================
@router.delete(
"/{org_id}/members/{member_id}",
status_code=status.HTTP_200_OK,
summary="Tag eltávolítása",
description="Eltávolít egy tagot a garázsból (soft delete: status → archived).",
)
async def remove_organization_member(
org_id: int,
member_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(deps.get_current_active_user),
_ = Depends(deps.RequirePermission("org:manage-members")),
) -> Dict[str, Any]:
"""
Tag eltávolítása a garázsból.
Soft delete megközelítés: a rekord nem törlődik, csak a status mező
'archived' értékre változik. Ez megőrzi a történeti adatokat.
"""
# 1. Ellenőrizzük a tag rekordot
member_stmt = select(OrganizationMember).where(
OrganizationMember.id == member_id,
OrganizationMember.organization_id == org_id,
)
member_result = await db.execute(member_stmt)
member = member_result.scalar_one_or_none()
if not member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tag nem található (ID: {member_id}) a szervezetben (ID: {org_id}).",
)
# 2. Soft delete: archived státusz
member.status = "archived"
await db.commit()
logger.info(
f"Admin {current_user.id} ({current_user.email}) removed member {member_id} from org {org_id} "
f"(status → archived)"
)
return {
"status": "success",
"message": f"Tag (ID: {member_id}) eltávolítva a szervezetből (ID: {org_id}).",
}