diff --git a/commerce_coordinator/apps/lms/signals.py b/commerce_coordinator/apps/lms/signals.py new file mode 100644 index 000000000..f6269d1e7 --- /dev/null +++ b/commerce_coordinator/apps/lms/signals.py @@ -0,0 +1,28 @@ +""" +LMS app signals and receivers. +""" +import logging + +from commerce_coordinator.apps.core.signal_helpers import CoordinatorSignal, coordinator_receiver +from commerce_coordinator.apps.lms.tasks import fulfill_order_placed_send_enroll_in_course_task + +logger = logging.getLogger(__name__) + +fulfill_order_placed_signal = CoordinatorSignal() + + +@coordinator_receiver(logger) +def fulfill_order_placed_send_enroll_in_course(**kwargs): + """ + Fulfill the order placed in Titan with a Celery task to LMS to enroll a user in a single course. + """ + fulfill_order_placed_send_enroll_in_course_task.delay( + coupon_code=kwargs['coupon_code'], + course_id=kwargs['course_id'], + date_placed=kwargs['date_placed'], + edx_lms_user_id=kwargs['edx_lms_user_id'], + edx_lms_username=kwargs['edx_lms_username'], + mode=kwargs['mode'], + partner_sku=kwargs['partner_sku'], + titan_order_uuid=kwargs['titan_order_uuid'], + ) diff --git a/commerce_coordinator/apps/lms/tasks.py b/commerce_coordinator/apps/lms/tasks.py new file mode 100644 index 000000000..1608281bc --- /dev/null +++ b/commerce_coordinator/apps/lms/tasks.py @@ -0,0 +1,35 @@ +""" +LMS Celery tasks +""" +from celery import shared_task +from celery.utils.log import get_task_logger + +# Use the special Celery logger for our tasks +logger = get_task_logger(__name__) + + +@shared_task() +def fulfill_order_placed_send_enroll_in_course_task( + coupon_code, + course_id, + date_placed, + edx_lms_user_id, + edx_lms_username, + mode, + partner_sku, + titan_order_uuid, +): + """ + Celery task for order placed fulfillment and enrollment via LMS Enrollment API. + """ + logger.info( + f'LMS fulfill_order_placed_send_enroll_in_course_task fired with coupon {coupon_code},' + f'course ID {course_id}, on {date_placed}, for LMS user ID {edx_lms_user_id}, with mode {mode},' + f'SKU {partner_sku}, for Titan Order: {titan_order_uuid}.' + ) + + # TODO: make the API call to LMS here. + # Temporary if statement below since username is PII and cannot + # be logged but will be used as enrollment data in the next commit + if edx_lms_username: + logger.info('Calling LMS enrollment API...') diff --git a/commerce_coordinator/apps/lms/views.py b/commerce_coordinator/apps/lms/views.py index 3e5b360bd..1a91d4a2f 100644 --- a/commerce_coordinator/apps/lms/views.py +++ b/commerce_coordinator/apps/lms/views.py @@ -5,7 +5,6 @@ import logging from edx_rest_framework_extensions.permissions import LoginRedirectIfUnauthenticated -from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView @@ -17,7 +16,6 @@ class EnrollmentView(APIView): API for LMS enrollment. """ permission_classes = [LoginRedirectIfUnauthenticated] - authentication_classes = [SessionAuthentication, BasicAuthentication] throttle_classes = [UserRateThrottle] logger.info('LMS app views') diff --git a/commerce_coordinator/apps/titan/signals.py b/commerce_coordinator/apps/titan/signals.py index a417922c3..64681074a 100644 --- a/commerce_coordinator/apps/titan/signals.py +++ b/commerce_coordinator/apps/titan/signals.py @@ -4,7 +4,7 @@ import logging -from commerce_coordinator.apps.core.signal_helpers import coordinator_receiver +from commerce_coordinator.apps.core.signal_helpers import CoordinatorSignal, coordinator_receiver from commerce_coordinator.apps.titan.tasks import ( enrollment_code_redemption_requested_create_order_oauth_task, enrollment_code_redemption_requested_create_order_task @@ -12,6 +12,8 @@ logger = logging.getLogger(__name__) +fulfill_order_placed_signal = CoordinatorSignal() + @coordinator_receiver(logger) def enrollment_code_redemption_requested_create_order(**kwargs): diff --git a/commerce_coordinator/apps/titan/urls.py b/commerce_coordinator/apps/titan/urls.py index 349dc1138..0769c7ce5 100644 --- a/commerce_coordinator/apps/titan/urls.py +++ b/commerce_coordinator/apps/titan/urls.py @@ -2,4 +2,12 @@ URLS for the titan app """ -# from django.urls import include, path +from django.urls import path + +from .views import OrderFulfillView + +app_name = 'titan' + +urlpatterns = [ + path('fulfill/', OrderFulfillView.as_view(), name='order_fulfill'), +] diff --git a/commerce_coordinator/apps/titan/views.py b/commerce_coordinator/apps/titan/views.py index 9fde4cfb6..e8422efbf 100644 --- a/commerce_coordinator/apps/titan/views.py +++ b/commerce_coordinator/apps/titan/views.py @@ -2,6 +2,85 @@ Views for the titan app """ -# from django.shortcuts import render +import logging -# Create your views here. +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle +from rest_framework.views import APIView + +from commerce_coordinator.apps.core.signal_helpers import format_signal_results + +from .signals import fulfill_order_placed_signal + +logger = logging.getLogger(__name__) + + +class OrderFulfillView(APIView): + """ + API for order fulfillment that is called from Titan. + """ + permission_classes = [IsAdminUser] + throttle_classes = [UserRateThrottle] + + def post(self, request): + """ + POST request handler for /order/fulfill + + Requires a JSON object of the following format: + { + "coupon_code": "WELCOME100", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "date_placed": "2022-08-24T16:57:00.127327+00:00", + "edx_lms_user_id": 1, + "edx_lms_username": "test-user", + "mode": "verified", + "partner_sku": "test-sku", + "titan_order_uuid": "123-abc", + + } + + Returns a JSON object of the following format: + { + "": { + "response": "", + "error": false, + + }, + + } + """ + coupon_code = request.data.get('coupon_code') + course_id = request.data.get('course_id') + date_placed = request.data.get('date_placed') + edx_lms_user_id = request.data.get('edx_lms_user_id') + edx_lms_username = request.data.get('edx_lms_username') + mode = request.data.get('mode') + partner_sku = request.data.get('partner_sku') + titan_order_uuid = request.data.get('titan_order_uuid') + + # TODO: add enterprise data for enrollment API here + + # TODO: add credit_provider data here + # /ecommerce/extensions/fulfillment/modules.py#L315 + + logger.info( + 'Attempting to fulfill Titan order ID [%s] for user ID [%s], course ID [%s], on [%s]', + titan_order_uuid, + edx_lms_user_id, + course_id, + date_placed, + ) + + results = fulfill_order_placed_signal.send_robust( + sender=self.__class__, + date_placed=date_placed, + edx_lms_user_id=edx_lms_user_id, + edx_lms_username=edx_lms_username, + course_id=course_id, + coupon_code=coupon_code, + mode=mode, + partner_sku=partner_sku, + titan_order_uuid=titan_order_uuid, + ) + return Response(format_signal_results(results)) diff --git a/commerce_coordinator/settings/base.py b/commerce_coordinator/settings/base.py index 2c0863e28..afd425dfb 100644 --- a/commerce_coordinator/settings/base.py +++ b/commerce_coordinator/settings/base.py @@ -294,6 +294,9 @@ def root(*x): 'commerce_coordinator.apps.ecommerce.signals.enrollment_code_redemption_requested_signal': [ 'commerce_coordinator.apps.titan.signals.enrollment_code_redemption_requested_create_order', ], + 'commerce_coordinator.apps.titan.signals.fulfill_order_placed_signal': [ + 'commerce_coordinator.apps.lms.signals.fulfill_order_placed_send_enroll_in_course', + ], } # Default timeouts for requests diff --git a/commerce_coordinator/urls.py b/commerce_coordinator/urls.py index f26ca6145..8716af3a3 100644 --- a/commerce_coordinator/urls.py +++ b/commerce_coordinator/urls.py @@ -34,6 +34,7 @@ from commerce_coordinator.apps.ecommerce import urls as ecommerce_urls from commerce_coordinator.apps.frontend_app_ecommerce import urls as orders_urls from commerce_coordinator.apps.lms import urls as lms_urls +from commerce_coordinator.apps.titan import urls as titan_urls admin.autodiscover() @@ -46,6 +47,7 @@ path('ecommerce/', include(ecommerce_urls), name='ecommerce'), path('lms/', include(lms_urls), name='lms'), path('health/', core_views.health, name='health'), + path('titan/', include(titan_urls), name='titan'), path('orders/', include(orders_urls)), # DEMO: Currently this is only test code, we may want to decouple LMS code here at some point... path('demo_lms/', include(demo_lms_urls))