-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: rollback_transaction() also cancels external fulfillments
This is needed for cases where an external fulfillment has been created, but the subsequent platform fulfillment fails (e.g. due to transient networking issues). Before this PR, the external fulfillment would not get rolled back, resulting in an "orphaned" fulfillment that allows learners to effectively redeem content for free. After this PR, that loophole is fixed. ENT-8866
- Loading branch information
Showing
4 changed files
with
126 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -475,13 +475,20 @@ def test_redeem_with_geag_exception(self, mock_get_content_summary, mock_enterpr | |
'source': 'edX', | ||
'mode': 'verified', | ||
'content_price': 10000, | ||
# When this key value is non-None, it triggers an attempt to create an external fulfillment. This attempt | ||
# will fail because the metadata below is missing a bunch of required keys, e.g. 'geag_date_of_birth'. | ||
'geag_variant_id': str(uuid4()), | ||
} | ||
mock_price_for_content.return_value = mock_content_price | ||
mock_enterprise_client.enroll.return_value = mock_enterprise_fulfillment_uuid | ||
tx_metadata = { | ||
'geag_first_name': 'Donny', | ||
'geag_last_name': 'Kerabatsos', | ||
# The following required keys are missing and will cause external fulfillment to fail. | ||
# 'geag_email': , | ||
# 'geag_date_of_birth': , | ||
# 'geag_terms_accepted_at': , | ||
# 'geag_data_share_consent': , | ||
} | ||
with pytest.raises(InvalidFulfillmentMetadataException): | ||
self.subsidy.redeem( | ||
|
@@ -493,6 +500,67 @@ def test_redeem_with_geag_exception(self, mock_get_content_summary, mock_enterpr | |
created_transaction = Transaction.objects.latest('created') | ||
assert created_transaction.state == TransactionStateChoices.FAILED | ||
|
||
@mock.patch('enterprise_subsidy.apps.subsidy.models.Subsidy.price_for_content') | ||
@mock.patch('enterprise_subsidy.apps.subsidy.models.Subsidy.enterprise_client') | ||
@mock.patch("enterprise_subsidy.apps.content_metadata.api.ContentMetadataApi.get_content_summary") | ||
@mock.patch("enterprise_subsidy.apps.api_client.enterprise.EnterpriseApiClient.get_enterprise_customer_data") | ||
@mock.patch("enterprise_subsidy.apps.fulfillment.api.GEAGFulfillmentHandler._fulfill_in_geag") | ||
@mock.patch("enterprise_subsidy.apps.fulfillment.api.GEAGFulfillmentHandler.cancel_fulfillment") | ||
def test_redeem_with_platform_exception_rolls_back_geag( | ||
self, | ||
mock_cancel_fulfillment, | ||
mock_fulfill_in_geag, | ||
mock_get_enterprise_customer_data, | ||
mock_get_content_summary, | ||
mock_enterprise_client, | ||
mock_price_for_content, | ||
): | ||
""" | ||
Test Subsidy.redeem() rollback upon platform networking exception handles geag cancellation. | ||
""" | ||
lms_user_id = 1 | ||
content_key = "course-v1:edX+test+course" | ||
subsidy_access_policy_uuid = str(uuid4()) | ||
mock_content_price = 1000 | ||
mock_fulfillment_order_uuid = str(uuid4()) | ||
mock_get_content_summary.return_value = { | ||
'content_uuid': 'course-v1:edX+test+course', | ||
'content_key': 'course-v1:edX+test+course', | ||
'source': 'edX', | ||
'mode': 'verified', | ||
'content_price': 10000, | ||
'geag_variant_id': str(uuid4()), | ||
} | ||
mock_price_for_content.return_value = mock_content_price | ||
# Create the conditions for a failed platform fulfillment after a successful external fulfillment. | ||
mock_enterprise_client.enroll.side_effect = HTTPError( | ||
response=MockResponse(None, status.HTTP_500_INTERNAL_SERVER_ERROR), | ||
) | ||
mock_get_enterprise_customer_data.return_value = {} | ||
mock_fulfill_in_geag.return_value.json.return_value = { | ||
'orderUuid': mock_fulfillment_order_uuid, | ||
} | ||
tx_metadata = { | ||
'geag_first_name': 'Donny', | ||
'geag_last_name': 'Kerabatsos', | ||
'geag_email': '[email protected]', | ||
'geag_date_of_birth': '1990-01-01', | ||
'geag_terms_accepted_at': '2024-01-01T00:00:00Z', | ||
'geag_data_share_consent': True, | ||
} | ||
with pytest.raises(HTTPError): | ||
self.subsidy.redeem( | ||
lms_user_id, | ||
content_key, | ||
subsidy_access_policy_uuid, | ||
metadata=tx_metadata | ||
) | ||
created_transaction = Transaction.objects.latest('created') | ||
assert created_transaction.state == TransactionStateChoices.FAILED | ||
# The meat of what's being tested: Did we attempt to cancel the external fulfillment? | ||
assert mock_cancel_fulfillment.called | ||
assert mock_cancel_fulfillment.call_args.args[0].external_reference_id == mock_fulfillment_order_uuid | ||
|
||
|
||
class SubsidyManagerTestCase(TestCase): | ||
""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters