from django.db import models from infrasynth.tenancy.mixins import TenantOwnedModel class OutboundEndpoint(TenantOwnedModel): name = models.CharField(max_length=255) url = models.URLField(max_length=1000) secret = models.CharField(max_length=500) is_active = models.BooleanField(default=True) retry_policy = models.JSONField(default=dict, blank=True) headers = models.JSONField(default=dict, blank=True) timeout_seconds = models.PositiveIntegerField(default=30) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "webhooks_outbound_endpoint" indexes = [models.Index(fields=["tenant_id", "is_active"])] def __str__(self): return self.name class OutboundSubscription(TenantOwnedModel): endpoint = models.ForeignKey(OutboundEndpoint, on_delete=models.CASCADE, related_name="subscriptions") event_name = models.CharField(max_length=255, db_index=True) is_active = models.BooleanField(default=True) payload_template = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "webhooks_outbound_subscription" constraints = [ models.UniqueConstraint(fields=["endpoint", "event_name"], name="uniq_subscription_event_per_endpoint"), ] def __str__(self): return f"{self.endpoint.name} / {self.event_name}" class OutboundDelivery(TenantOwnedModel): class Status(models.TextChoices): SUCCESS = "success", "Success" FAILED = "failed", "Failed" RETRYING = "retrying", "Retrying" subscription = models.ForeignKey(OutboundSubscription, on_delete=models.CASCADE, related_name="deliveries") payload = models.JSONField() response_status = models.PositiveSmallIntegerField(null=True, blank=True) response_body = models.TextField(blank=True) attempt = models.PositiveIntegerField(default=0) status = models.CharField(max_length=20, choices=Status.choices, default=Status.RETRYING) next_retry_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) completed_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "webhooks_outbound_delivery" indexes = [models.Index(fields=["tenant_id", "status"])] def __str__(self): return f"Delivery {self.id} — {self.status}" class InboundEndpoint(TenantOwnedModel): class Source(models.TextChoices): STRIPE = "stripe", "Stripe" GITHUB = "github", "GitHub" MERCADOPAGO = "mercadopago", "Mercado Pago" CUSTOM = "custom", "Custom" name = models.CharField(max_length=255) slug = models.SlugField() source = models.CharField(max_length=50, choices=Source.choices) secret = models.CharField(max_length=500) handler = models.CharField(max_length=500) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "webhooks_inbound_endpoint" constraints = [ models.UniqueConstraint(fields=["tenant", "slug"], name="uniq_inbound_endpoint_slug_per_tenant"), ] def __str__(self): return self.name class InboundEvent(TenantOwnedModel): endpoint = models.ForeignKey(InboundEndpoint, on_delete=models.CASCADE, related_name="events") event_type = models.CharField(max_length=255) external_id = models.CharField( max_length=255, blank=True, default="", help_text="Provider event id, used for idempotent re-delivery handling.", ) raw_payload = models.JSONField() result = models.JSONField(default=dict, blank=True) is_verified = models.BooleanField(default=False) is_processed = models.BooleanField(default=False) error = models.TextField(blank=True) received_at = models.DateTimeField(auto_now_add=True) processed_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "webhooks_inbound_event" indexes = [models.Index(fields=["tenant_id", "is_processed"])] constraints = [ models.UniqueConstraint( fields=["endpoint", "external_id"], condition=~models.Q(external_id=""), name="uniq_inbound_event_external_id_per_endpoint", ), ] def __str__(self): return f"{self.endpoint.slug} / {self.event_type}"