2026-01-26 11:09:40 -05:00

189 lines
7.8 KiB
Python

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