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)