313 lines
12 KiB
Python
313 lines
12 KiB
Python
"""
|
|
Serializers for Billing Models
|
|
"""
|
|
from typing import Any, Dict, Optional
|
|
from decimal import Decimal
|
|
from rest_framework import serializers
|
|
from .models import CreditTransaction, CreditUsageLog
|
|
from igny8_core.auth.models import Account
|
|
from igny8_core.business.billing.models import PaymentMethodConfig, Payment
|
|
|
|
|
|
class CreditTransactionSerializer(serializers.ModelSerializer):
|
|
"""Serializer for credit transactions"""
|
|
transaction_type_display: serializers.CharField = serializers.CharField(
|
|
source='get_transaction_type_display',
|
|
read_only=True
|
|
)
|
|
|
|
class Meta:
|
|
model = CreditTransaction
|
|
fields = [
|
|
'id', 'transaction_type', 'transaction_type_display', 'amount',
|
|
'balance_after', 'description', 'metadata', 'created_at'
|
|
]
|
|
read_only_fields = ['created_at', 'account']
|
|
|
|
|
|
class CreditUsageLogSerializer(serializers.ModelSerializer):
|
|
"""Serializer for credit usage logs"""
|
|
operation_type_display: serializers.CharField = serializers.CharField(
|
|
source='get_operation_type_display',
|
|
read_only=True
|
|
)
|
|
client_cost = serializers.SerializerMethodField(help_text='Client-facing cost (credits * price_per_credit)')
|
|
site_name = serializers.SerializerMethodField(help_text='Name of the associated site')
|
|
|
|
class Meta:
|
|
model = CreditUsageLog
|
|
fields = [
|
|
'id', 'operation_type', 'operation_type_display', 'credits_used',
|
|
'cost_usd', 'client_cost', 'model_used', 'tokens_input', 'tokens_output',
|
|
'related_object_type', 'related_object_id', 'site_name', 'metadata', 'created_at'
|
|
]
|
|
read_only_fields = ['created_at', 'account']
|
|
|
|
def get_client_cost(self, obj) -> str:
|
|
"""Calculate client-facing cost from credits * default_credit_price_usd (price per credit)"""
|
|
from igny8_core.business.billing.models import BillingConfiguration
|
|
config = BillingConfiguration.get_config()
|
|
price_per_credit = config.default_credit_price_usd
|
|
client_cost = Decimal(obj.credits_used) * price_per_credit
|
|
return str(client_cost.quantize(Decimal('0.0001')))
|
|
|
|
def get_site_name(self, obj) -> Optional[str]:
|
|
"""Get the site name if available"""
|
|
if obj.site:
|
|
return obj.site.name
|
|
return None
|
|
|
|
|
|
class CreditBalanceSerializer(serializers.Serializer):
|
|
"""Serializer for credit balance response"""
|
|
credits: serializers.IntegerField = serializers.IntegerField()
|
|
plan_credits_per_month: serializers.IntegerField = serializers.IntegerField()
|
|
credits_used_this_month: serializers.IntegerField = serializers.IntegerField()
|
|
credits_remaining: serializers.IntegerField = serializers.IntegerField()
|
|
|
|
|
|
class UsageSummarySerializer(serializers.Serializer):
|
|
"""Serializer for usage summary response"""
|
|
period: serializers.DictField = serializers.DictField()
|
|
total_credits_used: serializers.IntegerField = serializers.IntegerField()
|
|
total_cost_usd: serializers.DecimalField = serializers.DecimalField(max_digits=10, decimal_places=2)
|
|
by_operation: serializers.DictField = serializers.DictField()
|
|
by_model: serializers.DictField = serializers.DictField()
|
|
|
|
|
|
class PaymentMethodConfigSerializer(serializers.ModelSerializer):
|
|
"""Serializer for payment method configuration"""
|
|
payment_method_display: serializers.CharField = serializers.CharField(
|
|
source='get_payment_method_display',
|
|
read_only=True
|
|
)
|
|
|
|
class Meta:
|
|
model = PaymentMethodConfig
|
|
fields = [
|
|
'id', 'country_code', 'payment_method', 'payment_method_display',
|
|
'is_enabled', 'display_name', 'instructions',
|
|
'bank_name', 'account_title', 'account_number', 'routing_number', 'swift_code', 'iban',
|
|
'wallet_type', 'wallet_id', 'sort_order'
|
|
]
|
|
read_only_fields = ['id']
|
|
|
|
|
|
class PaymentConfirmationSerializer(serializers.Serializer):
|
|
"""Serializer for manual payment confirmation"""
|
|
invoice_id: serializers.IntegerField = serializers.IntegerField(required=True)
|
|
payment_method: serializers.ChoiceField = serializers.ChoiceField(
|
|
choices=['bank_transfer', 'local_wallet'],
|
|
required=True
|
|
)
|
|
manual_reference: serializers.CharField = serializers.CharField(
|
|
required=True,
|
|
max_length=255,
|
|
help_text="Transaction reference number"
|
|
)
|
|
manual_notes: serializers.CharField = serializers.CharField(
|
|
required=False,
|
|
allow_blank=True,
|
|
help_text="Additional notes about the payment"
|
|
)
|
|
amount: serializers.DecimalField = serializers.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
required=True
|
|
)
|
|
proof_url: serializers.URLField = serializers.URLField(
|
|
required=False,
|
|
allow_blank=True,
|
|
help_text="URL to receipt/proof of payment"
|
|
)
|
|
|
|
def validate_proof_url(self, value: Optional[str]) -> Optional[str]:
|
|
"""Validate proof_url is a valid URL format"""
|
|
if value and not value.strip():
|
|
raise serializers.ValidationError("Proof URL cannot be empty if provided")
|
|
if value:
|
|
# Additional validation: must be http or https
|
|
if not value.startswith(('http://', 'https://')):
|
|
raise serializers.ValidationError("Proof URL must start with http:// or https://")
|
|
return value
|
|
|
|
def validate_amount(self, value: Optional[Decimal]) -> Decimal:
|
|
"""Validate amount has max 2 decimal places"""
|
|
if value is None:
|
|
raise serializers.ValidationError("Amount is required")
|
|
if value <= 0:
|
|
raise serializers.ValidationError("Amount must be greater than 0")
|
|
# Check decimal precision (max 2 decimal places)
|
|
if value.as_tuple().exponent < -2:
|
|
raise serializers.ValidationError("Amount can have maximum 2 decimal places")
|
|
return value
|
|
|
|
|
|
class LimitCardSerializer(serializers.Serializer):
|
|
"""Serializer for individual limit card"""
|
|
title: serializers.CharField = serializers.CharField()
|
|
limit: serializers.IntegerField = serializers.IntegerField()
|
|
used: serializers.IntegerField = serializers.IntegerField()
|
|
available: serializers.IntegerField = serializers.IntegerField()
|
|
unit: serializers.CharField = serializers.CharField()
|
|
category: serializers.CharField = serializers.CharField()
|
|
percentage: serializers.FloatField = serializers.FloatField()
|
|
|
|
|
|
class UsageLimitsSerializer(serializers.Serializer):
|
|
"""Serializer for usage limits response"""
|
|
limits: LimitCardSerializer = LimitCardSerializer(many=True)
|
|
|
|
|
|
class AccountPaymentMethodSerializer(serializers.Serializer):
|
|
"""
|
|
Serializer for Account Payment Methods
|
|
Handles CRUD operations for account-specific payment methods
|
|
"""
|
|
id = serializers.IntegerField(read_only=True)
|
|
type = serializers.ChoiceField(
|
|
choices=[
|
|
('stripe', 'Stripe (Credit/Debit Card)'),
|
|
('paypal', 'PayPal'),
|
|
('bank_transfer', 'Bank Transfer (Manual)'),
|
|
('local_wallet', 'Local Wallet (Manual)'),
|
|
('manual', 'Manual Payment'),
|
|
]
|
|
)
|
|
display_name = serializers.CharField(max_length=100)
|
|
is_default = serializers.BooleanField(default=False)
|
|
is_enabled = serializers.BooleanField(default=True)
|
|
is_verified = serializers.BooleanField(read_only=True, default=False)
|
|
instructions = serializers.CharField(required=False, allow_blank=True, default='')
|
|
metadata = serializers.JSONField(required=False, default=dict)
|
|
created_at = serializers.DateTimeField(read_only=True)
|
|
updated_at = serializers.DateTimeField(read_only=True)
|
|
|
|
def validate_display_name(self, value):
|
|
"""Validate display_name uniqueness per account"""
|
|
account = self.context.get('account')
|
|
instance = getattr(self, 'instance', None)
|
|
|
|
if account:
|
|
from igny8_core.business.billing.models import AccountPaymentMethod
|
|
existing = AccountPaymentMethod.objects.filter(
|
|
account=account,
|
|
display_name=value
|
|
)
|
|
if instance:
|
|
existing = existing.exclude(pk=instance.pk)
|
|
if existing.exists():
|
|
raise serializers.ValidationError(
|
|
f"A payment method with name '{value}' already exists for this account."
|
|
)
|
|
return value
|
|
|
|
def create(self, validated_data):
|
|
from igny8_core.business.billing.models import AccountPaymentMethod
|
|
account = self.context.get('account')
|
|
if not account:
|
|
raise serializers.ValidationError("Account context is required")
|
|
|
|
# If this is marked as default, unset other defaults
|
|
if validated_data.get('is_default', False):
|
|
AccountPaymentMethod.objects.filter(
|
|
account=account,
|
|
is_default=True
|
|
).update(is_default=False)
|
|
|
|
return AccountPaymentMethod.objects.create(
|
|
account=account,
|
|
**validated_data
|
|
)
|
|
|
|
def update(self, instance, validated_data):
|
|
from igny8_core.business.billing.models import AccountPaymentMethod
|
|
|
|
# If this is marked as default, unset other defaults
|
|
if validated_data.get('is_default', False) and not instance.is_default:
|
|
AccountPaymentMethod.objects.filter(
|
|
account=instance.account,
|
|
is_default=True
|
|
).exclude(pk=instance.pk).update(is_default=False)
|
|
|
|
for attr, value in validated_data.items():
|
|
setattr(instance, attr, value)
|
|
instance.save()
|
|
return instance
|
|
|
|
|
|
class AIModelConfigSerializer(serializers.Serializer):
|
|
"""
|
|
Serializer for AI Model Configuration (Read-Only API)
|
|
Provides model information for frontend dropdowns and displays
|
|
"""
|
|
model_name = serializers.CharField(read_only=True)
|
|
display_name = serializers.CharField(read_only=True)
|
|
model_type = serializers.CharField(read_only=True)
|
|
provider = serializers.CharField(read_only=True)
|
|
|
|
# Text model fields
|
|
input_cost_per_1m = serializers.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=4,
|
|
read_only=True,
|
|
allow_null=True
|
|
)
|
|
output_cost_per_1m = serializers.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=4,
|
|
read_only=True,
|
|
allow_null=True
|
|
)
|
|
context_window = serializers.IntegerField(read_only=True, allow_null=True)
|
|
max_output_tokens = serializers.IntegerField(read_only=True, allow_null=True)
|
|
|
|
# Image model fields
|
|
cost_per_image = serializers.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=4,
|
|
read_only=True,
|
|
allow_null=True
|
|
)
|
|
valid_sizes = serializers.ListField(read_only=True, allow_null=True)
|
|
|
|
# Credit calculation fields (NEW)
|
|
credits_per_image = serializers.IntegerField(
|
|
read_only=True,
|
|
allow_null=True,
|
|
help_text="Credits charged per image generation"
|
|
)
|
|
tokens_per_credit = serializers.IntegerField(
|
|
read_only=True,
|
|
allow_null=True,
|
|
help_text="Tokens per credit for text models"
|
|
)
|
|
quality_tier = serializers.CharField(
|
|
read_only=True,
|
|
allow_null=True,
|
|
help_text="Quality tier: basic, quality, or premium"
|
|
)
|
|
|
|
# Capabilities
|
|
supports_json_mode = serializers.BooleanField(read_only=True)
|
|
supports_vision = serializers.BooleanField(read_only=True)
|
|
supports_function_calling = serializers.BooleanField(read_only=True)
|
|
|
|
# Status
|
|
is_default = serializers.BooleanField(read_only=True)
|
|
sort_order = serializers.IntegerField(read_only=True)
|
|
|
|
# Computed field
|
|
pricing_display = serializers.SerializerMethodField()
|
|
|
|
def get_pricing_display(self, obj):
|
|
"""Generate pricing display string based on model type"""
|
|
if obj.model_type == 'text':
|
|
input_cost = obj.cost_per_1k_input or 0
|
|
output_cost = obj.cost_per_1k_output or 0
|
|
return f"${input_cost}/{output_cost} per 1K tokens"
|
|
elif obj.model_type == 'image':
|
|
return f"${obj.cost_per_image} per image"
|
|
return ""
|
|
|