diff --git a/ecommerce/enterprise/tests/mixins.py b/ecommerce/enterprise/tests/mixins.py index 2473240bf51..f91b943b9c7 100644 --- a/ecommerce/enterprise/tests/mixins.py +++ b/ecommerce/enterprise/tests/mixins.py @@ -464,6 +464,15 @@ def mock_consent_get(self, username, course_id, ec_uuid): ec_uuid ) + def mock_consent_post(self, username, course_id, ec_uuid): + self.mock_consent_response( + username, + course_id, + ec_uuid, + method=responses.POST, + granted=True + ) + def mock_consent_missing(self, username, course_id, ec_uuid): self.mock_consent_response( username, diff --git a/ecommerce/enterprise/tests/test_utils.py b/ecommerce/enterprise/tests/test_utils.py index 73d89a4336e..1e10d88b313 100644 --- a/ecommerce/enterprise/tests/test_utils.py +++ b/ecommerce/enterprise/tests/test_utils.py @@ -18,6 +18,7 @@ from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin from ecommerce.enterprise.utils import ( CUSTOMER_CATALOGS_DEFAULT_RESPONSE, + create_enterprise_customer_user_consent, enterprise_customer_user_needs_consent, find_active_enterprise_customer_user, get_enterprise_catalog, @@ -144,6 +145,23 @@ def test_ecu_needs_consent(self): self.mock_consent_not_required(**opts) self.assertEqual(enterprise_customer_user_needs_consent(**kw), False) + @responses.activate + def test_ecu_create_consent(self): + opts = { + 'ec_uuid': 'fake-uuid', + 'course_id': 'course-v1:real+course+id', + 'username': 'johnsmith', + } + kw = { + 'enterprise_customer_uuid': 'fake-uuid', + 'course_id': 'course-v1:real+course+id', + 'username': 'johnsmith', + 'site': self.site + } + self.mock_access_token_response() + self.mock_consent_post(**opts) + self.assertEqual(create_enterprise_customer_user_consent(**kw), True) + def test_get_enterprise_customer_uuid(self): """ Verify that enterprise customer UUID is returned for a voucher with an associated enterprise customer. diff --git a/ecommerce/enterprise/utils.py b/ecommerce/enterprise/utils.py index 7e3aec5993e..192789e8fe0 100644 --- a/ecommerce/enterprise/utils.py +++ b/ecommerce/enterprise/utils.py @@ -355,6 +355,34 @@ def enterprise_customer_user_needs_consent(site, enterprise_customer_uuid, cours return response.json()['consent_required'] +def create_enterprise_customer_user_consent(site, enterprise_customer_uuid, course_id, username): + """ + Create a new consent for a particular username/EC UUID/course ID combination if one doesn't already exist. + + Args: + site (Site): The site which is handling the consent-sensitive request + enterprise_customer_uuid (str): The UUID of the relevant EnterpriseCustomer + course_id (str): The ID of the relevant course for enrollment + username (str): The username of the user attempting to enroll into the course + + Returns: + bool: consent recorded for the user specified by the username argument + for the EnterpriseCustomer specified by the enterprise_customer_uuid + argument and the course specified by the course_id argument. + """ + data = { + "username": username, + "enterprise_customer_uuid": enterprise_customer_uuid, + "course_id": course_id + } + api_client = site.siteconfiguration.oauth_api_client + consent_url = urljoin(f"{site.siteconfiguration.consent_api_url}/", "data_sharing_consent") + + response = api_client.post(consent_url, json=data) + response.raise_for_status() + return response.json()['consent_provided'] + + def get_enterprise_customer_uuid_from_voucher(voucher): """ Given a Voucher, find the associated Enterprise Customer UUID, if it exists. diff --git a/ecommerce/extensions/executive_education_2u/mixins.py b/ecommerce/extensions/executive_education_2u/mixins.py index 5ee55b2efc9..ff4015fb29e 100644 --- a/ecommerce/extensions/executive_education_2u/mixins.py +++ b/ecommerce/extensions/executive_education_2u/mixins.py @@ -17,6 +17,7 @@ def place_free_order( address, user_details, terms_accepted_at, + data_share_consent, request=None, ): # pylint: disable=arguments-differ """ @@ -49,6 +50,7 @@ def place_free_order( 'address': address, 'user_details': user_details, 'terms_accepted_at': terms_accepted_at, + 'data_share_consent': data_share_consent, }) # Place an order. If order placement succeeds, the order is committed diff --git a/ecommerce/extensions/executive_education_2u/serializers.py b/ecommerce/extensions/executive_education_2u/serializers.py index a4e2d9773d2..43692ff05ae 100644 --- a/ecommerce/extensions/executive_education_2u/serializers.py +++ b/ecommerce/extensions/executive_education_2u/serializers.py @@ -26,3 +26,4 @@ class CheckoutActionSerializer(serializers.Serializer): # pylint: disable=abstr address = AddressSerializer(required=False) user_details = UserDetailsSerializer() terms_accepted_at = serializers.CharField() + data_share_consent = serializers.BooleanField(required=False) diff --git a/ecommerce/extensions/executive_education_2u/tests/test_mixins.py b/ecommerce/extensions/executive_education_2u/tests/test_mixins.py index d4980311114..01de8aecaad 100644 --- a/ecommerce/extensions/executive_education_2u/tests/test_mixins.py +++ b/ecommerce/extensions/executive_education_2u/tests/test_mixins.py @@ -44,6 +44,7 @@ def setUp(self): 'mobile_phone': '1234567890' } self.mock_terms_accepted_at = '2022-08-05T15:28:46.493Z', + self.mock_data_share_consent = True # Ensure that the basket attribute type exists for these tests self.basket_attribute_type, _ = BasketAttributeType.objects.get_or_create( @@ -57,12 +58,14 @@ def test_order_note_created(self): 'address': self.mock_address, 'user_details': self.mock_user_details, 'terms_accepted_at': self.mock_terms_accepted_at, + 'data_share_consent': self.mock_data_share_consent, }) order = ExecutiveEducation2UOrderPlacementMixin().place_free_order( basket, self.mock_address, self.mock_user_details, self.mock_terms_accepted_at, + self.mock_data_share_consent, ) self.assertEqual(basket.status, Basket.SUBMITTED) @@ -77,4 +80,5 @@ def test_non_free_basket_order(self): self.mock_address, self.mock_user_details, self.mock_terms_accepted_at, + self.mock_data_share_consent, ) diff --git a/ecommerce/extensions/executive_education_2u/tests/test_views.py b/ecommerce/extensions/executive_education_2u/tests/test_views.py index 8f9614b4065..c83cf4c7e75 100644 --- a/ecommerce/extensions/executive_education_2u/tests/test_views.py +++ b/ecommerce/extensions/executive_education_2u/tests/test_views.py @@ -348,6 +348,7 @@ def _create_finish_checkout_payload(self, sku): 'mobile_phone': '1234567890' }, 'terms_accepted_at': '2022-08-05T15:28:46.493Z', + 'data_share_consent': True, } def _create_basket(self, product, has_offer=False): diff --git a/ecommerce/extensions/executive_education_2u/views.py b/ecommerce/extensions/executive_education_2u/views.py index 7930ccb91c4..cfe35c66c14 100644 --- a/ecommerce/extensions/executive_education_2u/views.py +++ b/ecommerce/extensions/executive_education_2u/views.py @@ -355,6 +355,7 @@ def finish_checkout(self, request): address=request.data.get('address', {}), user_details={**request.data['user_details'], 'email': request.user.email}, terms_accepted_at=request.data['terms_accepted_at'], + data_share_consent=request.data.get('data_share_consent', None), request=request ) diff --git a/ecommerce/extensions/fulfillment/modules.py b/ecommerce/extensions/fulfillment/modules.py index 3d45485a9a8..33ebe0f0e5f 100644 --- a/ecommerce/extensions/fulfillment/modules.py +++ b/ecommerce/extensions/fulfillment/modules.py @@ -29,10 +29,11 @@ ) from ecommerce.core.url_utils import get_lms_enrollment_api_url, get_lms_entitlement_api_url from ecommerce.courses.models import Course -from ecommerce.courses.utils import mode_for_product +from ecommerce.courses.utils import get_course_info_from_catalog, mode_for_product from ecommerce.enterprise.conditions import BasketAttributeType from ecommerce.enterprise.mixins import EnterpriseDiscountMixin from ecommerce.enterprise.utils import ( + create_enterprise_customer_user_consent, get_enterprise_customer_uuid_from_voucher, get_or_create_enterprise_customer_user ) @@ -949,8 +950,9 @@ def _create_enterprise_allocation_payload( # This will be the offer that was applied. We will let an error be thrown if this doesn't exist. discount = order.discounts.first() enterprise_customer_uuid = str(discount.offer.condition.enterprise_customer_uuid) + data_share_consent = fulfillment_details.get('data_share_consent', None) - return { + payload = { 'payment_reference': order.number, 'enterprise_customer_uuid': enterprise_customer_uuid, 'currency': currency, @@ -966,8 +968,30 @@ def _create_enterprise_allocation_payload( ], **fulfillment_details.get('address', {}), **fulfillment_details.get('user_details', {}), - 'terms_accepted_at': fulfillment_details.get('terms_accepted_at', '') + 'terms_accepted_at': fulfillment_details.get('terms_accepted_at', ''), } + if data_share_consent: + payload['data_share_consent'] = data_share_consent + + return payload + + def _create_enterprise_customer_user_consent( + self, + order, + line, + fulfillment_details + ): + data_share_consent = fulfillment_details.get('data_share_consent', None) + course_info = get_course_info_from_catalog(order.site, line.product) + if data_share_consent and course_info: + discount = order.discounts.first() + enterprise_customer_uuid = str(discount.offer.condition.enterprise_customer_uuid) + create_enterprise_customer_user_consent( + site=order.site, + enterprise_customer_uuid=enterprise_customer_uuid, + course_id=course_info['key'], + username=order.user.username + ) def _get_fulfillment_details(self, order): fulfillment_details_note = order.notes.filter(note_type='Fulfillment Details').first() @@ -1020,6 +1044,11 @@ def fulfill_product(self, order, lines, email_opt_in=False): ) try: + self._create_enterprise_customer_user_consent( + order=order, + line=line, + fulfillment_details=fulfillment_details + ) self.get_smarter_client.create_enterprise_allocation(**allocation_payload) except Exception as ex: # pylint: disable=broad-except reason = '' diff --git a/ecommerce/extensions/fulfillment/tests/test_modules.py b/ecommerce/extensions/fulfillment/tests/test_modules.py index 0d1b5dc2a2a..60be1a7c036 100644 --- a/ecommerce/extensions/fulfillment/tests/test_modules.py +++ b/ecommerce/extensions/fulfillment/tests/test_modules.py @@ -1253,6 +1253,7 @@ def setUp(self): 'mobile_phone': '+12015551234', }, 'terms_accepted_at': '2022-07-25T10:29:56Z', + 'data_share_consent': True }) self.mock_settings = { @@ -1304,37 +1305,59 @@ def test_fulfill_product_maformed_fulfillment_details(self, mock_logger): self.exec_ed_2u_entitlement_line.order.number ) + @mock.patch('ecommerce.extensions.fulfillment.modules.create_enterprise_customer_user_consent') + @mock.patch('ecommerce.extensions.fulfillment.modules.get_course_info_from_catalog') @mock.patch('ecommerce.extensions.fulfillment.modules.GetSmarterEnterpriseApiClient') - def test_fulfill_product_success(self, mock_geag_client): + def test_fulfill_product_success( + self, + mock_geag_client, + mock_get_course_info_from_catalog, + mock_create_enterprise_customer_user_consent + ): with self.settings(**self.mock_settings): mock_create_enterprise_allocation = mock.MagicMock() mock_geag_client.return_value = mock.MagicMock( create_enterprise_allocation=mock_create_enterprise_allocation ) + mock_get_course_info_from_catalog.return_value = { + 'key': 'test_course_key1' + } self.order.notes.create(message=self.fulfillment_details, note_type='Fulfillment Details') ExecutiveEducation2UFulfillmentModule().fulfill_product( self.order, [self.exec_ed_2u_entitlement_line, self.exec_ed_2u_entitlement_line_2] ) self.assertEqual(mock_create_enterprise_allocation.call_count, 2) + self.assertEqual(mock_create_enterprise_customer_user_consent.call_count, 2) self.assertEqual(self.exec_ed_2u_entitlement_line.status, LINE.COMPLETE) self.assertEqual(self.exec_ed_2u_entitlement_line_2.status, LINE.COMPLETE) self.assertFalse(self.order.notes.exists()) + @mock.patch('ecommerce.extensions.fulfillment.modules.create_enterprise_customer_user_consent') + @mock.patch('ecommerce.extensions.fulfillment.modules.get_course_info_from_catalog') @mock.patch('ecommerce.extensions.fulfillment.modules.GetSmarterEnterpriseApiClient') - def test_fulfill_product_error(self, mock_geag_client): + def test_fulfill_product_error( + self, + mock_geag_client, + mock_get_course_info_from_catalog, + mock_create_enterprise_customer_user_consent + ): with self.settings(**self.mock_settings): mock_create_enterprise_allocation = mock.MagicMock() mock_create_enterprise_allocation.side_effect = [None, Exception("Uh oh.")] mock_geag_client.return_value = mock.MagicMock( create_enterprise_allocation=mock_create_enterprise_allocation ) + mock_get_course_info_from_catalog.return_value = { + 'key': 'test_course_key1' + } self.order.notes.create(message=self.fulfillment_details, note_type='Fulfillment Details') ExecutiveEducation2UFulfillmentModule().fulfill_product( self.order, [self.exec_ed_2u_entitlement_line, self.exec_ed_2u_entitlement_line_2] ) self.assertEqual(mock_create_enterprise_allocation.call_count, 2) + self.assertEqual(mock_create_enterprise_customer_user_consent.call_count, 2) self.assertEqual(self.exec_ed_2u_entitlement_line.status, LINE.COMPLETE) self.assertEqual(self.exec_ed_2u_entitlement_line_2.status, LINE.FULFILLMENT_SERVER_ERROR) self.assertTrue(self.order.notes.exists()) diff --git a/requirements/base.txt b/requirements/base.txt index 07ec08b86b3..5ea33b23e8f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -258,7 +258,7 @@ funcsigs==1.0.2 # via cybersource-rest-client-python future==0.18.2 # via pyjwkest -getsmarter-api-clients==0.4.0 +getsmarter-api-clients==0.5.0 # via -r requirements/base.in google-api-core==1.30.0 # via google-api-python-client diff --git a/requirements/dev.txt b/requirements/dev.txt index cebcdeddd8b..d1515d0b83c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -373,7 +373,7 @@ future==0.18.2 # via # -r requirements/test.txt # pyjwkest -getsmarter-api-clients==0.4.0 +getsmarter-api-clients==0.5.0 # via -r requirements/test.txt gitdb==4.0.9 # via gitpython diff --git a/requirements/production.txt b/requirements/production.txt index 15fbe607eb6..f6eeea9ca95 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -268,7 +268,7 @@ future==0.18.2 # via # django-ses # pyjwkest -getsmarter-api-clients==0.4.0 +getsmarter-api-clients==0.5.0 # via -r requirements/base.in # via # django-ses diff --git a/requirements/test.txt b/requirements/test.txt index e2591d258da..45bb8b087d5 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -379,7 +379,7 @@ future==0.18.2 # via # -r requirements/base.txt # pyjwkest -getsmarter-api-clients==0.4.0 +getsmarter-api-clients==0.5.0 # via -r requirements/base.txt google-api-core==1.30.0 # via