"""Public/anonymous endpoints (no auth required).""" from datetime import datetime from typing import Annotated from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from app.core.deps import get_db from app.models.booking import Booking from app.models.property import Property from app.models.property_manager import PropertyManager from app.models.space import Space from app.models.user import User from app.schemas.booking import AnonymousBookingCreate, AvailabilityCheck, BookingResponse, ConflictingBooking from app.schemas.property import PropertyResponse from app.schemas.space import SpaceResponse from app.services.booking_service import validate_booking_rules from app.services.email_service import send_booking_notification from app.services.notification_service import create_notification router = APIRouter(prefix="/public", tags=["public"]) @router.get("/properties", response_model=list[PropertyResponse]) def list_public_properties( db: Annotated[Session, Depends(get_db)], ) -> list[PropertyResponse]: """List public properties (no auth required).""" 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, )) return result @router.get("/properties/{property_id}/spaces", response_model=list[SpaceResponse]) def list_public_property_spaces( property_id: int, db: Annotated[Session, Depends(get_db)], ) -> list[SpaceResponse]: """List spaces of a public property (no auth required).""" prop = db.query(Property).filter(Property.id == property_id).first() if not prop: raise HTTPException(status_code=404, detail="Property not found") if not prop.is_public: raise HTTPException(status_code=403, detail="Property is private") spaces = ( db.query(Space) .filter(Space.property_id == property_id, Space.is_active == True) # noqa: E712 .order_by(Space.name) .all() ) return [SpaceResponse.model_validate(s) for s in spaces] @router.get("/spaces/{space_id}/availability", response_model=AvailabilityCheck) def check_public_availability( space_id: int, start_datetime: Annotated[datetime, Query()], end_datetime: Annotated[datetime, Query()], db: Annotated[Session, Depends(get_db)], ) -> AvailabilityCheck: """Check availability for a space (no auth required).""" space = db.query(Space).filter(Space.id == space_id).first() if not space: raise HTTPException(status_code=404, detail="Space not found") # Verify space belongs to a public property if space.property_id: prop = db.query(Property).filter(Property.id == space.property_id).first() if prop and not prop.is_public: raise HTTPException(status_code=403, detail="Property is private") # Find conflicting bookings conflicts = ( db.query(Booking) .filter( Booking.space_id == space_id, Booking.status.in_(["approved", "pending"]), or_( and_( Booking.start_datetime <= start_datetime, Booking.end_datetime > start_datetime, ), and_( Booking.start_datetime < end_datetime, Booking.end_datetime >= end_datetime, ), and_( Booking.start_datetime >= start_datetime, Booking.end_datetime <= end_datetime, ), ), ) .all() ) if not conflicts: return AvailabilityCheck(available=True, conflicts=[], message="Time slot is available") approved_count = sum(1 for b in conflicts if b.status == "approved") pending_count = sum(1 for b in conflicts if b.status == "pending") if approved_count > 0: message = f"Time slot has {approved_count} approved booking(s)." else: message = f"Time slot has {pending_count} pending request(s)." return AvailabilityCheck( available=approved_count == 0, conflicts=[ ConflictingBooking( id=b.id, user_name=b.user.full_name if b.user else (b.guest_name or "Anonymous"), title=b.title, status=b.status, start_datetime=b.start_datetime, end_datetime=b.end_datetime, ) for b in conflicts ], message=message, ) @router.post("/bookings", response_model=BookingResponse, status_code=status.HTTP_201_CREATED) def create_anonymous_booking( data: AnonymousBookingCreate, background_tasks: BackgroundTasks, db: Annotated[Session, Depends(get_db)], ) -> BookingResponse: """Create an anonymous/guest booking (no auth required).""" # Validate space exists space = db.query(Space).filter(Space.id == data.space_id).first() if not space: raise HTTPException(status_code=404, detail="Space not found") # Verify space belongs to a public property if space.property_id: prop = db.query(Property).filter(Property.id == space.property_id).first() if prop and not prop.is_public: raise HTTPException(status_code=403, detail="Cannot book in a private property without authentication") else: raise HTTPException(status_code=400, detail="Space is not assigned to any property") # Basic validation (no user_id needed for anonymous) if data.end_datetime <= data.start_datetime: raise HTTPException(status_code=400, detail="End time must be after start time") # Check for overlapping approved bookings overlapping = db.query(Booking).filter( Booking.space_id == data.space_id, Booking.status == "approved", and_( Booking.start_datetime < data.end_datetime, Booking.end_datetime > data.start_datetime, ), ).first() if overlapping: raise HTTPException(status_code=400, detail="Time slot is already booked") # Create anonymous booking booking = Booking( user_id=None, space_id=data.space_id, start_datetime=data.start_datetime, end_datetime=data.end_datetime, title=data.title, description=data.description, status="pending", guest_name=data.guest_name, guest_email=data.guest_email, guest_organization=data.guest_organization, is_anonymous=True, created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Notify property managers if space.property_id: manager_ids = [ pm.user_id for pm in db.query(PropertyManager).filter(PropertyManager.property_id == space.property_id).all() ] managers = db.query(User).filter(User.id.in_(manager_ids)).all() if manager_ids else [] # Also notify superadmins superadmins = db.query(User).filter(User.role.in_(["admin", "superadmin"])).all() notify_users = {u.id: u for u in list(managers) + list(superadmins)} for user in notify_users.values(): create_notification( db=db, user_id=user.id, type="booking_created", title="Cerere Anonimă de Rezervare", message=f"Persoana {data.guest_name} ({data.guest_email}) a solicitat rezervarea spațiului {space.name}", booking_id=booking.id, ) background_tasks.add_task( send_booking_notification, booking, "anonymous_created", user.email, data.guest_name, {"guest_email": data.guest_email}, ) return BookingResponse.model_validate(booking)