""" M2M (Machine-to-Machine) GraphQL schema. Used by internal services (Temporal workflows, etc.) without user authentication. """ import graphene import logging from django.utils import timezone from graphene_django import DjangoObjectType from ..models import Team as TeamModel, TeamMember as TeamMemberModel, TeamAddress as TeamAddressModel, TeamInvitation as TeamInvitationModel, TeamInvitationToken as TeamInvitationTokenModel from .user_schema import _get_or_create_user_with_profile logger = logging.getLogger(__name__) class Team(DjangoObjectType): id = graphene.String() class Meta: model = TeamModel fields = ('uuid', 'name', 'logto_org_id', 'created_at', 'updated_at') def resolve_id(self, info): return self.uuid class M2MQuery(graphene.ObjectType): team = graphene.Field(Team, teamId=graphene.String(required=True)) invitation = graphene.Field(lambda: TeamInvitation, invitationUuid=graphene.String(required=True)) def resolve_team(self, info, teamId): try: return TeamModel.objects.get(uuid=teamId) except TeamModel.DoesNotExist: return None def resolve_invitation(self, info, invitationUuid): try: return TeamInvitationModel.objects.get(uuid=invitationUuid) except TeamInvitationModel.DoesNotExist: return None class TeamInvitation(DjangoObjectType): class Meta: model = TeamInvitationModel fields = ('uuid', 'team', 'email', 'role', 'status', 'invited_by', 'expires_at', 'created_at') class TeamInvitationToken(DjangoObjectType): class Meta: model = TeamInvitationTokenModel fields = ('uuid', 'invitation', 'workflow_status', 'expires_at', 'created_at') class CreateInvitationFromWorkflowInput(graphene.InputObjectType): teamUuid = graphene.String(required=True) email = graphene.String(required=True) role = graphene.String() invitedBy = graphene.String(required=True) expiresAt = graphene.DateTime(required=True) class CreateInvitationFromWorkflow(graphene.Mutation): class Arguments: input = CreateInvitationFromWorkflowInput(required=True) success = graphene.Boolean() message = graphene.String() invitationUuid = graphene.String() invitation = graphene.Field(TeamInvitation) def mutate(self, info, input): try: team = TeamModel.objects.get(uuid=input.teamUuid) invitation = TeamInvitationModel.objects.create( team=team, email=input.email, role=input.role or 'MEMBER', status='PENDING', invited_by=input.invitedBy, expires_at=input.expiresAt, ) return CreateInvitationFromWorkflow( success=True, message="Invitation created", invitationUuid=invitation.uuid, invitation=invitation, ) except Exception as exc: logger.exception("Failed to create invitation") return CreateInvitationFromWorkflow(success=False, message=str(exc)) class CreateInvitationTokenInput(graphene.InputObjectType): invitationUuid = graphene.String(required=True) tokenHash = graphene.String(required=True) expiresAt = graphene.DateTime(required=True) class CreateInvitationToken(graphene.Mutation): class Arguments: input = CreateInvitationTokenInput(required=True) success = graphene.Boolean() message = graphene.String() token = graphene.Field(TeamInvitationToken) def mutate(self, info, input): try: invitation = TeamInvitationModel.objects.get(uuid=input.invitationUuid) token = TeamInvitationTokenModel.objects.create( invitation=invitation, token_hash=input.tokenHash, expires_at=input.expiresAt, workflow_status='pending', ) return CreateInvitationToken(success=True, message="Token created", token=token) except Exception as exc: logger.exception("Failed to create invitation token") return CreateInvitationToken(success=False, message=str(exc)) class UpdateInvitationTokenStatusInput(graphene.InputObjectType): tokenUuid = graphene.String(required=True) status = graphene.String(required=True) # pending | active | error class UpdateInvitationTokenStatus(graphene.Mutation): class Arguments: input = UpdateInvitationTokenStatusInput(required=True) success = graphene.Boolean() message = graphene.String() token = graphene.Field(TeamInvitationToken) def mutate(self, info, input): try: token = TeamInvitationTokenModel.objects.get(uuid=input.tokenUuid) token.workflow_status = input.status token.save(update_fields=['workflow_status']) return UpdateInvitationTokenStatus(success=True, message="Status updated", token=token) except TeamInvitationTokenModel.DoesNotExist: return UpdateInvitationTokenStatus(success=False, message="Token not found") class SetLogtoOrgIdMutation(graphene.Mutation): """Set Logto organization ID (used by Temporal workflows)""" class Arguments: teamId = graphene.String(required=True) logtoOrgId = graphene.String(required=True) team = graphene.Field(Team) success = graphene.Boolean() def mutate(self, info, teamId, logtoOrgId): try: team = TeamModel.objects.get(uuid=teamId) team.logto_org_id = logtoOrgId team.save(update_fields=['logto_org_id']) logger.info("Team %s: logto_org_id set to %s", teamId, logtoOrgId) return SetLogtoOrgIdMutation(team=team, success=True) except TeamModel.DoesNotExist: raise Exception(f"Team {teamId} not found") class CreateAddressFromWorkflowMutation(graphene.Mutation): """Create TeamAddress from Temporal workflow (workflow-first pattern)""" class Arguments: workflowId = graphene.String(required=True) teamUuid = graphene.String(required=True) name = graphene.String(required=True) address = graphene.String(required=True) latitude = graphene.Float() longitude = graphene.Float() countryCode = graphene.String() isDefault = graphene.Boolean() success = graphene.Boolean() addressUuid = graphene.String() teamType = graphene.String() # "buyer" or "seller" message = graphene.String() def mutate(self, info, workflowId, teamUuid, name, address, latitude=None, longitude=None, countryCode=None, isDefault=False): try: team = TeamModel.objects.get(uuid=teamUuid) # Если новый адрес default - сбрасываем старые if isDefault: team.addresses.update(is_default=False) address_obj = TeamAddressModel.objects.create( team=team, name=name, address=address, latitude=latitude, longitude=longitude, country_code=countryCode, is_default=isDefault, status='pending', ) logger.info( "Created address %s for team %s (workflow=%s, team_type=%s)", address_obj.uuid, teamUuid, workflowId, team.team_type ) return CreateAddressFromWorkflowMutation( success=True, addressUuid=str(address_obj.uuid), teamType=team.team_type.lower() if team.team_type else "buyer", message="Address created" ) except TeamModel.DoesNotExist: return CreateAddressFromWorkflowMutation( success=False, message=f"Team {teamUuid} not found" ) except Exception as e: logger.error("Failed to create address: %s", e) return CreateAddressFromWorkflowMutation( success=False, message=str(e) ) class CreateTeamFromWorkflowMutation(graphene.Mutation): """Create Team from Temporal workflow (KYC approval flow)""" class Arguments: teamName = graphene.String(required=True) ownerId = graphene.String(required=True) # Logto user ID teamType = graphene.String() # BUYER | SELLER, default BUYER countryCode = graphene.String() success = graphene.Boolean() teamId = graphene.String() teamUuid = graphene.String() message = graphene.String() def mutate(self, info, teamName, ownerId, teamType=None, countryCode=None): try: # Получаем или создаём пользователя owner = _get_or_create_user_with_profile(ownerId) # Создаём команду team = TeamModel.objects.create( name=teamName, owner=owner, team_type=teamType or 'BUYER' ) # Добавляем owner как участника команды с ролью OWNER TeamMemberModel.objects.create( team=team, user=owner, role='OWNER' ) # Устанавливаем как активную команду if hasattr(owner, 'profile') and not owner.profile.active_team: owner.profile.active_team = team owner.profile.save(update_fields=['active_team']) logger.info( "Created team %s (%s) for owner %s from workflow", team.uuid, teamName, ownerId ) return CreateTeamFromWorkflowMutation( success=True, teamId=str(team.id), teamUuid=str(team.uuid), message="Team created" ) except Exception as e: logger.error("Failed to create team from workflow: %s", e) return CreateTeamFromWorkflowMutation( success=False, message=str(e) ) class UpdateAddressStatusMutation(graphene.Mutation): """Update address processing status (used by Temporal workflows)""" class Arguments: addressUuid = graphene.String(required=True) status = graphene.String(required=True) # pending, active, error errorMessage = graphene.String() success = graphene.Boolean() message = graphene.String() def mutate(self, info, addressUuid, status, errorMessage=None): try: address = TeamAddressModel.objects.get(uuid=addressUuid) address.status = status update_fields = ['status', 'updated_at'] if errorMessage: address.error_message = errorMessage update_fields.append('error_message') if status == 'active': address.processed_at = timezone.now() update_fields.append('processed_at') address.save(update_fields=update_fields) logger.info("Address %s status updated to %s", addressUuid, status) return UpdateAddressStatusMutation(success=True, message="Status updated") except TeamAddressModel.DoesNotExist: return UpdateAddressStatusMutation( success=False, message=f"Address {addressUuid} not found" ) class M2MMutation(graphene.ObjectType): setLogtoOrgId = SetLogtoOrgIdMutation.Field() createTeamFromWorkflow = CreateTeamFromWorkflowMutation.Field() createAddressFromWorkflow = CreateAddressFromWorkflowMutation.Field() updateAddressStatus = UpdateAddressStatusMutation.Field() createInvitationFromWorkflow = CreateInvitationFromWorkflow.Field() createInvitationToken = CreateInvitationToken.Field() updateInvitationTokenStatus = UpdateInvitationTokenStatus.Field() m2m_schema = graphene.Schema(query=M2MQuery, mutation=M2MMutation)