diff --git a/ecommerce/extensions/iap/migrations/0005_paymentprocessorresponseextension_meta_data.py b/ecommerce/extensions/iap/migrations/0005_paymentprocessorresponseextension_meta_data.py new file mode 100644 index 00000000000..ea9df106053 --- /dev/null +++ b/ecommerce/extensions/iap/migrations/0005_paymentprocessorresponseextension_meta_data.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.15 on 2023-06-20 06:38 + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('iap', '0004_create_disbale_mobile_repeat_order_switch'), + ] + + operations = [ + migrations.AddField( + model_name='paymentprocessorresponseextension', + name='meta_data', + field=jsonfield.fields.JSONField(default={}), + ), + ] diff --git a/ecommerce/extensions/iap/models.py b/ecommerce/extensions/iap/models.py index adce1871dfe..cf301084def 100644 --- a/ecommerce/extensions/iap/models.py +++ b/ecommerce/extensions/iap/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from jsonfield.fields import JSONField from solo.models import SingletonModel @@ -34,3 +35,4 @@ class PaymentProcessorResponseExtension(models.Model): related_name='extension') original_transaction_id = models.CharField(max_length=255, verbose_name=_('Original Transaction ID'), null=True, blank=True) + meta_data = JSONField(default={}) diff --git a/ecommerce/extensions/iap/processors/base_iap.py b/ecommerce/extensions/iap/processors/base_iap.py index b533bf85b9b..e908931c55e 100644 --- a/ecommerce/extensions/iap/processors/base_iap.py +++ b/ecommerce/extensions/iap/processors/base_iap.py @@ -119,6 +119,8 @@ def handle_processor_response(self, response, basket=None): # original_transaction_id is primary identifier for a purchase on iOS original_transaction_id = response.get('originalTransactionId', self._get_attribute_from_receipt( validation_response, 'original_transaction_id')) + currency_code = str(response.get('currency_code', '')) + price = str(response.get('price', '')) if self.NAME == 'ios-iap': if not original_transaction_id: @@ -133,7 +135,9 @@ def handle_processor_response(self, response, basket=None): validation_response, transaction_id=transaction_id, basket=basket, - original_transaction_id=original_transaction_id + original_transaction_id=original_transaction_id, + currency_code=currency_code, + price=price ) logger.info("Successfully executed [%s] payment [%s] for basket [%d].", self.NAME, product_id, basket.id) @@ -165,7 +169,8 @@ def parse_ios_response(self, response, product_id): return response - def record_processor_response(self, response, transaction_id=None, basket=None, original_transaction_id=None): # pylint: disable=arguments-differ + def record_processor_response(self, response, transaction_id=None, basket=None, original_transaction_id=None, # pylint: disable=arguments-differ + currency_code=None, price=None): # pylint: disable=arguments-differ """ Save the processor's response to the database for auditing. @@ -176,15 +181,20 @@ def record_processor_response(self, response, transaction_id=None, basket=None, transaction_id (string): Identifier for the transaction on the payment processor's servers original_transaction_id (string): Identifier for the transaction for purchase action only basket (Basket): Basket associated with the payment event (e.g., being purchased) + currency_code (string): (USD, PKR, AED etc) + price (string): Price paid by end user Return PaymentProcessorResponse """ processor_response = super(BaseIAP, self).record_processor_response(response, transaction_id=transaction_id, basket=basket) - if original_transaction_id: - PaymentProcessorResponseExtension.objects.create(processor_response=processor_response, - original_transaction_id=original_transaction_id) + + meta_data = self._get_metadata(currency_code=currency_code, price=price) + PaymentProcessorResponseExtension.objects.create( + processor_response=processor_response, original_transaction_id=original_transaction_id, + meta_data=meta_data) + return processor_response def issue_credit(self, order_number, basket, reference_number, amount, currency): @@ -209,3 +219,11 @@ def _get_attribute_from_receipt(self, validated_receipt, attribute): def _get_transaction_id_from_receipt(self, validated_receipt): transaction_key = 'transaction_id' if self.NAME == 'ios-iap' else 'orderId' return self._get_attribute_from_receipt(validated_receipt, transaction_key) + + def _get_metadata(self, price=None, currency_code=None): + meta_data = {} + if currency_code: + meta_data['currency_code'] = currency_code + if price: + meta_data['price'] = price + return meta_data diff --git a/ecommerce/extensions/iap/tests/processors/test_android_iap.py b/ecommerce/extensions/iap/tests/processors/test_android_iap.py index 2f842caa92c..8e16133147d 100644 --- a/ecommerce/extensions/iap/tests/processors/test_android_iap.py +++ b/ecommerce/extensions/iap/tests/processors/test_android_iap.py @@ -59,6 +59,8 @@ def setUp(self): 'transactionId': 'transactionId.android.test.purchased', 'productId': 'android.test.purchased', 'purchaseToken': 'inapp:org.edx.mobile:android.test.purchased', + 'price': 40.25, + 'currency_code': 'USD', } self.mock_validation_response = { 'resource': { @@ -181,3 +183,35 @@ def test_issue_credit_error(self): refund_id = "test id" result = self.processor.issue_credit(refund_id, refund_id, refund_id, refund_id, refund_id) self.assertEqual(refund_id, result) + + @mock.patch.object(GooglePlayValidator, 'validate') + def test_payment_processor_response_created(self, mock_google_validator): + """ + Verify that the PaymentProcessor object is created as expected. + """ + mock_google_validator.return_value = self.mock_validation_response + transaction_id = self.RETURN_DATA.get('transactionId') + + self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket) + payment_processor_response = PaymentProcessorResponse.objects.filter(transaction_id=transaction_id) + self.assertTrue(payment_processor_response.exists()) + self.assertEqual(payment_processor_response.first().processor_name, self.processor_name) + self.assertEqual(payment_processor_response.first().response, self.mock_validation_response) + + @mock.patch.object(GooglePlayValidator, 'validate') + def test_payment_processor_response_extension_created(self, mock_google_validator): + """ + Verify that the PaymentProcessorExtension object is created as expected. + """ + mock_google_validator.return_value = self.mock_validation_response + transaction_id = self.RETURN_DATA.get('transactionId') + price = str(self.RETURN_DATA.get('price')) + currency_code = self.RETURN_DATA.get('currency_code') + + self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket) + + payment_processor_response = PaymentProcessorResponse.objects.filter(transaction_id=transaction_id).first() + payment_processor_response_extension = payment_processor_response.extension + self.assertIsNotNone(payment_processor_response_extension) + self.assertEqual(payment_processor_response_extension.meta_data.get('price'), str(price)) + self.assertEqual(payment_processor_response_extension.meta_data.get('currency_code'), currency_code) diff --git a/ecommerce/extensions/iap/tests/processors/test_ios_iap.py b/ecommerce/extensions/iap/tests/processors/test_ios_iap.py index 76e8211c905..2098e8d09b4 100644 --- a/ecommerce/extensions/iap/tests/processors/test_ios_iap.py +++ b/ecommerce/extensions/iap/tests/processors/test_ios_iap.py @@ -61,6 +61,8 @@ def setUp(self): 'originalTransactionId': 'original_test_id', 'productId': 'test_product_id', 'purchaseToken': 'inapp:test.edx.edx:ios.test.purchased', + 'price': 40.25, + 'currency_code': 'USD', } self.mock_validation_response = { 'environment': 'Sandbox', @@ -224,3 +226,36 @@ def test_issue_credit_error(self): refund_id = "test id" result = self.processor.issue_credit(refund_id, refund_id, refund_id, refund_id, refund_id) self.assertEqual(refund_id, result) + + @mock.patch.object(IOSValidator, 'validate') + def test_payment_processor_response_created(self, mock_ios_validator): + """ + Verify that the PaymentProcessor object is created as expected. + """ + mock_ios_validator.return_value = self.mock_validation_response + transaction_id = self.RETURN_DATA.get('transactionId') + + self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket) + payment_processor_response = PaymentProcessorResponse.objects.filter(transaction_id=transaction_id) + self.assertTrue(payment_processor_response.exists()) + self.assertEqual(payment_processor_response.first().processor_name, self.processor_name) + self.assertEqual(payment_processor_response.first().response, self.mock_validation_response) + + @mock.patch.object(IOSValidator, 'validate') + def test_payment_processor_response_extension_created(self, mock_ios_validator): + """ + Verify that the PaymentProcessorExtension object is created as expected. + """ + mock_ios_validator.return_value = self.mock_validation_response + transaction_id = self.RETURN_DATA.get('transactionId') + original_transaction_id = self.RETURN_DATA.get('originalTransactionId') + price = str(self.RETURN_DATA.get('price')) + currency_code = self.RETURN_DATA.get('currency_code') + + self.processor.handle_processor_response(self.RETURN_DATA, basket=self.basket) + payment_processor_response = PaymentProcessorResponse.objects.filter(transaction_id=transaction_id) + payment_processor_response_extension = payment_processor_response.first().extension + self.assertIsNotNone(payment_processor_response_extension) + self.assertEqual(payment_processor_response_extension.original_transaction_id, original_transaction_id) + self.assertEqual(payment_processor_response_extension.meta_data.get('price'), price) + self.assertEqual(payment_processor_response_extension.meta_data.get('currency_code'), currency_code)