""" 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 ""