Phase 0: Update credit costs and CreditService, add credit checks to AI Engine
- Updated CREDIT_COSTS to match Phase 0 spec (flat structure) - Added get_credit_cost() method to CreditService - Updated check_credits() to accept operation_type and amount - Added deduct_credits_for_operation() convenience method - Updated AI Engine to check credits BEFORE AI call - Updated AI Engine to deduct credits AFTER successful execution - Added helper methods for operation type mapping and amount calculation
This commit is contained in:
@@ -13,9 +13,65 @@ class CreditService:
|
||||
"""Service for managing credits"""
|
||||
|
||||
@staticmethod
|
||||
def check_credits(account, required_credits):
|
||||
def get_credit_cost(operation_type, amount=None):
|
||||
"""
|
||||
Check if account has enough credits.
|
||||
Get credit cost for operation.
|
||||
|
||||
Args:
|
||||
operation_type: Type of operation (from CREDIT_COSTS)
|
||||
amount: Optional amount (word count, image count, etc.)
|
||||
|
||||
Returns:
|
||||
int: Number of credits required
|
||||
|
||||
Raises:
|
||||
CreditCalculationError: If operation type is unknown
|
||||
"""
|
||||
base_cost = CREDIT_COSTS.get(operation_type, 0)
|
||||
if base_cost == 0:
|
||||
raise CreditCalculationError(f"Unknown operation type: {operation_type}")
|
||||
|
||||
# Variable cost operations
|
||||
if operation_type == 'content_generation' and amount:
|
||||
# Per 100 words
|
||||
return max(1, int(base_cost * (amount / 100)))
|
||||
elif operation_type == 'optimization' and amount:
|
||||
# Per 200 words
|
||||
return max(1, int(base_cost * (amount / 200)))
|
||||
elif operation_type == 'image_generation' and amount:
|
||||
# Per image
|
||||
return base_cost * amount
|
||||
elif operation_type == 'idea_generation' and amount:
|
||||
# Per idea
|
||||
return base_cost * amount
|
||||
|
||||
# Fixed cost operations
|
||||
return base_cost
|
||||
|
||||
@staticmethod
|
||||
def check_credits(account, operation_type, amount=None):
|
||||
"""
|
||||
Check if account has sufficient credits for an operation.
|
||||
|
||||
Args:
|
||||
account: Account instance
|
||||
operation_type: Type of operation
|
||||
amount: Optional amount (word count, image count, etc.)
|
||||
|
||||
Raises:
|
||||
InsufficientCreditsError: If account doesn't have enough credits
|
||||
"""
|
||||
required = CreditService.get_credit_cost(operation_type, amount)
|
||||
if account.credits < required:
|
||||
raise InsufficientCreditsError(
|
||||
f"Insufficient credits. Required: {required}, Available: {account.credits}"
|
||||
)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def check_credits_legacy(account, required_credits):
|
||||
"""
|
||||
Legacy method: Check if account has enough credits (for backward compatibility).
|
||||
|
||||
Args:
|
||||
account: Account instance
|
||||
@@ -51,8 +107,8 @@ class CreditService:
|
||||
Returns:
|
||||
int: New credit balance
|
||||
"""
|
||||
# Check sufficient credits
|
||||
CreditService.check_credits(account, amount)
|
||||
# Check sufficient credits (legacy: amount is already calculated)
|
||||
CreditService.check_credits_legacy(account, amount)
|
||||
|
||||
# Deduct from account.credits
|
||||
account.credits -= amount
|
||||
@@ -84,6 +140,61 @@ class CreditService:
|
||||
|
||||
return account.credits
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def deduct_credits_for_operation(account, operation_type, amount=None, description=None, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
|
||||
"""
|
||||
Deduct credits for an operation (convenience method that calculates cost automatically).
|
||||
|
||||
Args:
|
||||
account: Account instance
|
||||
operation_type: Type of operation
|
||||
amount: Optional amount (word count, image count, etc.)
|
||||
description: Optional description (auto-generated if not provided)
|
||||
metadata: Optional metadata dict
|
||||
cost_usd: Optional cost in USD
|
||||
model_used: Optional AI model used
|
||||
tokens_input: Optional input tokens
|
||||
tokens_output: Optional output tokens
|
||||
related_object_type: Optional related object type
|
||||
related_object_id: Optional related object ID
|
||||
|
||||
Returns:
|
||||
int: New credit balance
|
||||
"""
|
||||
# Calculate credit cost
|
||||
credits_required = CreditService.get_credit_cost(operation_type, amount)
|
||||
|
||||
# Check sufficient credits
|
||||
CreditService.check_credits(account, operation_type, amount)
|
||||
|
||||
# Auto-generate description if not provided
|
||||
if not description:
|
||||
if operation_type == 'clustering':
|
||||
description = f"Clustering operation"
|
||||
elif operation_type == 'idea_generation':
|
||||
description = f"Generated {amount or 1} idea(s)"
|
||||
elif operation_type == 'content_generation':
|
||||
description = f"Generated content ({amount or 0} words)"
|
||||
elif operation_type == 'image_generation':
|
||||
description = f"Generated {amount or 1} image(s)"
|
||||
else:
|
||||
description = f"{operation_type} operation"
|
||||
|
||||
return CreditService.deduct_credits(
|
||||
account=account,
|
||||
amount=credits_required,
|
||||
operation_type=operation_type,
|
||||
description=description,
|
||||
metadata=metadata,
|
||||
cost_usd=cost_usd,
|
||||
model_used=model_used,
|
||||
tokens_input=tokens_input,
|
||||
tokens_output=tokens_output,
|
||||
related_object_type=related_object_type,
|
||||
related_object_id=related_object_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def add_credits(account, amount, transaction_type, description, metadata=None):
|
||||
@@ -120,6 +231,7 @@ class CreditService:
|
||||
def calculate_credits_for_operation(operation_type, **kwargs):
|
||||
"""
|
||||
Calculate credits needed for an operation.
|
||||
Legacy method - use get_credit_cost() instead.
|
||||
|
||||
Args:
|
||||
operation_type: Type of operation
|
||||
@@ -131,31 +243,22 @@ class CreditService:
|
||||
Raises:
|
||||
CreditCalculationError: If calculation fails
|
||||
"""
|
||||
if operation_type not in CREDIT_COSTS:
|
||||
raise CreditCalculationError(f"Unknown operation type: {operation_type}")
|
||||
|
||||
cost_config = CREDIT_COSTS[operation_type]
|
||||
|
||||
if operation_type == 'clustering':
|
||||
# 1 credit per 30 keywords
|
||||
keyword_count = kwargs.get('keyword_count', 0)
|
||||
credits = max(1, int(keyword_count * cost_config['per_keyword']))
|
||||
return credits
|
||||
elif operation_type == 'ideas':
|
||||
# 1 credit per idea
|
||||
idea_count = kwargs.get('idea_count', 1)
|
||||
return cost_config['base'] * idea_count
|
||||
# Map legacy operation types
|
||||
if operation_type == 'ideas':
|
||||
operation_type = 'idea_generation'
|
||||
elif operation_type == 'content':
|
||||
# 3 credits per content piece
|
||||
content_count = kwargs.get('content_count', 1)
|
||||
return cost_config['base'] * content_count
|
||||
operation_type = 'content_generation'
|
||||
elif operation_type == 'images':
|
||||
# 1 credit per image
|
||||
image_count = kwargs.get('image_count', 1)
|
||||
return cost_config['base'] * image_count
|
||||
elif operation_type == 'reparse':
|
||||
# 1 credit per reparse
|
||||
return cost_config['base']
|
||||
operation_type = 'image_generation'
|
||||
|
||||
return cost_config['base']
|
||||
# Extract amount from kwargs
|
||||
amount = None
|
||||
if 'word_count' in kwargs:
|
||||
amount = kwargs.get('word_count')
|
||||
elif 'image_count' in kwargs:
|
||||
amount = kwargs.get('image_count')
|
||||
elif 'idea_count' in kwargs:
|
||||
amount = kwargs.get('idea_count')
|
||||
|
||||
return CreditService.get_credit_cost(operation_type, amount)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user