"""Property management endpoints.""" from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.core.deps import ( get_current_admin, get_current_manager_or_superadmin, get_current_user, get_db, get_optional_user, ) from app.core.permissions import get_manager_property_ids, verify_property_access from app.models.organization import Organization from app.models.property import Property from app.models.property_access import PropertyAccess from app.models.property_manager import PropertyManager from app.models.property_settings import PropertySettings from app.models.space import Space from app.models.user import User from app.schemas.property import ( PropertyAccessCreate, PropertyAccessResponse, PropertyCreate, PropertyManagerInfo, PropertyResponse, PropertySettingsResponse, PropertySettingsUpdate, PropertyStatusUpdate, PropertyUpdate, PropertyWithSpaces, ) from app.schemas.space import SpaceResponse from app.services.audit_service import log_action def _get_property_managers(db: Session, property_id: int) -> list[PropertyManagerInfo]: """Get manager info for a property.""" managers = ( db.query(User) .join(PropertyManager, PropertyManager.user_id == User.id) .filter(PropertyManager.property_id == property_id) .all() ) return [ PropertyManagerInfo(user_id=m.id, full_name=m.full_name, email=m.email) for m in managers ] router = APIRouter(prefix="/properties", tags=["properties"]) manager_router = APIRouter(prefix="/manager/properties", tags=["properties-manager"]) admin_router = APIRouter(prefix="/admin/properties", tags=["properties-admin"]) # === User-facing endpoints === @router.get("", response_model=list[PropertyResponse]) def list_properties( db: Annotated[Session, Depends(get_db)], current_user: Annotated[User | None, Depends(get_optional_user)], managed_only: bool = False, ) -> list[PropertyResponse]: """List visible properties based on user role. Query params: - managed_only: If true, managers only see properties they manage (for management pages) """ if current_user and current_user.role in ("admin", "superadmin"): # Superadmin sees all properties = db.query(Property).filter(Property.is_active == True).order_by(Property.name).all() # noqa: E712 elif current_user and current_user.role == "manager": # Manager sees managed properties (+ public if not managed_only) managed_ids = get_manager_property_ids(db, current_user.id) if managed_only: properties = ( db.query(Property) .filter( Property.is_active == True, # noqa: E712 Property.id.in_(managed_ids), ) .order_by(Property.name) .all() ) else: properties = ( db.query(Property) .filter( Property.is_active == True, # noqa: E712 (Property.is_public == True) | (Property.id.in_(managed_ids)), # noqa: E712 ) .order_by(Property.name) .all() ) elif current_user: # Regular user sees public + explicitly granted from app.core.permissions import get_user_accessible_property_ids accessible_ids = get_user_accessible_property_ids(db, current_user.id) properties = ( db.query(Property) .filter(Property.is_active == True, Property.id.in_(accessible_ids)) # noqa: E712 .order_by(Property.name) .all() ) else: # Anonymous sees only public properties = ( db.query(Property) .filter(Property.is_public == True, Property.is_active == True) # noqa: E712 .order_by(Property.name) .all() ) result = [] for p in properties: space_count = db.query(Space).filter(Space.property_id == p.id, Space.is_active == True).count() # noqa: E712 result.append(PropertyResponse( id=p.id, name=p.name, description=p.description, address=p.address, is_public=p.is_public, is_active=p.is_active, created_at=p.created_at, space_count=space_count, managers=_get_property_managers(db, p.id), )) return result @router.get("/{property_id}", response_model=PropertyWithSpaces) def get_property( property_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User | None, Depends(get_optional_user)], ) -> PropertyWithSpaces: """Get property detail with visibility check.""" verify_property_access(db, current_user, property_id) prop = db.query(Property).filter(Property.id == property_id).first() spaces = db.query(Space).filter(Space.property_id == property_id, Space.is_active == True).all() # noqa: E712 space_count = len(spaces) return PropertyWithSpaces( id=prop.id, name=prop.name, description=prop.description, address=prop.address, is_public=prop.is_public, is_active=prop.is_active, created_at=prop.created_at, space_count=space_count, managers=_get_property_managers(db, prop.id), spaces=[SpaceResponse.model_validate(s) for s in spaces], ) @router.get("/{property_id}/spaces", response_model=list[SpaceResponse]) def get_property_spaces( property_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User | None, Depends(get_optional_user)], include_inactive: bool = False, ) -> list[SpaceResponse]: """List spaces in a property.""" verify_property_access(db, current_user, property_id) query = db.query(Space).filter(Space.property_id == property_id) # Managers/admins can see inactive spaces, regular users cannot is_admin_like = current_user and current_user.role in ("admin", "superadmin", "manager") if not (include_inactive and is_admin_like): query = query.filter(Space.is_active == True) # noqa: E712 spaces = query.order_by(Space.name).all() return [SpaceResponse.model_validate(s) for s in spaces] # === Manager endpoints === @manager_router.post("", response_model=PropertyResponse, status_code=status.HTTP_201_CREATED) def create_property( data: PropertyCreate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertyResponse: """Create a property. Creator becomes manager.""" prop = Property( name=data.name, description=data.description, address=data.address, is_public=data.is_public, ) db.add(prop) db.commit() db.refresh(prop) # Creator becomes manager pm = PropertyManager(property_id=prop.id, user_id=current_user.id) db.add(pm) db.commit() log_action( db=db, action="property_created", user_id=current_user.id, target_type="property", target_id=prop.id, details={"name": prop.name}, ) return PropertyResponse( id=prop.id, name=prop.name, description=prop.description, address=prop.address, is_public=prop.is_public, is_active=prop.is_active, created_at=prop.created_at, space_count=0, managers=_get_property_managers(db, prop.id), ) @manager_router.put("/{property_id}", response_model=PropertyResponse) def update_property( property_id: int, data: PropertyUpdate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertyResponse: """Update a property (ownership check).""" verify_property_access(db, current_user, property_id, require_manager=True) prop = db.query(Property).filter(Property.id == property_id).first() if data.name is not None: prop.name = data.name if data.description is not None: prop.description = data.description if data.address is not None: prop.address = data.address if data.is_public is not None: prop.is_public = data.is_public db.commit() db.refresh(prop) space_count = db.query(Space).filter(Space.property_id == prop.id, Space.is_active == True).count() # noqa: E712 return PropertyResponse( id=prop.id, name=prop.name, description=prop.description, address=prop.address, is_public=prop.is_public, is_active=prop.is_active, created_at=prop.created_at, space_count=space_count, managers=_get_property_managers(db, prop.id), ) @manager_router.patch("/{property_id}/status", response_model=PropertyResponse) def update_property_status( property_id: int, data: PropertyStatusUpdate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertyResponse: """Activate/deactivate a property.""" verify_property_access(db, current_user, property_id, require_manager=True) prop = db.query(Property).filter(Property.id == property_id).first() prop.is_active = data.is_active db.commit() db.refresh(prop) space_count = db.query(Space).filter(Space.property_id == prop.id, Space.is_active == True).count() # noqa: E712 return PropertyResponse( id=prop.id, name=prop.name, description=prop.description, address=prop.address, is_public=prop.is_public, is_active=prop.is_active, created_at=prop.created_at, space_count=space_count, managers=_get_property_managers(db, prop.id), ) @manager_router.delete("/{property_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_property( property_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> None: """Delete a property (only if it has no active bookings).""" verify_property_access(db, current_user, property_id, require_manager=True) prop = db.query(Property).filter(Property.id == property_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") from app.models.booking import Booking # Check for active bookings (pending or approved) in this property's spaces space_ids = [s.id for s in db.query(Space).filter(Space.property_id == property_id).all()] if space_ids: active_bookings = ( db.query(Booking) .filter( Booking.space_id.in_(space_ids), Booking.status.in_(["pending", "approved"]), ) .count() ) if active_bookings > 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Cannot delete property with {active_bookings} active booking(s). Cancel or reject them first.", ) # Delete related data db.query(PropertyManager).filter(PropertyManager.property_id == property_id).delete() db.query(PropertyAccess).filter(PropertyAccess.property_id == property_id).delete() db.query(PropertySettings).filter(PropertySettings.property_id == property_id).delete() # Unlink spaces (set property_id to None) rather than deleting them db.query(Space).filter(Space.property_id == property_id).update({"property_id": None}) db.delete(prop) db.commit() log_action( db=db, action="property_deleted", user_id=current_user.id, target_type="property", target_id=property_id, details={"name": prop.name}, ) @manager_router.get("/{property_id}/access", response_model=list[PropertyAccessResponse]) def list_property_access( property_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> list[PropertyAccessResponse]: """List access grants for a property.""" verify_property_access(db, current_user, property_id, require_manager=True) accesses = db.query(PropertyAccess).filter(PropertyAccess.property_id == property_id).all() result = [] for a in accesses: user_name = None user_email = None org_name = None if a.user_id: u = db.query(User).filter(User.id == a.user_id).first() if u: user_name = u.full_name user_email = u.email if a.organization_id: org = db.query(Organization).filter(Organization.id == a.organization_id).first() if org: org_name = org.name result.append(PropertyAccessResponse( id=a.id, property_id=a.property_id, user_id=a.user_id, organization_id=a.organization_id, granted_by=a.granted_by, user_name=user_name, user_email=user_email, organization_name=org_name, created_at=a.created_at, )) return result @manager_router.post("/{property_id}/access", response_model=PropertyAccessResponse, status_code=status.HTTP_201_CREATED) def grant_property_access( property_id: int, data: PropertyAccessCreate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertyAccessResponse: """Grant access to a property.""" verify_property_access(db, current_user, property_id, require_manager=True) if not data.user_id and not data.organization_id: raise HTTPException(status_code=400, detail="Must provide user_id or organization_id") access = PropertyAccess( property_id=property_id, user_id=data.user_id, organization_id=data.organization_id, granted_by=current_user.id, ) db.add(access) db.commit() db.refresh(access) user_name = None user_email = None org_name = None if access.user_id: u = db.query(User).filter(User.id == access.user_id).first() if u: user_name = u.full_name user_email = u.email if access.organization_id: org = db.query(Organization).filter(Organization.id == access.organization_id).first() if org: org_name = org.name return PropertyAccessResponse( id=access.id, property_id=access.property_id, user_id=access.user_id, organization_id=access.organization_id, granted_by=access.granted_by, user_name=user_name, user_email=user_email, organization_name=org_name, created_at=access.created_at, ) @manager_router.delete("/{property_id}/access/{access_id}", status_code=status.HTTP_204_NO_CONTENT) def revoke_property_access( property_id: int, access_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> None: """Revoke access to a property.""" verify_property_access(db, current_user, property_id, require_manager=True) access = db.query(PropertyAccess).filter( PropertyAccess.id == access_id, PropertyAccess.property_id == property_id, ).first() if not access: raise HTTPException(status_code=404, detail="Access grant not found") db.delete(access) db.commit() @manager_router.get("/{property_id}/settings", response_model=PropertySettingsResponse) def get_property_settings( property_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertySettingsResponse: """Get property settings.""" verify_property_access(db, current_user, property_id, require_manager=True) ps = db.query(PropertySettings).filter(PropertySettings.property_id == property_id).first() if not ps: # Create default settings ps = PropertySettings(property_id=property_id, require_approval=True) db.add(ps) db.commit() db.refresh(ps) return PropertySettingsResponse.model_validate(ps) @manager_router.put("/{property_id}/settings", response_model=PropertySettingsResponse) def update_property_settings( property_id: int, data: PropertySettingsUpdate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_manager_or_superadmin)], ) -> PropertySettingsResponse: """Update property settings.""" verify_property_access(db, current_user, property_id, require_manager=True) ps = db.query(PropertySettings).filter(PropertySettings.property_id == property_id).first() if not ps: ps = PropertySettings(property_id=property_id) db.add(ps) db.commit() db.refresh(ps) for field in data.model_fields: value = getattr(data, field) if value is not None or field == "require_approval": setattr(ps, field, value) db.commit() db.refresh(ps) return PropertySettingsResponse.model_validate(ps) # === Superadmin endpoints === @admin_router.get("", response_model=list[PropertyResponse]) def admin_list_all_properties( db: Annotated[Session, Depends(get_db)], _: Annotated[User, Depends(get_current_admin)], include_inactive: bool = Query(False), ) -> list[PropertyResponse]: """Superadmin: list all properties.""" query = db.query(Property) if not include_inactive: query = query.filter(Property.is_active == True) # noqa: E712 properties = query.order_by(Property.name).all() result = [] for p in properties: space_count = db.query(Space).filter(Space.property_id == p.id).count() result.append(PropertyResponse( id=p.id, name=p.name, description=p.description, address=p.address, is_public=p.is_public, is_active=p.is_active, created_at=p.created_at, space_count=space_count, managers=_get_property_managers(db, p.id), )) return result @admin_router.post("/{property_id}/managers", status_code=status.HTTP_201_CREATED) def assign_property_manager( property_id: int, user_id: int = Query(...), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin), ) -> dict: """Superadmin: assign a manager to a property.""" prop = db.query(Property).filter(Property.id == property_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") existing = db.query(PropertyManager).filter( PropertyManager.property_id == property_id, PropertyManager.user_id == user_id, ).first() if existing: raise HTTPException(status_code=400, detail="User is already a manager of this property") pm = PropertyManager(property_id=property_id, user_id=user_id) db.add(pm) # Ensure user has manager role if user.role == "user": user.role = "manager" db.commit() return {"message": f"User {user.full_name} assigned as manager of {prop.name}"} @admin_router.delete("/{property_id}/managers/{user_id}", status_code=status.HTTP_204_NO_CONTENT) def remove_property_manager( property_id: int, user_id: int, db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin), ) -> None: """Superadmin: remove a manager from a property.""" pm = db.query(PropertyManager).filter( PropertyManager.property_id == property_id, PropertyManager.user_id == user_id, ).first() if not pm: raise HTTPException(status_code=404, detail="Manager assignment not found") db.delete(pm) db.commit()