payment gateways and plans billing and signup pages refactored

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 13:02:53 +00:00
parent ad1756c349
commit ad75fa031e
17 changed files with 4587 additions and 500 deletions

View File

@@ -487,6 +487,7 @@ class Payment(AccountBaseModel):
manual_reference = models.CharField(
max_length=255,
blank=True,
null=True,
help_text="Bank transfer reference, wallet transaction ID, etc."
)
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
@@ -526,9 +527,24 @@ class Payment(AccountBaseModel):
models.Index(fields=['account', 'payment_method']),
models.Index(fields=['invoice', 'status']),
]
constraints = [
# Ensure manual_reference is unique when not null/empty
# This prevents duplicate bank transfer references
models.UniqueConstraint(
fields=['manual_reference'],
name='unique_manual_reference_when_not_null',
condition=models.Q(manual_reference__isnull=False) & ~models.Q(manual_reference='')
),
]
def __str__(self):
return f"Payment {self.id} - {self.get_payment_method_display()} - {self.amount} {self.currency}"
def save(self, *args, **kwargs):
"""Normalize empty manual_reference to NULL for proper uniqueness handling"""
if self.manual_reference == '':
self.manual_reference = None
super().save(*args, **kwargs)
class CreditPackage(models.Model):
@@ -854,3 +870,115 @@ class AIModelConfig(models.Model):
model_type='image',
is_active=True
).order_by('quality_tier', 'model_name')
class WebhookEvent(models.Model):
"""
Store all incoming webhook events for audit and replay capability.
This model provides:
- Audit trail of all webhook events
- Idempotency verification (via event_id)
- Ability to replay failed events
- Debugging and monitoring
"""
PROVIDER_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
]
# Unique identifier from the payment provider
event_id = models.CharField(
max_length=255,
unique=True,
db_index=True,
help_text="Unique event ID from the payment provider"
)
# Payment provider
provider = models.CharField(
max_length=20,
choices=PROVIDER_CHOICES,
db_index=True,
help_text="Payment provider (stripe or paypal)"
)
# Event type (e.g., 'checkout.session.completed', 'PAYMENT.CAPTURE.COMPLETED')
event_type = models.CharField(
max_length=100,
db_index=True,
help_text="Event type from the provider"
)
# Full payload for debugging and replay
payload = models.JSONField(
help_text="Full webhook payload"
)
# Processing status
processed = models.BooleanField(
default=False,
db_index=True,
help_text="Whether this event has been successfully processed"
)
processed_at = models.DateTimeField(
null=True,
blank=True,
help_text="When the event was processed"
)
# Error tracking
error_message = models.TextField(
blank=True,
help_text="Error message if processing failed"
)
retry_count = models.IntegerField(
default=0,
help_text="Number of processing attempts"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_webhook_events'
verbose_name = 'Webhook Event'
verbose_name_plural = 'Webhook Events'
ordering = ['-created_at']
indexes = [
models.Index(fields=['provider', 'event_type']),
models.Index(fields=['processed', 'created_at']),
models.Index(fields=['provider', 'processed']),
]
def __str__(self):
return f"{self.provider}:{self.event_type} - {self.event_id[:20]}..."
@classmethod
def record_event(cls, event_id: str, provider: str, event_type: str, payload: dict):
"""
Record a webhook event. Returns (event, created) tuple.
If the event already exists, returns the existing event.
"""
return cls.objects.get_or_create(
event_id=event_id,
defaults={
'provider': provider,
'event_type': event_type,
'payload': payload,
}
)
def mark_processed(self):
"""Mark the event as successfully processed"""
from django.utils import timezone
self.processed = True
self.processed_at = timezone.now()
self.save(update_fields=['processed', 'processed_at'])
def mark_failed(self, error_message: str):
"""Mark the event as failed with error message"""
self.error_message = error_message
self.retry_count += 1
self.save(update_fields=['error_message', 'retry_count'])