def _enable_payment_providers(self): for path in settings.PAYMENT_PROCESSORS: processor = get_processor_class(path) Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + processor.NAME, defaults={'active': True} )
def _assert_success_checkout_page(self): """ Verify that checkout page load successfully, and has necessary context. """ # Create the credit seat self.course.create_or_update_seat('credit', True, self.price, self.partner, self.provider, credit_hours=self.credit_hours) self._enable_payment_providers() response = self.client.get(self.path) self.assertEqual(response.status_code, 200) self.assertDictContainsSubset({'course': self.course}, response.context) # Verify that the payment processors are returned self.assertEqual( sorted(response.context['payment_processors'].keys()), sorted([ get_processor_class(path).NAME.lower() for path in settings.PAYMENT_PROCESSORS ])) self.assertContains( response, 'Congratulations! You are eligible to purchase academic course credit for this course.' ) self.assertContains(response, self.provider_data[0]['fulfillment_instructions'])
def test_disabled_providers(self, disabled_processors, expected_context): """ Verify that payment processors can be disabled with their Waffle switches, and that an error is shown if none are available. """ self._enable_payment_providers() for path in disabled_processors: processor_class = get_processor_class(path) switch, __ = Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + processor_class.NAME) switch.active = False switch.save() self.course.create_or_update_seat('credit', True, self.price, self.partner, self.provider, credit_hours=self.credit_hours) self._mock_eligibility_api(body=self.eligibilities) self._mock_providers_api(body=self.provider_data) response = self.client.get(self.path) self.assertEqual(response.status_code, 200) self.assertDictContainsSubset(expected_context, response.context)
def get_queryset(self): """Fetch the list of payment processor classes based on Django settings.""" processors = (get_processor_class(path) for path in settings.PAYMENT_PROCESSORS) return [ processor for processor in processors if processor.is_enabled() ]
def _assert_success_checkout_page(self): """ Verify that checkout page load successfully, and has necessary context. """ # Create the credit seat self.course.create_or_update_seat( 'credit', True, self.price, self.partner, self.provider, credit_hours=self.credit_hours ) self._enable_payment_providers() response = self.client.get(self.path) self.assertEqual(response.status_code, 200) self.assertDictContainsSubset({'course': self.course}, response.context) # Verify that the payment processors are returned self.assertEqual( sorted(response.context['payment_processors'].keys()), sorted([get_processor_class(path).NAME.lower() for path in settings.PAYMENT_PROCESSORS]) ) self.assertContains( response, 'Congratulations! You are eligible to purchase academic course credit for this course.' ) self.assertContains( response, self.provider_data[0]['fulfillment_instructions'] )
def get_context_data(self, **kwargs): context = super(Checkout, self).get_context_data(**kwargs) try: course = Course.objects.get(id=kwargs.get('course_id')) except Course.DoesNotExist: raise Http404 # Make button text for each processor which will be shown to user. processors_dict = OrderedDict() for path in settings.PAYMENT_PROCESSORS: processor = get_processor_class(path).NAME.lower() if processor == 'cybersource': processors_dict[processor] = 'Checkout' elif processor == 'paypal': processors_dict[processor] = 'Checkout with PayPal' else: processors_dict[processor] = 'Checkout with {}'.format(processor) credit_seats = [ seat for seat in course.seat_products if getattr(seat.attr, 'certificate_type', '') == self.CREDIT_MODE ] provider_ids = None if credit_seats: provider_ids = ",".join([seat.attr.credit_provider for seat in credit_seats if seat.attr.credit_provider]) context.update({ 'request': self.request, 'user': self.request.user, 'course': course, 'payment_processors': processors_dict, 'credit_seats': credit_seats, 'lms_url_root': settings.LMS_URL_ROOT, 'provider_ids': provider_ids, 'analytics_data': json.dumps({ 'course': { 'courseId': course.id }, 'tracking': { 'segmentApplicationId': settings.SEGMENT_KEY }, 'user': { 'username': self.request.user.get_username(), 'name': self.request.user.get_full_name(), 'email': self.request.user.email } }) }) return context
def post(self, request): """ Handle the response we've been given from the processor. """ payment_processor = get_processor_class(settings.PAYMENT_PROCESSORS[0]) # check the data we get params = request.POST.dict() result = payment_processor().handle_processor_response(params) if result[PC.SUCCESS]: # get the order order = Order.objects.get(number=result[PC.ORDER_NUMBER]) # register the money in Oscar self._register_payment(order, payment_processor.NAME) # fulfill the order self._fulfill_order(order) # It doesn't matter how we respond to the payment processor if the # payment failed. return HttpResponse()
def test_get_checkout_page_with_credit_seats(self): """ Verify page loads and has the necessary context. """ response = self.client.get(self.path) self.assertEqual(response.status_code, 200) expected = { 'course': self.course, 'credit_seats': [self.seat], } self.assertDictContainsSubset(expected, response.context) # Verify the payment processors are returned self.assertEqual(sorted(response.context['payment_processors'].keys()), sorted([get_processor_class(path).NAME.lower() for path in settings.PAYMENT_PROCESSORS])) self.assertContains( response, 'You are purchasing {} credit hours for'.format(self.credit_hours) )
def get_payment_processors(self): """ Returns payment processor classes enabled for the corresponding Site Returns: list[BasePaymentProcessor]: Returns payment processor classes enabled for the corresponding Site """ all_processors = [get_processor_class(path) for path in settings.PAYMENT_PROCESSORS] all_processor_names = {processor.NAME for processor in all_processors} missing_processor_configurations = self.payment_processors_set - all_processor_names if missing_processor_configurations: processor_config_repr = ", ".join(missing_processor_configurations) log.warning( 'Unknown payment processors [%s] are configured for site %s', processor_config_repr, self.site.id ) return [ processor for processor in all_processors if processor.NAME in self.payment_processors_set and processor.is_enabled() ]
def test_disabled_providers(self, disabled_processors, expected_context): """ Verify that payment processors can be disabled with their Waffle switches, and that an error is shown if none are available. """ for path in disabled_processors: processor_class = get_processor_class(path) switch, __ = Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + processor_class.NAME ) switch.active = False switch.save() self.course.create_or_update_seat( 'credit', True, self.price, self.partner, self.provider, credit_hours=self.credit_hours ) self._mock_eligibility_api(body=self.eligibilities) self._mock_providers_api(body=self.provider_data) response = self.client.get(self.path) self.assertEqual(response.status_code, 200) self.assertDictContainsSubset(expected_context, response.context)
def get_payment_processors(self): """ Retrieve the list of active payment processors. """ # TODO Retrieve this information from SiteConfiguration processors = (get_processor_class(path) for path in settings.PAYMENT_PROCESSORS) return [processor for processor in processors if processor.is_enabled()]
def get_queryset(self): """Fetch the list of payment processor classes based on Django settings.""" processors = (get_processor_class(path) for path in settings.PAYMENT_PROCESSORS) return [processor for processor in processors if processor.is_enabled()]
def _enable_payment_providers(self): for path in settings.PAYMENT_PROCESSORS: processor = get_processor_class(path) Switch.objects.get_or_create( name=settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + processor.NAME, defaults={'active': True})
def _all_payment_processors(self): """ Returns all processor classes declared in settings. """ all_processors = [get_processor_class(path) for path in settings.PAYMENT_PROCESSORS] return all_processors
def get_context_data(self, **kwargs): context = super(Checkout, self).get_context_data(**kwargs) course = get_object_or_404(Course, id=kwargs.get('course_id')) deadline = self._check_credit_eligibility(self.request.user, kwargs.get('course_id')) if not deadline: return { 'error': _(u'An error has occurred. We could not confirm that you are eligible for course credit. ' u'Try the transaction again.') } partner = get_partner_for_site(self.request) # Audit seats do not have a `certificate_type` attribute, so # we use getattr to avoid an exception. credit_seats = [ seat for seat in course.seat_products if getattr(seat.attr, 'certificate_type', None) == self.CREDIT_MODE and seat.stockrecords.filter(partner=partner).exists() ] if not credit_seats: return { 'error': _(u'No credit seat is available for this course.') } providers = self._get_providers_detail(credit_seats) if not providers: return { 'error': _(u'An error has occurred. We could not confirm that the institution you selected offers this ' u'course credit. Try the transaction again.') } # Make button text for each processor which will be shown to user. processors_dict = OrderedDict() for path in settings.PAYMENT_PROCESSORS: processor_class = get_processor_class(path) if not processor_class.is_enabled(): continue processor = processor_class.NAME.lower() if processor == 'cybersource': processors_dict[processor] = 'Checkout' elif processor == 'paypal': processors_dict[processor] = 'Checkout with PayPal' else: processors_dict[processor] = 'Checkout with {}'.format(processor) if len(processors_dict) == 0: context.update({ 'error': _( u'All payment options are currently unavailable. Try the transaction again in a few minutes.' ) }) context.update({ 'course': course, 'payment_processors': processors_dict, 'deadline': deadline, 'providers': providers, 'analytics_data': json.dumps({ 'course': { 'courseId': course.id }, 'tracking': { 'segmentApplicationId': settings.SEGMENT_KEY }, 'user': { 'username': self.request.user.get_username(), 'name': self.request.user.get_full_name(), 'email': self.request.user.email } }) }) return context
def get_queryset(self): """Fetch the list of payment processor classes based on Django settings.""" return [get_processor_class(path) for path in settings.PAYMENT_PROCESSORS]
def get_context_data(self, **kwargs): context = super(Checkout, self).get_context_data(**kwargs) course = get_object_or_404(Course, id=kwargs.get('course_id')) context['course'] = course deadline = self._check_credit_eligibility(self.request.user, kwargs.get('course_id')) if not deadline: context.update({ 'error': _('An error has occurred. We could not confirm that you are eligible for course credit. ' 'Try the transaction again.') }) return context partner = get_partner_for_site(self.request) strategy = self.request.strategy # Audit seats do not have a `certificate_type` attribute, so # we use getattr to avoid an exception. credit_seats = [] for seat in course.seat_products: if getattr(seat.attr, 'certificate_type', None) != self.CREDIT_MODE: continue purchase_info = strategy.fetch_for_product(seat) if purchase_info.availability.is_available_to_buy and seat.stockrecords.filter(partner=partner).exists(): credit_seats.append(seat) if not credit_seats: msg = _( 'Credit is not currently available for "{course_name}". If you are currently enrolled in the ' 'course, please try again after all grading is complete. If you need additional assistance, ' 'please contact the {site_name} Support Team.' ).format( course_name=course.name, site_name=self.request.site.name ) context.update({'error': msg}) return context providers = self._get_providers_detail(credit_seats) if not providers: context.update({ 'error': _('An error has occurred. We could not confirm that the institution you selected offers this ' 'course credit. Try the transaction again.') }) return context # Make button text for each processor which will be shown to user. processors_dict = OrderedDict() for path in settings.PAYMENT_PROCESSORS: processor_class = get_processor_class(path) if not processor_class.is_enabled(): continue processor = processor_class.NAME.lower() if processor == 'cybersource': processors_dict[processor] = 'Checkout' elif processor == 'paypal': processors_dict[processor] = 'Checkout with PayPal' else: processors_dict[processor] = 'Checkout with {}'.format(processor) if len(processors_dict) == 0: context.update({ 'error': _( 'All payment options are currently unavailable. Try the transaction again in a few minutes.' ) }) context.update({ 'course': course, 'payment_processors': processors_dict, 'deadline': deadline, 'providers': providers, 'analytics_data': prepare_analytics_data( self.request.user, self.request.site.siteconfiguration.segment_key, course.id ), }) return context
def get_context_data(self, **kwargs): context = super(Checkout, self).get_context_data(**kwargs) course = get_object_or_404(Course, id=kwargs.get('course_id')) context['course'] = course deadline = self._check_credit_eligibility(self.request.user, kwargs.get('course_id')) if not deadline: context.update({ 'error': _('An error has occurred. We could not confirm that you are eligible for course credit. ' 'Try the transaction again.') }) return context partner = get_partner_for_site(self.request) strategy = self.request.strategy # Audit seats do not have a `certificate_type` attribute, so # we use getattr to avoid an exception. credit_seats = [] for seat in course.seat_products: if getattr(seat.attr, 'certificate_type', None) != self.CREDIT_MODE: continue purchase_info = strategy.fetch_for_product(seat) if purchase_info.availability.is_available_to_buy and seat.stockrecords.filter( partner=partner).exists(): credit_seats.append(seat) if not credit_seats: msg = _( 'Credit is not currently available for "{course_name}". If you are currently enrolled in the ' 'course, please try again after all grading is complete. If you need additional assistance, ' 'please contact the {site_name} Support Team.').format( course_name=course.name, site_name=self.request.site.name) context.update({'error': msg}) return context providers = self._get_providers_detail(credit_seats) if not providers: context.update({ 'error': _('An error has occurred. We could not confirm that the institution you selected offers this ' 'course credit. Try the transaction again.') }) return context # Make button text for each processor which will be shown to user. processors_dict = OrderedDict() for path in settings.PAYMENT_PROCESSORS: processor_class = get_processor_class(path) if not processor_class.is_enabled(): continue processor = processor_class.NAME.lower() if processor == 'cybersource': processors_dict[processor] = 'Checkout' elif processor == 'paypal': processors_dict[processor] = 'Checkout with PayPal' else: processors_dict[processor] = 'Checkout with {}'.format( processor) if len(processors_dict) == 0: context.update({ 'error': _('All payment options are currently unavailable. Try the transaction again in a few minutes.' ) }) context.update({ 'course': course, 'payment_processors': processors_dict, 'deadline': deadline, 'providers': providers, 'analytics_data': prepare_analytics_data( self.request.user, self.request.site.siteconfiguration.segment_key, course.id), }) return context
def test_get_processor_class(self): """ Verify that the method retrieves the correct class. """ actual = helpers.get_processor_class( 'ecommerce.extensions.payment.tests.processors.DummyProcessor') self.assertIs(actual, DummyProcessor)
def create(self, request, *args, **kwargs): """Add one product to a basket, then prepare an order. Protected by JWT authentication. Consuming services (e.g., the LMS) must authenticate themselves by passing a JWT in the Authorization HTTP header, prepended with the string 'JWT'. The JWT payload should contain user details. At a minimum, these details must include a username; providing an email is recommended. Expects a SKU to be provided in the POST data, which is then used to populate the user's basket with the corresponding product, freeze that basket, and prepare an order using that basket. If the order total is zero (i.e., the ordered product was free), an attempt to fulfill the order is made. Arguments: request (HttpRequest) Returns: HTTP_200_OK if the order was created successfully, with order data in JSON format HTTP_400_BAD_REQUEST if the client has provided invalid data or has attempted to add an unavailable product to their basket, with reason for the failure in JSON format HTTP_401_UNAUTHORIZED if an unauthenticated request is denied permission to access the endpoint HTTP_429_TOO_MANY_REQUESTS if the client has made requests at a rate exceeding that allowed by the OrdersThrottle Example: Create an order for the user with username 'Saul' as follows. (Successful fulfillment requires that a user with username 'Saul' exists on the LMS, and that EDX_API_KEY be configured on both Oscar and the LMS.) >>> url = 'http://*****:*****@bettercallsaul.com'}, 'insecure-secret-key') >>> headers = { 'content-type': 'application/json', 'Authorization': 'JWT ' + token } >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.status_code 200 >>> response.content '{ "currency": "USD", "date_placed": "2015-02-27T18:42:34.017218Z", "lines": [ { "description": "Seat in DemoX Course with Honor Certificate", "status": "Complete", "title": "Seat in DemoX Course with Honor Certificate", "unit_price_excl_tax": 0.0 } ], "number": "OSCR-100021", "status": "Complete", "total_excl_tax": 0.0 }' """ sku = request.data.get('sku') if sku: try: product = data.get_product(sku) except exceptions.ProductNotFoundError as error: return self._report_bad_request(error.message, exceptions.PRODUCT_NOT_FOUND_USER_MESSAGE) else: return self._report_bad_request( exceptions.SKU_NOT_FOUND_DEVELOPER_MESSAGE, exceptions.SKU_NOT_FOUND_USER_MESSAGE ) basket = data.get_basket(request.user) availability = basket.strategy.fetch_for_product(product).availability # If an exception is raised before order creation but after basket creation, # an empty basket for the user will be left in the system. However, if this # user attempts to order again, the `get_basket` utility will merge all old # baskets with a new one, returning a fresh basket. if not availability.is_available_to_buy: return self._report_bad_request( exceptions.PRODUCT_UNAVAILABLE_DEVELOPER_MESSAGE.format( sku=sku, availability=availability.message ), exceptions.PRODUCT_UNAVAILABLE_USER_MESSAGE ) payment_processor = get_processor_class(settings.PAYMENT_PROCESSORS[0]) order = self._prepare_order(basket, product, sku, payment_processor) if order.total_excl_tax == self.FREE: logger.info( u"Attempting to immediately fulfill order [%s] totaling [%.2f %s]", order.number, order.total_excl_tax, order.currency, ) order = self.fulfill_order(order) order_data = self._assemble_order_data(order, payment_processor) return Response(order_data, status=status.HTTP_200_OK)
def test_get_processor_class(self): """ Verify that the method retrieves the correct class. """ actual = helpers.get_processor_class('ecommerce.extensions.payment.tests.processors.DummyProcessor') self.assertIs(actual, DummyProcessor)
def _all_payment_processors(self): """ Returns all processor classes declared in settings. """ all_processors = [ get_processor_class(path) for path in settings.PAYMENT_PROCESSORS ] return all_processors