Initial commit from monorepo
This commit is contained in:
1
billing_app/schemas/__init__.py
Normal file
1
billing_app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Billing schemas
|
||||
BIN
billing_app/schemas/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
billing_app/schemas/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
billing_app/schemas/__pycache__/m2m_schema.cpython-313.pyc
Normal file
BIN
billing_app/schemas/__pycache__/m2m_schema.cpython-313.pyc
Normal file
Binary file not shown.
BIN
billing_app/schemas/__pycache__/team_schema.cpython-313.pyc
Normal file
BIN
billing_app/schemas/__pycache__/team_schema.cpython-313.pyc
Normal file
Binary file not shown.
150
billing_app/schemas/m2m_schema.py
Normal file
150
billing_app/schemas/m2m_schema.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
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)
|
||||
134
billing_app/schemas/team_schema.py
Normal file
134
billing_app/schemas/team_schema.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Team-level GraphQL schema for Billing service.
|
||||
Requires teams:member scope (verified by middleware).
|
||||
Provides teamBalance and teamTransactions for the authenticated team.
|
||||
|
||||
All data comes directly from TigerBeetle.
|
||||
"""
|
||||
import graphene
|
||||
import uuid
|
||||
import logging
|
||||
from graphql import GraphQLError
|
||||
|
||||
from ..tigerbeetle_client import tigerbeetle_client
|
||||
from ..permissions import require_scopes
|
||||
from ..models import OperationCode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TeamBalance(graphene.ObjectType):
|
||||
"""Balance information for a team's account from TigerBeetle."""
|
||||
balance = graphene.Float(required=True, description="Current balance (credits - debits)")
|
||||
creditsPosted = graphene.Float(required=True, description="Total credits posted")
|
||||
debitsPosted = graphene.Float(required=True, description="Total debits posted")
|
||||
exists = graphene.Boolean(required=True, description="Whether account exists in TigerBeetle")
|
||||
|
||||
|
||||
class TeamTransaction(graphene.ObjectType):
|
||||
"""Transaction from TigerBeetle."""
|
||||
id = graphene.String(required=True)
|
||||
amount = graphene.Float(required=True)
|
||||
timestamp = graphene.Float(description="TigerBeetle timestamp")
|
||||
code = graphene.Int(description="Operation code")
|
||||
codeName = graphene.String(description="Operation code name from OperationCode table")
|
||||
direction = graphene.String(required=True, description="'credit' or 'debit' relative to team account")
|
||||
counterpartyUuid = graphene.String(description="UUID of the other account in transaction")
|
||||
|
||||
|
||||
class TeamQuery(graphene.ObjectType):
|
||||
teamBalance = graphene.Field(TeamBalance, description="Get balance for the authenticated team")
|
||||
teamTransactions = graphene.List(
|
||||
TeamTransaction,
|
||||
limit=graphene.Int(default_value=50),
|
||||
description="Get transactions for the authenticated team"
|
||||
)
|
||||
|
||||
@require_scopes("teams:member")
|
||||
def resolve_teamBalance(self, info):
|
||||
team_uuid = getattr(info.context, 'team_uuid', None)
|
||||
if not team_uuid:
|
||||
raise GraphQLError("Team UUID not found in context")
|
||||
|
||||
try:
|
||||
team_uuid_obj = uuid.UUID(team_uuid)
|
||||
tb_account_id = team_uuid_obj.int
|
||||
|
||||
# Look up account in TigerBeetle
|
||||
accounts = tigerbeetle_client.lookup_accounts([tb_account_id])
|
||||
|
||||
if not accounts:
|
||||
# Account doesn't exist yet - return zero balance
|
||||
return TeamBalance(
|
||||
balance=0.0,
|
||||
creditsPosted=0.0,
|
||||
debitsPosted=0.0,
|
||||
exists=False
|
||||
)
|
||||
|
||||
account = accounts[0]
|
||||
credits_posted = float(account.credits_posted)
|
||||
debits_posted = float(account.debits_posted)
|
||||
balance = credits_posted - debits_posted
|
||||
|
||||
return TeamBalance(
|
||||
balance=balance,
|
||||
creditsPosted=credits_posted,
|
||||
debitsPosted=debits_posted,
|
||||
exists=True
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid team UUID format: {team_uuid} - {e}")
|
||||
raise GraphQLError("Invalid team UUID format")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching team balance: {e}")
|
||||
raise GraphQLError("Failed to fetch team balance")
|
||||
|
||||
@require_scopes("teams:member")
|
||||
def resolve_teamTransactions(self, info, limit=50):
|
||||
team_uuid = getattr(info.context, 'team_uuid', None)
|
||||
if not team_uuid:
|
||||
raise GraphQLError("Team UUID not found in context")
|
||||
|
||||
try:
|
||||
team_uuid_obj = uuid.UUID(team_uuid)
|
||||
tb_account_id = team_uuid_obj.int
|
||||
|
||||
# Get transfers from TigerBeetle
|
||||
transfers = tigerbeetle_client.get_account_transfers(tb_account_id, limit=limit)
|
||||
|
||||
# Load operation codes for name lookup
|
||||
code_map = {oc.code: oc.name for oc in OperationCode.objects.all()}
|
||||
|
||||
result = []
|
||||
for t in transfers:
|
||||
# Determine direction relative to team account
|
||||
if t.credit_account_id == tb_account_id:
|
||||
direction = "credit"
|
||||
counterparty_id = t.debit_account_id
|
||||
else:
|
||||
direction = "debit"
|
||||
counterparty_id = t.credit_account_id
|
||||
|
||||
result.append(TeamTransaction(
|
||||
id=str(uuid.UUID(int=t.id)),
|
||||
amount=float(t.amount),
|
||||
timestamp=float(t.timestamp),
|
||||
code=t.code,
|
||||
codeName=code_map.get(t.code),
|
||||
direction=direction,
|
||||
counterpartyUuid=str(uuid.UUID(int=counterparty_id)),
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid team UUID format: {team_uuid} - {e}")
|
||||
raise GraphQLError("Invalid team UUID format")
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching team transactions: {e}")
|
||||
raise GraphQLError("Failed to fetch team transactions")
|
||||
|
||||
|
||||
team_schema = graphene.Schema(query=TeamQuery)
|
||||
Reference in New Issue
Block a user