330 lines
12 KiB
Python
330 lines
12 KiB
Python
"""
|
||
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)
|