payment gateways and plans billing and signup pages refactored
This commit is contained in:
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user