Repository: svthalia/concrexit

Issue:
Prevent deleting users that are still linked to payments
### What?

Deleting a user should be impossible when that user is payer of a payment. We need to change the on_delete cascade to protect on Payment.paid_by.

### Why? 

Since we have to store payments for 7 years by law, we cannot accept users being deleted while their payments still have to be stored (this does not influence the profile data minimization). 

Note that normally, users should not be deleted in any case. If someone specifically requests removal of all of their data, it may in some cases be done. But if they are still linked to payments, then until the 7 year period of those payments passes, we cannot legally _delete_ their account.


Discussion:

Resolving patch:
diff --git a/website/payments/migrations/0023_alter_payment_paid_by.py b/website/payments/migrations/0023_alter_payment_paid_by.py
new file mode 100644
--- /dev/null
+++ b/website/payments/migrations/0023_alter_payment_paid_by.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.2.5 on 2023-10-02 14:21
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("members", "0047_remove_profile_receive_magazine"),
+        ("payments", "0022_alter_bankaccount_iban"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="payment",
+            name="paid_by",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="paid_payment_set",
+                to="members.member",
+                verbose_name="paid by",
+            ),
+        ),
+    ]
diff --git a/website/payments/migrations/0024_alter_payment_paid_by.py b/website/payments/migrations/0024_alter_payment_paid_by.py
new file mode 100644
--- /dev/null
+++ b/website/payments/migrations/0024_alter_payment_paid_by.py
@@ -0,0 +1,26 @@
+# Generated by Django 4.2.5 on 2023-10-03 12:38
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("members", "0047_remove_profile_receive_magazine"),
+        ("payments", "0023_alter_payment_paid_by"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="payment",
+            name="paid_by",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.PROTECT,
+                related_name="paid_payment_set",
+                to="members.member",
+                verbose_name="paid by",
+            ),
+        ),
+    ]
diff --git a/website/payments/models.py b/website/payments/models.py
--- a/website/payments/models.py
+++ b/website/payments/models.py
@@ -153,10 +153,10 @@ class Payment(models.Model):
 
     paid_by = models.ForeignKey(
         "members.Member",
-        models.CASCADE,
+        models.PROTECT,
         verbose_name=_("paid by"),
         related_name="paid_payment_set",
-        blank=False,
+        blank=True,
         null=True,
     )
 


Test patch:
diff --git a/website/payments/tests/test_models.py b/website/payments/tests/test_models.py
--- a/website/payments/tests/test_models.py
+++ b/website/payments/tests/test_models.py
@@ -3,6 +3,7 @@
 from unittest.mock import PropertyMock, patch
 
 from django.core.exceptions import ValidationError
+from django.db.models.deletion import ProtectedError
 from django.test import TestCase, override_settings
 from django.utils import timezone
 
@@ -85,9 +86,9 @@ def test_add_payment_from_processed_batch_to_new_batch(self) -> None:
         with self.assertRaises(ValidationError):
             self.payment.save()
 
-    def test_deleting_member_who_made_a_payment_doesnt_crach(self) -> None:
-        """Check that https://github.com/svthalia/concrexit/issues/1328 is fixed."""
-        self.member.delete()
+    def test_delete_payer_raises_protectederror(self):
+        with self.assertRaises(ProtectedError):
+            self.member.delete()
 
     def test_clean(self):
         """Tests the model clean functionality."""