from typing import cast import strawberry from strawberry.types import Info from channels.db import database_sync_to_async from core.graphql.pubsub import pubsub from core.graphql.inputs.scope import ( ScopeInput, ScopeUpdateInput, AreaInput, AreaUpdateInput, TaskInput, TaskUpdateInput, TaskCompletionInput, TaskCompletionUpdateInput, ) from core.graphql.types.scope import ( ScopeType, AreaType, TaskType, TaskCompletionType, ) from core.models.scope import Scope, Area, Task, TaskCompletion from core.models.session import ServiceSession from core.graphql.utils import create_object, update_object, delete_object, _decode_global_id from core.services.events import ( publish_scope_created, publish_scope_updated, publish_scope_deleted, publish_area_created, publish_area_updated, publish_area_deleted, publish_task_created, publish_task_updated, publish_task_deleted, publish_task_completion_recorded, ) @strawberry.type class Mutation: @strawberry.mutation(description="Create a new scope") async def create_scope(self, input: ScopeInput, info: Info) -> ScopeType: instance = await create_object(input, Scope) await pubsub.publish("scope_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_created( scope_id=str(instance.id), account_id=str(instance.account_id), triggered_by=profile ) return cast(ScopeType, instance) @strawberry.mutation(description="Update an existing scope") async def update_scope(self, input: ScopeUpdateInput, info: Info) -> ScopeType: instance = await update_object(input, Scope) await pubsub.publish("scope_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_updated( scope_id=str(instance.id), account_id=str(instance.account_id), triggered_by=profile ) return cast(ScopeType, instance) @strawberry.mutation(description="Delete an existing scope") async def delete_scope(self, id: strawberry.ID, info: Info) -> strawberry.ID: def _delete_scope_sync(scope_id): """ Smart delete: soft-delete if sessions reference this scope, hard-delete otherwise. Returns (account_id, action) where action is 'deleted' or 'deactivated'. """ pk = _decode_global_id(scope_id) try: scope = Scope.objects.get(pk=pk) except Scope.DoesNotExist: return None, None account_id = scope.account_id # Check if any service sessions reference this scope has_sessions = ServiceSession.objects.filter(scope_id=pk).exists() if has_sessions: # Soft delete - deactivate the scope to preserve historical data scope.is_active = False scope.save(update_fields=['is_active']) else: # Hard delete - no sessions reference this scope scope.delete() return account_id, 'deactivated' if has_sessions else 'deleted' account_id, action = await database_sync_to_async(_delete_scope_sync)(id) if account_id is None: raise ValueError(f"Scope with ID {id} does not exist") await pubsub.publish("scope_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_scope_deleted( scope_id=str(id), account_id=str(account_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a new area") async def create_area(self, input: AreaInput, info: Info) -> AreaType: instance = await create_object(input, Area) await pubsub.publish("area_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_created( area_id=str(instance.id), scope_id=str(instance.scope_id), triggered_by=profile ) return cast(AreaType, instance) @strawberry.mutation(description="Update an existing area") async def update_area(self, input: AreaUpdateInput, info: Info) -> AreaType: instance = await update_object(input, Area) await pubsub.publish("area_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_updated( area_id=str(instance.id), scope_id=str(instance.scope_id), triggered_by=profile ) return cast(AreaType, instance) @strawberry.mutation(description="Delete an existing area") async def delete_area(self, id: strawberry.ID, info: Info) -> strawberry.ID: def _delete_area_sync(area_id): """ Delete an area if no task completions reference its tasks. Returns scope_id on success, raises ValueError if completions exist. """ pk = _decode_global_id(area_id) try: area = Area.objects.get(pk=pk) except Area.DoesNotExist: return None # Check if any task completions reference tasks in this area has_completions = TaskCompletion.objects.filter(task__area_id=pk).exists() if has_completions: raise ValueError( "Cannot delete area: it contains tasks with recorded completions. " "Deactivate the scope instead to preserve historical data." ) scope_id = area.scope_id area.delete() return scope_id scope_id = await database_sync_to_async(_delete_area_sync)(id) if scope_id is None: raise ValueError(f"Area with ID {id} does not exist") await pubsub.publish("area_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_area_deleted( area_id=str(id), scope_id=str(scope_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a new task") async def create_task(self, input: TaskInput, info: Info) -> TaskType: instance = await create_object(input, Task) await pubsub.publish("task_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_created( task_id=str(instance.id), area_id=str(instance.area_id), triggered_by=profile ) return cast(TaskType, instance) @strawberry.mutation(description="Update an existing task") async def update_task(self, input: TaskUpdateInput, info: Info) -> TaskType: instance = await update_object(input, Task) await pubsub.publish("task_updated", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_updated( task_id=str(instance.id), area_id=str(instance.area_id), triggered_by=profile ) return cast(TaskType, instance) @strawberry.mutation(description="Delete an existing task") async def delete_task(self, id: strawberry.ID, info: Info) -> strawberry.ID: def _delete_task_sync(task_id): """ Delete a task if no task completions reference it. Returns area_id on success, raises ValueError if completions exist. """ pk = _decode_global_id(task_id) try: task = Task.objects.get(pk=pk) except Task.DoesNotExist: return None # Check if any task completions reference this task has_completions = TaskCompletion.objects.filter(task_id=pk).exists() if has_completions: raise ValueError( "Cannot delete task: it has recorded completions. " "Deactivate the scope instead to preserve historical data." ) area_id = task.area_id task.delete() return area_id area_id = await database_sync_to_async(_delete_task_sync)(id) if area_id is None: raise ValueError(f"Task with ID {id} does not exist") await pubsub.publish("task_deleted", id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_deleted( task_id=str(id), area_id=str(area_id), triggered_by=profile ) return id @strawberry.mutation(description="Create a new task completion") async def create_task_completion(self, input: TaskCompletionInput, info: Info) -> TaskCompletionType: instance = await create_object(input, TaskCompletion) await pubsub.publish("task_completion_created", instance.id) # Publish event profile = getattr(info.context.request, 'profile', None) await publish_task_completion_recorded( completion_id=str(instance.id), task_id=str(instance.task_id), service_id=str(instance.service_id), triggered_by=profile ) return cast(TaskCompletionType, instance) @strawberry.mutation(description="Update an existing task completion") async def update_task_completion(self, input: TaskCompletionUpdateInput, info: Info) -> TaskCompletionType: instance = await update_object(input, TaskCompletion) await pubsub.publish("task_completion_updated", instance.id) # Publish event (reuse the same event for updates) profile = getattr(info.context.request, 'profile', None) await publish_task_completion_recorded( completion_id=str(instance.id), task_id=str(instance.task_id), service_id=str(instance.service_id), triggered_by=profile ) return cast(TaskCompletionType, instance) @strawberry.mutation(description="Delete an existing task completion") async def delete_task_completion(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, TaskCompletion) if not instance: raise ValueError(f"TaskCompletion with ID {id} does not exist") await pubsub.publish("task_completion_deleted", id) # Note: No event publication for deletion as there's no corresponding delete event # in the events.py file for task completions return id