151 lines
5.3 KiB
Python
151 lines
5.3 KiB
Python
"""
|
|
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)
|