Repository: svthalia/concrexit

Issue:
Payment processing crashes if payable does not exist
The following happens if

1. You create a payable (an order)
2. A user tries to pay the payment
3. Meanwhile, the payable is deleted
4. User confirms the payment
5. This crashes in a 500

While actually, an error message should be presented "this payable does not exist"

Sentry Issue: [CONCREXIT-8Y](https://sentry.io/organizations/thalia/issues/2647749240/?referrer=github_integration)

```
Order.DoesNotExist: Order matching query does not exist.
(5 additional frame(s) were not displayed)
...
  File "payments/views.py", line 184, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "payments/views.py", line 213, in post
    self.payable = payables.get_payable(payable_model.objects.get(pk=payable_pk))
  File "django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "django/db/models/query.py", line 435, in get
    raise self.model.DoesNotExist(
```


Discussion:

Resolving patch:
diff --git a/website/payments/views.py b/website/payments/views.py
--- a/website/payments/views.py
+++ b/website/payments/views.py
@@ -12,7 +12,7 @@
     SuspiciousOperation,
 )
 from django.db.models import QuerySet, Sum
-from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, Http404
 from django.shortcuts import redirect
 from django.urls import reverse_lazy
 from django.utils import timezone
@@ -184,6 +184,8 @@ def dispatch(self, request, *args, **kwargs):
         return super().dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
+        if self.payable is None:
+            raise Http404("Payable does not exist")
         context = super().get_context_data(**kwargs)
         context.update({"payable": self.payable})
         context.update({"payable_hash": hash(self.payable)})
@@ -237,8 +239,17 @@ def post(self, request, *args, **kwargs):
         payable_pk = request.POST["payable"]
         payable_hash = request.POST["payable_hash"]
 
-        payable_model = apps.get_model(app_label=app_label, model_name=model_name)
-        self.payable = payables.get_payable(payable_model.objects.get(pk=payable_pk))
+        try:
+            payable_model = apps.get_model(app_label=app_label, model_name=model_name)
+        except LookupError as error:
+            raise Http404("This app model does not exist.") from error
+
+        try:
+            payable_obj = payable_model.objects.get(pk=payable_pk)
+        except payable_model.DoesNotExist as error:
+            raise Http404("This payable does not exist.") from error
+
+        self.payable = payables.get_payable(payable_obj)
 
         error = self._check_payment_allowed(request, payable_hash)
         if error:
diff --git a/website/sales/models/order.py b/website/sales/models/order.py
--- a/website/sales/models/order.py
+++ b/website/sales/models/order.py
@@ -161,7 +161,7 @@ def accept_payment_from_any_user(self):
     def payment_url(self):
         return (
             settings.BASE_URL + reverse("sales:order-pay", kwargs={"pk": self.pk})
-            if not self.payment
+            if not self.payment and self.pk
             else None
         )
 


Test patch:
diff --git a/website/payments/tests/test_views.py b/website/payments/tests/test_views.py
--- a/website/payments/tests/test_views.py
+++ b/website/payments/tests/test_views.py
@@ -467,7 +467,7 @@ def setUp(self):
         self.model = MockModel(payer=self.user)
 
         self.original_get_model = apps.get_model
-        mock_get_model = mock_get_model = MagicMock()
+        mock_get_model = MagicMock()
 
         def side_effect(*args, **kwargs):
             if "app_label" in kwargs and kwargs["app_label"] == "mock_app":
@@ -623,6 +623,59 @@ def test_payment_create_error(self, create_payment, messages_error):
         self.assertEqual(302, response.status_code)
         self.assertEqual("/mock_next", response.url)
 
+    def test_payment_deleted_error(self):
+        test_body = {
+            "app_label": "sales",
+            "model_name": "orders",
+            "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
+            "payable_hash": "non_existent",
+            "next": "/mock_next",
+        }
+        response = self.client.get(
+            reverse("payments:payment-process"), follow=False, data=test_body
+        )
+        self.assertEqual(404, response.status_code)
+
+    def test_payment_accept_deleted_error(self):
+        test_body = {
+            "app_label": "sales",
+            "model_name": "order",
+            "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
+            "next": "/mock_next",
+            "payable_hash": "non_existent",
+            "_save": True,
+        }
+        response = self.client.post(
+            reverse("payments:payment-process"), follow=False, data=test_body
+        )
+        self.assertEqual(404, response.status_code)
+
+    def test_app_does_not_exist(self):
+        test_body = {
+            "app_label": "payments",
+            "model_name": "does_not_exist",
+            "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
+            "next": "/mock_next",
+            "payable_hash": "non_existent",
+        }
+        response = self.client.post(
+            reverse("payments:payment-process"), follow=False, data=test_body
+        )
+        self.assertEqual(404, response.status_code)
+
+    def test_model_does_not_exist(self):
+        test_body = {
+            "app_label": "does_not_exist",
+            "model_name": "does_not_exist",
+            "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
+            "next": "/mock_next",
+            "payable_hash": "non_existent",
+        }
+        response = self.client.post(
+            reverse("payments:payment-process"), follow=False, data=test_body
+        )
+        self.assertEqual(404, response.status_code)
+
     @mock.patch("django.contrib.messages.error")
     def test_payment_changed_payable(self, messages_error):
         body = self.test_body