def test_confirmation_email_error(self): CourseMode.objects.create( course_id=self.course_key, mode_slug="verified", mode_display_name="Verified", min_price=self.cost ) cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') # Simulate an error when sending the confirmation # email. This should NOT raise an exception. # If it does, then the implicit view-level # transaction could cause a roll-back, effectively # reversing order fulfillment. with patch.object(mail.message.EmailMessage, 'send') as mock_send: mock_send.side_effect = Exception("Kaboom!") cart.purchase() # Verify that the purchase completed successfully self.assertEqual(cart.status, 'purchased') # Verify that the user is enrolled as "verified" mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key) self.assertTrue(is_active) self.assertEqual(mode, 'verified')
def test_refund_cert_callback_before_expiration(self): # If the expiration date has not yet passed on a verified mode, the user can be refunded many_days = datetime.timedelta(days=60) course = CourseFactory.create() self.course_key = course.id course_mode = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days)) course_mode.save() # need to prevent analytics errors from appearing in stderr with patch('sys.stderr', sys.stdout.write): CourseEnrollment.enroll(self.user, self.course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') cart.purchase() CourseEnrollment.unenroll(self.user, self.course_key) target_certs = CertificateItem.objects.filter(course_id=self.course_key, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'refunded') self._assert_refund_tracked()
def test_single_item_template(self): cart = Order.get_cart_for_user(user=self.user) cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html') cert_item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') self.assertEquals(cert_item.single_item_receipt_template, 'shoppingcart/receipt.html')
def test_refund_cert_callback_before_expiration_email(self): """ Test that refund emails are being sent correctly. """ course = CourseFactory.create() course_key = course.id many_days = datetime.timedelta(days=60) course_mode = CourseMode(course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=datetime.datetime.now(pytz.utc) + many_days) course_mode.save() CourseEnrollment.enroll(self.user, course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, course_key, self.cost, 'verified') cart.purchase() mail.outbox = [] with patch('shoppingcart.models.log.error') as mock_error_logger: CourseEnrollment.unenroll(self.user, course_key) self.assertFalse(mock_error_logger.called) self.assertEquals(len(mail.outbox), 1) self.assertEquals('[Refund] User-Requested Refund', mail.outbox[0].subject) self.assertEquals(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].from_email) self.assertIn('has requested a refund on Order', mail.outbox[0].body)
def test_refund_cert_callback_before_expiration(self): # If the expiration date has not yet passed on a verified mode, the user can be refunded many_days = datetime.timedelta(days=60) course = CourseFactory.create(org="refund_before_expiration", number="test", display_name="one") course_key = course.id course_mode = CourseMode( course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days), ) course_mode.save() CourseEnrollment.enroll(self.user, course_key, "verified") cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, course_key, self.cost, "verified") cart.purchase() CourseEnrollment.unenroll(self.user, course_key) target_certs = CertificateItem.objects.filter( course_id=course_key, user_id=self.user, status="refunded", mode="verified" ) self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, "refunded")
def test_purchase_twice(self): cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') # purchase the cart more than once cart.purchase() cart.purchase() self.assertEquals(len(mail.outbox), 1)
def checkout_with_shoppingcart(request, user, course_key, course_mode, amount): """ Create an order and trigger checkout using shoppingcart.""" cart = Order.get_cart_for_user(user) cart.clear() enrollment_mode = course_mode.slug CertificateItem.add_to_order(cart, course_key, 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") ) payment_data = { 'payment_processor_name': settings.CC_PROCESSOR_NAME, 'payment_page_url': get_purchase_endpoint(), 'payment_form_data': get_signed_purchase_params( cart, callback_url=callback_url, extra_data=[unicode(course_key), course_mode.slug] ), } return payment_data
def test_cart_clear(self): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') CertificateItem.add_to_order(cart, 'test/course1', self.cost, 'verified') self.assertEquals(cart.orderitem_set.count(), 2) cart.clear() self.assertEquals(cart.orderitem_set.count(), 0)
def test_purchase_item_email_boto_failure(self, error_logger): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch.object(EmailMessage, 'send') as mock_send: mock_send.side_effect = BotoServerError("status", "reason") cart.purchase() self.assertTrue(error_logger.called)
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_clear_cart(self): self.login_user() PaidCourseRegistration.add_to_order(self.cart, self.course_key) CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.assertEquals(self.cart.orderitem_set.count(), 2) resp = self.client.post(reverse('shoppingcart.views.clear_cart', args=[])) self.assertEqual(resp.status_code, 200) self.assertEquals(self.cart.orderitem_set.count(), 0)
def test_retire_order_cart(self): """Test that an order in cart can successfully be retired""" cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='usd') cart.retire() self.assertEqual(cart.status, 'defunct-cart') self.assertEqual(cart.orderitem_set.get().status, 'defunct-cart')
def test_get_cart_for_user(self): # create a cart cart = Order.get_cart_for_user(user=self.user) # add something to it CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') # should return the same cart cart2 = Order.get_cart_for_user(user=self.user) self.assertEquals(cart2.orderitem_set.count(), 1)
def test_existing_enrollment(self): CourseEnrollment.enroll(self.user, self.course_id) cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') # verify that we are still enrolled self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id)) cart.purchase() enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id) self.assertEquals(enrollment.mode, u'verified')
def test_cart_clear(self): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'honor') CertificateItem.add_to_order(cart, 'org/test/Test_Course_1', self.cost, 'honor') self.assertEquals(cart.orderitem_set.count(), 2) self.assertTrue(cart.has_items()) cart.clear() self.assertEquals(cart.orderitem_set.count(), 0) self.assertFalse(cart.has_items())
def test_user_cart_has_both_items(self): """ This test exists b/c having both CertificateItem and PaidCourseRegistration in an order used to break PaidCourseRegistration.contained_in_order """ cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.assertTrue(PaidCourseRegistration.contained_in_order(cart, self.course_key))
def test_cart_clear(self): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') CertificateItem.add_to_order(cart, SlashSeparatedCourseKey('org', 'test', 'Test_Course_1'), self.cost, 'honor') self.assertEquals(cart.orderitem_set.count(), 2) self.assertTrue(cart.has_items()) cart.clear() self.assertEquals(cart.orderitem_set.count(), 0) self.assertFalse(cart.has_items())
def _enroll(self, purchase=True): # pylint: disable=missing-docstring CourseEnrollment.enroll(self.student, self.course_id, self.course_mode.mode_slug) if purchase: self.order = Order.get_cart_for_user(self.student) CertificateItem.add_to_order(self.order, self.course_id, 1, self.course_mode.mode_slug) self.order.purchase() self.course_mode.expiration_datetime = datetime.datetime(1983, 4, 6, tzinfo=pytz.UTC) self.course_mode.save()
def create_order(request): """ Submit PhotoVerification and create a new Order for this verified cert """ if not SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): attempt = SoftwareSecurePhotoVerification(user=request.user) b64_face_image = request.POST['face_image'].split(",")[1] b64_photo_id_image = request.POST['photo_id_image'].split(",")[1] attempt.upload_face_image(b64_face_image.decode('base64')) attempt.upload_photo_id_image(b64_photo_id_image.decode('base64')) attempt.mark_ready() attempt.save() course_id = request.POST['course_id'] course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) donation_for_course = request.session.get('donation_for_course', {}) current_donation = donation_for_course.get(course_id, decimal.Decimal(0)) contribution = request.POST.get("contribution", donation_for_course.get(course_id, 0)) try: amount = decimal.Decimal(contribution).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: return HttpResponseBadRequest(_("Selected price is not valid number.")) if amount != current_donation: donation_for_course[course_id] = amount request.session['donation_for_course'] = donation_for_course # prefer professional mode over verified_mode current_mode = CourseMode.verified_mode_for_course(course_id) if current_mode.slug == 'professional': amount = current_mode.min_price # make sure this course has a verified mode if not current_mode: return HttpResponseBadRequest(_("This course doesn't support verified certificates")) if amount < current_mode.min_price: return HttpResponseBadRequest(_("No selected price or selected price is below minimum.")) # I know, we should check this is valid. All kinds of stuff missing here cart = Order.get_cart_for_user(request.user) cart.clear() enrollment_mode = current_mode.slug CertificateItem.add_to_order(cart, course_id, amount, enrollment_mode) callback_url = request.build_absolute_uri( reverse("shoppingcart.views.postpay_callback") ) params = get_signed_purchase_params( cart, callback_url=callback_url ) return HttpResponse(json.dumps(params), content_type="text/json")
def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund CourseEnrollment.enroll(self.user, self.course_id, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') cart.purchase() CourseEnrollment.unenroll(self.user, self.course_id) target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0])
def test_purchase_item_failure(self): # once again, we're testing against the specific implementation of # CertificateItem cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') with patch('shoppingcart.models.CertificateItem.save', side_effect=DatabaseError): with self.assertRaises(DatabaseError): cart.purchase() # verify that we rolled back the entire transaction self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id))
def _create_and_purchase_verified(self, student, course_id): """ Creates a verified mode for the course and purchases it for the student. """ course_mode = CourseMode( course_id=course_id, mode_slug="verified", mode_display_name="verified cert", min_price=50 ) course_mode.save() # When there is no expiration date on a verified mode, the user can always get a refund cart = Order.get_cart_for_user(user=student) CertificateItem.add_to_order(cart, course_id, 50, "verified") cart.purchase()
def test_total_cost(self): cart = Order.get_cart_for_user(user=self.user) # add items to the order course_costs = [(self.other_course_keys[0], 30), (self.other_course_keys[1], 40), (self.other_course_keys[2], 10), (self.other_course_keys[3], 20)] for course, cost in course_costs: CertificateItem.add_to_order(cart, course, cost, 'honor') self.assertEquals(cart.orderitem_set.count(), len(course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs))
def test_add_item_to_cart_currency_match(self): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified', currency='eur') # verify that a new item has been added self.assertEquals(cart.orderitem_set.count(), 1) # verify that the cart's currency was updated self.assertEquals(cart.currency, 'eur') with self.assertRaises(InvalidCartItem): CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified', currency='usd') # assert that this item did not get added to the cart self.assertEquals(cart.orderitem_set.count(), 1)
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 # CertificateItem, which is not quite good unit test form. Sorry. cart = Order.get_cart_for_user(user=self.user) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') # course enrollment object should be created but still inactive self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_id)) cart.purchase() self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_id))
def test_total_cost(self): cart = Order.get_cart_for_user(user=self.user) # add items to the order course_costs = [('test/course1', 30), ('test/course2', 40), ('test/course3', 10), ('test/course4', 20)] for course, cost in course_costs: CertificateItem.add_to_order(cart, course, cost, 'verified') self.assertEquals(cart.orderitem_set.count(), len(course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs))
def test_total_cost(self): cart = Order.get_cart_for_user(user=self.user) # add items to the order course_costs = [('org/test/Test_Course_1', 30), ('org/test/Test_Course_2', 40), ('org/test/Test_Course_3', 10), ('org/test/Test_Course_4', 20)] for course, cost in course_costs: CertificateItem.add_to_order(cart, SlashSeparatedCourseKey.from_deprecated_string(course), cost, 'honor') self.assertEquals(cart.orderitem_set.count(), len(course_costs)) self.assertEquals(cart.total_cost, sum(cost for _course, cost in course_costs))
def rows(self): for course_id in course_ids_between(self.start_word, self.end_word): # If the first letter of the university is between start_word and end_word, then we include # it in the report. These comparisons are unicode-safe. cur_course = get_course_by_id(course_id) university = cur_course.org # TODO add term (i.e. Fall 2013) to course? course = cur_course.number + " " + cur_course.display_name_with_default_escaped counts = CourseEnrollment.objects.enrollment_counts(course_id) total_enrolled = counts['total'] audit_enrolled = counts['audit'] honor_enrolled = counts['honor'] if counts['verified'] == 0: verified_enrolled = 0 gross_rev = Decimal(0.00) gross_rev_over_min = Decimal(0.00) else: verified_enrolled = counts['verified'] gross_rev = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'purchased', 'unit_cost') gross_rev_over_min = gross_rev - (CourseMode.min_course_price_for_verified_for_currency(course_id, 'usd') * verified_enrolled) num_verified_over_the_minimum = CertificateItem.verified_certificates_contributing_more_than_minimum(course_id) # should I be worried about is_active here? number_of_refunds = CertificateItem.verified_certificates_count(course_id, 'refunded') if number_of_refunds == 0: dollars_refunded = Decimal(0.00) else: dollars_refunded = CertificateItem.verified_certificates_monetary_field_sum(course_id, 'refunded', 'unit_cost') course_announce_date = "" course_reg_start_date = "" course_reg_close_date = "" registration_period = "" yield [ university, course, course_announce_date, course_reg_start_date, course_reg_close_date, registration_period, total_enrolled, audit_enrolled, honor_enrolled, verified_enrolled, gross_rev, gross_rev_over_min, num_verified_over_the_minimum, number_of_refunds, dollars_refunded ]
def test_purchase_item_failure(self): # once again, we're testing against the specific implementation of # CertificateItem cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, "honor") with patch("shoppingcart.models.CertificateItem.save", side_effect=DatabaseError): with self.assertRaises(DatabaseError): cart.purchase() # verify that we rolled back the entire transaction self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) # verify that e-mail wasn't sent self.assertEquals(len(mail.outbox), 0)
def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund CourseEnrollment.enroll(self.user, self.course_key, "verified") cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, "verified") cart.purchase() CourseEnrollment.unenroll(self.user, self.course_key) target_certs = CertificateItem.objects.filter( course_id=self.course_key, user_id=self.user, status="refunded", mode="verified" ) self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, "refunded")
def test_retire_order_error(self, order_status, item_status, exception): """ Test error cases for retiring an order: 1) Order item has a different status than the order 2) The order's status isn't in "cart" or "paying" """ cart = Order.get_cart_for_user(user=self.user) item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='usd') cart.status = order_status cart.save() item.status = item_status item.save() with self.assertRaises(exception): cart.retire()
def test_retire_order_already_retired(self, status): """ Check that orders that have already been retired noop when the method is called on them again. """ cart = Order.get_cart_for_user(user=self.user) item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor', currency='usd') cart.status = item.status = status cart.save() item.save() cart.retire() self.assertEqual(cart.status, status) self.assertEqual(item.status, status)
def setUp(self): self.user = UserFactory.create() self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course') self.course_key = self.course.id course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) course_mode2.save() self.annotation = PaidCourseRegistrationAnnotation( course_id=self.course_key, annotation=self.TEST_ANNOTATION) self.annotation.save() self.cart = Order.get_cart_for_user(self.user) self.reg = PaidCourseRegistration.add_to_order(self.cart, self.course_key) self.cert_item = CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase() self.now = datetime.datetime.now(pytz.UTC) paid_reg = PaidCourseRegistration.objects.get( course_id=self.course_key, user=self.user) paid_reg.fulfilled_time = self.now paid_reg.refund_requested_time = self.now paid_reg.save() cert = CertificateItem.objects.get(course_id=self.course_key, user=self.user) cert.fulfilled_time = self.now cert.refund_requested_time = self.now cert.save() self.CORRECT_CSV = dedent(""" Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments {time_str},1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,Ba\xc3\xbc\xe5\x8c\x85 {time_str},1,purchased,1,40,40,usd,verified cert for course Robot Super Course, """.format(time_str=str(self.now)))
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 # CertificateItem, which is not quite good unit test form. Sorry. cart = Order.get_cart_for_user(user=self.user) self.assertFalse( CourseEnrollment.is_enrolled(self.user, self.course_key)) item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') # course enrollment object should be created but still inactive self.assertFalse( CourseEnrollment.is_enrolled(self.user, self.course_key)) # the analytics client pipes output to stderr when using the default client with patch('sys.stderr', sys.stdout.write): cart.purchase() self.assertTrue( CourseEnrollment.is_enrolled(self.user, self.course_key)) # test e-mail sending self.assertEquals(len(mail.outbox), 1) self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject) self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body) self.assertIn(unicode(cart.total_cost), mail.outbox[0].body) self.assertIn(item.additional_instruction_text, mail.outbox[0].body) # Assert Google Analytics event fired for purchase. self.mock_tracker.track.assert_called_once_with( # pylint: disable=maybe-no-member self.user.id, 'Completed Order', { 'orderId': 1, 'currency': 'usd', 'total': '40', 'products': [ { 'sku': u'CertificateItem.honor', 'name': unicode(self.course_key), 'category': unicode(self.course_key.org), 'price': '40', 'id': 1, 'quantity': 1 } ] }, context={'Google Analytics': {'clientId': None}} )
def test_show_receipt_success_with_upgrade(self): reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.login_user() # When we come from the upgrade flow, we'll have a session variable showing that s = self.client.session s['attempting_upgrade'] = True s.save() self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) # Once they've upgraded, they're no longer *attempting* to upgrade attempting_upgrade = self.client.session.get('attempting_upgrade', False) self.assertFalse(attempting_upgrade) self.assertEqual(resp.status_code, 200) self.assertIn('FirstNameTesting123', resp.content) self.assertIn('80.00', resp.content) ((template, context), _) = render_mock.call_args # When we come from the upgrade flow, we get these context variables self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(context['order'], self.cart) self.assertIn(reg_item, context['order_items']) self.assertIn(cert_item, context['order_items']) self.assertFalse(context['any_refunds']) course_enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) course_enrollment.emit_event('edx.course.enrollment.upgrade.succeeded') self.mock_tracker.emit.assert_any_call( # pylint: disable=maybe-no-member 'edx.course.enrollment.upgrade.succeeded', { 'user_id': course_enrollment.user.id, 'course_id': course_enrollment.course_id.to_deprecated_string(), 'mode': course_enrollment.mode } )
def test_show_receipt_success(self): reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.login_user() resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) self.assertIn('FirstNameTesting123', resp.content) self.assertIn('80.00', resp.content) ((template, context), _) = render_mock.call_args self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(context['order'], self.cart) self.assertIn(reg_item, context['order_items']) self.assertIn(cert_item, context['order_items']) self.assertFalse(context['any_refunds'])
def test_show_receipt_success_refund(self): reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_id) cert_item = CertificateItem.add_to_order(self.cart, 'test/course1', self.cost, 'verified') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') cert_item.status = "refunded" cert_item.save() self.assertEqual(self.cart.total_cost, 40) self.login_user() resp = self.client.get(reverse('shoppingcart.views.show_receipt', args=[self.cart.id])) self.assertEqual(resp.status_code, 200) self.assertIn('40.00', resp.content) ((template, context), _) = render_mock.call_args self.assertEqual(template, 'shoppingcart/receipt.html') self.assertEqual(context['order'], self.cart) self.assertIn(reg_item.orderitem_ptr, context['order_items']) self.assertIn(cert_item.orderitem_ptr, context['order_items']) self.assertTrue(context['any_refunds'])
def test_delete_certificate_item(self, info_log): reg_item = self.add_course_to_user_cart() cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.assertEquals(self.cart.orderitem_set.count(), 2) # Delete the discounted item, corresponding coupon redemption should be removed for that particular discounted item resp = self.client.post( reverse('shoppingcart.views.remove_item', args=[]), {'id': cert_item.id}) self.assertEqual(resp.status_code, 200) self.assertEquals(self.cart.orderitem_set.count(), 1) info_log.assert_called_with( 'order item {0} removed for user {1}'.format( cert_item.id, self.user))
def test_show_cart(self): self.login_user() reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') resp = self.client.get(reverse('shoppingcart.views.show_cart', args=[])) self.assertEqual(resp.status_code, 200) ((purchase_form_arg_cart,), _) = form_mock.call_args purchase_form_arg_cart_items = purchase_form_arg_cart.orderitem_set.all().select_subclasses() self.assertIn(reg_item, purchase_form_arg_cart_items) self.assertIn(cert_item, purchase_form_arg_cart_items) self.assertEqual(len(purchase_form_arg_cart_items), 2) ((template, context), _) = render_mock.call_args self.assertEqual(template, 'shoppingcart/list.html') self.assertEqual(len(context['shoppingcart_items']), 2) self.assertEqual(context['amount'], 80) self.assertIn("80.00", context['form_html'])
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 # CertificateItem, which is not quite good unit test form. Sorry. cart = Order.get_cart_for_user(user=self.user) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) item = CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') # course enrollment object should be created but still inactive self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course_key)) cart.purchase() self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) # test e-mail sending self.assertEquals(len(mail.outbox), 1) self.assertEquals('Order Payment Confirmation', mail.outbox[0].subject) self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].body) self.assertIn(unicode(cart.total_cost), mail.outbox[0].body) self.assertIn(item.additional_instruction_text, mail.outbox[0].body)
def test_remove_item(self, exception_log): self.login_user() reg_item = PaidCourseRegistration.add_to_order(self.cart, self.course_key) cert_item = CertificateItem.add_to_order(self.cart, self.verified_course_key, self.cost, 'honor') self.assertEquals(self.cart.orderitem_set.count(), 2) resp = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': reg_item.id}) self.assertEqual(resp.status_code, 200) self.assertEquals(self.cart.orderitem_set.count(), 1) self.assertNotIn(reg_item, self.cart.orderitem_set.all().select_subclasses()) self.cart.purchase() resp2 = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': cert_item.id}) self.assertEqual(resp2.status_code, 200) exception_log.assert_called_with( 'Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(cert_item.id)) resp3 = self.client.post(reverse('shoppingcart.views.remove_item', args=[]), {'id': -1}) self.assertEqual(resp3.status_code, 200) exception_log.assert_called_with( 'Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(-1))
def test_purchase_item_email_boto_failure(self, error_logger): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch('shoppingcart.models.send_mail', side_effect=BotoServerError("status", "reason")): cart.purchase() self.assertTrue(error_logger.called)
def setUp(self, cutoff_date): super(ReportTypeTests, self).setUp() cutoff_date.return_value = datetime.datetime.now( pytz.UTC) + datetime.timedelta(days=1) # Need to make a *lot* of users for this one self.first_verified_user = UserFactory.create(profile__name="John Doe") self.second_verified_user = UserFactory.create( profile__name="Jane Deer") self.first_audit_user = UserFactory.create(profile__name="Joe Miller") self.second_audit_user = UserFactory.create( profile__name="Simon Blackquill") self.third_audit_user = UserFactory.create(profile__name="Super Mario") self.honor_user = UserFactory.create(profile__name="Princess Peach") self.first_refund_user = UserFactory.create( profile__name="King Bowsér") self.second_refund_user = UserFactory.create( profile__name="Súsan Smith") # Two are verified, three are audit, one honor self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course') self.course_key = self.course.id course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) course_mode2.save() # User 1 & 2 will be verified self.cart1 = Order.get_cart_for_user(self.first_verified_user) CertificateItem.add_to_order(self.cart1, self.course_key, self.cost, 'verified') self.cart1.purchase() self.cart2 = Order.get_cart_for_user(self.second_verified_user) CertificateItem.add_to_order(self.cart2, self.course_key, self.cost, 'verified') self.cart2.purchase() # Users 3, 4, and 5 are audit CourseEnrollment.enroll(self.first_audit_user, self.course_key, "audit") CourseEnrollment.enroll(self.second_audit_user, self.course_key, "audit") CourseEnrollment.enroll(self.third_audit_user, self.course_key, "audit") # User 6 is honor CourseEnrollment.enroll(self.honor_user, self.course_key, "honor") self.now = datetime.datetime.now(pytz.UTC) # Users 7 & 8 are refunds self.cart = Order.get_cart_for_user(self.first_refund_user) CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase() CourseEnrollment.unenroll(self.first_refund_user, self.course_key) self.cart = Order.get_cart_for_user(self.second_refund_user) CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase(self.second_refund_user.username, self.course_key) CourseEnrollment.unenroll(self.second_refund_user, self.course_key) self.test_time = datetime.datetime.now(pytz.UTC) first_refund = CertificateItem.objects.get(id=3) first_refund.fulfilled_time = self.test_time first_refund.refund_requested_time = self.test_time first_refund.save() second_refund = CertificateItem.objects.get(id=4) second_refund.fulfilled_time = self.test_time second_refund.refund_requested_time = self.test_time second_refund.save() self.CORRECT_REFUND_REPORT_CSV = dedent(u""" Order Number,Customer Name,Date of Original Transaction,Date of Refund,Amount of Refund,Service Fees (if any) 3,King Bowsér,{time_str},{time_str},40.00,0.00 4,Súsan Smith,{time_str},{time_str},40.00,0.00 """.format(time_str=str(self.test_time))) self.CORRECT_CERT_STATUS_CSV = dedent(""" University,Course,Course Announce Date,Course Start Date,Course Registration Close Date,Course Registration Period,Total Enrolled,Audit Enrollment,Honor Code Enrollment,Verified Enrollment,Gross Revenue,Gross Revenue over the Minimum,Number of Verified Students Contributing More than the Minimum,Number of Refunds,Dollars Refunded MITx,999 Robot Super Course,,,,,6,3,1,2,80.00,0.00,0,2,80.00 """.format(time_str=str(self.test_time))) self.CORRECT_UNI_REVENUE_SHARE_CSV = dedent(""" University,Course,Number of Transactions,Total Payments Collected,Service Fees (if any),Number of Successful Refunds,Total Amount of Refunds MITx,999 Robot Super Course,6,80.00,0.00,2,80.00 """.format(time_str=str(self.test_time)))
def setUp(self): super(TestOrderHistoryOnSiteDashboard, self).setUp() patcher = patch('student.models.tracker') self.mock_tracker = patcher.start() self.user = UserFactory.create() self.user.set_password('password') self.user.save() self.addCleanup(patcher.stop) # First Order with our (fakeX) site's course. course1 = CourseFactory.create(org='fakeX', number='999', display_name='fakeX Course') course1_key = course1.id course1_mode = CourseMode(course_id=course1_key, mode_slug="honor", mode_display_name="honor cert", min_price=20) course1_mode.save() cart = Order.get_cart_for_user(self.user) PaidCourseRegistration.add_to_order(cart, course1_key) cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.fakex_site_order_id = cart.id # Second Order with another(fooX) site's course course2 = CourseFactory.create(org='fooX', number='888', display_name='fooX Course') course2_key = course2.id course2_mode = CourseMode(course_id=course2.id, mode_slug="honor", mode_display_name="honor cert", min_price=20) course2_mode.save() cart = Order.get_cart_for_user(self.user) PaidCourseRegistration.add_to_order(cart, course2_key) cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.foox_site_order_id = cart.id # Third Order with course not attributed to any site. course3 = CourseFactory.create(org='fakeOtherX', number='777', display_name='fakeOtherX Course') course3_key = course3.id course3_mode = CourseMode(course_id=course3.id, mode_slug="honor", mode_display_name="honor cert", min_price=20) course3_mode.save() cart = Order.get_cart_for_user(self.user) PaidCourseRegistration.add_to_order(cart, course3_key) cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.order_id = cart.id # Fourth Order with course not attributed to any site but with a CertificateItem course4 = CourseFactory.create(org='fakeOtherX', number='888') course4_key = course4.id course4_mode = CourseMode(course_id=course4.id, mode_slug="verified", mode_display_name="verified cert", min_price=20) course4_mode.save() cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, course4_key, 20.0, 'verified') cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.certificate_order_id = cart.id # Fifth Order with course not attributed to any site but with a Donation course5 = CourseFactory.create(org='fakeOtherX', number='999') course5_key = course5.id cart = Order.get_cart_for_user(self.user) Donation.add_to_order(cart, 20.0, course5_key) cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.donation_order_id = cart.id # also add a donation not associated with a course to make sure the None case works OK Donation.add_to_order(cart, 10.0, None) cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.courseless_donation_order_id = cart.id
def create_order(request): """ Submit PhotoVerification and create a new Order for this verified cert """ if not SoftwareSecurePhotoVerification.user_has_valid_or_pending( request.user): attempt = SoftwareSecurePhotoVerification(user=request.user) try: b64_face_image = request.POST['face_image'].split(",")[1] b64_photo_id_image = request.POST['photo_id_image'].split(",")[1] except IndexError: context = { 'success': False, } return JsonResponse(context) attempt.upload_face_image(b64_face_image.decode('base64')) attempt.upload_photo_id_image(b64_photo_id_image.decode('base64')) attempt.mark_ready() attempt.save() course_id = request.POST['course_id'] course_id = CourseKey.from_string(course_id) donation_for_course = request.session.get('donation_for_course', {}) current_donation = donation_for_course.get(unicode(course_id), decimal.Decimal(0)) contribution = request.POST.get( "contribution", donation_for_course.get(unicode(course_id), 0)) try: amount = decimal.Decimal(contribution).quantize( decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: return HttpResponseBadRequest(_("Selected price is not valid number.")) if amount != current_donation: donation_for_course[unicode(course_id)] = amount request.session['donation_for_course'] = donation_for_course # prefer professional mode over verified_mode current_mode = CourseMode.verified_mode_for_course(course_id) # make sure this course has a verified mode if not current_mode: return HttpResponseBadRequest( _("This course doesn't support verified certificates")) if current_mode.slug == 'professional': amount = current_mode.min_price if amount < current_mode.min_price: return HttpResponseBadRequest( _("No selected price or selected price is below minimum.")) # I know, we should check this is valid. All kinds of stuff missing here cart = Order.get_cart_for_user(request.user) cart.clear() 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")) params = get_signed_purchase_params(cart, callback_url=callback_url, extra_data=[unicode(course_id)]) params['success'] = True return HttpResponse(json.dumps(params), content_type="text/json")
def test_user_cart_has_certificate_items(self): cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') self.assertTrue(Order.user_cart_has_items(self.user, CertificateItem)) self.assertFalse(Order.user_cart_has_items(self.user, PaidCourseRegistration))
def test_purchase_item_email_smtp_failure(self, error_logger): cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'honor') with patch('shoppingcart.models.send_mail', side_effect=smtplib.SMTPException): cart.purchase() self.assertTrue(error_logger.called)
def create_order(request): """ Submit PhotoVerification and create a new Order for this verified cert """ # Only submit photos if photo data is provided by the client. # TODO (ECOM-188): Once the A/B test of decoupling verified / payment # completes, we may be able to remove photo submission from this step # entirely. submit_photo = ( 'face_image' in request.POST and 'photo_id_image' in request.POST ) if ( submit_photo and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user) ): attempt = SoftwareSecurePhotoVerification(user=request.user) try: b64_face_image = request.POST['face_image'].split(",")[1] b64_photo_id_image = request.POST['photo_id_image'].split(",")[1] except IndexError: log.error(u"Invalid image data during photo verification.") context = { 'success': False, } return JsonResponse(context) attempt.upload_face_image(b64_face_image.decode('base64')) attempt.upload_photo_id_image(b64_photo_id_image.decode('base64')) attempt.mark_ready() attempt.save() course_id = request.POST['course_id'] course_id = CourseKey.from_string(course_id) donation_for_course = request.session.get('donation_for_course', {}) contribution = request.POST.get("contribution", donation_for_course.get(unicode(course_id), 0)) try: amount = decimal.Decimal(contribution).quantize(decimal.Decimal('.01'), rounding=decimal.ROUND_DOWN) except decimal.InvalidOperation: return HttpResponseBadRequest(_("Selected price is not valid number.")) current_mode = None paid_modes = CourseMode.paid_modes_for_course(course_id) # Check if there are more than 1 paid(mode with min_price>0 e.g verified/professional/no-id-professional) modes # for course exist then choose the first one if paid_modes: if len(paid_modes) > 1: log.warn(u"Multiple paid course modes found for course '%s' for create order request", course_id) current_mode = paid_modes[0] # Make sure this course has a paid mode if not current_mode: log.warn(u"Create order requested for course '%s' without a paid mode.", course_id) return HttpResponseBadRequest(_("This course doesn't support paid certificates")) if CourseMode.is_professional_mode(current_mode): amount = current_mode.min_price if amount < current_mode.min_price: return HttpResponseBadRequest(_("No selected price or selected price is below minimum.")) if current_mode.sku: return create_order_with_ecommerce_service(request.user, course_id, current_mode) # I know, we should check this is valid. All kinds of stuff missing here cart = Order.get_cart_for_user(request.user) cart.clear() 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") ) params = get_signed_purchase_params( cart, callback_url=callback_url, extra_data=[unicode(course_id), current_mode.slug] ) params['success'] = True return HttpResponse(json.dumps(params), content_type="text/json")