"""비용 계산기 (Phase 8). 기능: - 작업 비용 계산 - 할당량 확인 - 사용량 추적 """ import logging from datetime import datetime, UTC from typing import Dict, Optional, List from ont_platform.billing.models import ( Usage, Subscription, OperationType, UsageStatistics, SubscriptionTier, ) logger = logging.getLogger(__name__) class CostCalculator: """비용 계산기.""" # 작업별 단가 (USD) PRICING: Dict[OperationType, float] = { OperationType.LLM_CALL: 0.001, # 토큰당 $0.001 OperationType.LLM_STREAM: 0.1, # 분당 $0.1 OperationType.GRAPH_QUERY: 0.0001, # 노드당 $0.0001 OperationType.STORAGE: 10.0, # GB당 $10/월 OperationType.API_CALL: 0.0001, # 호출당 $0.0001 OperationType.ANALYSIS: 0.5, # 분석당 $0.5 } # 구독 계층별 월 한도 (USD) SUBSCRIPTION_LIMITS: Dict[SubscriptionTier, float] = { SubscriptionTier.FREE: 10.0, SubscriptionTier.PRO: 100.0, SubscriptionTier.ENTERPRISE: 10000.0, } def __init__(self): """초기화.""" self.in_memory_usages: List[Usage] = [] async def calculate_cost( self, operation_type: OperationType, quantity: float, ) -> float: """작업 비용 계산. Args: operation_type: 작업 타입 quantity: 수량 (토큰, 노드, GB 등) Returns: 비용 (USD) """ price_per_unit = self.PRICING.get(operation_type, 0) cost = quantity * price_per_unit logger.debug(f"Cost calculated: {operation_type.value} x {quantity} = ${cost}") return cost async def record_usage( self, org_id: str, user_id: str, operation_type: OperationType, quantity: float, metadata: Optional[Dict] = None, ) -> Usage: """사용량 기록. Args: org_id: 조직 ID user_id: 사용자 ID operation_type: 작업 타입 quantity: 수량 metadata: 메타데이터 Returns: Usage 객체 """ cost = await self.calculate_cost(operation_type, quantity) usage = Usage( org_id=org_id, user_id=user_id, operation_type=operation_type, quantity=quantity, cost=cost, metadata=metadata or {}, ) # 메모리에 저장 (테스트) self.in_memory_usages.append(usage) logger.info( f"Usage recorded: {operation_type.value} " f"({quantity}) for org {org_id} - ${cost}" ) return usage async def check_quota( self, org_id: str, subscription: Subscription, estimated_cost: float, ) -> tuple[bool, str]: """할당량 확인. Args: org_id: 조직 ID subscription: 구독 정보 estimated_cost: 예상 비용 Returns: (할당량 내인지, 메시지) """ monthly_limit = self.SUBSCRIPTION_LIMITS.get(subscription.tier, 0) remaining = monthly_limit - subscription.current_month_cost if estimated_cost <= remaining: return True, f"OK. Remaining: ${remaining:.2f}" else: return False, f"Quota exceeded. Need: ${estimated_cost}, Remaining: ${remaining:.2f}" async def check_overage_allowed( self, subscription: Subscription, ) -> bool: """초과 사용이 허용되는지 확인. Args: subscription: 구독 정보 Returns: 초과 사용 허용 여부 """ # Enterprise는 항상 초과 사용 가능 if subscription.tier == SubscriptionTier.ENTERPRISE: return True # Free는 초과 사용 불가 if subscription.tier == SubscriptionTier.FREE: return False # Pro는 선택적 (metadata에서 설정) return subscription.metadata.get("allow_overage", False) async def get_usage_statistics( self, org_id: str, period_days: int = 30, ) -> UsageStatistics: """사용량 통계 조회. Args: org_id: 조직 ID period_days: 기간 (일) Returns: UsageStatistics 객체 """ from datetime import timedelta cutoff_time = datetime.now(UTC) - timedelta(days=period_days) # 필터링 relevant_usages = [ usage for usage in self.in_memory_usages if usage.org_id == org_id and usage.timestamp >= cutoff_time ] # 집계 total_cost = sum(usage.cost for usage in relevant_usages) by_operation_type = {} for usage in relevant_usages: op_type = usage.operation_type.value by_operation_type[op_type] = ( by_operation_type.get(op_type, 0) + usage.cost ) by_user = {} for usage in relevant_usages: user_id = usage.user_id by_user[user_id] = by_user.get(user_id, 0) + usage.cost # 작업별 수량 api_calls = sum( 1 for usage in relevant_usages if usage.operation_type == OperationType.API_CALL ) llm_tokens = sum( usage.quantity for usage in relevant_usages if usage.operation_type == OperationType.LLM_CALL ) storage_gb = sum( usage.quantity for usage in relevant_usages if usage.operation_type == OperationType.STORAGE ) stats = UsageStatistics( period_start=cutoff_time, period_end=datetime.now(UTC), total_cost=total_cost, by_operation_type=by_operation_type, by_user=by_user, api_calls=api_calls, llm_tokens=llm_tokens, storage_gb=storage_gb, ) logger.info(f"Usage statistics for org {org_id}: ${total_cost} in {period_days} days") return stats async def get_cost_forecast( self, org_id: str, days_into_month: int = None, ) -> Dict[str, float]: """비용 예측. Args: org_id: 조직 ID days_into_month: 월간 경과 일수 (None이면 자동) Returns: 예측 정보 """ if days_into_month is None: days_into_month = datetime.utcnow().day # 현재 월 사용량 cutoff_time = datetime( datetime.utcnow().year, datetime.utcnow().month, 1, ) current_month_usages = [ usage for usage in self.in_memory_usages if usage.org_id == org_id and usage.timestamp >= cutoff_time ] current_cost = sum(usage.cost for usage in current_month_usages) # 예측 if days_into_month > 0: daily_average = current_cost / days_into_month projected_cost = daily_average * 30 else: projected_cost = current_cost return { "current_cost": current_cost, "daily_average": current_cost / max(days_into_month, 1), "projected_monthly_cost": projected_cost, "days_into_month": days_into_month, } def clear_in_memory_usages(self) -> None: """메모리 사용량 삭제 (테스트용).""" self.in_memory_usages.clear()