def _add_enterprise_data_to_enrollment_api_post(self, data, order): """ Augment enrollment api POST data with enterprise specific data. Checks the order to see if there was a discount applied and if that discount was associated with an EnterpriseCustomer. If so, enterprise specific data is added to the POST data and an EnterpriseCustomerUser model is created if one does not already exist. Arguments: data (dict): The POST data for the enrollment API. order (Order): The order. """ # Collect the EnterpriseCustomer UUID from the coupon, if any. enterprise_customer_uuid = None for discount in order.discounts.all(): try: enterprise_customer_uuid = discount.voucher.benefit.range.enterprise_customer except AttributeError: # The voucher did not have an enterprise customer associated with it. pass if enterprise_customer_uuid is not None: data['linked_enterprise_customer'] = str(enterprise_customer_uuid) break # If an EnterpriseCustomer UUID is associated with the coupon, create an EnterpriseCustomerUser # on the Enterprise service if one doesn't already exist. if enterprise_customer_uuid is not None: get_or_create_enterprise_customer_user( order.site, enterprise_customer_uuid, order.user.username )
def _create_enterprise_customer_user(self, order): """ Create the enterprise customer user if an EnterpriseCustomer UUID is associated in the order's discount voucher. """ enterprise_customer_uuid = None for discount in order.discounts.all(): if discount.voucher: enterprise_customer_uuid = get_enterprise_customer_uuid_from_voucher( discount.voucher) if enterprise_customer_uuid is not None: get_or_create_enterprise_customer_user( order.site, enterprise_customer_uuid, order.user.username) break
def _add_enterprise_data_to_enrollment_api_post(self, data, order): """ Augment enrollment api POST data with enterprise specific data. Checks the order to see if there was a discount applied and if that discount was associated with an EnterpriseCustomer. If so, enterprise specific data is added to the POST data and an EnterpriseCustomerUser model is created if one does not already exist. Arguments: data (dict): The POST data for the enrollment API. order (Order): The order. """ # Collect the EnterpriseCustomer UUID from the coupon, if any. enterprise_customer_uuid = None for discount in order.discounts.all(): if discount.voucher: logger.info("Getting enterprise_customer_uuid from discount voucher for order [%s]", order.number) enterprise_customer_uuid = get_enterprise_customer_uuid_from_voucher(discount.voucher) logger.info( "enterprise_customer_uuid on discount voucher for order [%s] is [%s]", order.number, enterprise_customer_uuid ) if enterprise_customer_uuid is not None: logger.info( "Adding linked_enterprise_customer to data with enterprise_customer_uuid [%s] for order [%s]", enterprise_customer_uuid, order.number ) data['linked_enterprise_customer'] = str(enterprise_customer_uuid) break # If an EnterpriseCustomer UUID is associated with the coupon, create an EnterpriseCustomerUser # on the Enterprise service if one doesn't already exist. if enterprise_customer_uuid is not None: logger.info( "Getting or creating enterprise_customer_user " "for site [%s], enterprise customer [%s], and username [%s], for order [%s]", order.site, enterprise_customer_uuid, order.user.username, order.number ) get_or_create_enterprise_customer_user( order.site, enterprise_customer_uuid, order.user.username ) logger.info( "Finished get_or_create enterpruise customer user for order [%s]", order.number )
def test_post_enterprise_customer_user(self, mock_helpers, expected_return): """ Verify that "get_enterprise_customer" returns an appropriate response from the "enterprise-customer" Enterprise service API endpoint. """ for mock in mock_helpers: getattr(self, mock)() response = get_or_create_enterprise_customer_user( self.site, TEST_ENTERPRISE_CUSTOMER_UUID, self.learner.username) self.assertDictContainsSubset(expected_return, response)
def fulfill_product(self, order, lines): """ Fulfills the purchase of a 'seat' by enrolling the associated student. Uses the order and the lines to determine which courses to enroll a student in, and with certain certificate types. May result in an error if the Enrollment API cannot be reached, or if there is additional business logic errors when trying to enroll the student. Args: order (Order): The Order associated with the lines to be fulfilled. The user associated with the order is presumed to be the student to enroll in a course. lines (List of Lines): Order Lines, associated with purchased products in an Order. These should only be "Seat" products. Returns: The original set of lines, with new statuses set based on the success or failure of fulfillment. """ logger.info("Attempting to fulfill 'Seat' product types for order [%s]", order.number) api_key = getattr(settings, 'EDX_API_KEY', None) if not api_key: logger.error( 'EDX_API_KEY must be set to use the EnrollmentFulfillmentModule' ) for line in lines: line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR) return order, lines for line in lines: try: mode = mode_for_seat(line.product) course_key = line.product.attr.course_key except AttributeError: logger.error("Supported Seat Product does not have required attributes, [certificate_type, course_key]") line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR) continue try: provider = line.product.attr.credit_provider except AttributeError: logger.debug("Seat [%d] has no credit_provider attribute. Defaulted to None.", line.product.id) provider = None data = { 'user': order.user.username, 'is_active': True, 'mode': mode, 'course_details': { 'course_id': course_key }, 'enrollment_attributes': [ { 'namespace': 'order', 'name': 'order_number', 'value': order.number } ] } if provider: data['enrollment_attributes'].append( { 'namespace': 'credit', 'name': 'provider_id', 'value': provider } ) try: # Collect the EnterpriseCustomer UUID from the coupon, if any. enterprise_customer_uuid = None for discount in order.discounts.all(): if discount.voucher: enterprise_customer_uuid = discount.voucher.benefit.range.enterprise_customer if enterprise_customer_uuid is not None: data['enterprise_course_consent'] = True break # If an EnterpriseCustomer UUID is associated with the coupon, create an EnterpriseCustomerUser # on the Enterprise service if one doesn't already exist. if enterprise_customer_uuid is not None: get_or_create_enterprise_customer_user( order.site, enterprise_customer_uuid, order.user.username ) # Post to the Enrollment API. The LMS will take care of posting a new EnterpriseCourseEnrollment to # the Enterprise service if the user+course has a corresponding EnterpriseCustomerUser. response = self._post_to_enrollment_api(data, user=order.user) if response.status_code == status.HTTP_200_OK: line.set_status(LINE.COMPLETE) audit_log( 'line_fulfilled', order_line_id=line.id, order_number=order.number, product_class=line.product.get_product_class().name, course_id=course_key, mode=mode, user_id=order.user.id, credit_provider=provider, ) else: try: data = response.json() reason = data.get('message') except Exception: # pylint: disable=broad-except reason = '(No detail provided.)' logger.error( "Fulfillment of line [%d] on order [%s] failed with status code [%d]: %s", line.id, order.number, response.status_code, reason ) line.set_status(LINE.FULFILLMENT_SERVER_ERROR) except ConnectionError: logger.error( "Unable to fulfill line [%d] of order [%s] due to a network problem", line.id, order.number ) line.set_status(LINE.FULFILLMENT_NETWORK_ERROR) except Timeout: logger.error( "Unable to fulfill line [%d] of order [%s] due to a request time out", line.id, order.number ) line.set_status(LINE.FULFILLMENT_TIMEOUT_ERROR) logger.info("Finished fulfilling 'Seat' product types for order [%s]", order.number) return order, lines
def is_satisfied(self, offer, basket): # pylint: disable=unused-argument """ Determines if a user is eligible for an enterprise customer offer based on their association with the enterprise customer. It also filter out the offer if the `enterprise_customer_catalog_uuid` value set on the offer condition does not match with the basket catalog value when explicitly provided by the enterprise learner. Note: Currently there is no mechanism to prioritize or apply multiple offers that may apply as opposed to disqualifying offers if the catalog doesn't explicitly match. Arguments: basket (Basket): Contains information about order line items, the current site, and the user attempting to make the purchase. Returns: bool """ if not basket.owner: # An anonymous user is never linked to any EnterpriseCustomer. return False enterprise_in_condition = str(self.enterprise_customer_uuid) enterprise_catalog = str(self.enterprise_customer_catalog_uuid) if self.enterprise_customer_catalog_uuid \ else None enterprise_name_in_condition = str(self.enterprise_customer_name) username = basket.owner.username course_run_ids = [] for line in basket.all_lines(): course = line.product.course if not course: # Basket contains products not related to a course_run. # Only log for non-site offers to avoid noise. if offer.offer_type != ConditionalOffer.SITE: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'the Basket contains a product not related to a course_run. ' 'User: %s, Offer: %s, Product: %s, Enterprise: %s, Catalog: %s', username, offer.id, line.product.id, enterprise_in_condition, enterprise_catalog) return False course_run_ids.append(course.id) courses_in_basket = ','.join(course_run_ids) user_enterprise = get_enterprise_id_for_user(basket.site, basket.owner) if user_enterprise and enterprise_in_condition != user_enterprise: # Learner is not linked to the EnterpriseCustomer associated with this condition. if offer.offer_type == ConditionalOffer.VOUCHER: logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because Learner\'s ' 'enterprise (%s) does not match this conditions\'s enterprise (%s). ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', user_enterprise, enterprise_in_condition, username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket) logger.info( '[Code Redemption Issue] Linking learner with the enterprise in Condition. ' 'User [%s], Enterprise [%s]', username, enterprise_in_condition) get_or_create_enterprise_customer_user( basket.site, enterprise_in_condition, username, False) msg = _( 'This coupon has been made available through {new_enterprise}. ' 'To redeem this coupon, you must first logout. When you log back in, ' 'please select {new_enterprise} as your enterprise ' 'and try again.').format( new_enterprise=enterprise_name_in_condition) messages.warning( crum.get_current_request(), msg, ) return False # Verify that the current conditional offer is related to the provided # enterprise catalog, this will also filter out offers which don't # have `enterprise_customer_catalog_uuid` value set on the condition. catalog = self._get_enterprise_catalog_uuid_from_basket(basket) if catalog: if offer.condition.enterprise_customer_catalog_uuid != catalog: logger.warning( 'Unable to apply enterprise offer %s because ' 'Enterprise catalog id on the basket (%s) ' 'does not match the catalog for this condition (%s).', offer.id, catalog, offer.condition.enterprise_customer_catalog_uuid) return False try: catalog_contains_course = catalog_contains_course_runs( basket.site, course_run_ids, enterprise_in_condition, enterprise_customer_catalog_uuid=enterprise_catalog) except (ReqConnectionError, KeyError, SlumberHttpBaseException, Timeout) as exc: logger.exception( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'we failed to check if course_runs exist in the catalog. ' 'User: %s, Offer: %s, Message: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, exc, enterprise_in_condition, enterprise_catalog, courses_in_basket) return False if not catalog_contains_course: # Basket contains course runs that do not exist in the EnterpriseCustomerCatalogs # associated with the EnterpriseCustomer. logger.warning( '[Code Redemption Failure] Unable to apply enterprise offer because ' 'Enterprise catalog does not contain the course(s) in this basket. ' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket) return False if not is_offer_max_discount_available(basket, offer): logger.warning( '[Enterprise Offer Failure] Unable to apply enterprise offer because bookings limit is consumed.' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s, BookingsLimit: %s, TotalDiscount: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket, offer.max_discount, offer.total_discount, ) return False if not is_offer_max_user_discount_available(basket, offer): logger.warning( '[Enterprise Offer Failure] Unable to apply enterprise offer because user bookings limit is consumed.' 'User: %s, Offer: %s, Enterprise: %s, Catalog: %s, Courses: %s, UserBookingsLimit: %s', username, offer.id, enterprise_in_condition, enterprise_catalog, courses_in_basket, offer.max_user_discount) return False return True