Files
teams/teams_app/schemas/m2m_schema.py
2026-01-07 09:17:34 +07:00

330 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)