Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt ceil method for ios inapp prices #4

Merged
merged 6 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ecommerce/extensions/iap/api/v1/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ERROR_ALREADY_PURCHASED = "You have already purchased these products"
ERROR_BASKET_NOT_FOUND = "Basket [{}] not found."
ERROR_BASKET_ID_NOT_PROVIDED = "Basket id is not provided"
ERROR_USER_CANCELLED_PAYMENT = "User cancelled this payment."
ERROR_DURING_IOS_REFUND_EXECUTION = "Could not execute IOS refund."
ERROR_DURING_ORDER_CREATION = "An error occurred during order creation."
ERROR_DURING_PAYMENT_HANDLING = "An error occurred during payment handling."
Expand All @@ -32,6 +33,8 @@
LOGGER_CHECKOUT_ERROR = "Checkout failed with the error [%s] and status code [%s]."
LOGGER_EXECUTE_ALREADY_PURCHASED = "Execute payment failed for user [%s] and basket [%s]. " \
"Products already purchased."
LOGGER_EXECUTE_CANCELLED_PAYMENT_ERROR = "Execute payment failed for user [%s] and basket [%s]. " \
"Payment error [%s]."
LOGGER_EXECUTE_GATEWAY_ERROR = "Execute payment validation failed for user [%s] and basket [%s]. Error: [%s]"
LOGGER_EXECUTE_ORDER_CREATION_FAILED = "Execute payment failed for user [%s] and basket [%s]. " \
"Order Creation failed with error [%s]."
Expand Down
26 changes: 17 additions & 9 deletions ecommerce/extensions/iap/api/v1/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,25 @@ def test_create_ios_product(self, _, __, ___, ____, _____, ______, _______):
course = {
'key': 'test',
'name': 'test',
'price': '123'
'price': 123
}
error_msg = create_ios_product(course, self.ios_seat, self.configuration)
self.assertEqual(error_msg, None)

# @mock.patch('ecommerce.extensions.iap.api.v1.utils.create_inapp_purchase')
def test_error_on_ios_product_price_threshhold(self, _,):
course = {
'key': 'test',
'name': 'test',
'price': 1001
}
error_msg = create_ios_product(course, self.ios_seat, self.configuration)
self.assertEqual(error_msg, 'Error: Appstore does not allow price > 1000')

def test_create_ios_product_with_failure(self, _):
course = {
'key': 'test',
'name': 'test',
'price': '123'
'price': 123
}
error_msg = create_ios_product(course, self.ios_seat, self.configuration)
expected_msg = "[Couldn't create inapp purchase id] for course [{}] with sku [{}]".format(
Expand All @@ -110,7 +118,7 @@ def test_create_inapp_purchase(self, _):
course = {
'key': 'test',
'name': 'test',
'price': '123'
'price': 123
}
headers = get_auth_headers(self.configuration)
create_inapp_purchase(course, 'test.sku', '123', headers)
Expand Down Expand Up @@ -149,6 +157,7 @@ def test_apply_price_of_inapp_purchase(self, _):
apply_price_of_inapp_purchase(100, '123', headers)

get_call.return_value.status_code = 200
post_call.return_value.status_code = 201
get_call.return_value.json.return_value = {
'data': [
{
Expand All @@ -159,12 +168,11 @@ def test_apply_price_of_inapp_purchase(self, _):
}
]
}
with self.assertRaises(AppStoreRequestException, msg="Couldn't find nearest low price point"):
# Make sure it doesn't select higher price point
apply_price_of_inapp_purchase(80, '123', headers)
with self.assertRaises(AppStoreRequestException, msg="Couldn't find nearest high price point"):
# Make sure it doesn't select lower price point
apply_price_of_inapp_purchase(100, '123', headers)

post_call.return_value.status_code = 201
apply_price_of_inapp_purchase(100, '123', headers)
apply_price_of_inapp_purchase(98, '123', headers)
price_url = 'https://api.appstoreconnect.apple.com/v1/inAppPurchasePriceSchedules'
self.assertEqual(post_call.call_args[0][0], price_url)
self.assertEqual(post_call.call_args[1]['headers'], headers)
Expand Down
27 changes: 26 additions & 1 deletion ecommerce/extensions/iap/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.urls import reverse
from oauth2client.service_account import ServiceAccountCredentials
from oscar.apps.order.exceptions import UnableToPlaceOrder
from oscar.apps.payment.exceptions import GatewayError, PaymentError
from oscar.apps.payment.exceptions import GatewayError, PaymentError, UserCancelled
from oscar.core.loading import get_class, get_model
from oscar.test.factories import BasketFactory
from rest_framework import status
Expand All @@ -38,6 +38,7 @@
ERROR_ORDER_NOT_FOUND_FOR_REFUND,
ERROR_REFUND_NOT_COMPLETED,
ERROR_TRANSACTION_NOT_FOUND_FOR_REFUND,
ERROR_USER_CANCELLED_PAYMENT,
IGNORE_NON_REFUND_NOTIFICATION_FROM_APPLE,
LOGGER_BASKET_ALREADY_PURCHASED,
LOGGER_BASKET_CREATED,
Expand Down Expand Up @@ -339,6 +340,30 @@ def test_payment_error(self):
),
)

def test_user_cancelled_payment_error(self):
"""
Verify that user cancelled payment error is returned for Usercancelled exception
"""
with mock.patch.object(MobileCoursePurchaseExecutionView, 'handle_payment',
side_effect=UserCancelled('Test Error')) as fake_handle_payment:
with LogCapture(self.logger_name) as logger:
self._assert_response({'error': ERROR_USER_CANCELLED_PAYMENT})
self.assertTrue(fake_handle_payment.called)

logger.check(
(
self.logger_name,
'INFO',
LOGGER_EXECUTE_STARTED % (self.user.username, self.basket.id, self.processor_name)
),
(
self.logger_name,
'ERROR',
LOGGER_EXECUTE_PAYMENT_ERROR % (self.user.username, self.basket.id,
str(fake_handle_payment.side_effect))
),
)

def test_gateway_error(self):
"""
Verify that an error is thrown when an approved payment fails to execute
Expand Down
18 changes: 11 additions & 7 deletions ecommerce/extensions/iap/api/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def create_ios_product(course, ios_product, configuration):
Create in app ios product on connect store.
return error message in case of failure.
"""
if course['price'] > 1000:
return 'Error: Appstore does not allow price > 1000'

headers = get_auth_headers(configuration)
try:
in_app_purchase_id = get_or_create_inapp_purchase(ios_product, course, configuration, headers)
Expand Down Expand Up @@ -188,15 +191,16 @@ def apply_price_of_inapp_purchase(price, in_app_purchase_id, headers):
if response.status_code != 200:
raise AppStoreRequestException("Couldn't fetch price points")

nearest_low_price = nearest_low_price_id = 0
# Apple doesn't allow in app price > 1000
nearest_high_price = nearest_high_price_id = 1001
for price_point in response.json()['data']:
customer_price = float(price_point['attributes']['customerPrice'])
if nearest_low_price < customer_price <= price:
nearest_low_price = customer_price
nearest_low_price_id = price_point['id']
if nearest_high_price > customer_price >= price:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what is the scenario where price is greater than customer_price?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customer price range is 0-1000 and we are already filtering > 1000 cases. Therefore there is no chance for this scenario.
Even if it occurs this function will return exception and we will not apply price for that product and will investigate issue afterwards.

nearest_high_price = customer_price
nearest_high_price_id = price_point['id']

if not nearest_low_price:
raise AppStoreRequestException("Couldn't find nearest low price point")
if nearest_high_price == 1001:
raise AppStoreRequestException("Couldn't find nearest high price point")

url = APP_STORE_BASE_URL + "/v1/inAppPurchasePriceSchedules"
data = {
Expand Down Expand Up @@ -233,7 +237,7 @@ def apply_price_of_inapp_purchase(price, in_app_purchase_id, headers):
"inAppPurchasePricePoint": {
"data": {
"type": "inAppPurchasePricePoints",
"id": nearest_low_price_id
"id": nearest_high_price_id
}
}
},
Expand Down
7 changes: 6 additions & 1 deletion ecommerce/extensions/iap/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from oscar.apps.basket.views import * # pylint: disable=wildcard-import, unused-wildcard-import
from oscar.apps.payment.exceptions import GatewayError, PaymentError
from oscar.apps.payment.exceptions import GatewayError, PaymentError, UserCancelled
from oscar.core.loading import get_class, get_model
from rest_framework import status
from rest_framework.permissions import IsAdminUser, IsAuthenticated
Expand Down Expand Up @@ -51,6 +51,7 @@
ERROR_ORDER_NOT_FOUND_FOR_REFUND,
ERROR_REFUND_NOT_COMPLETED,
ERROR_TRANSACTION_NOT_FOUND_FOR_REFUND,
ERROR_USER_CANCELLED_PAYMENT,
FOUND_MULTIPLE_PRODUCTS_ERROR,
GOOGLE_PUBLISHER_API_SCOPE,
IGNORE_NON_REFUND_NOTIFICATION_FROM_APPLE,
Expand All @@ -60,6 +61,7 @@
LOGGER_BASKET_NOT_FOUND,
LOGGER_CHECKOUT_ERROR,
LOGGER_EXECUTE_ALREADY_PURCHASED,
LOGGER_EXECUTE_CANCELLED_PAYMENT_ERROR,
LOGGER_EXECUTE_GATEWAY_ERROR,
LOGGER_EXECUTE_ORDER_CREATION_FAILED,
LOGGER_EXECUTE_PAYMENT_ERROR,
Expand Down Expand Up @@ -302,6 +304,9 @@ def post(self, request):
except RedundantPaymentNotificationError:
logger.exception(LOGGER_EXECUTE_REDUNDANT_PAYMENT, request.user.username, basket_id)
return JsonResponse({'error': COURSE_ALREADY_PAID_ON_DEVICE}, status=409)
except UserCancelled as exception:
logger.exception(LOGGER_EXECUTE_CANCELLED_PAYMENT_ERROR, request.user.username, basket_id, str(exception))
return JsonResponse({'error': ERROR_USER_CANCELLED_PAYMENT}, status=400)
except PaymentError as exception:
logger.exception(LOGGER_EXECUTE_PAYMENT_ERROR, request.user.username, basket_id, str(exception))
return JsonResponse({'error': ERROR_DURING_PAYMENT_HANDLING}, status=400)
Expand Down
1 change: 1 addition & 0 deletions ecommerce/extensions/iap/processors/base_iap.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def record_processor_response(self, response, transaction_id=None, basket=None,
basket=basket)

meta_data = self._get_metadata(currency_code=currency_code, price=price)
original_transaction_id = original_transaction_id or transaction_id

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: how does this change relate to the inapp price change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't. I noticed that nit and will be sending as a separate commit in this PR.

PaymentProcessorResponseExtension.objects.create(
processor_response=processor_response, original_transaction_id=original_transaction_id,
meta_data=meta_data)
Expand Down
Loading