from datetime import timedelta from django.utils import timezone from django.db import models from django.core.exceptions import ValidationError from accounts.models import BaseModel, IAmPrincipal, IAmPrincipalType from django.utils.translation import gettext_lazy as _ from django_quill.fields import QuillField class Subscription(BaseModel): MONTH = "month" DAY = "day" WEEK = "week" YEAR = "year" INTERVAL_TYPES = [ (MONTH, "month"), (DAY, "day"), (WEEK, "week"), (YEAR, "year"), ] title = models.CharField(max_length=255) price_id = models.CharField(max_length=255, blank=True, null=True) product_id = models.CharField(max_length=255, blank=True, null=True) short_description = models.CharField(max_length=255, null=True, blank=True) long_description = QuillField() image = models.ImageField(upload_to="subscription_img", null=True, blank=True) interval = models.CharField(max_length=10, choices=INTERVAL_TYPES) interval_count = models.IntegerField(default=1) high_amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00) amount = models.DecimalField(max_digits=14, decimal_places=2, default=0.00) principal_types = models.ManyToManyField( IAmPrincipalType, related_name="principal_type_subscriptions", blank=True ) referral_percentage = models.DecimalField(max_digits=5, decimal_places=2) is_free = models.BooleanField( default=False, help_text="Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.", ) class Meta: db_table = "subscription" def __str__(self): return self.title def clean(self): # Ensure amount is greater than 1 if self.amount <= 1: raise ValidationError({"amount": "Amount must be greater than 1."}) # Ensure high_amount is greater than amount if self.high_amount <= self.amount: raise ValidationError( {"high_amount": "High amount must be greater than amount."} ) def save(self, *args, **kwargs): from goodtimes.services import StripeService if self.pk and self.deleted: return super().save(*args, **kwargs) self.clean() if self.is_free: # If is_free is True, set amounts to 0 and remove Stripe price and product IDs self.high_amount = 0.00 self.amount = 0.00 self.price_id = None self.product_id = None else: if self.pk and self.price_id: # Update existing subscription # Retrieve existing price and product from Stripe price = StripeService.retrieve_price(self.price_id) if not price["success"]: raise Exception(price['message']) # Update price active status if it differs from local active status if self.active != price["data"].active: StripeService.update_price(price_id=self.price_id, active=self.active) # Retrieve existing product from Stripe product = StripeService.retrive_product(self.product_id) if not product["success"]: raise Exception(product['message']) # Update product data if it has changed if product["data"].name != self.title or product["data"].description != self.short_description: StripeService.update_product( product_id=self.product_id, name=self.title, description=self.short_description ) else: print("new pricde create is clled =========================================================") # Create new product and price price = StripeService.create_price( product_data={ "name": self.title, "description": self.short_description, }, unit_amount=int(self.amount * 100), currency="gbp", recurring={ "interval": self.interval, "interval_count": self.interval_count, }, metadata={ "subscription_id": self.id } ) if not price["success"]: raise Exception(price['message']) # Add the IDs to the record self.price_id = price["data"].id self.product_id = price["data"].product super().save(*args, **kwargs) def calculate_days(self): count = { self.DAY: 1, self.MONTH: 30, # assuming a month is 30 days self.YEAR: 365, self.WEEK: 7 } return count[self.interval] * self.interval_count class SubscriptionStatus(models.TextChoices): ACTIVE = "active", _("Active") EXPIRED = "expired", _("Expired") INACTIVE = "inactive", _("Inactive") class PrincipalSubscription(BaseModel): subscription = models.ForeignKey( Subscription, related_name="subscription_reference", on_delete=models.CASCADE ) principal = models.ForeignKey( IAmPrincipal, related_name="principal_subscription", on_delete=models.CASCADE ) is_paid = models.BooleanField(default=False) auto_renew = models.BooleanField(default=False) status = models.CharField( max_length=255, choices=SubscriptionStatus.choices, default=SubscriptionStatus.ACTIVE, ) start_date = models.DateField() end_date = models.DateField() order_id = models.CharField(max_length=255, null=True, blank=True) cancelled_date_time = models.DateTimeField(null=True, blank=True) grace_period_end_date = models.DateField(null=True, blank=True) stripe_customer_id = models.CharField(max_length=255, null=True, blank=True) stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True) comments = models.CharField(max_length=255, null=True, blank=True) payment_intent_id = models.CharField(max_length=255, null=True, blank=True) payment_intent_client_secret = models.CharField( max_length=255, null=True, blank=True ) coupon_code = models.CharField(max_length=255, null=True, blank=True) class Meta: db_table = "principal_subscription" def __str__(self): return f"{self.subscription} - {self.principal.first_name}" def save(self, *args, **kwargs): # If the subscription status is expired or inactive, set the active flag to False if self.status in [SubscriptionStatus.EXPIRED, SubscriptionStatus.INACTIVE]: self.active = False # If the active flag is False, set the status to inactive if not self.active: self.status = SubscriptionStatus.INACTIVE super().save(*args, **kwargs) def generate_order_id(email): return f"order_{str(timezone.localtime().timestamp())}{str(email)}" def generate_grace_period_end_date(date): return date + timedelta(days=15) @classmethod def has_principal_subscription(cls, principal): return cls.get_grace_period_princial_subscription(principal).exists() @classmethod def get_grace_period_princial_subscription(cls, principal): return cls.objects.filter( principal=principal, is_paid=True, active=True, status=SubscriptionStatus.ACTIVE, grace_period_end_date__gt=timezone.now().date(), ) @classmethod def get_active_princial_subscription(cls, principal): return cls.objects.filter( principal=principal, is_paid=True, active=True, status=SubscriptionStatus.ACTIVE, end_date__gt=timezone.now().date(), ).order_by('-end_date').last() @classmethod def get_principal_subscription(cls, principal): return cls.objects.filter( principal=principal, is_paid=True, active=True, status=SubscriptionStatus.ACTIVE, ).order_by("-grace_period_end_date").first() @classmethod def cancel_stipe_auto_renew_subscription(cls, subscription): subscription.auto_renew = False subscription.cancelled_date_time = timezone.now() subscription.save() class WebhookEvent(BaseModel): event_id = models.CharField(max_length=255, unique=True, db_index=True) event_type = models.CharField(max_length=255, null=True, blank=True) received_at = models.DateTimeField(auto_now_add=True) processed_at = models.DateTimeField(null=True, blank=True) status = models.CharField( max_length=20, default="received" ) # e.g., 'received', 'processed', 'failed' error_message = models.TextField(null=True, blank=True) event_payload = models.JSONField( null=True, blank=True ) # Optional: Store the payload for debugging. def __str__(self): return f"Webhook Event {self.event_id} - Status: {self.status}" class Meta: db_table = "webhook_event"