""" Services for park-related business logic. Following Django styleguide pattern for business logic encapsulation. """ from typing import Optional, Dict, Any, Tuple from django.db import transaction from django.db.models import Q from django.core.exceptions import ValidationError from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractBaseUser from .models import Park, ParkArea from location.models import Location # Use AbstractBaseUser for type hinting UserType = AbstractBaseUser User = get_user_model() class ParkService: """Service for managing park operations.""" @staticmethod def create_park( *, name: str, description: str = "", status: str = "OPERATING", operator_id: Optional[int] = None, property_owner_id: Optional[int] = None, opening_date: Optional[str] = None, closing_date: Optional[str] = None, operating_season: str = "", size_acres: Optional[float] = None, website: str = "", location_data: Optional[Dict[str, Any]] = None, created_by: Optional[UserType] = None ) -> Park: """ Create a new park with validation and location handling. Args: name: Park name description: Park description status: Operating status operator_id: ID of operating company property_owner_id: ID of property owner company opening_date: Opening date closing_date: Closing date operating_season: Operating season description size_acres: Park size in acres website: Park website URL location_data: Dictionary containing location information created_by: User creating the park Returns: Created Park instance Raises: ValidationError: If park data is invalid """ with transaction.atomic(): # Create park instance park = Park( name=name, description=description, status=status, opening_date=opening_date, closing_date=closing_date, operating_season=operating_season, size_acres=size_acres, website=website ) # Set foreign key relationships if provided if operator_id: from .models import Company park.operator = Company.objects.get(id=operator_id) if property_owner_id: from .models import Company park.property_owner = Company.objects.get(id=property_owner_id) # CRITICAL STYLEGUIDE FIX: Call full_clean before save park.full_clean() park.save() # Handle location if provided if location_data: LocationService.create_park_location( park=park, **location_data ) return park @staticmethod def update_park( *, park_id: int, updates: Dict[str, Any], updated_by: Optional[UserType] = None ) -> Park: """ Update an existing park with validation. Args: park_id: ID of park to update updates: Dictionary of field updates updated_by: User performing the update Returns: Updated Park instance Raises: Park.DoesNotExist: If park doesn't exist ValidationError: If update data is invalid """ with transaction.atomic(): park = Park.objects.select_for_update().get(id=park_id) # Apply updates for field, value in updates.items(): if hasattr(park, field): setattr(park, field, value) # CRITICAL STYLEGUIDE FIX: Call full_clean before save park.full_clean() park.save() return park @staticmethod def delete_park(*, park_id: int, deleted_by: Optional[UserType] = None) -> bool: """ Soft delete a park by setting status to DEMOLISHED. Args: park_id: ID of park to delete deleted_by: User performing the deletion Returns: True if successfully deleted Raises: Park.DoesNotExist: If park doesn't exist """ with transaction.atomic(): park = Park.objects.select_for_update().get(id=park_id) park.status = 'DEMOLISHED' # CRITICAL STYLEGUIDE FIX: Call full_clean before save park.full_clean() park.save() return True @staticmethod def create_park_area( *, park_id: int, name: str, description: str = "", created_by: Optional[UserType] = None ) -> ParkArea: """ Create a new area within a park. Args: park_id: ID of the parent park name: Area name description: Area description created_by: User creating the area Returns: Created ParkArea instance Raises: Park.DoesNotExist: If park doesn't exist ValidationError: If area data is invalid """ park = Park.objects.get(id=park_id) area = ParkArea( park=park, name=name, description=description ) # CRITICAL STYLEGUIDE FIX: Call full_clean before save area.full_clean() area.save() return area @staticmethod def update_park_statistics(*, park_id: int) -> Park: """ Recalculate and update park statistics (ride counts, ratings). Args: park_id: ID of park to update statistics for Returns: Updated Park instance with fresh statistics """ from rides.models import Ride from .models import ParkReview from django.db.models import Count, Avg with transaction.atomic(): park = Park.objects.select_for_update().get(id=park_id) # Calculate ride counts ride_stats = Ride.objects.filter(park=park).aggregate( total_rides=Count('id'), coaster_count=Count('id', filter=Q(category__in=['RC', 'WC'])) ) # Calculate average rating avg_rating = ParkReview.objects.filter( park=park, is_published=True ).aggregate(avg_rating=Avg('rating'))['avg_rating'] # Update park fields park.ride_count = ride_stats['total_rides'] or 0 park.coaster_count = ride_stats['coaster_count'] or 0 park.average_rating = avg_rating # CRITICAL STYLEGUIDE FIX: Call full_clean before save park.full_clean() park.save() return park class LocationService: """Service for managing location operations.""" @staticmethod def create_park_location( *, park: Park, latitude: Optional[float] = None, longitude: Optional[float] = None, street_address: str = "", city: str = "", state: str = "", country: str = "", postal_code: str = "" ) -> Location: """ Create a location for a park. Args: park: Park instance latitude: Latitude coordinate longitude: Longitude coordinate street_address: Street address city: City name state: State/region name country: Country name postal_code: Postal/ZIP code Returns: Created Location instance Raises: ValidationError: If location data is invalid """ location = Location( content_object=park, name=park.name, location_type='park', latitude=latitude, longitude=longitude, street_address=street_address, city=city, state=state, country=country, postal_code=postal_code ) # CRITICAL STYLEGUIDE FIX: Call full_clean before save location.full_clean() location.save() return location @staticmethod def update_park_location( *, park_id: int, location_updates: Dict[str, Any] ) -> Location: """ Update location information for a park. Args: park_id: ID of the park location_updates: Dictionary of location field updates Returns: Updated Location instance Raises: Location.DoesNotExist: If location doesn't exist ValidationError: If location data is invalid """ with transaction.atomic(): park = Park.objects.get(id=park_id) try: location = park.location except Location.DoesNotExist: # Create location if it doesn't exist return LocationService.create_park_location( park=park, **location_updates ) # Apply updates for field, value in location_updates.items(): if hasattr(location, field): setattr(location, field, value) # CRITICAL STYLEGUIDE FIX: Call full_clean before save location.full_clean() location.save() return location