credits adn tokens final correct setup
This commit is contained in:
@@ -9,6 +9,7 @@ from simple_history.admin import SimpleHistoryAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from igny8_core.business.billing.models import (
|
||||
CreditCostConfig,
|
||||
BillingConfiguration,
|
||||
Invoice,
|
||||
Payment,
|
||||
CreditPackage,
|
||||
@@ -426,55 +427,57 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
list_display = [
|
||||
'operation_type',
|
||||
'display_name',
|
||||
'credits_cost_display',
|
||||
'unit',
|
||||
'tokens_per_credit_display',
|
||||
'price_per_credit_usd',
|
||||
'min_credits',
|
||||
'is_active',
|
||||
'cost_change_indicator',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
]
|
||||
|
||||
list_filter = ['is_active', 'unit', 'updated_at']
|
||||
list_filter = ['is_active', 'updated_at']
|
||||
search_fields = ['operation_type', 'display_name', 'description']
|
||||
|
||||
fieldsets = (
|
||||
('Operation', {
|
||||
'fields': ('operation_type', 'display_name', 'description')
|
||||
}),
|
||||
('Cost Configuration', {
|
||||
'fields': ('credits_cost', 'unit', 'is_active')
|
||||
('Token-to-Credit Configuration', {
|
||||
'fields': ('tokens_per_credit', 'min_credits', 'price_per_credit_usd', 'is_active'),
|
||||
'description': 'Configure how tokens are converted to credits for this operation'
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('previous_cost', 'updated_by', 'created_at', 'updated_at'),
|
||||
'fields': ('previous_tokens_per_credit', 'updated_by', 'created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_cost']
|
||||
readonly_fields = ['created_at', 'updated_at', 'previous_tokens_per_credit']
|
||||
|
||||
def credits_cost_display(self, obj):
|
||||
"""Show cost with color coding"""
|
||||
if obj.credits_cost >= 20:
|
||||
color = 'red'
|
||||
elif obj.credits_cost >= 10:
|
||||
def tokens_per_credit_display(self, obj):
|
||||
"""Show token ratio with color coding"""
|
||||
if obj.tokens_per_credit <= 50:
|
||||
color = 'red' # Expensive (low tokens per credit)
|
||||
elif obj.tokens_per_credit <= 100:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'green'
|
||||
color = 'green' # Cheap (high tokens per credit)
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{} credits</span>',
|
||||
'<span style="color: {}; font-weight: bold;">{} tokens/credit</span>',
|
||||
color,
|
||||
obj.credits_cost
|
||||
obj.tokens_per_credit
|
||||
)
|
||||
credits_cost_display.short_description = 'Cost'
|
||||
tokens_per_credit_display.short_description = 'Token Ratio'
|
||||
|
||||
def cost_change_indicator(self, obj):
|
||||
"""Show if cost changed recently"""
|
||||
if obj.previous_cost is not None:
|
||||
if obj.credits_cost > obj.previous_cost:
|
||||
icon = '📈' # Increased
|
||||
"""Show if token ratio changed recently"""
|
||||
if obj.previous_tokens_per_credit is not None:
|
||||
if obj.tokens_per_credit < obj.previous_tokens_per_credit:
|
||||
icon = '📈' # More expensive (fewer tokens per credit)
|
||||
color = 'red'
|
||||
elif obj.credits_cost < obj.previous_cost:
|
||||
icon = '📉' # Decreased
|
||||
elif obj.tokens_per_credit > obj.previous_tokens_per_credit:
|
||||
icon = '📉' # Cheaper (more tokens per credit)
|
||||
color = 'green'
|
||||
else:
|
||||
icon = '➡️' # Same
|
||||
@@ -484,8 +487,8 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'{} <span style="color: {};">({} → {})</span>',
|
||||
icon,
|
||||
color,
|
||||
obj.previous_cost,
|
||||
obj.credits_cost
|
||||
obj.previous_tokens_per_credit,
|
||||
obj.tokens_per_credit
|
||||
)
|
||||
return '—'
|
||||
cost_change_indicator.short_description = 'Recent Change'
|
||||
@@ -538,3 +541,47 @@ class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
return f"{obj.period_start} to {obj.period_end}"
|
||||
period_display.short_description = 'Billing Period'
|
||||
|
||||
|
||||
@admin.register(BillingConfiguration)
|
||||
class BillingConfigurationAdmin(Igny8ModelAdmin):
|
||||
"""Admin for global billing configuration (Singleton)"""
|
||||
list_display = [
|
||||
'id',
|
||||
'default_tokens_per_credit',
|
||||
'default_credit_price_usd',
|
||||
'credit_rounding_mode',
|
||||
'enable_token_based_reporting',
|
||||
'updated_at',
|
||||
'updated_by'
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Global Token-to-Credit Settings', {
|
||||
'fields': ('default_tokens_per_credit', 'default_credit_price_usd', 'credit_rounding_mode'),
|
||||
'description': 'These settings apply when no operation-specific config exists'
|
||||
}),
|
||||
('Reporting Settings', {
|
||||
'fields': ('enable_token_based_reporting',),
|
||||
'description': 'Control token-based reporting features'
|
||||
}),
|
||||
('Audit Trail', {
|
||||
'fields': ('updated_by', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
readonly_fields = ['updated_at']
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton)"""
|
||||
from igny8_core.business.billing.models import BillingConfiguration
|
||||
return not BillingConfiguration.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Prevent deletion of the singleton"""
|
||||
return False
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Track who made the change"""
|
||||
obj.updated_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-19 18:20
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0017_add_history_tracking'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='creditcostconfig',
|
||||
name='credits_cost',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='creditcostconfig',
|
||||
name='previous_cost',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='creditcostconfig',
|
||||
name='unit',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='credits_cost',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='previous_cost',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='unit',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='creditcostconfig',
|
||||
name='min_credits',
|
||||
field=models.IntegerField(default=1, help_text='Minimum credits to charge regardless of token usage', validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='creditcostconfig',
|
||||
name='previous_tokens_per_credit',
|
||||
field=models.IntegerField(blank=True, help_text='Tokens per credit before last update (for audit trail)', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='creditcostconfig',
|
||||
name='price_per_credit_usd',
|
||||
field=models.DecimalField(decimal_places=4, default=Decimal('0.01'), help_text='USD price per credit (for revenue reporting)', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='creditcostconfig',
|
||||
name='tokens_per_credit',
|
||||
field=models.IntegerField(default=100, help_text='Number of tokens that equal 1 credit (e.g., 100 tokens = 1 credit)', validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='min_credits',
|
||||
field=models.IntegerField(default=1, help_text='Minimum credits to charge regardless of token usage', validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='previous_tokens_per_credit',
|
||||
field=models.IntegerField(blank=True, help_text='Tokens per credit before last update (for audit trail)', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='price_per_credit_usd',
|
||||
field=models.DecimalField(decimal_places=4, default=Decimal('0.01'), help_text='USD price per credit (for revenue reporting)', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))]),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcreditcostconfig',
|
||||
name='tokens_per_credit',
|
||||
field=models.IntegerField(default=100, help_text='Number of tokens that equal 1 credit (e.g., 100 tokens = 1 credit)', validators=[django.core.validators.MinValueValidator(1)]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BillingConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('default_tokens_per_credit', models.IntegerField(default=100, help_text='Default: How many tokens equal 1 credit (e.g., 100)', validators=[django.core.validators.MinValueValidator(1)])),
|
||||
('default_credit_price_usd', models.DecimalField(decimal_places=4, default=Decimal('0.01'), help_text='Default price per credit in USD', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.0001'))])),
|
||||
('enable_token_based_reporting', models.BooleanField(default=True, help_text='Show token metrics in all reports')),
|
||||
('credit_rounding_mode', models.CharField(choices=[('up', 'Round Up'), ('down', 'Round Down'), ('nearest', 'Round to Nearest')], default='up', help_text='How to round fractional credits', max_length=10)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, help_text='Admin who last updated', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Billing Configuration',
|
||||
'verbose_name_plural': 'Billing Configuration',
|
||||
'db_table': 'igny8_billing_configuration',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,111 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-19 18:28
|
||||
|
||||
from django.db import migrations
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def populate_token_config(apps, schema_editor):
|
||||
"""
|
||||
Populate BillingConfiguration singleton and update CreditCostConfig records.
|
||||
Token-based pricing ratios (tokens per 1 credit):
|
||||
- Default: 100 tokens = 1 credit
|
||||
- AI operations vary by complexity
|
||||
"""
|
||||
BillingConfiguration = apps.get_model('billing', 'BillingConfiguration')
|
||||
CreditCostConfig = apps.get_model('billing', 'CreditCostConfig')
|
||||
|
||||
# Create BillingConfiguration singleton (if not exists)
|
||||
if not BillingConfiguration.objects.exists():
|
||||
BillingConfiguration.objects.create(
|
||||
default_tokens_per_credit=100,
|
||||
default_credit_price_usd=Decimal('0.01'),
|
||||
enable_token_based_reporting=True,
|
||||
credit_rounding_mode='up'
|
||||
)
|
||||
|
||||
# Token-to-credit ratios for each operation type
|
||||
# Lower number = more expensive (fewer tokens per credit)
|
||||
# Higher number = cheaper (more tokens per credit)
|
||||
operation_configs = {
|
||||
'clustering': {
|
||||
'tokens_per_credit': 150, # Clustering is fairly complex
|
||||
'min_credits': 2,
|
||||
'price_per_credit_usd': Decimal('0.01'),
|
||||
'display_name': 'Content Clustering',
|
||||
'description': 'AI-powered keyword clustering'
|
||||
},
|
||||
'idea_generation': {
|
||||
'tokens_per_credit': 200, # Idea generation is mid-complexity
|
||||
'min_credits': 1,
|
||||
'price_per_credit_usd': Decimal('0.01'),
|
||||
'display_name': 'Idea Generation',
|
||||
'description': 'AI content idea generation'
|
||||
},
|
||||
'content_generation': {
|
||||
'tokens_per_credit': 100, # Content generation is expensive (outputs many tokens)
|
||||
'min_credits': 3,
|
||||
'price_per_credit_usd': Decimal('0.01'),
|
||||
'display_name': 'Content Generation',
|
||||
'description': 'AI content writing'
|
||||
},
|
||||
'image_generation': {
|
||||
'tokens_per_credit': 50, # Image generation is most expensive
|
||||
'min_credits': 5,
|
||||
'price_per_credit_usd': Decimal('0.02'),
|
||||
'display_name': 'Image Generation',
|
||||
'description': 'AI image generation'
|
||||
},
|
||||
'optimization': {
|
||||
'tokens_per_credit': 200, # Optimization is algorithm-based with minimal AI
|
||||
'min_credits': 1,
|
||||
'price_per_credit_usd': Decimal('0.005'),
|
||||
'display_name': 'Content Optimization',
|
||||
'description': 'SEO and content optimization'
|
||||
},
|
||||
'linking': {
|
||||
'tokens_per_credit': 300, # Linking is mostly algorithmic
|
||||
'min_credits': 1,
|
||||
'price_per_credit_usd': Decimal('0.005'),
|
||||
'display_name': 'Internal Linking',
|
||||
'description': 'Automatic internal link injection'
|
||||
},
|
||||
'reparse': {
|
||||
'tokens_per_credit': 150, # Reparse is mid-complexity
|
||||
'min_credits': 1,
|
||||
'price_per_credit_usd': Decimal('0.01'),
|
||||
'display_name': 'Content Reparse',
|
||||
'description': 'Content reparsing and analysis'
|
||||
},
|
||||
}
|
||||
|
||||
# Update or create CreditCostConfig records
|
||||
for operation_type, config in operation_configs.items():
|
||||
CreditCostConfig.objects.update_or_create(
|
||||
operation_type=operation_type,
|
||||
defaults={
|
||||
'tokens_per_credit': config['tokens_per_credit'],
|
||||
'min_credits': config['min_credits'],
|
||||
'price_per_credit_usd': config['price_per_credit_usd'],
|
||||
'display_name': config['display_name'],
|
||||
'description': config['description'],
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def reverse_token_config(apps, schema_editor):
|
||||
"""Reverse migration - clean up configuration"""
|
||||
BillingConfiguration = apps.get_model('billing', 'BillingConfiguration')
|
||||
BillingConfiguration.objects.all().delete()
|
||||
# Note: We don't delete CreditCostConfig records as they may have historical data
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0018_remove_creditcostconfig_credits_cost_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(populate_token_config, reverse_token_config),
|
||||
]
|
||||
Reference in New Issue
Block a user