Initial commit from monorepo

This commit is contained in:
Ruslan Bakiev
2026-01-07 09:17:34 +07:00
commit 3e2570ae0b
69 changed files with 3777 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
"""
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)