""" Company endpoints for API v1. Provides CRUD operations for Company entities with filtering and search. """ from typing import List, Optional from uuid import UUID from django.shortcuts import get_object_or_404 from django.db.models import Q from ninja import Router, Query from ninja.pagination import paginate, PageNumberPagination from apps.entities.models import Company from apps.entities.services.company_submission import CompanySubmissionService from apps.users.permissions import jwt_auth, require_auth from ..schemas import ( CompanyCreate, CompanyUpdate, CompanyOut, CompanyListOut, ErrorResponse, HistoryListResponse, HistoryEventDetailSchema, HistoryComparisonSchema, HistoryDiffCurrentSchema, FieldHistorySchema, HistoryActivitySummarySchema, RollbackRequestSchema, RollbackResponseSchema, ErrorSchema ) from ..services.history_service import HistoryService from django.core.exceptions import ValidationError import logging logger = logging.getLogger(__name__) router = Router(tags=["Companies"]) class CompanyPagination(PageNumberPagination): """Custom pagination for companies.""" page_size = 50 @router.get( "/", response={200: List[CompanyOut]}, summary="List companies", description="Get a paginated list of companies with optional filtering" ) @paginate(CompanyPagination) def list_companies( request, search: Optional[str] = Query(None, description="Search by company name"), company_type: Optional[str] = Query(None, description="Filter by company type"), location_id: Optional[UUID] = Query(None, description="Filter by location"), ordering: Optional[str] = Query("-created", description="Sort by field (prefix with - for descending)") ): """ List all companies with optional filters. **Filters:** - search: Search company names (case-insensitive partial match) - company_type: Filter by specific company type - location_id: Filter by headquarters location - ordering: Sort results (default: -created) **Returns:** Paginated list of companies """ queryset = Company.objects.all() # Apply search filter if search: queryset = queryset.filter( Q(name__icontains=search) | Q(description__icontains=search) ) # Apply company type filter if company_type: queryset = queryset.filter(company_types__contains=[company_type]) # Apply location filter if location_id: queryset = queryset.filter(location_id=location_id) # Apply ordering valid_order_fields = ['name', 'created', 'modified', 'founded_date', 'park_count', 'ride_count'] order_field = ordering.lstrip('-') if order_field in valid_order_fields: queryset = queryset.order_by(ordering) else: queryset = queryset.order_by('-created') return queryset @router.get( "/{company_id}", response={200: CompanyOut, 404: ErrorResponse}, summary="Get company", description="Retrieve a single company by ID" ) def get_company(request, company_id: UUID): """ Get a company by ID. **Parameters:** - company_id: UUID of the company **Returns:** Company details """ company = get_object_or_404(Company, id=company_id) return company @router.post( "/", response={201: CompanyOut, 202: dict, 400: ErrorResponse, 401: ErrorResponse}, summary="Create company", description="Create a new company through the Sacred Pipeline (requires authentication)" ) @require_auth def create_company(request, payload: CompanyCreate): """ Create a new company through the Sacred Pipeline. **Authentication:** Required **Parameters:** - payload: Company data (name, company_types, headquarters, etc.) **Returns:** Created company (moderators) or submission confirmation (regular users) **Flow:** - Moderators: Company created immediately (bypass moderation) - Regular users: Submission created, enters moderation queue **Note:** All companies flow through ContentSubmission pipeline for moderation. """ try: user = request.auth # Create company through Sacred Pipeline submission, company = CompanySubmissionService.create_entity_submission( user=user, data=payload.dict(), source='api', ip_address=request.META.get('REMOTE_ADDR'), user_agent=request.META.get('HTTP_USER_AGENT', '') ) # If moderator bypass happened, Company was created immediately if company: logger.info(f"Company created (moderator): {company.id} by {user.email}") return 201, company # Regular user: submission pending moderation logger.info(f"Company submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Company submission pending moderation. You will be notified when it is approved.', } except ValidationError as e: return 400, {'detail': str(e)} except Exception as e: logger.error(f"Error creating company: {e}") return 400, {'detail': str(e)} @router.put( "/{company_id}", response={200: CompanyOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Update company", description="Update an existing company through the Sacred Pipeline (requires authentication)" ) @require_auth def update_company(request, company_id: UUID, payload: CompanyUpdate): """ Update a company through the Sacred Pipeline. **Authentication:** Required **Parameters:** - company_id: UUID of the company - payload: Updated company data **Returns:** Updated company (moderators) or submission confirmation (regular users) **Flow:** - Moderators: Updates applied immediately (bypass moderation) - Regular users: Submission created, enters moderation queue **Note:** All updates flow through ContentSubmission pipeline for moderation. """ try: user = request.auth company = get_object_or_404(Company, id=company_id) data = payload.dict(exclude_unset=True) # Update company through Sacred Pipeline submission, updated_company = CompanySubmissionService.update_entity_submission( entity=company, user=user, update_data=data, source='api', ip_address=request.META.get('REMOTE_ADDR'), user_agent=request.META.get('HTTP_USER_AGENT', '') ) # If moderator bypass happened, company was updated immediately if updated_company: logger.info(f"Company updated (moderator): {updated_company.id} by {user.email}") return 200, updated_company # Regular user: submission pending moderation logger.info(f"Company update submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Company update pending moderation. You will be notified when it is approved.', } except ValidationError as e: return 400, {'detail': str(e)} except Exception as e: logger.error(f"Error updating company: {e}") return 400, {'detail': str(e)} @router.patch( "/{company_id}", response={200: CompanyOut, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Partial update company", description="Partially update an existing company through the Sacred Pipeline (requires authentication)" ) @require_auth def partial_update_company(request, company_id: UUID, payload: CompanyUpdate): """ Partially update a company through the Sacred Pipeline. **Authentication:** Required **Parameters:** - company_id: UUID of the company - payload: Fields to update (only provided fields are updated) **Returns:** Updated company (moderators) or submission confirmation (regular users) **Flow:** - Moderators: Updates applied immediately (bypass moderation) - Regular users: Submission created, enters moderation queue **Note:** All updates flow through ContentSubmission pipeline for moderation. """ try: user = request.auth company = get_object_or_404(Company, id=company_id) data = payload.dict(exclude_unset=True) # Update company through Sacred Pipeline submission, updated_company = CompanySubmissionService.update_entity_submission( entity=company, user=user, update_data=data, source='api', ip_address=request.META.get('REMOTE_ADDR'), user_agent=request.META.get('HTTP_USER_AGENT', '') ) # If moderator bypass happened, company was updated immediately if updated_company: logger.info(f"Company partially updated (moderator): {updated_company.id} by {user.email}") return 200, updated_company # Regular user: submission pending moderation logger.info(f"Company partial update submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Company update pending moderation. You will be notified when it is approved.', } except ValidationError as e: return 400, {'detail': str(e)} except Exception as e: logger.error(f"Error partially updating company: {e}") return 400, {'detail': str(e)} @router.delete( "/{company_id}", response={200: dict, 202: dict, 404: ErrorResponse, 400: ErrorResponse, 401: ErrorResponse}, summary="Delete company", description="Delete a company through the Sacred Pipeline (requires authentication)" ) @require_auth def delete_company(request, company_id: UUID): """ Delete a company through the Sacred Pipeline. **Authentication:** Required **Parameters:** - company_id: UUID of the company **Returns:** Deletion confirmation (moderators) or submission confirmation (regular users) **Flow:** - Moderators: Company hard-deleted immediately (removed from database) - Regular users: Deletion request created, enters moderation queue **Deletion Strategy:** - Hard Delete: Removes company from database (Company has no status field for soft delete) **Note:** All deletions flow through ContentSubmission pipeline for moderation. **Warning:** Deleting a company may affect related parks and rides. """ try: user = request.auth company = get_object_or_404(Company, id=company_id) # Delete company through Sacred Pipeline (hard delete - no status field) submission, deleted = CompanySubmissionService.delete_entity_submission( entity=company, user=user, deletion_type='hard', # Company has no status field deletion_reason='', source='api', ip_address=request.META.get('REMOTE_ADDR'), user_agent=request.META.get('HTTP_USER_AGENT', '') ) # If moderator bypass happened, deletion was applied immediately if deleted: logger.info(f"Company deleted (moderator): {company_id} by {user.email}") return 200, { 'message': 'Company deleted successfully', 'entity_id': str(company_id), 'deletion_type': 'hard' } # Regular user: deletion pending moderation logger.info(f"Company deletion submission created: {submission.id} by {user.email}") return 202, { 'submission_id': str(submission.id), 'status': submission.status, 'message': 'Company deletion request pending moderation. You will be notified when it is approved.', 'entity_id': str(company_id) } except ValidationError as e: return 400, {'detail': str(e)} except Exception as e: logger.error(f"Error deleting company: {e}") return 400, {'detail': str(e)} @router.get( "/{company_id}/parks", response={200: List[dict], 404: ErrorResponse}, summary="Get company parks", description="Get all parks operated by a company" ) def get_company_parks(request, company_id: UUID): """ Get parks operated by a company. **Parameters:** - company_id: UUID of the company **Returns:** List of parks """ company = get_object_or_404(Company, id=company_id) parks = company.operated_parks.all().values('id', 'name', 'slug', 'status', 'park_type') return list(parks) @router.get( "/{company_id}/rides", response={200: List[dict], 404: ErrorResponse}, summary="Get company rides", description="Get all rides manufactured by a company" ) def get_company_rides(request, company_id: UUID): """ Get rides manufactured by a company. **Parameters:** - company_id: UUID of the company **Returns:** List of rides """ company = get_object_or_404(Company, id=company_id) rides = company.manufactured_rides.all().values('id', 'name', 'slug', 'status', 'ride_category') return list(rides) # ============================================================================ # History Endpoints # ============================================================================ @router.get( '/{company_id}/history/', response={200: HistoryListResponse, 404: ErrorSchema}, summary="Get company history", description="Get historical changes for a company" ) def get_company_history( request, company_id: UUID, page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=100), date_from: Optional[str] = Query(None, description="Filter from date (YYYY-MM-DD)"), date_to: Optional[str] = Query(None, description="Filter to date (YYYY-MM-DD)") ): """Get history for a company.""" from datetime import datetime # Verify company exists company = get_object_or_404(Company, id=company_id) # Parse dates if provided date_from_obj = datetime.fromisoformat(date_from).date() if date_from else None date_to_obj = datetime.fromisoformat(date_to).date() if date_to else None # Get history offset = (page - 1) * page_size events, accessible_count = HistoryService.get_history( 'company', str(company_id), request.user, date_from=date_from_obj, date_to=date_to_obj, limit=page_size, offset=offset ) # Format events formatted_events = [] for event in events: formatted_events.append({ 'id': event['id'], 'timestamp': event['timestamp'], 'operation': event['operation'], 'snapshot': event['snapshot'], 'changed_fields': event.get('changed_fields'), 'change_summary': event.get('change_summary', ''), 'can_rollback': HistoryService.can_rollback(request.user) }) # Calculate pagination total_pages = (accessible_count + page_size - 1) // page_size return { 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, 'total_events': accessible_count, 'accessible_events': accessible_count, 'access_limited': HistoryService.is_access_limited(request.user), 'access_reason': HistoryService.get_access_reason(request.user), 'events': formatted_events, 'pagination': { 'page': page, 'page_size': page_size, 'total_pages': total_pages, 'total_items': accessible_count } } @router.get( '/{company_id}/history/{event_id}/', response={200: HistoryEventDetailSchema, 404: ErrorSchema}, summary="Get specific company history event", description="Get detailed information about a specific historical event" ) def get_company_history_event(request, company_id: UUID, event_id: int): """Get a specific history event for a company.""" company = get_object_or_404(Company, id=company_id) event = HistoryService.get_event('company', event_id, request.user) if not event: return 404, {"error": "Event not found or not accessible"} return { 'id': event['id'], 'timestamp': event['timestamp'], 'operation': event['operation'], 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, 'snapshot': event['snapshot'], 'changed_fields': event.get('changed_fields'), 'metadata': event.get('metadata', {}), 'can_rollback': HistoryService.can_rollback(request.user), 'rollback_preview': None } @router.get( '/{company_id}/history/compare/', response={200: HistoryComparisonSchema, 400: ErrorSchema, 404: ErrorSchema}, summary="Compare two company history events", description="Compare two historical events for a company" ) def compare_company_history( request, company_id: UUID, event1: int = Query(..., description="First event ID"), event2: int = Query(..., description="Second event ID") ): """Compare two historical events for a company.""" company = get_object_or_404(Company, id=company_id) try: comparison = HistoryService.compare_events( 'company', event1, event2, request.user ) if not comparison: return 404, {"error": "One or both events not found"} return { 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, 'event1': comparison['event1'], 'event2': comparison['event2'], 'differences': comparison['differences'], 'changed_field_count': comparison['changed_field_count'], 'unchanged_field_count': comparison['unchanged_field_count'], 'time_between': comparison['time_between'] } except ValueError as e: return 400, {"error": str(e)} @router.get( '/{company_id}/history/{event_id}/diff-current/', response={200: HistoryDiffCurrentSchema, 404: ErrorSchema}, summary="Compare historical event with current state", description="Compare a historical event with the current company state" ) def diff_company_history_with_current(request, company_id: UUID, event_id: int): """Compare historical event with current company state.""" company = get_object_or_404(Company, id=company_id) try: diff = HistoryService.compare_with_current( 'company', event_id, company, request.user ) if not diff: return 404, {"error": "Event not found"} return { 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, 'event': diff['event'], 'current_state': diff['current_state'], 'differences': diff['differences'], 'changed_field_count': diff['changed_field_count'], 'time_since': diff['time_since'] } except ValueError as e: return 404, {"error": str(e)} @router.post( '/{company_id}/history/{event_id}/rollback/', response={200: RollbackResponseSchema, 400: ErrorSchema, 403: ErrorSchema}, summary="Rollback company to historical state", description="Rollback company to a historical state (Moderators/Admins only)" ) def rollback_company(request, company_id: UUID, event_id: int, payload: RollbackRequestSchema): """ Rollback company to a historical state. **Permission:** Moderators, Admins, Superusers only """ # Check authentication if not request.user or not request.user.is_authenticated: return 401, {"error": "Authentication required"} # Check rollback permission if not HistoryService.can_rollback(request.user): return 403, {"error": "Only moderators and administrators can perform rollbacks"} company = get_object_or_404(Company, id=company_id) try: result = HistoryService.rollback_to_event( company, 'company', event_id, request.user, fields=payload.fields, comment=payload.comment, create_backup=payload.create_backup ) return result except (ValueError, PermissionError) as e: return 400, {"error": str(e)} @router.get( '/{company_id}/history/field/{field_name}/', response={200: FieldHistorySchema, 404: ErrorSchema}, summary="Get field-specific history", description="Get history of changes to a specific company field" ) def get_company_field_history(request, company_id: UUID, field_name: str): """Get history of changes to a specific company field.""" company = get_object_or_404(Company, id=company_id) history = HistoryService.get_field_history( 'company', str(company_id), field_name, request.user ) return { 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, 'field': field_name, 'field_type': 'CharField', # Could introspect this **history } @router.get( '/{company_id}/history/summary/', response={200: HistoryActivitySummarySchema, 404: ErrorSchema}, summary="Get company activity summary", description="Get activity summary for a company" ) def get_company_activity_summary(request, company_id: UUID): """Get activity summary for a company.""" company = get_object_or_404(Company, id=company_id) summary = HistoryService.get_activity_summary( 'company', str(company_id), request.user ) return { 'entity_id': str(company_id), 'entity_type': 'company', 'entity_name': company.name, **summary }