diff --git a/pyproject.toml b/pyproject.toml
index cd0f7fd..575383b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -74,6 +74,8 @@ migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations"
createsuperuser = "./src/manage.py createsuperuser"
shell = "./src/manage.py shell"
+# You need to install Stripe CLI from here to run this: https://github.com/stripe/stripe-cli/releases
+stripe_cli = "stripe listen --forward-to 0.0.0.0:8000/order/stripe/webhook/"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE="tests.settings"
diff --git a/src/accounting/migrations/0010_alter_orderproduct_options_and_more.py b/src/accounting/migrations/0010_alter_orderproduct_options_and_more.py
new file mode 100644
index 0000000..4a710b3
--- /dev/null
+++ b/src/accounting/migrations/0010_alter_orderproduct_options_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.1b1 on 2024-07-31 22:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounting', '0009_alter_orderproduct_order_alter_orderproduct_product'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='orderproduct',
+ options={'verbose_name': 'ordered product', 'verbose_name_plural': 'ordered products'},
+ ),
+ migrations.RemoveField(
+ model_name='payment',
+ name='stripe_charge_id',
+ ),
+ ]
diff --git a/src/accounting/models.py b/src/accounting/models.py
index 0aa75cc..d7f7315 100644
--- a/src/accounting/models.py
+++ b/src/accounting/models.py
@@ -163,8 +163,6 @@ class Payment(CreatedModifiedAbstract):
payment_type = models.ForeignKey("PaymentType", on_delete=models.PROTECT)
external_transaction_id = models.CharField(max_length=255, default="", blank=True)
- stripe_charge_id = models.CharField(max_length=255, default="", blank=True)
-
class Meta:
verbose_name = _("payment")
verbose_name_plural = _("payments")
diff --git a/src/accounting/signals.py b/src/accounting/signals.py
index 4e90656..f33263d 100644
--- a/src/accounting/signals.py
+++ b/src/accounting/signals.py
@@ -30,7 +30,7 @@ def mark_order_paid(sender: models.Payment, instance: models.Payment, **kwargs:
instance.order.save()
-@receiver(post_save, sender=models.Payment)
+@receiver(post_save, sender=models.Order)
def activate_membership(sender: models.Order, instance: models.Order, **kwargs: dict) -> None: # noqa: ARG001
"""Mark a membership as activated when its order is marked as paid."""
if instance.is_paid:
diff --git a/src/accounting/templates/accounting/order/detail.html b/src/accounting/templates/accounting/order/detail.html
index 4fec0ed..a3a0b0f 100644
--- a/src/accounting/templates/accounting/order/detail.html
+++ b/src/accounting/templates/accounting/order/detail.html
@@ -40,9 +40,10 @@
{% trans "Total price" %}: {{ order.total_with_vat }}
+ {% if not order.is_paid %}
{% trans "Pay now" %}
-
+ {% endif %}
{% endblock %}
diff --git a/src/accounting/views.py b/src/accounting/views.py
index 05b745f..acfc077 100644
--- a/src/accounting/views.py
+++ b/src/accounting/views.py
@@ -7,11 +7,13 @@ from django.core.mail import send_mail
from django.db import transaction
from django.http import HttpRequest
from django.http import HttpResponse
+from django.shortcuts import get_object_or_404
from django.shortcuts import redirect
from django.shortcuts import render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django_view_decorator import namespaced_decorator_factory
+from djmoney.money import Money
from . import models
@@ -52,6 +54,8 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
order = models.Order.objects.get(pk=order_id, member=user)
current_site = Site.objects.get_current(request)
base_domain = f"https://{current_site.domain}"
+ if settings.DEBUG:
+ f"http://{current_site.domain}"
try:
line_items = []
@@ -70,6 +74,7 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
)
checkout_session = stripe.checkout.Session.create(
line_items=line_items,
+ metadata={"order_id": order.id},
mode="payment",
success_url=base_domain + reverse("order:success", kwargs={"order_id": order.id}),
cancel_url=base_domain + "/cancel",
@@ -132,12 +137,12 @@ def cancel(request: HttpRequest, order_id: int) -> HttpResponse:
)
-@csrf_exempt
+@transaction.atomic
@order_view(
paths="stripe/webhook/",
name="webhook",
- login_required=True,
)
+@csrf_exempt
def stripe_webhook(request: HttpRequest) -> HttpResponse:
"""Handle Stripe webhook.
@@ -157,7 +162,16 @@ def stripe_webhook(request: HttpRequest) -> HttpResponse:
return HttpResponse(status=400)
if event["type"] == "checkout.session.completed" or event["type"] == "checkout.session.async_payment_succeeded":
- # TODO: Implement the payment
- pass
+ # Order is marked paid via signals, Membership is activated via signals.
+ order_id = event["data"]["object"]["metadata"]["order_id"]
+ order = get_object_or_404(models.Order, pk=order_id)
+ if not models.Payment.objects.filter(order=order).exists():
+ models.Payment.objects.create(
+ order=order,
+ amount=Money(event["data"]["object"]["amount_total"], event["data"]["object"]["currency"]),
+ description="Paid via Stripe",
+ payment_type=models.PaymentType.objects.get_or_create(name="Stripe")[0],
+ external_transaction_id=event["id"],
+ )
return HttpResponse(status=200)
diff --git a/src/membership/admin.py b/src/membership/admin.py
index 823d911..33c5d9d 100644
--- a/src/membership/admin.py
+++ b/src/membership/admin.py
@@ -66,7 +66,7 @@ def ensure_membership_type_exists(
# Get the default account of the member. We don't really know what to do if a person owns multiple accounts.
account, __ = Account.objects.get_or_create(owner=member)
# Create an Order for the products in the membership
- order = Order.objects.create(member=member, account=account)
+ order = Order.objects.create(member=member, account=account, description=membership_type.name)
# Add stuff to the order
for product in membership_type.products.all():
OrderProduct.objects.create(order=order, product=product, price=product.price, vat=product.vat)