Repository: svthalia/concrexit

Issue:
Sales order payments are not always saved
### Describe the bug
When paying for a Thalia pay order (via the sales payment view, so the QR code flow), the payment is not always stored back to the order. The payment is created properly, but after payment, the foreign key to the payment in the order is not saved. 

### How to reproduce
I am not sure exactly when this happens, at least it happens for the current shift 2 on the current staging environment. It might be because the shift has already been ended.

### Expected behaviour
Store the payment properly

### Additional context
Might be related to https://github.com/svthalia/concrexit/blob/6d0866022afb7fdf3edab34709d4d99e28039d59/website/sales/models/order.py#L123


Discussion:
Maybe you could add an example? I don't really understand why the ended shift would influence something in the payments app.
> Maybe you could add an example? I don't really understand why the ended shift would influence something in the payments app.

No the problem is not the payments app. The payment itself is created just fine. Only the order's save to store the ForeignKey to the payment is not properly executed
Well it uses the PaymentProcessView from the payments app right? And that is exactly the place where save is called on the payable object. https://github.com/svthalia/concrexit/blob/master/website/payments/views.py#L252

Yep, so I think something in the order is preventing the save. For example https://github.com/svthalia/concrexit/blob/6d0866022afb7fdf3edab34709d4d99e28039d59/website/sales/models/order.py#L123

Resolving patch:
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
@@ -115,16 +115,17 @@ def save(
         self, force_insert=False, force_update=False, using=None, update_fields=None
     ):
         if self.shift.locked:
-            return
+            raise ValueError("The shift this order belongs to is locked.")
         if self.shift.start > timezone.now():
-            return
+            raise ValueError("The shift hasn't started yet.")
         if (
             self.payment
-            and float(sum(self.order_items.values_list("total", flat=True)))
-            - (self.discount or 0)
-            != self.payment.amount
+            and self.subtotal - Decimal(self.discount or 0) != self.payment.amount
         ):
-            return
+            # We cannot use self.total_amount as it is a requires a database query and hence will not use any updated values
+            raise ValueError(
+                "The payment amount does not match the order total amount."
+            )
         if self.payment and not self.payer:
             self.payer = self.payment.paid_by
 
@@ -228,9 +229,9 @@ def save(
         self, force_insert=False, force_update=False, using=None, update_fields=None
     ):
         if self.order.shift.locked:
-            return
+            raise ValueError("The shift this order belongs to is locked.")
         if self.order.payment:
-            return
+            raise ValueError("This order has already been paid for.")
 
         if not self.total:
             self.total = self.product.price * self.amount


Test patch:
diff --git a/website/sales/tests/test_models.py b/website/sales/tests/test_models.py
--- a/website/sales/tests/test_models.py
+++ b/website/sales/tests/test_models.py
@@ -195,29 +195,21 @@ def test_num_items(self):
     def test_create_order_shift_locked(self):
         self.shift.locked = True
         self.shift.save()
-        order = Order.objects.create(shift=self.shift)
-
-        with self.assertRaises(ValidationError):
-            order.clean()
 
-        with self.assertRaises(Order.DoesNotExist):
-            Order.objects.get(pk=order.pk)
+        with self.assertRaises(ValueError):
+            Order.objects.create(shift=self.shift)
 
     def test_create_order_shift_not_started(self):
         self.shift.start = self.shift.start + timezone.timedelta(days=2)
         self.shift.end = self.shift.end + timezone.timedelta(days=2)
         self.shift.save()
-        order = Order.objects.create(shift=self.shift)
 
-        with self.assertRaises(ValidationError):
-            order.clean()
-
-        with self.assertRaises(Order.DoesNotExist):
-            Order.objects.get(pk=order.pk)
+        with self.assertRaises(ValueError):
+            Order.objects.create(shift=self.shift)
 
     def test_update_order_shift_locked(self):
         order = Order.objects.create(shift=self.shift)
-        i1 = OrderItem.objects.create(
+        OrderItem.objects.create(
             order=order,
             product=self.shift.product_list.product_items.get(product=self.beer),
             amount=2,
@@ -250,21 +242,20 @@ def test_update_order_paid(self):
 
         order.discount = 0.5
 
-        order.save()
-        order.refresh_from_db()
-        self.assertIsNone(order.discount)
+        with self.assertRaises(ValueError):
+            order.save()
 
-        i2 = OrderItem.objects.create(
-            order=order,
-            product=self.shift.product_list.product_items.get(product=self.wine),
-            amount=1,
-        )
-        i2.save()
-        with self.assertRaises(OrderItem.DoesNotExist):
-            OrderItem.objects.get(pk=i2.pk)
+        with self.assertRaises(ValueError):
+            OrderItem.objects.create(
+                order=order,
+                product=self.shift.product_list.product_items.get(product=self.wine),
+                amount=1,
+            )
 
         i1.amount = 3
-        i1.save()
+        with self.assertRaises(ValueError):
+            i1.save()
+
         i1.refresh_from_db()
         self.assertEqual(i1.amount, 2)