Creditsupdates in fotoer wdigets adn hoemapge and singe site settigns page #Run MIgration 0033 #MAJOR

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 07:22:08 +00:00
parent 368601f68c
commit b390e02aa5
17 changed files with 251 additions and 45 deletions

View File

@@ -444,6 +444,9 @@ class AIEngine:
tokens_input = raw_response.get('input_tokens', 0)
tokens_output = raw_response.get('output_tokens', 0)
# Extract site_id from save_result (could be from content, cluster, or task)
site_id = save_result.get('site_id') or save_result.get('site')
# Deduct credits based on actual token usage
CreditService.deduct_credits_for_operation(
account=self.account,
@@ -454,6 +457,7 @@ class AIEngine:
model_used=raw_response.get('model', ''),
related_object_type=self._get_related_object_type(function_name),
related_object_id=save_result.get('id') or save_result.get('cluster_id') or save_result.get('task_id'),
site=site_id,
metadata={
'function_name': function_name,
'clusters_created': clusters_created,

View File

@@ -340,6 +340,7 @@ class AutoClusterFunction(BaseAIFunction):
return {
'count': clusters_created,
'clusters_created': clusters_created,
'keywords_updated': keywords_updated
'keywords_updated': keywords_updated,
'site_id': site.id if site else None
}

View File

@@ -329,6 +329,7 @@ class GenerateContentFunction(BaseAIFunction):
'content_id': content_record.id,
'task_id': task.id,
'word_count': word_count,
'site_id': task.site_id if task.site_id else None,
}

View File

@@ -233,9 +233,19 @@ class GenerateIdeasFunction(BaseAIFunction):
cluster.status = 'mapped'
cluster.save()
# Get site_id from the first cluster if available
site_id = None
if clusters:
first_cluster = clusters[0]
if first_cluster.site_id:
site_id = first_cluster.site_id
elif first_cluster.sector and first_cluster.sector.site_id:
site_id = first_cluster.sector.site_id
return {
'count': ideas_created,
'ideas_created': ideas_created
'ideas_created': ideas_created,
'site_id': site_id
}

View File

@@ -214,6 +214,7 @@ class GenerateImagePromptsFunction(BaseAIFunction):
return {
'count': prompts_created,
'prompts_created': prompts_created,
'site_id': content.site_id if content.site_id else None
}
# Helper methods

View File

@@ -196,7 +196,8 @@ class GenerateImagesFunction(BaseAIFunction):
return {
'count': 1,
'images_created': 1,
'image_id': image.id
'image_id': image.id,
'site_id': task.site_id if task.site_id else None
}

View File

@@ -150,6 +150,7 @@ class OptimizeContentFunction(BaseAIFunction):
'html_content': optimized_html,
'meta_title': optimized_meta_title,
'meta_description': optimized_meta_description,
'site_id': content.site_id if content.site_id else None
}
# Helper methods

View File

@@ -303,6 +303,10 @@ class DashboardStatsViewSet(viewsets.ViewSet):
account=account,
created_at__gte=start_date
)
# Filter by site if provided
if site_id:
usage_query = usage_query.filter(site_id=site_id)
# Get operations grouped by type
operations_data = usage_query.values('operation_type').annotate(

View File

@@ -85,7 +85,16 @@ class CreditUsageLog(AccountBaseModel):
('content', 'Content Generation'), # Legacy
('images', 'Image Generation'), # Legacy
]
# Site relationship - stored at creation time for proper filtering
site = models.ForeignKey(
'igny8_core_auth.Site',
on_delete=models.CASCADE,
null=True,
blank=True,
help_text='Site where the operation was performed'
)
operation_type = models.CharField(max_length=50, choices=OPERATION_TYPE_CHOICES, db_index=True)
credits_used = models.IntegerField(validators=[MinValueValidator(0)])
cost_usd = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True)
@@ -105,6 +114,8 @@ class CreditUsageLog(AccountBaseModel):
models.Index(fields=['account', 'operation_type']),
models.Index(fields=['account', 'created_at']),
models.Index(fields=['account', 'operation_type', 'created_at']),
models.Index(fields=['site', 'created_at']),
models.Index(fields=['account', 'site', 'created_at']),
]
def __str__(self):

View File

@@ -336,10 +336,10 @@ class CreditService:
@staticmethod
@transaction.atomic
def deduct_credits(account, amount, operation_type, description, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None):
def deduct_credits(account, amount, operation_type, description, metadata=None, cost_usd=None, model_used=None, tokens_input=None, tokens_output=None, related_object_type=None, related_object_id=None, site=None):
"""
Deduct credits and log transaction.
Args:
account: Account instance
amount: Number of credits to deduct
@@ -352,20 +352,21 @@ class CreditService:
tokens_output: Optional output tokens
related_object_type: Optional related object type
related_object_id: Optional related object ID
site: Optional Site instance or site_id
Returns:
int: New credit balance
"""
# Check sufficient credits (legacy: amount is already calculated)
CreditService.check_credits_legacy(account, amount)
# Store previous balance for low credits check
previous_balance = account.credits
# Deduct from account.credits
account.credits -= amount
account.save(update_fields=['credits'])
# Create CreditTransaction
CreditTransaction.objects.create(
account=account,
@@ -375,10 +376,11 @@ class CreditService:
description=description,
metadata=metadata or {}
)
# Create CreditUsageLog
CreditUsageLog.objects.create(
account=account,
site=site,
operation_type=operation_type,
credits_used=amount,
cost_usd=cost_usd,
@@ -389,30 +391,31 @@ class CreditService:
related_object_id=related_object_id,
metadata=metadata or {}
)
# Check and send low credits warning if applicable
_check_low_credits_warning(account, previous_balance)
return account.credits
@staticmethod
@transaction.atomic
def deduct_credits_for_operation(
account,
operation_type,
tokens_input,
account,
operation_type,
tokens_input,
tokens_output,
description=None,
metadata=None,
cost_usd=None,
model_used=None,
related_object_type=None,
related_object_id=None
description=None,
metadata=None,
cost_usd=None,
model_used=None,
related_object_type=None,
related_object_id=None,
site=None
):
"""
Deduct credits for an operation based on actual token usage.
This is the ONLY way to deduct credits in the token-based system.
Args:
account: Account instance
operation_type: Type of operation
@@ -424,10 +427,11 @@ class CreditService:
model_used: Optional AI model used
related_object_type: Optional related object type
related_object_id: Optional related object ID
site: Optional Site instance or site_id
Returns:
int: New credit balance
Raises:
ValueError: If tokens_input or tokens_output not provided
"""
@@ -437,18 +441,18 @@ class CreditService:
f"tokens_input and tokens_output are REQUIRED for credit deduction. "
f"Got: tokens_input={tokens_input}, tokens_output={tokens_output}"
)
# Calculate credits from actual token usage
credits_required = CreditService.calculate_credits_from_tokens(
operation_type, tokens_input, tokens_output
)
# Check sufficient credits
if account.credits < credits_required:
raise InsufficientCreditsError(
f"Insufficient credits. Required: {credits_required}, Available: {account.credits}"
)
# Auto-generate description if not provided
if not description:
total_tokens = tokens_input + tokens_output
@@ -456,7 +460,7 @@ class CreditService:
f"{operation_type}: {total_tokens} tokens "
f"({tokens_input} in, {tokens_output} out) = {credits_required} credits"
)
return CreditService.deduct_credits(
account=account,
amount=credits_required,
@@ -468,7 +472,8 @@ class CreditService:
tokens_input=tokens_input,
tokens_output=tokens_output,
related_object_type=related_object_type,
related_object_id=related_object_id
related_object_id=related_object_id,
site=site
)
@staticmethod
@@ -513,11 +518,12 @@ class CreditService:
metadata: dict = None,
cost_usd: float = None,
related_object_type: str = None,
related_object_id: int = None
related_object_id: int = None,
site = None
):
"""
Deduct credits for image generation based on model's credits_per_image.
Args:
account: Account instance
model_name: AI model used (e.g., 'dall-e-3', 'flux-1-1-pro')
@@ -527,20 +533,21 @@ class CreditService:
cost_usd: Optional cost in USD
related_object_type: Optional related object type
related_object_id: Optional related object ID
site: Optional Site instance or site_id
Returns:
int: New credit balance
"""
credits_required = CreditService.calculate_credits_for_image(model_name, num_images)
if account.credits < credits_required:
raise InsufficientCreditsError(
f"Insufficient credits. Required: {credits_required}, Available: {account.credits}"
)
if not description:
description = f"Image generation: {num_images} images with {model_name} = {credits_required} credits"
return CreditService.deduct_credits(
account=account,
amount=credits_required,
@@ -552,6 +559,7 @@ class CreditService:
tokens_input=None,
tokens_output=None,
related_object_type=related_object_type,
related_object_id=related_object_id
related_object_id=related_object_id,
site=site
)

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.10 on 2026-01-12 06:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0032_historicalaimodelconfig_landscape_size_and_more'),
('igny8_core_auth', '0020_fix_historical_account'),
]
operations = [
migrations.AddField(
model_name='creditusagelog',
name='site',
field=models.ForeignKey(blank=True, help_text='Site where the operation was performed', null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.site'),
),
migrations.AddIndex(
model_name='creditusagelog',
index=models.Index(fields=['site', 'created_at'], name='igny8_credi_site_id_b628ed_idx'),
),
migrations.AddIndex(
model_name='creditusagelog',
index=models.Index(fields=['account', 'site', 'created_at'], name='igny8_credi_tenant__ca31e1_idx'),
),
]

View File

@@ -0,0 +1,90 @@
# Generated by Django 5.2.10 on 2026-01-12 07:02
from django.db import migrations
def backfill_site_id(apps, schema_editor):
"""
Backfill site_id for existing CreditUsageLog records by looking up
the related objects (Content, Images, ContentIdeas, Clusters).
"""
CreditUsageLog = apps.get_model('billing', 'CreditUsageLog')
Content = apps.get_model('writer', 'Content')
Images = apps.get_model('writer', 'Images')
ContentIdeas = apps.get_model('planner', 'ContentIdeas')
Clusters = apps.get_model('planner', 'Clusters')
# Backfill for content records
content_logs = CreditUsageLog.objects.filter(
site__isnull=True,
related_object_type='content',
related_object_id__isnull=False
)
for log in content_logs:
try:
content = Content.objects.get(id=log.related_object_id)
log.site_id = content.site_id
log.save(update_fields=['site'])
except Content.DoesNotExist:
pass
# Backfill for image records
image_logs = CreditUsageLog.objects.filter(
site__isnull=True,
related_object_type='image',
related_object_id__isnull=False
)
for log in image_logs:
try:
image = Images.objects.get(id=log.related_object_id)
log.site_id = image.site_id
log.save(update_fields=['site'])
except Images.DoesNotExist:
pass
# Backfill for content idea records
idea_logs = CreditUsageLog.objects.filter(
site__isnull=True,
related_object_type='content_idea',
related_object_id__isnull=False
)
for log in idea_logs:
try:
idea = ContentIdeas.objects.get(id=log.related_object_id)
log.site_id = idea.site_id
log.save(update_fields=['site'])
except ContentIdeas.DoesNotExist:
pass
# Backfill for cluster records
cluster_logs = CreditUsageLog.objects.filter(
site__isnull=True,
related_object_type='cluster',
related_object_id__isnull=False
)
for log in cluster_logs:
try:
cluster = Clusters.objects.get(id=log.related_object_id)
log.site_id = cluster.site_id
log.save(update_fields=['site'])
except Clusters.DoesNotExist:
pass
def reverse_backfill(apps, schema_editor):
"""Reverse migration - set site_id back to NULL for backfilled records."""
# We don't reverse this as we can't distinguish which were backfilled
pass
class Migration(migrations.Migration):
dependencies = [
('billing', '0033_add_site_to_credit_usage_log'),
('writer', '0016_images_unique_position_constraint'),
('planner', '0008_soft_delete'),
]
operations = [
migrations.RunPython(backfill_site_id, reverse_backfill),
]

View File

@@ -165,6 +165,14 @@ class CreditUsageViewSet(AccountModelViewSet):
created_at__gte=start_date,
created_at__lte=end_date
)
# Filter by site if provided
site_id = request.query_params.get('site_id')
if site_id:
try:
usage_logs = usage_logs.filter(site_id=int(site_id))
except (ValueError, TypeError):
pass
# Calculate totals
total_credits_used = usage_logs.aggregate(total=Sum('credits_used'))['total'] or 0