Repository: svthalia/concrexit

Issue:
Fix payable registry cache using old data
### Describe the bug
Payments are a mess. For example, if you pay for an event registration, delete the payment (through the admin or api), creating a new TPay payment through the api fails with 409 Conflict, there is still a payment in the registration model, but not in the payments api. Furthermore, paying with a different payment type works, but those payments can then not be removed. Also see #1806. I suspect there are many more related problems.

### How to reproduce
Play around with payable things, deleting and recreating them, or deleting and recreating payments.

### Expected behaviour
When a payable is not paid and should be payable with TPay, paying does not fail. Deleting a payment makes the payable not-paid as it was before creating the payment. Deleting or changing a payable is either impossible, or also deletes a payment that belongs to it.

### Additional context
I think it would be a good idea to combine this with #1000. Some test-driven development would make sense for payments, and I think the expected behaviour should be well-testable. Of course the problems may not be entirely within the payments app, but also in the payables defined by other apps.



Discussion:
> Play around with payable things

This is not really something I can reproduce. 

The payments app is the most reliable and most tested app in whole concrexit (although I agree with #1000), so it is not that we have never looked at cases like deleting payments and that 'just playing around' will make us bump into numerous bugs.


> if you pay for an event registration, delete the payment (through the admin or api), creating a new TPay payment through the api fails with 409 Conflict, there is still a payment in the registration model, but not in the payments api.  

This is a clear bug that we can actually follow up. Note however that this is not a problem with the payment model, but with the payable. I think that it is more an events issue than a payments issue. So I think this is what the issue should be

I have took a deep dive in the code for this issue and tried to split the different problems up in different tasks that we can convert into issues later. Bear with me.

--

1. Taking a look at the code, I see that the non-admin `PayableDetailView` is a `RetrieveAPIView` with a custom `patch()` method. It does NOT have a `delete()` method, so it is just impossible to delete payments from payables via the payable API **as a normal user**. I'm not sure whether this is a problem @DeD1rk experiences, but the idea is still that only managers can create and under certain circumstances delete payments. Then, Thalia Pay payments can be created by regular users for themselves, but cannot be undone (adding this possibility is a feature request that we purposefully did not implement yet as it has a lot of practical things to consider). Best thing we can do is open an issue for this,
 
- [ ] Allow users to delete Thalia Pay payments under some circumstances



This still doesn't explain however, how it could be that an event registration says it has a payment, even though the payment itself is deleted (which is what @DeD1rk reports). That kind of logic is just impossible because it's managed by the database with a foreign key.


The only way right now to actually delete a payment, is by going to the payment admin and hit the delete button there. When trying to reproduce that, though, the API (as to be expected) just shows that the payment field is null. That makes sense, because the payment doesn't exist anymore.

What is interesting, however, is that I can delete a payment and it will actually be gone (the object doesn't exist anymore at the database level, I checked in the db), then recreate a payment for that same event registration and the payment will be re-created **with the same uuid and same `created_at` field**. 

At first I couldn't explain this, but then I thought further and tried some things.

It turns out that if I delete the payment, **restart the python process** and *then* try to re-create the payment, it actually has a different `uuid` and `created_at` field. Therefore, I think we have a problem with caching. 

Removing the `@lru_cahce` from the Payable registry solves this problem!

So the problem turns out to be the following: because at many places in our codebase, we are getting payables from the payable registry (because of #1673), which is a cached registry, it could occur that our code is using outdated data at certain moments causing a lot of weird bugs that @DeD1rk is observing here. Removing the `@lru_cache` seems to solve the problem.

@se-bastiaan is there a (performance?) reason why the `@lru_cache` was required here, and is it save to delete it?

- [ ] Fix cached payable registry or revert #1673 


--

Also, while debugging I experienced that it would be nice to show the `uuid` of payments in the payment admin

- [ ] Display payment UUID in payments admin

Resolving patch:
diff --git a/website/payments/payables.py b/website/payments/payables.py
--- a/website/payments/payables.py
+++ b/website/payments/payables.py
@@ -56,7 +56,6 @@ class Payables:
     def _get_key(self, model):
         return f"{model._meta.app_label}_{model._meta.model_name}"
 
-    @lru_cache(maxsize=None)
     def get_payable(self, model: Model) -> Payable:
         if self._get_key(model) not in self._registry:
             raise NotRegistered(f"No Payable registered for {self._get_key(model)}")


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
@@ -580,7 +580,7 @@ def test_renders_confirmation(self):
 
         self.assertEqual(200, response.status_code)
         self.assertEqual(
-            payables.get_payable(self.model), response.context_data["payable"]
+            payables.get_payable(self.model).pk, response.context_data["payable"].pk
         )
         self.assertContains(response, "Please confirm your payment.")
         self.assertContains(response, 'name="_save"')
@@ -594,9 +594,8 @@ def test_creates_payment(self, create_payment, messages_success):
             data={**self.test_body, "_save": True},
         )
 
-        create_payment.assert_called_with(
-            payables.get_payable(self.model), self.user, Payment.TPAY
-        )
+        create_payment.assert_called_with(ANY, self.user, Payment.TPAY)
+        self.assertEqual(create_payment.call_args.args[0].pk, self.model.pk)
 
         messages_success.assert_called_with(
             ANY, "Your payment has been processed successfully."
diff --git a/website/payments/tests/test_widgets.py b/website/payments/tests/test_widgets.py
--- a/website/payments/tests/test_widgets.py
+++ b/website/payments/tests/test_widgets.py
@@ -27,7 +27,7 @@ def test_get_context(self):
 
         with self.subTest("With object only"):
             context = widget.get_context("payment", None, {})
-            self.assertEqual(context["obj"], payables.get_payable(self.obj))
+            self.assertEqual(context["obj"].pk, payables.get_payable(self.obj).pk)
             self.assertEqual(context["app_label"], "mock_app")
             self.assertEqual(context["model_name"], "mock_model")