Commit 89ff8dc1 by Marko Jevtic

[SOL-1953] New Receipt Page Backend

parent 03592581
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0019_auto_20161012_1404'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='enable_otto_receipt_page',
field=models.BooleanField(default=False, help_text='Enable the usage of Otto receipt page.', verbose_name='Enable Otto receipt page'),
),
]
...@@ -108,6 +108,12 @@ class SiteConfiguration(models.Model): ...@@ -108,6 +108,12 @@ class SiteConfiguration(models.Model):
blank=True, blank=True,
default="", default="",
) )
enable_otto_receipt_page = models.BooleanField(
verbose_name=_('Enable Otto receipt page'),
help_text=_('Enable the usage of Otto receipt page.'),
blank=True,
default=False
)
class Meta(object): class Meta(object):
unique_together = ('site', 'partner') unique_together = ('site', 'partner')
......
...@@ -68,16 +68,27 @@ class CourseCatalogMockMixin(object): ...@@ -68,16 +68,27 @@ class CourseCatalogMockMixin(object):
}], }],
} }
course_run_info_json = json.dumps(course_run_info) course_run_info_json = json.dumps(course_run_info)
course_run_url = '{}course_runs/?q={}'.format( course_run_url_with_query = '{}course_runs/?q={}'.format(
settings.COURSE_CATALOG_API_URL, settings.COURSE_CATALOG_API_URL,
query if query else 'id:course*' query if query else 'id:course*'
) )
httpretty.register_uri( httpretty.register_uri(
httpretty.GET, course_run_url, httpretty.GET,
course_run_url_with_query,
body=course_run_info_json, body=course_run_info_json,
content_type='application/json' content_type='application/json'
) )
course_run_url_with_key = '{}course_runs/{}/'.format(
settings.COURSE_CATALOG_API_URL,
course_run.id if course_run else 'course-v1:test+test+test'
)
httpretty.register_uri(
httpretty.GET, course_run_url_with_key,
body=json.dumps(course_run_info['results'][0]),
content_type='application/json'
)
def mock_dynamic_catalog_contains_api(self, course_run_ids, query): def mock_dynamic_catalog_contains_api(self, course_run_ids, query):
""" Helper function to register a dynamic course catalog API endpoint for the contains information. """ """ Helper function to register a dynamic course catalog API endpoint for the contains information. """
course_contains_info = { course_contains_info = {
......
...@@ -208,14 +208,51 @@ class LineSerializer(serializers.ModelSerializer): ...@@ -208,14 +208,51 @@ class LineSerializer(serializers.ModelSerializer):
class OrderSerializer(serializers.ModelSerializer): class OrderSerializer(serializers.ModelSerializer):
"""Serializer for parsing order data.""" """Serializer for parsing order data."""
billing_address = BillingAddressSerializer(allow_null=True)
date_placed = serializers.DateTimeField(format=ISO_8601_FORMAT) date_placed = serializers.DateTimeField(format=ISO_8601_FORMAT)
discount = serializers.SerializerMethodField()
lines = LineSerializer(many=True) lines = LineSerializer(many=True)
billing_address = BillingAddressSerializer(allow_null=True) payment_processor = serializers.SerializerMethodField()
user = UserSerializer() user = UserSerializer()
vouchers = serializers.SerializerMethodField()
def get_vouchers(self, obj):
try:
serializer = VoucherSerializer(
obj.basket.vouchers.all(), many=True, context={'request': self.context['request']}
)
return serializer.data
except (AttributeError, ValueError):
return None
def get_payment_processor(self, obj):
try:
return obj.sources.all()[0].source_type.name
except IndexError:
return None
def get_discount(self, obj):
try:
discount = obj.discounts.all()[0]
return str(discount.amount)
except IndexError:
return '0'
class Meta(object): class Meta(object):
model = Order model = Order
fields = ('number', 'date_placed', 'status', 'currency', 'total_excl_tax', 'lines', 'billing_address', 'user') fields = (
'billing_address',
'currency',
'date_placed',
'discount',
'lines',
'number',
'payment_processor',
'status',
'total_excl_tax',
'user',
'vouchers',
)
class PaymentProcessorSerializer(serializers.Serializer): # pylint: disable=abstract-method class PaymentProcessorSerializer(serializers.Serializer): # pylint: disable=abstract-method
...@@ -640,3 +677,14 @@ class SiteConfigurationSerializer(serializers.ModelSerializer): ...@@ -640,3 +677,14 @@ class SiteConfigurationSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = SiteConfiguration model = SiteConfiguration
class ProviderSerializer(serializers.Serializer): # pylint: disable=abstract-method
description = serializers.CharField()
display_name = serializers.CharField()
enable_integration = serializers.BooleanField()
fulfillment_instructions = serializers.CharField()
id = serializers.CharField()
status_url = serializers.CharField()
thumbnail_url = serializers.CharField()
url = serializers.CharField()
import json
import ddt
from django.core.urlresolvers import reverse
import httpretty
from rest_framework import status
from ecommerce.extensions.api.serializers import ProviderSerializer
from ecommerce.tests.testcases import TestCase
@ddt.ddt
class ProvidersViewSetTest(TestCase):
path = reverse('api:v2:providers:list_providers')
def setUp(self):
super(ProvidersViewSetTest, self).setUp()
user = self.create_user()
self.client.login(username=user.username, password=self.password)
self.provider = 'test-provider'
self.data = {
'id': self.provider,
'display_name': self.provider,
'url': 'http://example.com/',
'status_url': 'http://status.example.com/',
'description': 'Description',
'enable_integration': False,
'fulfillment_instructions': '',
'thumbnail_url': 'http://thumbnail.example.com/',
}
def mock_provider_api(self):
provider_url = '{lms_url}{provider}/'.format(
lms_url=self.site.siteconfiguration.build_lms_url('api/credit/v1/providers/'),
provider=self.provider
)
httpretty.register_uri(
httpretty.GET,
provider_url,
body=json.dumps(self.data),
content_type='application/json'
)
@httpretty.activate
def test_getting_provider(self):
"""Verify endpoint returns correct provider data."""
self.mock_provider_api()
response = self.client.get('{path}?credit_provider_id={provider}'.format(
path=self.path, provider=self.provider
))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(json.loads(response.content), ProviderSerializer(self.data).data)
def test_invalid_provider(self):
"""Verify endpoint response is empty for invalid provider."""
response = self.client.get('{path}?credit_provider_id={provider}'.format(
path=self.path, provider='invalid-provider'
))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.content, '')
...@@ -12,6 +12,7 @@ from ecommerce.extensions.api.v2.views import ( ...@@ -12,6 +12,7 @@ from ecommerce.extensions.api.v2.views import (
partners as partner_views, partners as partner_views,
payments as payment_views, payments as payment_views,
products as product_views, products as product_views,
providers as provider_views,
publication as publication_views, publication as publication_views,
refunds as refund_views, refunds as refund_views,
siteconfiguration as siteconfiguration_views, siteconfiguration as siteconfiguration_views,
...@@ -65,13 +66,18 @@ ATOMIC_PUBLICATION_URLS = [ ...@@ -65,13 +66,18 @@ ATOMIC_PUBLICATION_URLS = [
), ),
] ]
PROVIDER_URLS = [
url(r'^$', provider_views.ProviderViewSet.as_view(), name='list_providers')
]
urlpatterns = [ urlpatterns = [
url(r'^baskets/', include(BASKET_URLS, namespace='baskets')), url(r'^baskets/', include(BASKET_URLS, namespace='baskets')),
url(r'^checkout/$', include(CHECKOUT_URLS, namespace='checkout')), url(r'^checkout/$', include(CHECKOUT_URLS, namespace='checkout')),
url(r'^coupons/', include(COUPON_URLS, namespace='coupons')), url(r'^coupons/', include(COUPON_URLS, namespace='coupons')),
url(r'^payment/', include(PAYMENT_URLS, namespace='payment')), url(r'^payment/', include(PAYMENT_URLS, namespace='payment')),
url(r'^refunds/', include(REFUND_URLS, namespace='refunds')), url(r'^providers/', include(PROVIDER_URLS, namespace='providers')),
url(r'^publication/', include(ATOMIC_PUBLICATION_URLS, namespace='publication')), url(r'^publication/', include(ATOMIC_PUBLICATION_URLS, namespace='publication')),
url(r'^refunds/', include(REFUND_URLS, namespace='refunds')),
] ]
router = ExtendedSimpleRouter() router = ExtendedSimpleRouter()
......
"""HTTP endpoint for displaying information about providers."""
import logging
from rest_framework.views import APIView
from rest_framework.response import Response
from ecommerce.extensions.api.serializers import ProviderSerializer
from ecommerce.extensions.checkout.utils import get_credit_provider_details
logger = logging.getLogger(__name__)
class ProviderViewSet(APIView):
"""Gets the credit provider data from LMS"""
def get(self, request):
credit_provider_id = request.GET.get('credit_provider_id')
provider_data = get_credit_provider_details(
access_token=request.user.access_token,
credit_provider_id=credit_provider_id,
site_configuration=request.site.siteconfiguration
)
if not provider_data:
response_data = None
elif isinstance(provider_data, dict):
response_data = ProviderSerializer(provider_data).data
else:
response_data = ProviderSerializer(provider_data, many=True).data
return Response(response_data)
import logging import logging
from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from oscar.core.loading import get_class from oscar.core.loading import get_class
import waffle import waffle
from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.utils import mode_for_seat from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, silence_exceptions from ecommerce.extensions.analytics.utils import is_segment_configured, parse_tracking_context, silence_exceptions
from ecommerce.extensions.checkout.utils import get_credit_provider_details from ecommerce.extensions.checkout.utils import get_credit_provider_details, get_receipt_page_url
from ecommerce.notifications.notifications import send_notification from ecommerce.notifications.notifications import send_notification
...@@ -79,15 +77,19 @@ def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable ...@@ -79,15 +77,19 @@ def send_course_purchase_email(sender, order=None, **kwargs): # pylint: disable
credit_provider_id=credit_provider_id, credit_provider_id=credit_provider_id,
site_configuration=order.site.siteconfiguration site_configuration=order.site.siteconfiguration
) )
receipt_page_url = get_receipt_page_url(
order_number=order.number,
site_configuration=order.site.siteconfiguration
)
if provider_data: if provider_data:
send_notification( send_notification(
order.user, order.user,
'CREDIT_RECEIPT', 'CREDIT_RECEIPT',
{ {
'course_title': product.title, 'course_title': product.title,
'receipt_page_url': get_lms_url( 'receipt_page_url': receipt_page_url,
'{}?orderNum={}'.format(settings.RECEIPT_PAGE_PATH, order.number)
),
'credit_hours': product.attr.credit_hours, 'credit_hours': product.attr.credit_hours,
'credit_provider': provider_data['display_name'], 'credit_provider': provider_data['display_name'],
}, },
......
...@@ -21,6 +21,8 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -21,6 +21,8 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
def setUp(self): def setUp(self):
super(SignalTests, self).setUp() super(SignalTests, self).setUp()
self.user = self.create_user() self.user = self.create_user()
self.request.user = self.user
self.site.siteconfiguration.enable_otto_receipt_page = True
toggle_switch('ENABLE_NOTIFICATIONS', True) toggle_switch('ENABLE_NOTIFICATIONS', True)
def prepare_order(self, seat_type, credit_provider_id=None): def prepare_order(self, seat_type, credit_provider_id=None):
...@@ -87,8 +89,8 @@ class SignalTests(CourseCatalogTestMixin, TestCase): ...@@ -87,8 +89,8 @@ class SignalTests(CourseCatalogTestMixin, TestCase):
credit_hours=2, credit_hours=2,
credit_provider_name=credit_provider_name, credit_provider_name=credit_provider_name,
platform_name=self.site.name, platform_name=self.site.name,
receipt_url=self.site.siteconfiguration.build_lms_url( receipt_url=self.site.siteconfiguration.build_ecommerce_url(
'{}?orderNum={}'.format(settings.RECEIPT_PAGE_PATH, order.number) '{}{}'.format(settings.RECEIPT_PAGE_PATH, order.number)
) )
) )
) )
......
import logging import logging
from babel.numbers import format_currency
from django.conf import settings
from django.utils.translation import get_language, to_locale
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException from slumber.exceptions import SlumberHttpBaseException
...@@ -26,3 +29,43 @@ def get_credit_provider_details(access_token, credit_provider_id, site_configura ...@@ -26,3 +29,43 @@ def get_credit_provider_details(access_token, credit_provider_id, site_configura
except (ConnectionError, SlumberHttpBaseException, Timeout): except (ConnectionError, SlumberHttpBaseException, Timeout):
logger.exception('Failed to retrieve credit provider details for provider [%s].', credit_provider_id) logger.exception('Failed to retrieve credit provider details for provider [%s].', credit_provider_id)
return None return None
def get_receipt_page_url(site_configuration, order_number=None):
""" Returns the receipt page URL.
Args:
order_number (str): Order number
site_configuration (SiteConfiguration): Site Configuration containing the flag for enabling Otto receipt page.
Returns:
str: Receipt page URL.
"""
if site_configuration.enable_otto_receipt_page:
return site_configuration.build_ecommerce_url('{base_url}{order_number}'.format(
base_url=settings.RECEIPT_PAGE_PATH,
order_number=order_number if order_number else ''
))
return site_configuration.build_lms_url(
'{base_url}{order_number}'.format(
base_url='/commerce/checkout/receipt',
order_number='?orderNum={}'.format(order_number) if order_number else ''
)
)
def add_currency(amount):
""" Adds currency to the price amount.
Args:
amount (Decimal): Price amount
Returns:
str: Formatted price with currency.
"""
return format_currency(
amount,
settings.OSCAR_DEFAULT_CURRENCY,
format=u'#,##0.00',
locale=to_locale(get_language())
)
...@@ -16,10 +16,11 @@ import requests ...@@ -16,10 +16,11 @@ import requests
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME
from ecommerce.core.url_utils import get_ecommerce_url, get_lms_enrollment_api_url, get_lms_url from ecommerce.core.url_utils import get_lms_enrollment_api_url
from ecommerce.courses.models import Course from ecommerce.courses.models import Course
from ecommerce.courses.utils import mode_for_seat from ecommerce.courses.utils import mode_for_seat
from ecommerce.extensions.analytics.utils import audit_log, parse_tracking_context from ecommerce.extensions.analytics.utils import audit_log, parse_tracking_context
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.fulfillment.status import LINE from ecommerce.extensions.fulfillment.status import LINE
from ecommerce.extensions.voucher.models import OrderLineVouchers from ecommerce.extensions.voucher.models import OrderLineVouchers
from ecommerce.extensions.voucher.utils import create_vouchers from ecommerce.extensions.voucher.utils import create_vouchers
...@@ -446,18 +447,24 @@ class EnrollmentCodeFulfillmentModule(BaseFulfillmentModule): ...@@ -446,18 +447,24 @@ class EnrollmentCodeFulfillmentModule(BaseFulfillmentModule):
# Note (multi-courses): Change from a course_name to a list of course names. # Note (multi-courses): Change from a course_name to a list of course names.
product = order.lines.first().product product = order.lines.first().product
course = Course.objects.get(id=product.attr.course_key) course = Course.objects.get(id=product.attr.course_key)
receipt_page_url = get_receipt_page_url(
order_number=order.number,
site_configuration=order.site.siteconfiguration
)
send_notification( send_notification(
order.user, order.user,
'ORDER_WITH_CSV', 'ORDER_WITH_CSV',
context={ context={
'contact_url': get_lms_url('/contact'), 'contact_url': order.site.siteconfiguration.build_lms_url('/contact'),
'course_name': course.name, 'course_name': course.name,
'download_csv_link': get_ecommerce_url(reverse('coupons:enrollment_code_csv', args=[order.number])), 'download_csv_link': order.site.siteconfiguration.build_ecommerce_url(
reverse('coupons:enrollment_code_csv', args=[order.number])
),
'enrollment_code_title': product.title, 'enrollment_code_title': product.title,
'lms_url': order.site.siteconfiguration.build_lms_url(),
'order_number': order.number, 'order_number': order.number,
'partner_name': order.site.siteconfiguration.partner.name, 'partner_name': order.site.siteconfiguration.partner.name,
'lms_url': get_lms_url(), 'receipt_page_url': receipt_page_url,
'receipt_page_url': get_lms_url('{}?orderNum={}'.format(settings.RECEIPT_PAGE_PATH, order.number)),
}, },
site=order.site site=order.site
) )
...@@ -443,6 +443,7 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase): ...@@ -443,6 +443,7 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase):
def setUp(self): def setUp(self):
super(EnrollmentCodeFulfillmentModuleTests, self).setUp() super(EnrollmentCodeFulfillmentModuleTests, self).setUp()
toggle_switch(ENROLLMENT_CODE_SWITCH, True) toggle_switch(ENROLLMENT_CODE_SWITCH, True)
self.site.siteconfiguration.enable_otto_receipt_page = True
course = CourseFactory() course = CourseFactory()
course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True) course.create_or_update_seat('verified', True, 50, self.partner, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
...@@ -477,6 +478,16 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase): ...@@ -477,6 +478,16 @@ class EnrollmentCodeFulfillmentModuleTests(CourseCatalogTestMixin, TestCase):
self.assertEqual(OrderLineVouchers.objects.count(), 1) self.assertEqual(OrderLineVouchers.objects.count(), 1)
self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY) self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY)
def test_fulfill_product_with_lms_receipt_page(self):
"""Test disabling otto_receipt_page switch still results in successfully fulfilling Enrollment code product."""
self.site.siteconfiguration.enable_otto_receipt_page = False
self.assertEqual(OrderLineVouchers.objects.count(), 0)
lines = self.order.lines.all()
__, completed_lines = EnrollmentCodeFulfillmentModule().fulfill_product(self.order, lines)
self.assertEqual(completed_lines[0].status, LINE.COMPLETE)
self.assertEqual(OrderLineVouchers.objects.count(), 1)
self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY)
def test_revoke_line(self): def test_revoke_line(self):
line = self.order.lines.first() line = self.order.lines.first()
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
......
from decimal import Decimal from decimal import Decimal
import ddt import ddt
from babel.numbers import format_currency
from django.conf import settings
from django.utils.translation import get_language, to_locale
from oscar.core.loading import get_model from oscar.core.loading import get_model
from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wildcard-import from oscar.test.factories import * # pylint:disable=wildcard-import,unused-wildcard-import
from ecommerce.courses.tests.factories import CourseFactory from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
from ecommerce.extensions.checkout.utils import add_currency
from ecommerce.extensions.offer.utils import _remove_exponent_and_trailing_zeros, format_benefit_value from ecommerce.extensions.offer.utils import _remove_exponent_and_trailing_zeros, format_benefit_value
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
...@@ -36,9 +33,7 @@ class UtilTests(CourseCatalogTestMixin, TestCase): ...@@ -36,9 +33,7 @@ class UtilTests(CourseCatalogTestMixin, TestCase):
self.assertEqual(benefit_value, '35%') self.assertEqual(benefit_value, '35%')
benefit_value = format_benefit_value(self.value_benefit) benefit_value = format_benefit_value(self.value_benefit)
expected_benefit = format_currency( expected_benefit = add_currency(Decimal((self.seat_price - 10)))
Decimal((self.seat_price - 10)), settings.OSCAR_DEFAULT_CURRENCY, format=u'#,##0.00',
locale=to_locale(get_language()))
self.assertEqual(benefit_value, '${expected_benefit}'.format(expected_benefit=expected_benefit)) self.assertEqual(benefit_value, '${expected_benefit}'.format(expected_benefit=expected_benefit))
@ddt.data( @ddt.data(
......
"""Offer Utility Methods. """ """Offer Utility Methods. """
from decimal import Decimal from decimal import Decimal
from babel.numbers import format_currency from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.utils.translation import get_language, to_locale, ugettext_lazy as _
from oscar.core.loading import get_model from oscar.core.loading import get_model
from ecommerce.extensions.checkout.utils import add_currency
Benefit = get_model('offer', 'Benefit') Benefit = get_model('offer', 'Benefit')
...@@ -23,6 +22,30 @@ def _remove_exponent_and_trailing_zeros(decimal): ...@@ -23,6 +22,30 @@ def _remove_exponent_and_trailing_zeros(decimal):
return decimal.quantize(Decimal(1)) if decimal == decimal.to_integral() else decimal.normalize() return decimal.quantize(Decimal(1)) if decimal == decimal.to_integral() else decimal.normalize()
def get_discount_percentage(discount_value, product_price):
"""
Get discount percentage of discount value applied to a product price.
Arguments:
discount_value (float): Discount value
product_price (float): Price of a product the discount is used on
Returns:
float: Discount percentage
"""
return discount_value / product_price * 100
def get_discount_value(discount_percentage, product_price):
"""
Get discount value of discount percentage applied to a product price.
Arguments:
discount_percentage (float): Discount percentage
product_price (float): Price of a product the discount is used on
Returns:
float: Discount value
"""
return discount_percentage * product_price / 100.0
def format_benefit_value(benefit): def format_benefit_value(benefit):
""" """
Format benefit value for display based on the benefit type Format benefit value for display based on the benefit type
...@@ -37,8 +60,6 @@ def format_benefit_value(benefit): ...@@ -37,8 +60,6 @@ def format_benefit_value(benefit):
if benefit.type == Benefit.PERCENTAGE: if benefit.type == Benefit.PERCENTAGE:
benefit_value = _('{benefit_value}%'.format(benefit_value=benefit_value)) benefit_value = _('{benefit_value}%'.format(benefit_value=benefit_value))
else: else:
converted_benefit = format_currency( converted_benefit = add_currency(Decimal(benefit.value))
Decimal(benefit.value), settings.OSCAR_DEFAULT_CURRENCY, format=u'#,##0.00',
locale=to_locale(get_language()))
benefit_value = _('${benefit_value}'.format(benefit_value=converted_benefit)) benefit_value = _('${benefit_value}'.format(benefit_value=converted_benefit))
return benefit_value return benefit_value
...@@ -17,6 +17,7 @@ class PaymentProcessorResponse(models.Model): ...@@ -17,6 +17,7 @@ class PaymentProcessorResponse(models.Model):
created = models.DateTimeField(auto_now_add=True, db_index=True) created = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta(object): class Meta(object):
get_latest_by = 'created'
index_together = ('processor_name', 'transaction_id') index_together = ('processor_name', 'transaction_id')
verbose_name = _('Payment Processor Response') verbose_name = _('Payment Processor Response')
verbose_name_plural = _('Payment Processor Responses') verbose_name_plural = _('Payment Processor Responses')
......
...@@ -14,8 +14,9 @@ from suds.sudsobject import asdict ...@@ -14,8 +14,9 @@ from suds.sudsobject import asdict
from suds.wsse import Security, UsernameToken from suds.wsse import Security, UsernameToken
from threadlocals.threadlocals import get_current_request from threadlocals.threadlocals import get_current_request
from ecommerce.core.url_utils import get_ecommerce_url, get_lms_url
from ecommerce.core.constants import ISO_8601_FORMAT from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.extensions.payment.constants import CYBERSOURCE_CARD_TYPE_MAP from ecommerce.extensions.payment.constants import CYBERSOURCE_CARD_TYPE_MAP
from ecommerce.extensions.payment.exceptions import (InvalidSignatureError, InvalidCybersourceDecision, from ecommerce.extensions.payment.exceptions import (InvalidSignatureError, InvalidCybersourceDecision,
...@@ -64,10 +65,6 @@ class Cybersource(BasePaymentProcessor): ...@@ -64,10 +65,6 @@ class Cybersource(BasePaymentProcessor):
self.language_code = settings.LANGUAGE_CODE self.language_code = settings.LANGUAGE_CODE
@property @property
def receipt_page_url(self):
return get_lms_url(self.configuration['receipt_path'])
@property
def cancel_page_url(self): def cancel_page_url(self):
return get_ecommerce_url(self.configuration['cancel_checkout_path']) return get_ecommerce_url(self.configuration['cancel_checkout_path'])
...@@ -98,7 +95,10 @@ class Cybersource(BasePaymentProcessor): ...@@ -98,7 +95,10 @@ class Cybersource(BasePaymentProcessor):
'amount': str(basket.total_incl_tax), 'amount': str(basket.total_incl_tax),
'currency': basket.currency, 'currency': basket.currency,
'consumer_id': basket.owner.username, 'consumer_id': basket.owner.username,
'override_custom_receipt_page': '{}?orderNum={}'.format(self.receipt_page_url, basket.order_number), 'override_custom_receipt_page': get_receipt_page_url(
order_number=basket.order_number,
site_configuration=basket.site.siteconfiguration
),
'override_custom_cancel_page': self.cancel_page_url, 'override_custom_cancel_page': self.cancel_page_url,
} }
......
...@@ -10,7 +10,7 @@ from oscar.core.loading import get_model ...@@ -10,7 +10,7 @@ from oscar.core.loading import get_model
import paypalrestsdk import paypalrestsdk
import waffle import waffle
from ecommerce.core.url_utils import get_ecommerce_url, get_lms_url from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.extensions.payment.processors import BasePaymentProcessor from ecommerce.extensions.payment.processors import BasePaymentProcessor
from ecommerce.extensions.payment.models import PaypalWebProfile from ecommerce.extensions.payment.models import PaypalWebProfile
...@@ -60,10 +60,6 @@ class Paypal(BasePaymentProcessor): ...@@ -60,10 +60,6 @@ class Paypal(BasePaymentProcessor):
}) })
@property @property
def receipt_url(self):
return get_lms_url(self.configuration['receipt_path'])
@property
def cancel_url(self): def cancel_url(self):
return get_ecommerce_url(self.configuration['cancel_checkout_path']) return get_ecommerce_url(self.configuration['cancel_checkout_path'])
......
...@@ -17,6 +17,7 @@ from oscar.test import factories ...@@ -17,6 +17,7 @@ from oscar.test import factories
from threadlocals.threadlocals import get_current_request from threadlocals.threadlocals import get_current_request
from ecommerce.core.constants import ISO_8601_FORMAT from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.exceptions import ( from ecommerce.extensions.payment.exceptions import (
InvalidSignatureError, InvalidCybersourceDecision, PartialAuthorizationError InvalidSignatureError, InvalidCybersourceDecision, PartialAuthorizationError
) )
...@@ -38,6 +39,11 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase ...@@ -38,6 +39,11 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase
processor_class = Cybersource processor_class = Cybersource
processor_name = 'cybersource' processor_name = 'cybersource'
def setUp(self):
super(CybersourceTests, self).setUp()
self.site.siteconfiguration.enable_otto_receipt_page = True
self.basket.site = self.site
def get_expected_transaction_parameters(self, transaction_uuid, include_level_2_3_details=True): def get_expected_transaction_parameters(self, transaction_uuid, include_level_2_3_details=True):
""" """
Builds expected transaction parameters dictionary Builds expected transaction parameters dictionary
...@@ -58,8 +64,10 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase ...@@ -58,8 +64,10 @@ class CybersourceTests(CybersourceMixin, PaymentProcessorTestCaseMixin, TestCase
'amount': unicode(self.basket.total_incl_tax), 'amount': unicode(self.basket.total_incl_tax),
'currency': self.basket.currency, 'currency': self.basket.currency,
'consumer_id': self.basket.owner.username, 'consumer_id': self.basket.owner.username,
'override_custom_receipt_page': '{}?orderNum={}'.format(self.processor.receipt_page_url, 'override_custom_receipt_page': get_receipt_page_url(
self.basket.order_number), order_number=self.basket.order_number,
site_configuration=self.basket.site.siteconfiguration
),
'override_custom_cancel_page': self.processor.cancel_page_url, 'override_custom_cancel_page': self.processor.cancel_page_url,
'merchant_defined_data1': self.course.id, 'merchant_defined_data1': self.course.id,
'merchant_defined_data2': self.CERTIFICATE_TYPE, 'merchant_defined_data2': self.CERTIFICATE_TYPE,
......
...@@ -20,7 +20,7 @@ from paypalrestsdk.resource import Resource ...@@ -20,7 +20,7 @@ from paypalrestsdk.resource import Resource
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.core.tests import toggle_switch from ecommerce.core.tests import toggle_switch
from ecommerce.core.url_utils import get_ecommerce_url from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.models import PaypalWebProfile from ecommerce.extensions.payment.models import PaypalWebProfile
from ecommerce.extensions.payment.processors.paypal import Paypal from ecommerce.extensions.payment.processors.paypal import Paypal
from ecommerce.extensions.payment.tests.mixins import PaypalMixin from ecommerce.extensions.payment.tests.mixins import PaypalMixin
...@@ -82,6 +82,10 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase): ...@@ -82,6 +82,10 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase):
actual = self.processor.get_transaction_parameters(self.basket, request=self.request) actual = self.processor.get_transaction_parameters(self.basket, request=self.request)
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
def _get_receipt_url(self):
"""DRY helper for getting receipt page URL."""
return get_receipt_page_url(site_configuration=self.site.siteconfiguration)
def _assert_payment_event_and_source(self, payer_info): def _assert_payment_event_and_source(self, payer_info):
"""DRY helper for verifying a payment event and source.""" """DRY helper for verifying a payment event and source."""
source, payment_event = self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket) source, payment_event = self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket)
...@@ -107,9 +111,23 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase): ...@@ -107,9 +111,23 @@ class PaypalTests(PaypalMixin, PaymentProcessorTestCaseMixin, TestCase):
self.assert_processor_response_recorded(self.processor.NAME, self.PAYMENT_ID, response, basket=self.basket) self.assert_processor_response_recorded(self.processor.NAME, self.PAYMENT_ID, response, basket=self.basket)
last_request_body = json.loads(httpretty.last_request().body) last_request_body = json.loads(httpretty.last_request().body)
expected = urljoin(get_ecommerce_url(), reverse('paypal_execute')) expected = urljoin(self.site.siteconfiguration.build_ecommerce_url(), reverse('paypal_execute'))
self.assertEqual(last_request_body['redirect_urls']['return_url'], expected) self.assertEqual(last_request_body['redirect_urls']['return_url'], expected)
def test_switch_enabled_otto_url(self):
"""
Ensures that when the otto_receipt_page waffle switch is enabled, the processor uses the new receipt page.
"""
self.site.siteconfiguration.enable_otto_receipt_page = True
assert self._get_receipt_url() == self.site.siteconfiguration.build_ecommerce_url(settings.RECEIPT_PAGE_PATH)
def test_switch_disabled_lms_url(self):
"""
Ensures that when the otto_receipt_page waffle switch is disabled, the processor uses the LMS receipt page.
"""
self.site.siteconfiguration.enable_otto_receipt_page = False
assert self._get_receipt_url() == self.site.siteconfiguration.build_lms_url('/commerce/checkout/receipt')
@httpretty.activate @httpretty.activate
@mock.patch('ecommerce.extensions.payment.processors.paypal.paypalrestsdk.Payment') @mock.patch('ecommerce.extensions.payment.processors.paypal.paypalrestsdk.Payment')
@ddt.data(None, Paypal.DEFAULT_PROFILE_NAME, "some-other-name") @ddt.data(None, Paypal.DEFAULT_PROFILE_NAME, "some-other-name")
......
...@@ -11,6 +11,7 @@ from oscar.test import factories ...@@ -11,6 +11,7 @@ from oscar.test import factories
from oscar.test.contextmanagers import mock_signal_receiver from oscar.test.contextmanagers import mock_signal_receiver
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.fulfillment.status import ORDER from ecommerce.extensions.fulfillment.status import ORDER
from ecommerce.extensions.payment.processors.cybersource import Cybersource from ecommerce.extensions.payment.processors.cybersource import Cybersource
from ecommerce.extensions.payment.processors.paypal import Paypal from ecommerce.extensions.payment.processors.paypal import Paypal
...@@ -36,6 +37,8 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase) ...@@ -36,6 +37,8 @@ class CybersourceNotifyViewTests(CybersourceMixin, PaymentEventsMixin, TestCase)
def setUp(self): def setUp(self):
super(CybersourceNotifyViewTests, self).setUp() super(CybersourceNotifyViewTests, self).setUp()
self.site.siteconfiguration.enable_otto_receipt_page = True
self.user = factories.UserFactory() self.user = factories.UserFactory()
self.billing_address = self.make_billing_address() self.billing_address = self.make_billing_address()
...@@ -301,6 +304,7 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase) ...@@ -301,6 +304,7 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase)
self.basket = factories.create_basket() self.basket = factories.create_basket()
self.basket.owner = factories.UserFactory() self.basket.owner = factories.UserFactory()
self.basket.site = self.site
self.basket.freeze() self.basket.freeze()
self.processor = Paypal() self.processor = Paypal()
...@@ -312,7 +316,7 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase) ...@@ -312,7 +316,7 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase)
@httpretty.activate @httpretty.activate
def _assert_execution_redirect(self, payer_info=None, url_redirect=None): def _assert_execution_redirect(self, payer_info=None, url_redirect=None):
"""Verify redirection to the configured receipt page after attempted payment execution.""" """Verify redirection to Otto receipt page after attempted payment execution."""
self.mock_oauth2_response() self.mock_oauth2_response()
# Create a payment record the view can use to retrieve a basket # Create a payment record the view can use to retrieve a basket
...@@ -325,7 +329,10 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase) ...@@ -325,7 +329,10 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase)
response = self.client.get(reverse('paypal_execute'), self.RETURN_DATA) response = self.client.get(reverse('paypal_execute'), self.RETURN_DATA)
self.assertRedirects( self.assertRedirects(
response, response,
url_redirect or u'{}?orderNum={}'.format(self.processor.receipt_url, self.basket.order_number), url_redirect or get_receipt_page_url(
order_number=self.basket.order_number,
site_configuration=self.basket.site.siteconfiguration
),
fetch_redirect_response=False fetch_redirect_response=False
) )
...@@ -357,6 +364,30 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase) ...@@ -357,6 +364,30 @@ class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase)
(logger_name, 'ERROR', error_message) (logger_name, 'ERROR', error_message)
) )
@httpretty.activate
def test_execution_redirect_to_lms(self):
"""
Verify redirection to LMS receipt page after attempted payment execution if Otto receipt page waffle
switch is disabled.
"""
self.site.siteconfiguration.enable_otto_receipt_page = False
self.mock_oauth2_response()
# Create a payment record the view can use to retrieve a basket
self.mock_payment_creation_response(self.basket)
self.processor.get_transaction_parameters(self.basket, request=self.request)
self.mock_payment_execution_response(self.basket)
response = self.client.get(reverse('paypal_execute'), self.RETURN_DATA)
self.assertRedirects(
response,
get_receipt_page_url(
order_number=self.basket.order_number,
site_configuration=self.basket.site.siteconfiguration
),
fetch_redirect_response=False
)
@ddt.data( @ddt.data(
None, # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object None, # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
{"shipping_address": None}, # minimal data, which may be sent in some Paypal execution responses {"shipping_address": None}, # minimal data, which may be sent in some Paypal execution responses
......
...@@ -16,6 +16,7 @@ from oscar.apps.payment.exceptions import PaymentError, UserCancelled, Transacti ...@@ -16,6 +16,7 @@ from oscar.apps.payment.exceptions import PaymentError, UserCancelled, Transacti
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.exceptions import InvalidSignatureError from ecommerce.extensions.payment.exceptions import InvalidSignatureError
from ecommerce.extensions.payment.processors.cybersource import Cybersource from ecommerce.extensions.payment.processors.cybersource import Cybersource
from ecommerce.extensions.payment.processors.paypal import Paypal from ecommerce.extensions.payment.processors.paypal import Paypal
...@@ -225,7 +226,10 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View): ...@@ -225,7 +226,10 @@ class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View):
if not basket: if not basket:
return redirect(self.payment_processor.error_url) return redirect(self.payment_processor.error_url)
receipt_url = u'{}?orderNum={}'.format(self.payment_processor.receipt_url, basket.order_number) receipt_url = get_receipt_page_url(
order_number=basket.order_number,
site_configuration=basket.site.siteconfiguration
)
try: try:
with transaction.atomic(): with transaction.atomic():
......
...@@ -16,6 +16,7 @@ import pytz ...@@ -16,6 +16,7 @@ import pytz
from ecommerce.core.url_utils import get_ecommerce_url from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.api import exceptions from ecommerce.extensions.api import exceptions
from ecommerce.extensions.offer.utils import get_discount_percentage, get_discount_value
from ecommerce.invoice.models import Invoice from ecommerce.invoice.models import Invoice
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -479,17 +480,17 @@ def get_voucher_discount_info(benefit, price): ...@@ -479,17 +480,17 @@ def get_voucher_discount_info(benefit, price):
if benefit.type == Benefit.PERCENTAGE: if benefit.type == Benefit.PERCENTAGE:
return { return {
'discount_percentage': benefit_value, 'discount_percentage': benefit_value,
'discount_value': benefit_value * price / 100.0, 'discount_value': get_discount_value(discount_percentage=benefit_value, product_price=price),
'is_discounted': True if benefit.value < 100 else False 'is_discounted': True if benefit.value < 100 else False
} }
else: else:
discount_percentage = benefit_value / price * 100.0 discount_percentage = get_discount_percentage(discount_value=benefit_value, product_price=price)
if discount_percentage > 100: if discount_percentage > 100:
discount_percentage = 100.00 discount_percentage = 100.00
discount_value = price discount_value = price
else: else:
discount_percentage = float(discount_percentage) discount_percentage = discount_percentage
discount_value = benefit.value discount_value = benefit_value
return { return {
'discount_percentage': discount_percentage, 'discount_percentage': discount_percentage,
'discount_value': float(discount_value), 'discount_value': float(discount_value),
......
...@@ -104,7 +104,7 @@ PAYMENT_PROCESSORS = ( ...@@ -104,7 +104,7 @@ PAYMENT_PROCESSORS = (
'ecommerce.extensions.payment.processors.paypal.Paypal', 'ecommerce.extensions.payment.processors.paypal.Paypal',
) )
PAYMENT_PROCESSOR_RECEIPT_PATH = '/commerce/checkout/receipt/' PAYMENT_PROCESSOR_RECEIPT_PATH = '/checkout/receipt/'
PAYMENT_PROCESSOR_CANCEL_PATH = '/checkout/cancel-checkout/' PAYMENT_PROCESSOR_CANCEL_PATH = '/checkout/cancel-checkout/'
PAYMENT_PROCESSOR_ERROR_PATH = '/checkout/error/' PAYMENT_PROCESSOR_ERROR_PATH = '/checkout/error/'
......
...@@ -503,7 +503,7 @@ CELERY_ALWAYS_EAGER = False ...@@ -503,7 +503,7 @@ CELERY_ALWAYS_EAGER = False
THEME_SCSS = 'sass/themes/default.scss' THEME_SCSS = 'sass/themes/default.scss'
# Path to the receipt page # Path to the receipt page
RECEIPT_PAGE_PATH = '/commerce/checkout/receipt/' RECEIPT_PAGE_PATH = '/checkout/receipt/'
# URL for Course Catalog service # URL for Course Catalog service
COURSE_CATALOG_API_URL = 'http://localhost:8008/api/v1/' COURSE_CATALOG_API_URL = 'http://localhost:8008/api/v1/'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment