""" 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)