Repository: saleor/saleor

Issue:
Dashboard 2.0: Order mutations don't check if an order is in the correct state
Mutations like order cancellation, payment release etc. can be called over and over on the same order ID without errors:

![screenshot](https://user-images.githubusercontent.com/81205/45295262-5a088200-b4fe-11e8-839e-8010c4d79cbc.png)



Discussion:

Resolving patch:
diff --git a/saleor/graphql/order/mutations/orders.py b/saleor/graphql/order/mutations/orders.py
--- a/saleor/graphql/order/mutations/orders.py
+++ b/saleor/graphql/order/mutations/orders.py
@@ -19,6 +19,40 @@ def try_payment_action(action, money, errors):
         errors.append(Error(field='payment', message=str(e)))
 
 
+def clean_order_cancel(order, errors):
+    if order and not order.can_cancel():
+        errors.append(
+            Error(
+                field='order',
+                message='This order can\'t be canceled.'))
+    return errors
+
+
+def clean_order_mark_as_paid(order, errors):
+    if order and order.payments.exists():
+        errors.append(
+            Error(
+                field='payment',
+                message='Orders with payments can not be manually '
+                        'marked as paid.'))
+    return errors
+
+
+def clean_order_capture(payment, amount, errors):
+    if not payment:
+        errors.append(
+            Error(
+                field='payment',
+                message='There\'s no payment associated with the order.'))
+        return errors
+    if payment.status != PaymentStatus.PREAUTH:
+        errors.append(
+            Error(
+                field='payment',
+                message='Only pre-authorized payments can be captured'))
+    return errors
+
+
 def clean_release_payment(payment, errors):
     """Check for payment errors."""
     if payment.status != PaymentStatus.PREAUTH:
@@ -37,7 +71,6 @@ def clean_refund_payment(payment, amount, errors):
         errors.append(
             Error(field='payment',
                   message='Manual payments can not be refunded.'))
-    try_payment_action(payment.refund, amount, errors)
     return errors
 
 
@@ -118,6 +151,7 @@ class Meta:
     def mutate(cls, root, info, id, restock):
         errors = []
         order = cls.get_node_or_error(info, id, errors, 'id', Order)
+        clean_order_cancel(order, errors)
         if errors:
             return OrderCancel(errors=errors)
 
@@ -150,12 +184,7 @@ class Meta:
     def mutate(cls, root, info, id):
         errors = []
         order = cls.get_node_or_error(info, id, errors, 'id', Order)
-        if order:
-            if order.payments.exists():
-                cls.add_error(
-                    errors, 'payment',
-                    'Orders with payments can not be manually marked as paid.')
-
+        clean_order_mark_as_paid(order, errors)
         if errors:
             return OrderMarkAsPaid(errors=errors)
 
@@ -196,10 +225,9 @@ class Meta:
     def mutate(cls, root, info, id, amount):
         errors = []
         order = cls.get_node_or_error(info, id, errors, 'id', Order)
-        if order:
-            payment = order.get_last_payment()
-            try_payment_action(payment.capture, amount, errors)
-
+        payment = order.get_last_payment()
+        clean_order_capture(payment, amount, errors)
+        try_payment_action(payment.capture, amount, errors)
         if errors:
             return OrderCapture(errors=errors)
 
@@ -260,7 +288,7 @@ def mutate(cls, root, info, id, amount):
         if order:
             payment = order.get_last_payment()
             clean_refund_payment(payment, amount, errors)
-
+            try_payment_action(payment.refund, amount, errors)
         if errors:
             return OrderRefund(errors=errors)
 


Test patch:
diff --git a/tests/api/test_order.py b/tests/api/test_order.py
--- a/tests/api/test_order.py
+++ b/tests/api/test_order.py
@@ -5,14 +5,17 @@
 
 import graphene
 from django.shortcuts import reverse
+from payments import PaymentStatus
 from saleor.account.models import Address
 from saleor.graphql.order.mutations.draft_orders import (
     check_for_draft_order_errors)
 from saleor.graphql.order.mutations.orders import (
+    clean_order_cancel, clean_order_capture, clean_order_mark_as_paid,
     clean_refund_payment, clean_release_payment)
 from saleor.graphql.order.types import OrderEventsEmailsEnum, PaymentStatusEnum
-from saleor.order import CustomPaymentChoices, OrderEvents, OrderEventsEmails
-from saleor.order.models import Order, OrderStatus, Payment, PaymentStatus
+from saleor.order import (
+    CustomPaymentChoices, OrderEvents, OrderEventsEmails, OrderStatus)
+from saleor.order.models import Order, Payment
 from tests.utils import get_graphql_content
 
 
@@ -634,9 +637,32 @@ def test_clean_order_refund_payment():
     assert errors[0].field == 'payment'
     assert errors[0].message == 'Manual payments can not be refunded.'
 
-    payment.variant = None
-    error_msg = 'error has happened.'
-    payment.refund = Mock(side_effect=ValueError(error_msg))
-    errors = clean_refund_payment(payment, amount, [])
+
+def test_clean_order_capture():
+    amount = Mock(spec='string')
+    errors = clean_order_capture(None, amount, [])
     assert errors[0].field == 'payment'
-    assert errors[0].message == error_msg
+    assert errors[0].message == (
+        'There\'s no payment associated with the order.')
+
+
+def test_clean_order_mark_as_paid(payment_preauth):
+    order = payment_preauth.order
+    errors = clean_order_mark_as_paid(order, [])
+    assert errors[0].field == 'payment'
+    assert errors[0].message == (
+        'Orders with payments can not be manually marked as paid.')
+
+    order.payments.all().delete()
+    assert clean_order_mark_as_paid(order, []) == []
+
+
+def test_clean_order_cancel(order):
+    assert clean_order_cancel(order, []) == []
+
+    order.status = OrderStatus.DRAFT
+    order.save()
+
+    errors = clean_order_cancel(order, [])
+    assert errors[0].field == 'order'
+    assert errors[0].message == 'This order can\'t be canceled.'