Esempio n. 1
0
    def revoke_line(self, line):
        """
        Revokes the purchase of a 'Journal'.  This will attempt to revoke the access that was granted when this order
        was fulfilled.

        Args:
            line (Line): A line has data about the purchase.  Access will be revoked for the 'journalaccess' record
                associated with this line order.

        Returns:
            Returns True if journal access was successfully revoked.
        """
        try:
            logger.info('Attempting to revoke fulfillment of Line [%d]...',
                        line.id)

            journal_uuid = line.product.attr.UUID

            # TODO: WL-1680: All calls from ecommerce to other services should be async
            revoke_journal_access(
                site_configuration=line.order.site.siteconfiguration,
                order_number=line.order.number)

            audit_log('line_revoked',
                      order_line_id=line.id,
                      order_number=line.order.number,
                      product_class=line.product.get_product_class().name,
                      user_id=line.order.user.id,
                      journal_uuid=journal_uuid)
            return True
        except Exception:  # pylint: disable=broad-except
            logger.exception('Failed to revoke fulfillment of Line [%d].',
                             line.id)

        return False
Esempio n. 2
0
    def fulfill_product(self, order, lines, email_opt_in=False):
        """
        Fulfills the purchase of a 'Journal'
        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 grant access to the journal
            lines (List of Lines): Order Lines, associated with purchased products in the Order.  These should only be
                'Journal' products.
            email_opt_in (bool): Whether the user should be opted in to emails
                as part of the fulfillment. Defaults to False.
        Returns:
            The original set of lines, with new statuses set based on the success or failure of fulfillment.
        """
        logger.info(
            'Attempting to fulfill "Journal" product types for order [%s]',
            order.number)

        for line in lines:
            try:
                journal_uuid = line.product.attr.UUID
            except AttributeError:
                logger.error(
                    'Journal fulfillment failed, journal does not have uuid. Order [%s]',
                    order.number)
                line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR)
                continue

            try:
                # TODO: WL-1680: All calls from ecommerce to other services should be async
                post_journal_access(
                    site_configuration=order.site.siteconfiguration,
                    order_number=order.number,
                    username=order.user.username,
                    journal_uuid=journal_uuid)

                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,
                    user_id=order.user.id,
                    journal_uuid=journal_uuid,
                )
                logger.info(
                    'Successfully fulfilled Journal purchase for line [%d] of order [%s]',
                    line.id, order.number)
            except (Timeout, ConnectionError) as neterr:
                logger.error(
                    'Error fulfilling Journal purchase, line [%d] of order [%s] due to a network problem, err=[%s]',
                    line.id, order.number, neterr)
                line.set_status(LINE.FULFILLMENT_NETWORK_ERROR)
            except (Exception, HttpClientError) as err:  # pylint: disable=broad-except
                logger.error(
                    'Error fulfilling Journal purchase, line [%d] of order [%s] due to an error, err=[%s]',
                    line.id, order.number, err)
                line.set_status(LINE.FULFILLMENT_SERVER_ERROR)

        return order, lines
Esempio n. 3
0
 def record_payment(self, basket, handled_processor_response):
     self.emit_checkout_step_events(basket, handled_processor_response,
                                    self.payment_processor)
     track_segment_event(basket.site, basket.owner, 'Payment Info Entered',
                         {'checkout_id': basket.order_number})
     source_type, __ = SourceType.objects.get_or_create(
         name=self.payment_processor.NAME)
     total = handled_processor_response.total
     reference = handled_processor_response.transaction_id
     source = Source(source_type=source_type,
                     currency=handled_processor_response.currency,
                     amount_allocated=total,
                     amount_debited=total,
                     reference=reference,
                     label=handled_processor_response.card_number,
                     card_type=handled_processor_response.card_type)
     event_type, __ = PaymentEventType.objects.get_or_create(
         name=PaymentEventTypeName.PAID)
     payment_event = PaymentEvent(
         event_type=event_type,
         amount=total,
         reference=reference,
         processor_name=self.payment_processor.NAME)
     self.add_payment_source(source)
     self.add_payment_event(payment_event)
     audit_log('payment_received',
               amount=payment_event.amount,
               basket_id=basket.id,
               currency=source.currency,
               processor_name=payment_event.processor_name,
               reference=payment_event.reference,
               user_id=basket.owner.id)
Esempio n. 4
0
    def _issue_credit(self):
        """Issue a credit/refund to the purchaser via the payment processor used for the original order."""
        try:
            # NOTE: Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)(
                self.order.site)
            amount = self.total_credit_excl_tax

            refund_reference_number = processor.issue_credit(
                self.order.number, self.order.basket, source.reference, amount,
                self.currency)
            source.refund(amount, reference=refund_reference_number)
            event_type, __ = PaymentEventType.objects.get_or_create(
                name=PaymentEventTypeName.REFUNDED)
            PaymentEvent.objects.create(event_type=event_type,
                                        order=self.order,
                                        amount=amount,
                                        reference=refund_reference_number,
                                        processor_name=processor.NAME)

            audit_log('credit_issued',
                      amount=amount,
                      currency=self.currency,
                      processor_name=processor.NAME,
                      refund_id=self.id,
                      user_id=self.user.id)
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Esempio n. 5
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # NOTE: Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)(self.order.site)
            amount = self.total_credit_excl_tax

            refund_reference_number = processor.issue_credit(self.order, source.reference, amount, self.currency)
            source.refund(amount, reference=refund_reference_number)
            event_type, __ = PaymentEventType.objects.get_or_create(name=PaymentEventTypeName.REFUNDED)
            PaymentEvent.objects.create(
                event_type=event_type,
                order=self.order,
                amount=amount,
                reference=refund_reference_number,
                processor_name=processor.NAME
            )

            audit_log(
                'credit_issued',
                amount=amount,
                currency=self.currency,
                processor_name=processor.NAME,
                refund_id=self.id,
                user_id=self.user.id
            )
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Esempio n. 6
0
    def revoke_line(self, line):
        try:
            logger.info('Attempting to revoke fulfillment of Line [%d]...', line.id)

            UUID = line.product.attr.UUID
            entitlement_option = Option.objects.get(code='course_entitlement')
            course_entitlement_uuid = line.attributes.get(option=entitlement_option).value

            entitlement_api_client = EdxRestApiClient(
                get_lms_entitlement_api_url(),
                jwt=line.order.site.siteconfiguration.access_token
            )

            # DELETE to the Entitlement API.
            entitlement_api_client.entitlements(course_entitlement_uuid).delete()

            audit_log(
                'line_revoked',
                order_line_id=line.id,
                order_number=line.order.number,
                product_class=line.product.get_product_class().name,
                UUID=UUID,
                certificate_type=getattr(line.product.attr, 'certificate_type', ''),
                user_id=line.order.user.id
            )

            return True
        except Exception:  # pylint: disable=broad-except
            logger.exception('Failed to revoke fulfillment of Line [%d].', line.id)

        return False
Esempio n. 7
0
    def fulfill_product(self, order, lines):
        """
        Fulfills the purchase of a 'Journal'
        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 grant access to the journal
            lines (List of Lines): Order Lines, associated with purchased products in the Order.  These should only be
                'Journal' products.
        Returns:
            The original set of lines, with new statuses set based on the success or failure of fulfillment.
        """
        logger.info(
            'Attempting to fulfill "Journal" product types for order [%s]',
            order.number)

        for line in lines:
            try:
                journal_uuid = line.product.attr.UUID
            except AttributeError:
                logger.error(
                    'Journal Product does not have required attributes, [uuid]'
                )
                line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR)
                continue

            try:
                # TODO: WL-1680: All calls from ecommerce to other services should be async
                post_journal_access(
                    site_configuration=order.site.siteconfiguration,
                    order_number=order.number,
                    username=order.user.username,
                    journal_uuid=journal_uuid)

                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,
                    user_id=order.user.id,
                    journal_uuid=journal_uuid,
                )
            except (Timeout, ConnectionError):
                logger.exception(
                    '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 Exception:  # pylint: disable=broad-except
                logger.exception('Unable to fulfill line [%d] of order [%s]',
                                 line.id, order.number)
                line.set_status(LINE.FULFILLMENT_SERVER_ERROR)

        logger.info(
            'Finished fulfilling "Journal" product types for order [%s]',
            order.number)
        return order, lines
Esempio n. 8
0
    def create_with_lines(cls, order, lines):
        """Given an order and order lines, creates a Refund with corresponding RefundLines.

        Only creates RefundLines for unrefunded order lines. Refunds corresponding to a total
        credit of $0 are approved upon creation.

        Arguments:
            order (order.Order): The order to which the newly-created refund corresponds.
            lines (list of order.Line): Order lines to be refunded.

        Returns:
            None: If no unrefunded order lines have been provided.
            Refund: With RefundLines corresponding to each given unrefunded order line.
        """
        unrefunded_lines = [
            line for line in lines if not line.refund_lines.exclude(
                status=REFUND_LINE.DENIED).exists()
        ]

        if not unrefunded_lines:
            return None

        status = getattr(settings, 'OSCAR_INITIAL_REFUND_STATUS', REFUND.OPEN)
        total_credit_excl_tax = sum(
            [line.line_price_excl_tax for line in unrefunded_lines])
        refund = cls.objects.create(
            order=order,
            user=order.user,
            status=status,
            total_credit_excl_tax=total_credit_excl_tax)

        audit_log('refund_created',
                  amount=total_credit_excl_tax,
                  currency=refund.currency,
                  order_number=order.number,
                  refund_id=refund.id,
                  user_id=refund.user.id)

        status = getattr(settings, 'OSCAR_INITIAL_REFUND_LINE_STATUS',
                         REFUND_LINE.OPEN)
        for line in unrefunded_lines:
            RefundLine.objects.create(
                refund=refund,
                order_line=line,
                line_credit_excl_tax=line.line_price_excl_tax,
                quantity=line.quantity,
                status=status)

        if total_credit_excl_tax == 0:
            refund.approve()

        return refund
Esempio n. 9
0
    def handle_successful_order(self, order):
        """Send a signal so that receivers can perform relevant tasks (e.g., fulfill the order)."""
        audit_log(
            'order_placed',
            amount=order.total_excl_tax,
            basket_id=order.basket.id,
            currency=order.currency,
            order_number=order.number,
            user_id=order.user.id
        )

        post_checkout.send_robust(sender=self, order=order)

        return order
Esempio n. 10
0
    def revoke_line(self, line):
        try:
            logger.info('Attempting to revoke fulfillment of Line [%d]...',
                        line.id)

            mode = mode_for_product(line.product)
            course_key = line.product.attr.course_key
            data = {
                'user': line.order.user.username,
                'is_active': False,
                'mode': mode,
                'course_details': {
                    'course_id': course_key,
                },
            }

            response = self._post_to_enrollment_api(data,
                                                    user=line.order.user,
                                                    usage='revoke enrollment')

            if response.status_code == status.HTTP_200_OK:
                audit_log('line_revoked',
                          order_line_id=line.id,
                          order_number=line.order.number,
                          product_class=line.product.get_product_class().name,
                          course_id=course_key,
                          certificate_type=getattr(line.product.attr,
                                                   'certificate_type', ''),
                          user_id=line.order.user.id)

                return True
            else:
                # check if the error / message are something we can recover from.
                data = response.json()
                detail = data.get('message', '(No details provided.)')
                if response.status_code == 400 and "Enrollment mode mismatch" in detail:
                    # The user is currently enrolled in different mode than the one
                    # we are refunding an order for.  Don't revoke that enrollment.
                    logger.info('Skipping revocation for line [%d]: %s',
                                line.id, detail)
                    return True
                else:
                    logger.error(
                        'Failed to revoke fulfillment of Line [%d]: %s',
                        line.id, detail)
        except Exception:  # pylint: disable=broad-except
            logger.exception('Failed to revoke fulfillment of Line [%d].',
                             line.id)

        return False
Esempio n. 11
0
    def create_with_lines(cls, order, lines):
        """Given an order and order lines, creates a Refund with corresponding RefundLines.

        Only creates RefundLines for unrefunded order lines. Refunds corresponding to a total
        credit of $0 are approved upon creation.

        Arguments:
            order (order.Order): The order to which the newly-created refund corresponds.
            lines (list of order.Line): Order lines to be refunded.

        Returns:
            None: If no unrefunded order lines have been provided.
            Refund: With RefundLines corresponding to each given unrefunded order line.
        """
        unrefunded_lines = [line for line in lines if not line.refund_lines.exclude(status=REFUND_LINE.DENIED).exists()]

        if unrefunded_lines:
            status = getattr(settings, 'OSCAR_INITIAL_REFUND_STATUS', REFUND.OPEN)
            total_credit_excl_tax = sum([line.line_price_excl_tax for line in unrefunded_lines])
            refund = cls.objects.create(
                order=order,
                user=order.user,
                status=status,
                total_credit_excl_tax=total_credit_excl_tax
            )

            audit_log(
                'refund_created',
                amount=total_credit_excl_tax,
                currency=refund.currency,
                order_number=order.number,
                refund_id=refund.id,
                user_id=refund.user.id
            )

            status = getattr(settings, 'OSCAR_INITIAL_REFUND_LINE_STATUS', REFUND_LINE.OPEN)
            for line in unrefunded_lines:
                RefundLine.objects.create(
                    refund=refund,
                    order_line=line,
                    line_credit_excl_tax=line.line_price_excl_tax,
                    quantity=line.quantity,
                    status=status
                )

            if total_credit_excl_tax == 0:
                refund.approve(notify_purchaser=False)

            return refund
Esempio n. 12
0
    def _checkout(self, basket, payment_processor, request=None):
        """Perform checkout operations for the given basket.

        If the contents of the basket are free, places an order immediately. Otherwise,
        performs any operations necessary to prepare for payment.

        To prevent stale items from ending up in a basket at checkout, baskets should
        always be frozen during checkout. Baskets with a status of 'Frozen' or 'Submitted'
        are not retrieved when fetching a basket for the user.

        Arguments:
            basket (Basket): The basket on which to perform checkout operations.
            payment_processor (class): An instance of the payment processor class corresponding
                to the payment processor the user will visit to pay for the items in their basket.

        Returns:
            dict: Response data.
        """
        basket.freeze()

        audit_log(
            'basket_frozen',
            amount=basket.total_excl_tax,
            basket_id=basket.id,
            currency=basket.currency,
            user_id=basket.owner.id
        )

        response_data = self._generate_basic_response(basket)

        if basket.total_excl_tax == 0:
            order = self.place_free_order(basket, request)

            # Note: Our order serializer could be used here, but in an effort to pare down the information
            # returned by this endpoint, simply returning the order number will suffice for now.
            response_data['order'] = {'number': order.number}
        else:
            parameters = payment_processor.get_transaction_parameters(basket, request=self.request)
            payment_page_url = parameters.pop('payment_page_url')

            response_data['payment_data'] = {
                'payment_processor_name': payment_processor.NAME,
                'payment_form_data': parameters,
                'payment_page_url': payment_page_url,
            }

        return response_data
Esempio n. 13
0
    def _checkout(self, basket, payment_processor):
        """Perform checkout operations for the given basket.

        If the contents of the basket are free, places an order immediately. Otherwise,
        performs any operations necessary to prepare for payment.

        To prevent stale items from ending up in a basket at checkout, baskets should
        always be frozen during checkout. Baskets with a status of 'Frozen' or 'Submitted'
        are not retrieved when fetching a basket for the user.

        Arguments:
            basket (Basket): The basket on which to perform checkout operations.
            payment_processor (class): An instance of the payment processor class corresponding
                to the payment processor the user will visit to pay for the items in their basket.

        Returns:
            dict: Response data.
        """
        basket.freeze()

        audit_log(
            'basket_frozen',
            amount=basket.total_excl_tax,
            basket_id=basket.id,
            currency=basket.currency,
            user_id=basket.owner.id
        )

        response_data = self._generate_basic_response(basket)

        if basket.total_excl_tax == AC.FREE:
            order = self.place_free_order(basket)

            # Note: Our order serializer could be used here, but in an effort to pare down the information
            # returned by this endpoint, simply returning the order number will suffice for now.
            response_data[AC.KEYS.ORDER] = {AC.KEYS.ORDER_NUMBER: order.number}
        else:
            parameters = payment_processor.get_transaction_parameters(basket, request=self.request)
            payment_page_url = parameters.pop('payment_page_url')

            response_data[AC.KEYS.PAYMENT_DATA] = {
                AC.KEYS.PAYMENT_PROCESSOR_NAME: payment_processor.NAME,
                AC.KEYS.PAYMENT_FORM_DATA: parameters,
                AC.KEYS.PAYMENT_PAGE_URL: payment_page_url,
            }

        return response_data
Esempio n. 14
0
    def revoke_line(self, line):
        try:
            logger.info('Attempting to revoke fulfillment of Line [%d]...', line.id)

            mode = mode_for_seat(line.product)
            course_key = line.product.attr.course_key
            data = {
                'user': line.order.user.username,
                'is_active': False,
                'mode': mode,
                'course_details': {
                    'course_id': course_key,
                },
            }

            __, client_id = parse_tracking_context(line.order.user)
            response = self._post_to_enrollment_api(data, client_id=client_id)

            if response.status_code == status.HTTP_200_OK:
                audit_log(
                    'line_revoked',
                    order_line_id=line.id,
                    order_number=line.order.number,
                    product_class=line.product.get_product_class().name,
                    course_id=course_key,
                    certificate_type=getattr(line.product.attr, 'certificate_type', ''),
                    user_id=line.order.user.id
                )

                return True
            else:
                # check if the error / message are something we can recover from.
                data = response.json()
                detail = data.get('message', '(No details provided.)')
                if response.status_code == 400 and "Enrollment mode mismatch" in detail:
                    # The user is currently enrolled in different mode than the one
                    # we are refunding an order for.  Don't revoke that enrollment.
                    logger.info('Skipping revocation for line [%d]: %s', line.id, detail)
                    return True
                else:
                    logger.error('Failed to revoke fulfillment of Line [%d]: %s', line.id, detail)
        except Exception:  # pylint: disable=broad-except
            logger.exception('Failed to revoke fulfillment of Line [%d].', line.id)

        return False
Esempio n. 15
0
    def handle_successful_order(self, order, request=None):  # pylint: disable=arguments-differ
        """
        Fulfill the order immediately.
        """
        for line in order.lines.all():
            line.set_status(LINE.COMPLETE)

        order.set_status(ORDER.COMPLETE)

        audit_log('manual_order_fulfilled',
                  amount=order.total_excl_tax,
                  basket_id=order.basket.id,
                  currency=order.currency,
                  order_number=order.number,
                  user_id=order.user.id,
                  contains_coupon=order.contains_coupon)

        return order
Esempio n. 16
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # TODO Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)()
            processor.issue_credit(source, self.total_credit_excl_tax,
                                   self.currency)

            audit_log('credit_issued',
                      amount=self.total_credit_excl_tax,
                      currency=self.currency,
                      processor_name=processor.NAME,
                      refund_id=self.id,
                      user_id=self.user.id)
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Esempio n. 17
0
    def handle_successful_order(self, order):
        """Send a signal so that receivers can perform relevant tasks (e.g., fulfill the order)."""
        audit_log('order_placed',
                  amount=order.total_excl_tax,
                  basket_id=order.basket.id,
                  currency=order.currency,
                  order_number=order.number,
                  user_id=order.user.id)

        if waffle.switch_is_active('async_order_fulfillment'):
            # Always commit transactions before sending tasks depending on state from the current transaction!
            # There's potential for a race condition here if the task starts executing before the active
            # transaction has been committed; the necessary order doesn't exist in the database yet.
            # See http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions.
            fulfill_order.delay(order.number)
        else:
            post_checkout.send(sender=self, order=order)

        return order
Esempio n. 18
0
    def handle_successful_order(self, order, request=None):  # pylint: disable=arguments-differ
        """Send a signal so that receivers can perform relevant tasks (e.g., fulfill the order)."""
        audit_log('order_placed',
                  amount=order.total_excl_tax,
                  basket_id=order.basket.id,
                  currency=order.currency,
                  order_number=order.number,
                  user_id=order.user.id,
                  contains_coupon=order.contains_coupon)

        # Check for the user's email opt in preference, defaulting to false if it hasn't been set
        try:
            email_opt_in = BasketAttribute.objects.get(
                basket=order.basket,
                attribute_type=BasketAttributeType.objects.get(
                    name=EMAIL_OPT_IN_ATTRIBUTE),
            ).value_text == 'True'
        except BasketAttribute.DoesNotExist:
            email_opt_in = False

        # create offer assignment for MULTI_USE_PER_CUSTOMER
        self.create_assignments_for_multi_use_per_customer(order)

        # update offer assignment with voucher application
        self.update_assigned_voucher_offer_assignment(order)

        if waffle.sample_is_active('async_order_fulfillment'):
            # Always commit transactions before sending tasks depending on state from the current transaction!
            # There's potential for a race condition here if the task starts executing before the active
            # transaction has been committed; the necessary order doesn't exist in the database yet.
            # See http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions.
            fulfill_order.delay(
                order.number,
                site_code=order.site.siteconfiguration.partner.short_code,
                email_opt_in=email_opt_in)
        else:
            post_checkout.send(sender=self,
                               order=order,
                               request=request,
                               email_opt_in=email_opt_in)

        return order
Esempio n. 19
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # TODO Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)()
            processor.issue_credit(source, self.total_credit_excl_tax, self.currency)

            audit_log(
                'credit_issued',
                amount=self.total_credit_excl_tax,
                currency=self.currency,
                processor_name=processor.NAME,
                refund_id=self.id,
                user_id=self.user.id
            )
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Esempio n. 20
0
    def handle_successful_order(self, order):
        """Send a signal so that receivers can perform relevant tasks (e.g., fulfill the order)."""
        audit_log(
            'order_placed',
            amount=order.total_excl_tax,
            basket_id=order.basket.id,
            currency=order.currency,
            order_number=order.number,
            user_id=order.user.id
        )

        if waffle.sample_is_active('async_order_fulfillment'):
            # Always commit transactions before sending tasks depending on state from the current transaction!
            # There's potential for a race condition here if the task starts executing before the active
            # transaction has been committed; the necessary order doesn't exist in the database yet.
            # See http://celery.readthedocs.org/en/latest/userguide/tasks.html#database-transactions.
            fulfill_order.delay(order.number)
        else:
            post_checkout.send(sender=self, order=order)

        return order
Esempio n. 21
0
    def handle_payment(self, response, basket):
        """
        Handle any payment processing and record payment sources and events.

        This method is responsible for handling payment and recording the
        payment sources (using the add_payment_source method) and payment
        events (using add_payment_event) so they can be
        linked to the order when it is saved later on.
        """
        handled_processor_response = self.payment_processor.handle_processor_response(response, basket=basket)
        source_type, __ = SourceType.objects.get_or_create(name=self.payment_processor.NAME)
        total = handled_processor_response.total
        reference = handled_processor_response.transaction_id

        source = Source(
            source_type=source_type,
            currency=handled_processor_response.currency,
            amount_allocated=total,
            amount_debited=total,
            reference=reference,
            label=handled_processor_response.card_number,
            card_type=handled_processor_response.card_type
        )

        event_type, __ = PaymentEventType.objects.get_or_create(name=PaymentEventTypeName.PAID)
        payment_event = PaymentEvent(event_type=event_type, amount=total, reference=reference,
                                     processor_name=self.payment_processor.NAME)

        self.add_payment_source(source)
        self.add_payment_event(payment_event)

        audit_log(
            'payment_received',
            amount=payment_event.amount,
            basket_id=basket.id,
            currency=source.currency,
            processor_name=payment_event.processor_name,
            reference=payment_event.reference,
            user_id=basket.owner.id
        )
Esempio n. 22
0
    def handle_payment(self, response, basket):
        """
        Handle any payment processing and record payment sources and events.

        This method is responsible for handling payment and recording the
        payment sources (using the add_payment_source method) and payment
        events (using add_payment_event) so they can be
        linked to the order when it is saved later on.
        """
        handled_processor_response = self.payment_processor.handle_processor_response(response, basket=basket)
        source_type, __ = SourceType.objects.get_or_create(name=self.payment_processor.NAME)
        total = handled_processor_response.total
        reference = handled_processor_response.transaction_id

        source = Source(
            source_type=source_type,
            currency=handled_processor_response.currency,
            amount_allocated=total,
            amount_debited=total,
            reference=reference,
            label=handled_processor_response.card_number,
            card_type=handled_processor_response.card_type
        )

        event_type, __ = PaymentEventType.objects.get_or_create(name=PaymentEventTypeName.PAID)
        payment_event = PaymentEvent(event_type=event_type, amount=total, reference=reference,
                                     processor_name=self.payment_processor.NAME)

        self.add_payment_source(source)
        self.add_payment_event(payment_event)

        audit_log(
            'payment_received',
            amount=payment_event.amount,
            basket_id=basket.id,
            currency=source.currency,
            processor_name=payment_event.processor_name,
            reference=payment_event.reference,
            user_id=basket.owner.id
        )
Esempio n. 23
0
    def handle_payment(self, response, basket):
        """
        Handle any payment processing and record payment sources and events.

        This method is responsible for handling payment and recording the
        payment sources (using the add_payment_source method) and payment
        events (using add_payment_event) so they can be
        linked to the order when it is saved later on.
        """
        source, payment_event = self.payment_processor.handle_processor_response(
            response, basket=basket)

        self.add_payment_source(source)
        self.add_payment_event(payment_event)

        audit_log('payment_received',
                  amount=payment_event.amount,
                  basket_id=basket.id,
                  currency=source.currency,
                  processor_name=payment_event.processor_name,
                  reference=payment_event.reference,
                  user_id=basket.owner.id)
Esempio n. 24
0
    def handle_payment(self, response, basket):
        """
        Handle any payment processing and record payment sources and events.

        This method is responsible for handling payment and recording the
        payment sources (using the add_payment_source method) and payment
        events (using add_payment_event) so they can be
        linked to the order when it is saved later on.
        """
        source, payment_event = self.payment_processor.handle_processor_response(response, basket=basket)

        self.add_payment_source(source)
        self.add_payment_event(payment_event)

        audit_log(
            'payment_received',
            amount=payment_event.amount,
            basket_id=basket.id,
            currency=source.currency,
            processor_name=payment_event.processor_name,
            reference=payment_event.reference,
            user_id=basket.owner.id
        )
Esempio n. 25
0
    def fulfill_product(self, order, lines):
        """ Fulfills the purchase of a 'Course Entitlement'.
        Uses the order and the lines to determine which courses to grant an entitlement for, and with certain
        certificate types. May result in an error if the Entitlement API cannot be reached, or if there is
        additional business logic errors when trying grant the entitlement.
        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 grant an entitlement.
            lines (List of Lines): Order Lines, associated with purchased products in an Order. These should only
                be "Course Entitlement" products.
        Returns:
            The original set of lines, with new statuses set based on the success or failure of fulfillment.
        """
        logger.info('Attempting to fulfill "Course Entitlement" product types for order [%s]', order.number)

        for line in lines:
            try:
                mode = mode_for_product(line.product)
                UUID = line.product.attr.UUID
            except AttributeError:
                logger.error('Entitlement Product does not have required attributes, [certificate_type, UUID]')
                line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR)
                continue

            data = {
                'user': order.user.username,
                'course_uuid': UUID,
                'mode': mode,
                'order_number': order.number,
            }

            try:
                entitlement_option = Option.objects.get(code='course_entitlement')

                entitlement_api_client = EdxRestApiClient(
                    get_lms_entitlement_api_url(),
                    jwt=order.site.siteconfiguration.access_token
                )

                # POST to the Entitlement API.
                response = entitlement_api_client.entitlements.post(data)
                line.attributes.create(option=entitlement_option, value=response['uuid'])
                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,
                    UUID=UUID,
                    mode=mode,
                    user_id=order.user.id,
                )
            except (Timeout, ConnectionError):
                logger.exception(
                    '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 Exception:  # pylint: disable=broad-except
                logger.exception(
                    'Unable to fulfill line [%d] of order [%s]', line.id, order.number
                )
                line.set_status(LINE.FULFILLMENT_SERVER_ERROR)

        logger.info('Finished fulfilling "Course Entitlement" product types for order [%s]', order.number)
        return order, lines
Esempio n. 26
0
    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_product(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:
                self._add_enterprise_data_to_enrollment_api_post(data, order)

                # 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
Esempio n. 27
0
    def _checkout(self, basket, payment_processor):
        """Perform checkout operations for the given basket.

        If the contents of the basket are free, places an order immediately. Otherwise,
        performs any operations necessary to prepare for payment.

        To prevent stale items from ending up in a basket at checkout, baskets should
        always be frozen during checkout. Baskets with a status of 'Frozen' or 'Submitted'
        are not retrieved when fetching a basket for the user.

        Arguments:
            basket (Basket): The basket on which to perform checkout operations.
            payment_processor (class): An instance of the payment processor class corresponding
                to the payment processor the user will visit to pay for the items in their basket.

        Returns:
            dict: Response data.
        """
        basket.freeze()

        audit_log(
            'basket_frozen',
            amount=basket.total_excl_tax,
            basket_id=basket.id,
            currency=basket.currency,
            user_id=basket.owner.id
        )

        response_data = self._generate_basic_response(basket)

        if basket.total_excl_tax == AC.FREE:
            order_metadata = data_api.get_order_metadata(basket)

            logger.info(
                'Preparing to place order [%s] for the contents of basket [%d]',
                order_metadata[AC.KEYS.ORDER_NUMBER],
                basket.id,
            )

            # Place an order. If order placement succeeds, the order is committed
            # to the database so that it can be fulfilled asynchronously.
            order = self.handle_order_placement(
                order_number=order_metadata[AC.KEYS.ORDER_NUMBER],
                user=basket.owner,
                basket=basket,
                shipping_address=None,
                shipping_method=order_metadata[AC.KEYS.SHIPPING_METHOD],
                shipping_charge=order_metadata[AC.KEYS.SHIPPING_CHARGE],
                billing_address=None,
                order_total=order_metadata[AC.KEYS.ORDER_TOTAL],
            )

            # Note: Our order serializer could be used here, but in an effort to pare down the information
            # returned by this endpoint, simply returning the order number will suffice for now.
            response_data[AC.KEYS.ORDER] = {AC.KEYS.ORDER_NUMBER: order.number}
        else:
            parameters = payment_processor.get_transaction_parameters(basket, request=self.request)
            payment_page_url = parameters.pop('payment_page_url')

            response_data[AC.KEYS.PAYMENT_DATA] = {
                AC.KEYS.PAYMENT_PROCESSOR_NAME: payment_processor.NAME,
                AC.KEYS.PAYMENT_FORM_DATA: parameters,
                AC.KEYS.PAYMENT_PAGE_URL: payment_page_url,
            }

        return response_data
Esempio n. 28
0
    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)

        enrollment_api_url = getattr(settings, 'ENROLLMENT_API_URL', None)
        api_key = getattr(settings, 'EDX_API_KEY', None)
        if not (enrollment_api_url and api_key):
            logger.error(
                'ENROLLMENT_API_URL and 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': []
            }
            if provider:
                data['enrollment_attributes'].append(
                    {
                        'namespace': 'credit',
                        'name': 'provider_id',
                        'value': provider
                    }
                )
            try:
                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(
                        "Unable to fulfill line [%d] of order [%s] due to a server-side error: %s", line.id,
                        order.number, 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
Esempio n. 29
0
    def fulfill_product(self, order, lines, email_opt_in=False):
        """ Fulfills the purchase of a 'Course Entitlement'.
        Uses the order and the lines to determine which courses to grant an entitlement for, and with certain
        certificate types. May result in an error if the Entitlement API cannot be reached, or if there is
        additional business logic errors when trying grant the entitlement.

        Updates the user's email preferences based on email_opt_in as a side effect.

        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 grant an entitlement.
            lines (List of Lines): Order Lines, associated with purchased products in an Order. These should only
                be "Course Entitlement" products.
            email_opt_in (bool): Whether the user should be opted in to emails
                as part of the fulfillment. Defaults to False.

        Returns:
            The original set of lines, with new statuses set based on the success or failure of fulfillment.
        """
        logger.info(
            'Attempting to fulfill "Course Entitlement" product types for order [%s]',
            order.number)

        for line in lines:
            try:
                mode = mode_for_product(line.product)
                UUID = line.product.attr.UUID
            except AttributeError:
                logger.error(
                    'Entitlement Product does not have required attributes, [certificate_type, UUID]'
                )
                line.set_status(LINE.FULFILLMENT_CONFIGURATION_ERROR)
                continue

            data = {
                'user': order.user.username,
                'course_uuid': UUID,
                'mode': mode,
                'order_number': order.number,
                'email_opt_in': email_opt_in,
            }

            try:
                self._create_enterprise_customer_user(order)
                self.update_orderline_with_enterprise_discount_metadata(
                    order, line)
                entitlement_option = Option.objects.get(
                    code='course_entitlement')

                api_client = line.order.site.siteconfiguration.oauth_api_client
                entitlement_url = urljoin(get_lms_entitlement_api_url(),
                                          'entitlements/')

                # POST to the Entitlement API.
                response = api_client.post(entitlement_url, json=data)
                response.raise_for_status()
                response = response.json()
                line.attributes.create(option=entitlement_option,
                                       value=response['uuid'])
                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,
                    UUID=UUID,
                    mode=mode,
                    user_id=order.user.id,
                )
            except (Timeout, ReqConnectionError):
                logger.exception(
                    'Unable to fulfill line [%d] of order [%s] due to a network problem',
                    line.id, order.number)
                order.notes.create(
                    message=
                    'Fulfillment of order failed due to a network problem.',
                    note_type='Error')
                line.set_status(LINE.FULFILLMENT_NETWORK_ERROR)
            except Exception:  # pylint: disable=broad-except
                logger.exception('Unable to fulfill line [%d] of order [%s]',
                                 line.id, order.number)
                order.notes.create(
                    message='Fulfillment of order failed due to an Exception.',
                    note_type='Error')
                line.set_status(LINE.FULFILLMENT_SERVER_ERROR)

        logger.info(
            'Finished fulfilling "Course Entitlement" product types for order [%s]',
            order.number)
        return order, lines
Esempio n. 30
0
    def _checkout(self, basket, payment_processor):
        """Perform checkout operations for the given basket.

        If the contents of the basket are free, places an order immediately. Otherwise,
        performs any operations necessary to prepare for payment.

        To prevent stale items from ending up in a basket at checkout, baskets should
        always be frozen during checkout. Baskets with a status of 'Frozen' or 'Submitted'
        are not retrieved when fetching a basket for the user.

        Arguments:
            basket (Basket): The basket on which to perform checkout operations.
            payment_processor (class): An instance of the payment processor class corresponding
                to the payment processor the user will visit to pay for the items in their basket.

        Returns:
            dict: Response data.
        """
        basket.freeze()

        audit_log(
            'basket_frozen',
            amount=basket.total_excl_tax,
            basket_id=basket.id,
            currency=basket.currency,
            user_id=basket.owner.id
        )

        response_data = self._generate_basic_response(basket)

        if basket.total_excl_tax == AC.FREE:
            order_metadata = data_api.get_order_metadata(basket)

            logger.info(
                u"Preparing to place order [%s] for the contents of basket [%d]",
                order_metadata[AC.KEYS.ORDER_NUMBER],
                basket.id,
            )

            # Place an order. If order placement succeeds, the order is committed
            # to the database so that it can be fulfilled asynchronously.
            order = self.handle_order_placement(
                order_number=order_metadata[AC.KEYS.ORDER_NUMBER],
                user=basket.owner,
                basket=basket,
                shipping_address=None,
                shipping_method=order_metadata[AC.KEYS.SHIPPING_METHOD],
                shipping_charge=order_metadata[AC.KEYS.SHIPPING_CHARGE],
                billing_address=None,
                order_total=order_metadata[AC.KEYS.ORDER_TOTAL],
            )

            # Note: Our order serializer could be used here, but in an effort to pare down the information
            # returned by this endpoint, simply returning the order number will suffice for now.
            response_data[AC.KEYS.ORDER] = {AC.KEYS.ORDER_NUMBER: order.number}
        else:
            parameters = payment_processor.get_transaction_parameters(basket, request=self.request)
            payment_page_url = parameters.pop('payment_page_url')

            response_data[AC.KEYS.PAYMENT_DATA] = {
                AC.KEYS.PAYMENT_PROCESSOR_NAME: payment_processor.NAME,
                AC.KEYS.PAYMENT_FORM_DATA: parameters,
                AC.KEYS.PAYMENT_PAGE_URL: payment_page_url,
            }

        return response_data