diff --git a/purchase/models.py b/purchase/models.py index 97004c8..f8320bd 100644 --- a/purchase/models.py +++ b/purchase/models.py @@ -7,6 +7,8 @@ from django.conf import settings from django.contrib.auth.models import Group from django.db import models from wagtail.admin.panels import FieldPanel +from modelcluster.fields import ParentalKey +from wagtail.models import Orderable GITEA_ORG_NAME = "Studio77" logger = lg.getLogger(__name__) @@ -136,14 +138,14 @@ class CoursePurchase(models.Model): self.add_to_gitea_team() -class PurchasableProduct(models.Model): +class PurchasableProduct(Orderable, models.Model): """A product that can be purchased. When created it will create a Stripe Product and Price. On delete it will try to deactivate the Price and delete the Product in Stripe. The code is defensive: if STRIPE_API_KEY is not configured the model will still work locally. """ - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, blank=True) description = models.TextField(blank=True) price_cents = models.PositiveIntegerField(help_text="Price in cents") currency = models.CharField( @@ -151,13 +153,33 @@ class PurchasableProduct(models.Model): ) stripe_product_id = models.CharField(max_length=255, blank=True, null=True) stripe_price_id = models.CharField(max_length=255, blank=True, null=True) + stripe_payment_url = models.URLField( + blank=True, + null=True, + help_text="Stripe Checkout URL for this product", + ) created_at = models.DateTimeField(auto_now_add=True) + course = ParentalKey( + "home.CoursePage", + on_delete=models.CASCADE, + related_name="purchasable_products", + null=True, + blank=True, + ) + panels = [ FieldPanel("price_cents"), FieldPanel("currency"), + FieldPanel("stripe_product_id", read_only=True), + FieldPanel("stripe_price_id", read_only=True), + FieldPanel("stripe_payment_url", read_only=True), ] + @property + def price(self): + return self.price_cents / 100 + def __str__(self): return f"{self.name} ({self.price_cents / 100:.2f} {self.currency.upper()})" @@ -180,6 +202,15 @@ class PurchasableProduct(models.Model): except self.__class__.DoesNotExist: previous = None + # Get name, description and image from the linked course if not set explicitly on the product + if self.course: + course_name = self.course.title + course_description = self.course.description or "" + if not self.name: + self.name = course_name + if not self.description: + self.description = course_description + # Persist local changes first so we have an id super().save(*args, **kwargs) @@ -222,6 +253,32 @@ class PurchasableProduct(models.Model): f"Created Stripe price {self.stripe_price_id} for PurchasableProduct {self.id}" ) + # Create Stripe Payment Link if missing or if price changed + if not self.stripe_payment_url and self.stripe_price_id: + try: + payment_link = stripe.PaymentLink.create( + line_items=[{"price": self.stripe_price_id, "quantity": 1}], + after_completion={ + "type": "redirect", + "redirect": { + "url": getattr( + settings, + "STRIPE_SUCCESS_URL", + "https://example.com/success", + ) + }, + }, + ) + self.stripe_payment_url = payment_link.url + changed_fields.append("stripe_payment_url") + logger.info( + f"Created Stripe payment link {self.stripe_payment_url} for PurchasableProduct {self.id}" + ) + except Exception as e: + logger.exception( + f"Failed to create Stripe payment link for PurchasableProduct {self.id}: {e}" + ) + # If this is an update (we had previous state) perform updates if previous: # Update product metadata/name/description if they changed