Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
fix: Pick the right purchase from ios response (#3921)
Browse files Browse the repository at this point in the history
* fix: Pick the right purchase from ios response

iOS response contain multiple purchases, instead of picking the first purchase,
pick the one which have given product id and latest date.
LEARNER-9261
  • Loading branch information
jawad-khan authored Mar 16, 2023
1 parent c74de17 commit 9495e3e
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 20 deletions.
3 changes: 2 additions & 1 deletion ecommerce/extensions/iap/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ def test_iap_payment_execution_ios(self):
'receipt': {
'in_app': [{
'original_transaction_id': '123456',
'transaction_id': '123456'
'transaction_id': '123456',
'product_id': 'fake_product_id'
}]
}
}
Expand Down
18 changes: 18 additions & 0 deletions ecommerce/extensions/iap/processors/base_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def handle_processor_response(self, response, basket=None):
)
raise GatewayError(validation_response)

if self.NAME == 'ios-iap':
validation_response = self.parse_ios_response(validation_response, product_id)

transaction_id = response.get('transactionId', self._get_transaction_id_from_receipt(validation_response))
# original_transaction_id is primary identifier for a purchase on iOS
original_transaction_id = response.get('originalTransactionId', self._get_attribute_from_receipt(
Expand Down Expand Up @@ -147,6 +150,21 @@ def handle_processor_response(self, response, basket=None):
card_type=None
)

def parse_ios_response(self, response, product_id):
"""
iOS response has multiple receipts data, and we need to select the purchase we just made
with the given product id.
"""
purchases = response['receipt'].get('in_app', [])
for purchase in purchases:
if purchase['product_id'] == product_id and \
response['receipt']['receipt_creation_date_ms'] == purchase['purchase_date_ms']:

response['receipt']['in_app'] = [purchase]
break

return response

def record_processor_response(self, response, transaction_id=None, basket=None, original_transaction_id=None): # pylint: disable=arguments-differ
"""
Save the processor's response to the database for auditing.
Expand Down
59 changes: 40 additions & 19 deletions ecommerce/extensions/iap/tests/processors/test_ios_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,40 @@ def setUp(self):
u"IOSInAppPurchase's response was recorded in entry [{entry_id}]."
)
self.RETURN_DATA = {
'transactionId': 'transactionId.ios.test.purchased',
'originalTransactionId': 'originalTransactionId.ios.test.purchased',
'productId': 'ios.test.purchased',
'purchaseToken': 'inapp:org.edx.mobile:ios.test.purchased',
'transactionId': 'test_id',
'originalTransactionId': 'original_test_id',
'productId': 'test_product_id',
'purchaseToken': 'inapp:test.edx.edx:ios.test.purchased',
}
self.mock_validation_response = {
'environment': 'Sandbox',
'receipt': {
'bundle_id': 'test_bundle_id',
'in_app': [
{
'in_app_ownership_type': 'PURCHASED',
'original_transaction_id': 'very_old_purchase_id',
'product_id': 'org.edx.mobile.test_product1',
'purchase_date_ms': '1676562309000',
'transaction_id': 'vaery_old_purchase_id'
},
{
'in_app_ownership_type': 'PURCHASED',
'original_transaction_id': 'old_purchase_id',
'product_id': 'org.edx.mobile.test_product3',
'purchase_date_ms': '1676562544000',
'transaction_id': 'old_purchase_id'
},
{
'in_app_ownership_type': 'PURCHASED',
'original_transaction_id': 'original_test_id',
'product_id': 'test_product_id',
'purchase_date_ms': '1676562978000',
'transaction_id': 'test_id'
}
],
'receipt_creation_date_ms': '1676562978000',
}
}

def _get_receipt_url(self):
Expand Down Expand Up @@ -124,14 +154,13 @@ def test_handle_processor_response_payment_error(self, mock_ios_validator):
"""
Verify that appropriate PaymentError is raised in absence of originalTransactionId parameter.
"""
mock_ios_validator.return_value = {
'resource': {
'orderId': 'orderId.ios.test.purchased'
}
}
modified_validation_response = self.mock_validation_response
modified_validation_response['receipt']['in_app'][2].pop('original_transaction_id')
mock_ios_validator.return_value = modified_validation_response
with self.assertRaises(PaymentError):
modified_return_data = self.RETURN_DATA
modified_return_data.pop('originalTransactionId')

self.processor.handle_processor_response(modified_return_data, basket=self.basket)

@mock.patch.object(BaseIAP, '_is_payment_redundant')
Expand All @@ -141,11 +170,7 @@ def test_handle_processor_response_redundant_error(self, mock_ios_validator, moc
Verify that appropriate RedundantPaymentNotificationError is raised in case payment with same
originalTransactionId exists with another user
"""
mock_ios_validator.return_value = {
'resource': {
'orderId': 'orderId.ios.test.purchased'
}
}
mock_ios_validator.return_value = self.mock_validation_response
mock_payment_redundant.return_value = True

with self.assertRaises(RedundantPaymentNotificationError):
Expand All @@ -156,11 +181,7 @@ def test_handle_processor_response(self, mock_ios_validator): # pylint: disable
"""
Verify that the processor creates the appropriate PaymentEvent and Source objects.
"""
mock_ios_validator.return_value = {
'resource': {
'orderId': 'orderId.ios.test.purchased'
}
}
mock_ios_validator.return_value = self.mock_validation_response

handled_response = self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket)
self.assertEqual(handled_response.currency, self.basket.currency)
Expand Down

0 comments on commit 9495e3e

Please sign in to comment.