""" M2M (Machine-to-Machine) GraphQL schema for Billing service. Used by internal services (Temporal workflows, etc.) without user authentication. Provides: - createTransaction mutation (auto-creates accounts in TigerBeetle if needed) - operationCodes query (справочник кодов операций) - serviceAccounts query (bank, revenue и т.д.) """ import graphene import uuid import logging from graphene_django import DjangoObjectType import tigerbeetle as tb from ..tigerbeetle_client import tigerbeetle_client from ..models import Account as AccountModel, OperationCode as OperationCodeModel, ServiceAccount as ServiceAccountModel, AccountType logger = logging.getLogger(__name__) # Hardcoded ledger for RUB LEDGER = 1 DEFAULT_ACCOUNT_CODE = 100 class OperationCodeType(DjangoObjectType): """Код операции (справочник).""" class Meta: model = OperationCodeModel fields = ('code', 'name', 'description') class ServiceAccountType(DjangoObjectType): """Сервисный аккаунт (bank, revenue и т.д.).""" accountUuid = graphene.UUID() class Meta: model = ServiceAccountModel fields = ('slug',) def resolve_accountUuid(self, info): return self.account.uuid class CreateTransactionInput(graphene.InputObjectType): fromUuid = graphene.String(required=True, description="Source account UUID") toUuid = graphene.String(required=True, description="Destination account UUID") amount = graphene.Int(required=True, description="Amount in kopecks") code = graphene.Int(required=True, description="Operation code from OperationCode table") class CreateTransaction(graphene.Mutation): """ Creates a financial transaction between two accounts. If accounts do not exist in TigerBeetle, they are created automatically. """ class Arguments: input = CreateTransactionInput(required=True) success = graphene.Boolean() message = graphene.String() transferId = graphene.String() def mutate(self, info, input): # 1. Validate UUIDs try: from_uuid = uuid.UUID(input.fromUuid) to_uuid = uuid.UUID(input.toUuid) except ValueError: return CreateTransaction(success=False, message="Invalid UUID format") # 2. Ensure local Account records exist all_uuids = [from_uuid, to_uuid] existing_accounts = {acc.uuid for acc in AccountModel.objects.filter(uuid__in=all_uuids)} accounts_to_create_in_tb = [] for acc_uuid in all_uuids: if acc_uuid not in existing_accounts: # Create local record AccountModel.objects.create( uuid=acc_uuid, name=f"Team {str(acc_uuid)[:8]}", account_type=AccountType.USER, ) logger.info(f"Created local Account record for {acc_uuid}") # Check if account exists in TigerBeetle tb_accounts = tigerbeetle_client.lookup_accounts([acc_uuid.int]) if not tb_accounts: accounts_to_create_in_tb.append( tb.Account( id=acc_uuid.int, ledger=LEDGER, code=DEFAULT_ACCOUNT_CODE, ) ) # 3. Create accounts in TigerBeetle if needed if accounts_to_create_in_tb: errors = tigerbeetle_client.create_accounts(accounts_to_create_in_tb) if errors: error_details = [str(e) for e in errors] error_msg = f"Failed to create accounts in TigerBeetle: {error_details}" logger.error(error_msg) return CreateTransaction(success=False, message=error_msg) logger.info(f"Created {len(accounts_to_create_in_tb)} accounts in TigerBeetle") # 4. Execute transfer transfer_id = uuid.uuid4() transfer = tb.Transfer( id=transfer_id.int, debit_account_id=from_uuid.int, credit_account_id=to_uuid.int, amount=input.amount, ledger=LEDGER, code=input.code, ) errors = tigerbeetle_client.create_transfers([transfer]) if errors: error_details = [str(e) for e in errors] error_msg = f"Transfer failed: {error_details}" logger.error(error_msg) return CreateTransaction(success=False, message=error_msg) logger.info(f"Transfer {transfer_id} completed: {from_uuid} -> {to_uuid}, amount={input.amount}") return CreateTransaction( success=True, message="Transaction completed", transferId=str(transfer_id) ) class M2MQuery(graphene.ObjectType): operationCodes = graphene.List(OperationCodeType, description="List of operation codes") serviceAccounts = graphene.List(ServiceAccountType, description="List of service accounts (bank, revenue)") def resolve_operationCodes(self, info): return OperationCodeModel.objects.all() def resolve_serviceAccounts(self, info): return ServiceAccountModel.objects.select_related('account').all() class M2MMutation(graphene.ObjectType): createTransaction = CreateTransaction.Field() m2m_schema = graphene.Schema(query=M2MQuery, mutation=M2MMutation)