Files
igny8/backend/igny8_core/business/billing/models.py
2025-12-04 23:56:38 +00:00

407 lines
14 KiB
Python

"""
Billing Models for Credit System
"""
from django.db import models
from django.core.validators import MinValueValidator
from django.conf import settings
from igny8_core.auth.models import AccountBaseModel
class CreditTransaction(AccountBaseModel):
"""Track all credit transactions (additions, deductions)"""
TRANSACTION_TYPE_CHOICES = [
('purchase', 'Purchase'),
('subscription', 'Subscription Renewal'),
('refund', 'Refund'),
('deduction', 'Usage Deduction'),
('adjustment', 'Manual Adjustment'),
]
transaction_type = models.CharField(max_length=20, choices=TRANSACTION_TYPE_CHOICES, db_index=True)
amount = models.IntegerField(help_text="Positive for additions, negative for deductions")
balance_after = models.IntegerField(help_text="Credit balance after this transaction")
description = models.CharField(max_length=255)
metadata = models.JSONField(default=dict, help_text="Additional context (AI call details, etc.)")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_credit_transactions'
ordering = ['-created_at']
indexes = [
models.Index(fields=['account', 'transaction_type']),
models.Index(fields=['account', 'created_at']),
]
def __str__(self):
account = getattr(self, 'account', None)
return f"{self.get_transaction_type_display()} - {self.amount} credits - {account.name if account else 'No Account'}"
class CreditUsageLog(AccountBaseModel):
"""Detailed log of credit usage per AI operation"""
OPERATION_TYPE_CHOICES = [
('clustering', 'Keyword Clustering'),
('idea_generation', 'Content Ideas Generation'),
('content_generation', 'Content Generation'),
('image_generation', 'Image Generation'),
('reparse', 'Content Reparse'),
('ideas', 'Content Ideas Generation'), # Legacy
('content', 'Content Generation'), # Legacy
('images', 'Image Generation'), # Legacy
]
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)
model_used = models.CharField(max_length=100, blank=True)
tokens_input = models.IntegerField(null=True, blank=True, validators=[MinValueValidator(0)])
tokens_output = models.IntegerField(null=True, blank=True, validators=[MinValueValidator(0)])
related_object_type = models.CharField(max_length=50, blank=True) # 'keyword', 'cluster', 'task'
related_object_id = models.IntegerField(null=True, blank=True)
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_credit_usage_logs'
ordering = ['-created_at']
indexes = [
models.Index(fields=['account', 'operation_type']),
models.Index(fields=['account', 'created_at']),
models.Index(fields=['account', 'operation_type', 'created_at']),
]
def __str__(self):
account = getattr(self, 'account', None)
return f"{self.get_operation_type_display()} - {self.credits_used} credits - {account.name if account else 'No Account'}"
class CreditCostConfig(models.Model):
"""
Configurable credit costs per AI function
Admin-editable alternative to hardcoded constants
"""
# Operation identification
operation_type = models.CharField(
max_length=50,
unique=True,
choices=CreditUsageLog.OPERATION_TYPE_CHOICES,
help_text="AI operation type"
)
# Cost configuration
credits_cost = models.IntegerField(
validators=[MinValueValidator(0)],
help_text="Credits required for this operation"
)
# Unit of measurement
UNIT_CHOICES = [
('per_request', 'Per Request'),
('per_100_words', 'Per 100 Words'),
('per_200_words', 'Per 200 Words'),
('per_item', 'Per Item'),
('per_image', 'Per Image'),
]
unit = models.CharField(
max_length=50,
default='per_request',
choices=UNIT_CHOICES,
help_text="What the cost applies to"
)
# Metadata
display_name = models.CharField(max_length=100, help_text="Human-readable name")
description = models.TextField(blank=True, help_text="What this operation does")
# Status
is_active = models.BooleanField(default=True, help_text="Enable/disable this operation")
# Audit fields
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='credit_cost_updates',
help_text="Admin who last updated"
)
# Change tracking
previous_cost = models.IntegerField(
null=True,
blank=True,
help_text="Cost before last update (for audit trail)"
)
class Meta:
app_label = 'billing'
db_table = 'igny8_credit_cost_config'
verbose_name = 'Credit Cost Configuration'
verbose_name_plural = 'Credit Cost Configurations'
ordering = ['operation_type']
def __str__(self):
return f"{self.display_name} - {self.credits_cost} credits {self.unit}"
def save(self, *args, **kwargs):
# Track cost changes
if self.pk:
try:
old = CreditCostConfig.objects.get(pk=self.pk)
if old.credits_cost != self.credits_cost:
self.previous_cost = old.credits_cost
except CreditCostConfig.DoesNotExist:
pass
super().save(*args, **kwargs)
class Invoice(AccountBaseModel):
"""
Invoice for subscription or credit purchases
Tracks billing invoices with line items and payment status
"""
STATUS_CHOICES = [
('draft', 'Draft'),
('pending', 'Pending'),
('paid', 'Paid'),
('void', 'Void'),
('uncollectible', 'Uncollectible'),
]
invoice_number = models.CharField(max_length=50, unique=True, db_index=True)
subscription = models.ForeignKey(
'igny8_core_auth.Subscription',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='invoices'
)
# Amounts
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
total = models.DecimalField(max_digits=10, decimal_places=2)
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
# Dates
invoice_date = models.DateField(db_index=True)
due_date = models.DateField()
paid_at = models.DateTimeField(null=True, blank=True)
# Line items
line_items = models.JSONField(default=list, help_text="Invoice line items: [{description, amount, quantity}]")
# Payment integration
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(max_length=50, null=True, blank=True)
# Metadata
notes = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_invoices'
ordering = ['-invoice_date', '-created_at']
indexes = [
models.Index(fields=['account', 'status']),
models.Index(fields=['account', 'invoice_date']),
models.Index(fields=['invoice_number']),
]
def __str__(self):
return f"Invoice {self.invoice_number} - {self.account.name if self.account else 'No Account'}"
class Payment(AccountBaseModel):
"""
Payment record for invoices
Supports: Stripe, PayPal, Manual (Bank Transfer, Local Wallet)
"""
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded'),
('cancelled', 'Cancelled'),
]
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe (Credit/Debit Card)'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer (Manual)'),
('local_wallet', 'Local Wallet (Manual)'),
('manual', 'Manual Payment'),
]
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments')
# Amount
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
# Payment method
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES, db_index=True)
# Stripe integration
stripe_payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
stripe_charge_id = models.CharField(max_length=255, null=True, blank=True)
# PayPal integration
paypal_order_id = models.CharField(max_length=255, null=True, blank=True)
paypal_capture_id = models.CharField(max_length=255, null=True, blank=True)
# Manual payment details
manual_reference = models.CharField(
max_length=255,
blank=True,
help_text="Bank transfer reference, wallet transaction ID, etc."
)
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
approved_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='approved_payments'
)
approved_at = models.DateTimeField(null=True, blank=True)
# Timestamps
processed_at = models.DateTimeField(null=True, blank=True)
failed_at = models.DateTimeField(null=True, blank=True)
refunded_at = models.DateTimeField(null=True, blank=True)
# Error tracking
failure_reason = models.TextField(blank=True)
# Metadata
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_payments'
ordering = ['-created_at']
indexes = [
models.Index(fields=['account', 'status']),
models.Index(fields=['account', 'payment_method']),
models.Index(fields=['invoice', 'status']),
]
def __str__(self):
return f"Payment {self.id} - {self.get_payment_method_display()} - {self.amount} {self.currency}"
class CreditPackage(models.Model):
"""
One-time credit purchase packages
Defines available credit bundles for purchase
"""
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True, db_index=True)
# Credits
credits = models.IntegerField(validators=[MinValueValidator(1)])
# Pricing
price = models.DecimalField(max_digits=10, decimal_places=2)
discount_percentage = models.IntegerField(default=0, help_text="Discount percentage (0-100)")
# Stripe
stripe_product_id = models.CharField(max_length=255, null=True, blank=True)
stripe_price_id = models.CharField(max_length=255, null=True, blank=True)
# PayPal
paypal_plan_id = models.CharField(max_length=255, null=True, blank=True)
# Status
is_active = models.BooleanField(default=True, db_index=True)
is_featured = models.BooleanField(default=False, help_text="Show as featured package")
# Display
description = models.TextField(blank=True)
features = models.JSONField(default=list, help_text="Bonus features or highlights")
# Sort order
sort_order = models.IntegerField(default=0, help_text="Display order (lower = first)")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_credit_packages'
ordering = ['sort_order', 'price']
def __str__(self):
return f"{self.name} - {self.credits} credits - ${self.price}"
class PaymentMethodConfig(models.Model):
"""
Configure payment methods availability per country
Allows enabling/disabling manual payments by region
"""
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
]
country_code = models.CharField(
max_length=2,
db_index=True,
help_text="ISO 2-letter country code (e.g., US, GB, IN)"
)
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
is_enabled = models.BooleanField(default=True)
# Display info
display_name = models.CharField(max_length=100, blank=True)
instructions = models.TextField(blank=True, help_text="Payment instructions for users")
# Manual payment details (for bank_transfer/local_wallet)
bank_name = models.CharField(max_length=255, blank=True)
account_number = models.CharField(max_length=255, blank=True)
routing_number = models.CharField(max_length=255, blank=True)
swift_code = models.CharField(max_length=255, blank=True)
# Additional fields for local wallets
wallet_type = models.CharField(max_length=100, blank=True, help_text="E.g., PayTM, PhonePe, etc.")
wallet_id = models.CharField(max_length=255, blank=True)
# Order/priority
sort_order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_payment_method_config'
unique_together = [['country_code', 'payment_method']]
ordering = ['country_code', 'sort_order']
verbose_name = 'Payment Method Configuration'
verbose_name_plural = 'Payment Method Configurations'
def __str__(self):
return f"{self.country_code} - {self.get_payment_method_display()}"