from typing import cast import strawberry from strawberry.types import Info from asgiref.sync import sync_to_async from channels.db import database_sync_to_async from core.graphql.pubsub import pubsub from core.graphql.inputs.project import ProjectInput, ProjectUpdateInput from core.graphql.types.project import ProjectType from core.models.account import AccountAddress from core.models.profile import TeamProfile from core.models.project import Project from core.models.enums import ServiceChoices from core.graphql.utils import create_object, update_object, delete_object from core.services.events import ( publish_project_created, publish_project_status_changed, publish_project_completed, publish_project_dispatched, publish_project_deleted, ) # Helper to get admin profile async def _get_admin_profile(): return await sync_to_async( lambda: TeamProfile.objects.filter(role='ADMIN').first() )() # Helper to check if admin is in team member IDs (handles GlobalID objects) def _admin_in_team_members(admin_id, team_member_ids): if not team_member_ids or not admin_id: return False # team_member_ids may be GlobalID objects with .node_id attribute member_uuids = [] for mid in team_member_ids: if hasattr(mid, 'node_id'): member_uuids.append(str(mid.node_id)) else: member_uuids.append(str(mid)) return str(admin_id) in member_uuids # Helper to get old team member IDs from instance async def _get_old_team_member_ids(instance): return await sync_to_async( lambda: set(str(m.id) for m in instance.team_members.all()) )() @strawberry.type class Mutation: @strawberry.mutation(description="Create a new project") async def create_project(self, input: ProjectInput, info: Info) -> ProjectType: # Exclude m2m id fields from model constructor payload = {k: v for k, v in input.__dict__.items() if k not in {"team_member_ids"}} m2m_data = {"team_members": input.team_member_ids} instance = await create_object(payload, Project, m2m_data) await pubsub.publish("project_created", instance.id) # Publish event for notifications profile = getattr(info.context.request, 'profile', None) await publish_project_created( project_id=str(instance.id), triggered_by=profile, metadata={ 'status': instance.status, 'customer_id': str(instance.customer_id), 'name': instance.name, 'date': str(instance.date) } ) # Check if project was dispatched (admin in team members) admin = await _get_admin_profile() if admin and _admin_in_team_members(admin.id, input.team_member_ids): # Build metadata account_address_id = None account_name = None if instance.account_address_id: account_address_id = str(instance.account_address_id) account_address = await sync_to_async( lambda: AccountAddress.objects.select_related('account').get(id=instance.account_address_id) )() account_name = account_address.account.name if account_address.account else None await publish_project_dispatched( project_id=str(instance.id), triggered_by=profile, metadata={ 'project_id': str(instance.id), 'project_name': instance.name, 'customer_id': str(instance.customer_id), 'account_address_id': account_address_id, 'account_name': account_name, 'date': str(instance.date), 'status': instance.status } ) return cast(ProjectType, instance) @strawberry.mutation(description="Update an existing project") async def update_project(self, input: ProjectUpdateInput, info: Info) -> ProjectType: # Get old project to check for status changes old_project = await database_sync_to_async(Project.objects.get)(pk=input.id.node_id) old_status = old_project.status # Get old team member IDs before update (for dispatched detection) old_team_member_ids = await _get_old_team_member_ids(old_project) # Keep id and non-m2m fields; drop m2m *_ids from the update payload payload = {k: v for k, v in input.__dict__.items() if k not in {"team_member_ids"}} m2m_data = {"team_members": getattr(input, "team_member_ids", None)} instance = await update_object(payload, Project, m2m_data) await pubsub.publish("project_updated", instance.id) # Publish events for notifications profile = getattr(info.context.request, 'profile', None) # Check if status changed if hasattr(input, 'status') and input.status and input.status != old_status: await publish_project_status_changed( project_id=str(instance.id), old_status=old_status, new_status=instance.status, triggered_by=profile ) # Check if project was completed if instance.status == ServiceChoices.COMPLETED: await publish_project_completed( project_id=str(instance.id), triggered_by=profile, metadata={ 'customer_id': str(instance.customer_id), 'name': instance.name, 'date': str(instance.date) } ) # Check if admin was newly added (dispatched) if input.team_member_ids is not None: admin = await _get_admin_profile() if admin: admin_was_in_old = str(admin.id) in old_team_member_ids admin_in_new = _admin_in_team_members(admin.id, input.team_member_ids) if not admin_was_in_old and admin_in_new: # Admin was just added - project was dispatched account_address_id = None account_name = None if instance.account_address_id: account_address_id = str(instance.account_address_id) account_address = await sync_to_async( lambda: AccountAddress.objects.select_related('account').get(id=instance.account_address_id) )() account_name = account_address.account.name if account_address.account else None await publish_project_dispatched( project_id=str(instance.id), triggered_by=profile, metadata={ 'project_id': str(instance.id), 'project_name': instance.name, 'customer_id': str(instance.customer_id), 'account_address_id': account_address_id, 'account_name': account_name, 'date': str(instance.date), 'status': instance.status } ) return cast(ProjectType, instance) @strawberry.mutation(description="Delete an existing project") async def delete_project(self, id: strawberry.ID, info: Info) -> strawberry.ID: instance = await delete_object(id, Project) if not instance: raise ValueError(f"Project with ID {id} does not exist") await pubsub.publish("project_deleted", id) # Publish event for notifications profile = getattr(info.context.request, 'profile', None) await publish_project_deleted( project_id=str(id), triggered_by=profile ) return id