feat(purchase/models.py): create payment links and sync name and description from coursepage
This commit is contained in:
@@ -7,6 +7,8 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from wagtail.admin.panels import FieldPanel
|
from wagtail.admin.panels import FieldPanel
|
||||||
|
from modelcluster.fields import ParentalKey
|
||||||
|
from wagtail.models import Orderable
|
||||||
|
|
||||||
GITEA_ORG_NAME = "Studio77"
|
GITEA_ORG_NAME = "Studio77"
|
||||||
logger = lg.getLogger(__name__)
|
logger = lg.getLogger(__name__)
|
||||||
@@ -136,14 +138,14 @@ class CoursePurchase(models.Model):
|
|||||||
self.add_to_gitea_team()
|
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.
|
"""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.
|
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.
|
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)
|
description = models.TextField(blank=True)
|
||||||
price_cents = models.PositiveIntegerField(help_text="Price in cents")
|
price_cents = models.PositiveIntegerField(help_text="Price in cents")
|
||||||
currency = models.CharField(
|
currency = models.CharField(
|
||||||
@@ -151,13 +153,33 @@ class PurchasableProduct(models.Model):
|
|||||||
)
|
)
|
||||||
stripe_product_id = models.CharField(max_length=255, blank=True, null=True)
|
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_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)
|
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 = [
|
panels = [
|
||||||
FieldPanel("price_cents"),
|
FieldPanel("price_cents"),
|
||||||
FieldPanel("currency"),
|
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):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.price_cents / 100:.2f} {self.currency.upper()})"
|
return f"{self.name} ({self.price_cents / 100:.2f} {self.currency.upper()})"
|
||||||
|
|
||||||
@@ -180,6 +202,15 @@ class PurchasableProduct(models.Model):
|
|||||||
except self.__class__.DoesNotExist:
|
except self.__class__.DoesNotExist:
|
||||||
previous = None
|
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
|
# Persist local changes first so we have an id
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@@ -222,6 +253,32 @@ class PurchasableProduct(models.Model):
|
|||||||
f"Created Stripe price {self.stripe_price_id} for PurchasableProduct {self.id}"
|
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 this is an update (we had previous state) perform updates
|
||||||
if previous:
|
if previous:
|
||||||
# Update product metadata/name/description if they changed
|
# Update product metadata/name/description if they changed
|
||||||
|
|||||||
Reference in New Issue
Block a user