diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index b54077df7d..73943328af 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -40,8 +40,18 @@ from microsite_configuration import microsite log = logging.getLogger("shoppingcart") ORDER_STATUSES = ( + # The user is selecting what he/she wants to purchase. ('cart', 'cart'), + + # The user has been sent to the external payment processor. + # At this point, the order should NOT be modified. + # If the user returns to the payment flow, he/she will start a new order. + ('paying', 'paying'), + + # The user has successfully purchased the items in the order. ('purchased', 'purchased'), + + # The user's order has been refunded. ('refunded', 'refunded'), ) @@ -129,6 +139,22 @@ class Order(models.Model): """ self.orderitem_set.all().delete() + @transaction.commit_on_success + def start_purchase(self): + """ + Start the purchase process. This will set the order status to "paying", + at which point it should no longer be modified. + + Future calls to `Order.get_cart_for_user()` will filter out orders with + status "paying", effectively creating a new (empty) cart. + """ + if self.status == 'cart': + self.status = 'paying' + self.save() + + for item in OrderItem.objects.filter(order=self).select_subclasses(): + item.start_purchase() + def purchase(self, first='', last='', street1='', street2='', city='', state='', postalcode='', country='', ccnum='', cardtype='', processor_reply_dump=''): """ @@ -269,6 +295,14 @@ class OrderItem(models.Model): self.fulfilled_time = datetime.now(pytz.utc) self.save() + def start_purchase(self): + """ + Start the purchase process. This will set the order item status to "paying", + at which point it should no longer be modified. + """ + self.status = 'paying' + self.save() + def purchased_callback(self): """ This is called on each inventory item in the shopping cart when the diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index 2e340a87d8..c878e90a3b 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -102,6 +102,38 @@ class OrderTest(ModuleStoreTestCase): self.assertEquals(cart.orderitem_set.count(), len(course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs)) + def test_start_purchase(self): + # Start the purchase, which will mark the cart as "paying" + cart = Order.get_cart_for_user(user=self.user) + CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='usd') + cart.start_purchase() + self.assertEqual(cart.status, 'paying') + for item in cart.orderitem_set.all(): + self.assertEqual(item.status, 'paying') + + # Starting the purchase should be idempotent + cart.start_purchase() + self.assertEqual(cart.status, 'paying') + for item in cart.orderitem_set.all(): + self.assertEqual(item.status, 'paying') + + # If we retrieve the cart for the user, we should get a different order + next_cart = Order.get_cart_for_user(user=self.user) + self.assertNotEqual(cart, next_cart) + self.assertEqual(next_cart.status, 'cart') + + # Complete the first purchase + cart.purchase() + self.assertEqual(cart.status, 'purchased') + for item in cart.orderitem_set.all(): + self.assertEqual(item.status, 'purchased') + + # Starting the purchase again should be a no-op + cart.start_purchase() + self.assertEqual(cart.status, 'purchased') + for item in cart.orderitem_set.all(): + self.assertEqual(item.status, 'purchased') + def test_purchase(self): # This test is for testing the subclassing functionality of OrderItem, but in # order to do this, we end up testing the specific functionality of diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 29d1d1d808..fd35621d0d 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -31,8 +31,8 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from student.tests.factories import UserFactory from student.models import CourseEnrollment from course_modes.tests.factories import CourseModeFactory -from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from course_modes.models import CourseMode +from shoppingcart.models import Order, CertificateItem from verify_student.views import render_to_response from verify_student.models import SoftwareSecurePhotoVerification from reverification.tests.factories import MidcourseReverificationWindowFactory @@ -66,8 +66,8 @@ class StartView(TestCase): self.assertHttpForbidden(self.client.get(self.start_url())) -@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) -class TestCreateOrderView(TestCase): +@override_settings(MODULESTORE=MODULESTORE_CONFIG) +class TestCreateOrderView(ModuleStoreTestCase): """ Tests for the create_order view of verified course registration process """ @@ -75,7 +75,7 @@ class TestCreateOrderView(TestCase): self.user = UserFactory.create(username="rusty", password="test") self.client.login(username="rusty", password="test") self.course_id = 'Robot/999/Test_Course' - CourseFactory.create(org='Robot', number='999', display_name='Test Course') + self.course = CourseFactory.create(org='Robot', number='999', display_name='Test Course') verified_mode = CourseMode( course_id=SlashSeparatedCourseKey("Robot", "999", 'Test_Course'), mode_slug="verified", @@ -156,6 +156,14 @@ class TestCreateOrderView(TestCase): self.assertTrue(json_response.get('success')) self.assertIsNotNone(json_response.get('orderNumber')) + # Verify that the order exists and is configured correctly + order = Order.objects.get(user=self.user) + self.assertEqual(order.status, 'paying') + item = CertificateItem.objects.get(order=order) + self.assertEqual(item.status, 'paying') + self.assertEqual(item.course_id, self.course.id) + self.assertEqual(item.mode, 'verified') + @override_settings(MODULESTORE=MODULESTORE_CONFIG) class TestVerifyView(ModuleStoreTestCase): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 862edc2fdb..a854b9346c 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -227,6 +227,15 @@ def create_order(request): enrollment_mode = current_mode.slug CertificateItem.add_to_order(cart, course_id, amount, enrollment_mode) + # Change the order's status so that we don't accidentally modify it later. + # We need to do this to ensure that the parameters we send to the payment system + # match what we store in the database. + # (Ordinarily we would do this client-side when the user submits the form, but since + # the JavaScript on this page does that immediately, we make the change here instead. + # This avoids a second AJAX call and some additional complication of the JavaScript.) + # If a user later re-enters the verification / payment flow, she will create a new order. + cart.start_purchase() + callback_url = request.build_absolute_uri( reverse("shoppingcart.views.postpay_callback") )