Skip to content

Commit

Permalink
feat: Store price and currency for mobile IAP
Browse files Browse the repository at this point in the history
  • Loading branch information
moeez96 committed Jun 22, 2023
1 parent 6b88537 commit f3b2c35
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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={}),
),
]
2 changes: 2 additions & 0 deletions ecommerce/extensions/iap/models.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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={})
28 changes: 23 additions & 5 deletions ecommerce/extensions/iap/processors/base_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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
34 changes: 34 additions & 0 deletions ecommerce/extensions/iap/tests/processors/test_android_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down Expand Up @@ -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)
35 changes: 35 additions & 0 deletions ecommerce/extensions/iap/tests/processors/test_ios_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)

0 comments on commit f3b2c35

Please sign in to comment.