Repository: svthalia/concrexit

Issue:
Under-age people can pay for age restricted orders through API
### Describe the bug
When an under-age person tries to pay for an age-restricted order, they are set as the `payer`. Although the website won't show a 'pay with thalia pay' button, and thadmin will warn its user, it is still possible for the payer to create a payment with the API.

### How to reproduce
Steps to reproduce the behaviour:
1. Create an age-restricted order.
2. Be signed in as a minor.
3. Open the payment url.
4. Observe that you are not allowed to pay.
5. PATCH `api/v2/payments/sales/order/<order uuid>/`
6. Observe that you have now created a payment.

### Expected behaviour
The API endpoint should give 403.


I haven't checked how thadmin handles this. I hope it will keep showing the under-age warning, but it might be that once a payment is made the 'paid' state overrules the 'under-age' state. 



Discussion:

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,6 +56,10 @@ def payment_payer(self):
     def tpay_allowed(self):
         return True
 
+    @property
+    def paying_allowed(self):
+        return True
+
     def can_manage_payment(self, member):
         raise NotImplementedError
 
diff --git a/website/payments/services.py b/website/payments/services.py
--- a/website/payments/services.py
+++ b/website/payments/services.py
@@ -55,6 +55,9 @@ def create_payment(
     if pay_type == Payment.TPAY and not payer.tpay_enabled:
         raise PaymentError(_("This user does not have Thalia Pay enabled"))
 
+    if not payable.paying_allowed:
+        raise PaymentError(_("Payment restricted"))
+
     if payable.payment is not None:
         payable.payment.amount = payable.payment_amount
         payable.payment.notes = payable.payment_notes
diff --git a/website/sales/payables.py b/website/sales/payables.py
--- a/website/sales/payables.py
+++ b/website/sales/payables.py
@@ -2,7 +2,7 @@
 
 from payments import Payable, payables
 from sales.models.order import Order, OrderItem
-from sales.services import is_manager
+from sales.services import is_manager, is_adult
 
 
 class OrderPayable(Payable):
@@ -22,6 +22,14 @@ def payment_notes(self):
     def payment_payer(self):
         return self.model.payer
 
+    @property
+    def paying_allowed(self):
+        return not (
+            self.model.age_restricted
+            and self.model.payer
+            and not is_adult(self.model.payer)
+        )
+
     def can_manage_payment(self, member):
         return is_manager(member, self.model.shift) and member.has_perm(
             "sales.change_order"


Test patch:
diff --git a/website/payments/tests/__mocks__.py b/website/payments/tests/__mocks__.py
--- a/website/payments/tests/__mocks__.py
+++ b/website/payments/tests/__mocks__.py
@@ -28,6 +28,7 @@ def __init__(
         notes="mock notes",
         payment=None,
         can_manage=True,
+        paying_allowed=True,
     ) -> None:
         self.payer = payer
         self.amount = amount
@@ -35,6 +36,7 @@ def __init__(
         self.notes = notes
         self.payment = payment
         self.can_manage = can_manage
+        self.paying_allowed = paying_allowed
 
         # Because we have to do as if this is a model sometimes
         self.verbose_name = "MockPayable"
@@ -68,6 +70,10 @@ def payment_notes(self):
     def payment_payer(self):
         return self.model.payer
 
+    @property
+    def paying_allowed(self):
+        return self.model.paying_allowed
+
     def can_manage_payment(self, member):
         return self.model.can_manage
 
diff --git a/website/payments/tests/test_services.py b/website/payments/tests/test_services.py
--- a/website/payments/tests/test_services.py
+++ b/website/payments/tests/test_services.py
@@ -69,11 +69,19 @@ def test_create_payment(self):
         with self.subTest("Only allow valid payment types"):
             with self.assertRaises(PaymentError):
                 services.create_payment(
-                    MockPayable(MockModel(payer=self.member, amount=0)),
+                    MockPayable(MockModel(payer=self.member)),
                     self.member,
                     "no_payment",
                 )
 
+        with self.subTest("Do not allow creating payment when not paying_allowed"):
+            with self.assertRaises(PaymentError):
+                services.create_payment(
+                    MockPayable(MockModel(payer=self.member, paying_allowed=False)),
+                    self.member,
+                    Payment.CASH,
+                )
+
     def test_delete_payment(self):
         existing_payment = MagicMock(batch=None)
         payable = MockPayable(MockModel(payer=self.member, payment=existing_payment))