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

Commit

Permalink
chore: Added management command to update voucher name
Browse files Browse the repository at this point in the history
fix: sorted imports

Fix: Support create-mobile-skus to run again if failed (#4129)

* fix: Support create-mobile-skus to run again if we found an error previously

fix: Enable enrollment code purchase with mobile seats (#4130)

* fix: Enable enrollment code purchase with mobile seats

* test: Add unit test

---------

Co-authored-by: Abdul  Moeez Zahid <[email protected]>

fix: PEP8 conventions

fix: import issue

chore: added more test cases

fix: added new line at the end of file

fix: blank line issue

fix: long line issue

fix: pylint issues

fix: import issues

fix: removed extra blank line

fix: updated test

fix: issues in command

fix:: isort issue
  • Loading branch information
zubair-ce07 committed Feb 20, 2024
1 parent d8a1218 commit b405ec9
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 11 deletions.
9 changes: 5 additions & 4 deletions ecommerce/extensions/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,11 +861,12 @@ def _update_app_store_product(self, mobile_seat, price):
partner_short_code = self.context['request'].site.siteconfiguration.partner.short_code
configuration = settings.PAYMENT_PROCESSOR_CONFIG[partner_short_code.lower()][IOSIAP.NAME.lower()]
headers = get_auth_headers(configuration)
try:
ios_product_id = mobile_seat.attr.app_store_id
apply_price_of_inapp_purchase(price, ios_product_id, headers)
except AttributeError:
ios_product_id = getattr(mobile_seat.attr, 'app_store_id', None)
if not ios_product_id:
logger.error("app_store_id not associated with [%s]", mobile_seat.course)
return

apply_price_of_inapp_purchase(price, ios_product_id, headers)

def get_partner(self):
"""Validate partner"""
Expand Down
2 changes: 2 additions & 0 deletions ecommerce/extensions/fulfillment/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import requests
import waffle
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from getsmarter_api_clients.geag import GetSmarterEnterpriseApiClient
from oscar.core.loading import get_model
Expand Down Expand Up @@ -553,6 +554,7 @@ def fulfill_product(self, order, lines, email_opt_in=False):
attributes__name='course_key',
attribute_values__value_text=line.product.attr.course_key
).get(
~Q(stockrecords__partner_sku__icontains="mobile"),
attributes__name='certificate_type',
attribute_values__value_text=line.product.attr.seat_type
)
Expand Down
48 changes: 46 additions & 2 deletions ecommerce/extensions/fulfillment/tests/test_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,42 @@ def format_hubspot_request_url(self):
settings.HUBSPOT_PORTAL_ID,
settings.HUBSPOT_SALES_LEAD_FORM_GUID)

def create_mobile_seat_for_course(self, sku_prefix):
""" Create a mobile seat for a course given the sku_prefix """
web_seat = Product.objects.filter(
parent__isnull=False,
course=self.course,
attributes__name="id_verification_required",
parent__product_class__name="Seat"
).first()
web_stock_record = web_seat.stockrecords.first()

mobile_seat = Product.objects.create(
course=self.course,
parent=web_seat.parent,
structure=web_seat.structure,
expires=web_seat.expires,
is_public=web_seat.is_public,
title="{} {}".format(sku_prefix.capitalize(), web_seat.title.lower())
)

mobile_seat.attr.certificate_type = web_seat.attr.certificate_type
mobile_seat.attr.course_key = web_seat.attr.course_key
mobile_seat.attr.id_verification_required = web_seat.attr.id_verification_required
mobile_seat.attr.save()

StockRecord.objects.create(
partner=web_stock_record.partner,
product=mobile_seat,
partner_sku="mobile.{}.{}".format(sku_prefix.lower(), web_stock_record.partner_sku.lower()),
price_currency=web_stock_record.price_currency,
)
return mobile_seat

def setUp(self):
super(EnrollmentCodeFulfillmentModuleTests, self).setUp()
course = CourseFactory(partner=self.partner)
course.create_or_update_seat('verified', True, 50, create_enrollment_code=True)
self.course = CourseFactory(partner=self.partner)
self.course.create_or_update_seat('verified', True, 50, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
user = UserFactory()
basket = factories.BasketFactory(owner=user, site=self.site)
Expand Down Expand Up @@ -709,6 +741,18 @@ def test_fulfill_product(self):
self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY)
self.assertIsNotNone(OrderLineVouchers.objects.first().vouchers.first().benefit.range.catalog)

def test_fulfill_product_with_existing_mobile_seats(self):
"""Test fulfilling an Enrollment code product with mobile seats for the same course."""
self.assertEqual(OrderLineVouchers.objects.count(), 0)
lines = self.order.lines.all()
self.create_mobile_seat_for_course('android')
self.create_mobile_seat_for_course('ios')
__, completed_lines = EnrollmentCodeFulfillmentModule().fulfill_product(self.order, lines)
self.assertEqual(completed_lines[0].status, LINE.COMPLETE)
self.assertEqual(OrderLineVouchers.objects.count(), 1)
self.assertEqual(OrderLineVouchers.objects.first().vouchers.count(), self.QUANTITY)
self.assertIsNotNone(OrderLineVouchers.objects.first().vouchers.first().benefit.range.catalog)

def test_revoke_line(self):
line = self.order.lines.first()
with self.assertRaises(NotImplementedError):
Expand Down
20 changes: 17 additions & 3 deletions ecommerce/extensions/iap/api/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def create_ios_product(course, ios_product, configuration):
"""
headers = get_auth_headers(configuration)
try:
in_app_purchase_id = create_inapp_purchase(course, ios_product.partner_sku, configuration['apple_id'], headers)
ios_product.product.attr.app_store_id = in_app_purchase_id
ios_product.product.save()
in_app_purchase_id = get_or_create_inapp_purchase(ios_product, course, configuration, headers)
localize_inapp_purchase(in_app_purchase_id, headers)
apply_price_of_inapp_purchase(course['price'], in_app_purchase_id, headers)
upload_screenshot_of_inapp_purchase(in_app_purchase_id, headers)
Expand All @@ -51,6 +49,22 @@ def create_ios_product(course, ios_product, configuration):
return error_msg


def get_or_create_inapp_purchase(ios_stock_record, course, configuration, headers):
"""
Returns inapp_purchase_id from product attr
If not present there create a product on ios store and return its inapp_purchase_id
"""

in_app_purchase_id = getattr(ios_stock_record.product.attr, 'app_store_id', '')
if not in_app_purchase_id:
in_app_purchase_id = create_inapp_purchase(course, ios_stock_record.partner_sku,
configuration['apple_id'], headers)
ios_stock_record.product.attr.app_store_id = in_app_purchase_id
ios_stock_record.product.save()

return in_app_purchase_id


def request_connect_store(url, headers, data=None, method="post"):
""" Request the given endpoint with multiple tries and backoff time """
# Adding backoff and retries because of following two reasons
Expand Down
2 changes: 1 addition & 1 deletion ecommerce/extensions/iap/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ def post(self, request):
product_class__name=SEAT_PRODUCT_CLASS_NAME,
children__expires__gt=now(),
course=course_run,
)
).distinct()

if not parent_product.exists():
failed_course_runs.append(course_run_key)
Expand Down
4 changes: 3 additions & 1 deletion ecommerce/extensions/iap/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def create_mobile_seat(sku_prefix, existing_web_seat):
if 'ios' in sku_prefix:
# We need this attribute defined for ios products
# Actual values will be assigned when we create product on appstore
new_mobile_seat.attr.app_store_id = ''
app_store_id = getattr(new_mobile_seat.attr, 'app_store_id', None)
if not app_store_id:
new_mobile_seat.attr.app_store_id = ''

new_mobile_seat.attr.save()

Expand Down
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from datetime import timedelta
from unittest import mock

from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone

from ecommerce.extensions.voucher.models import Voucher
from ecommerce.extensions.voucher.tasks import update_voucher_names


class ManagementCommandTests(TestCase):
def setUp(self):
self.data = {
'name': 'Test voucher',
'code': 'TESTCODE',
'start_datetime': timezone.now() ,
'end_datetime': timezone.now() + timedelta(days=7)
}
voucher = Voucher.objects.create(**self.data)

@mock.patch('ecommerce.extensions.voucher.tasks.update_voucher_names.delay')
def test_update_voucher_names_command(self, mock_delay):

call_command('update_voucher_names')
# Assert that the Celery task is scheduled
self.assertTrue(mock_delay.called)

@mock.patch('ecommerce.extensions.voucher.models.Voucher.objects.all')
def test_update_voucher_names_task(self, mock_all):
# Mock Voucher objects
start_datetime = timezone.now()
end_datetime = timezone.now() + timedelta(days=7)

mock_vouchers = [
Voucher(id=1, name='Name1', code='SASAFR',
start_datetime=start_datetime, end_datetime=start_datetime ),
Voucher(id=2, name='Name2', code='EWRRFEC',
start_datetime=start_datetime, end_datetime=end_datetime),
]
mock_all.return_value = mock_vouchers

# Call the Celery task
update_voucher_names(mock_vouchers)
# Assert that the names are updated as expected
self.assertEqual(mock_vouchers[0].name, '1 - Name1')
self.assertEqual(mock_vouchers[1].name, '2 - Name2')

@mock.patch('ecommerce.extensions.voucher.tasks.update_voucher_names.delay')
def test_voucher_name_update_once(self, mock_delay):
original_name = 'Original Name'
code = 'ABC123XSD'
start_datetime = timezone.now()
end_datetime = start_datetime + timedelta(days=7)
voucher = Voucher.objects.create(name=original_name,
code=code,
start_datetime=start_datetime,
end_datetime=end_datetime
)

call_command('update_voucher_names')
call_command('update_voucher_names')

updated_voucher = Voucher.objects.get(id=voucher.id)

self.assertEqual(mock_delay.call_count, 2)
self.assertEqual(updated_voucher.name, f"{voucher.id} - {original_name}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ecommerce/extensions/vouchers/management/commands/update_voucher_names.py
import logging

from django.core.management.base import BaseCommand

from ecommerce.extensions.voucher.models import Voucher
from ecommerce.extensions.voucher.tasks import update_voucher_names

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = 'Update voucher names asynchronously'

def add_arguments(self, parser):
parser.add_argument('--batch-size', type=int, default=1000, help='Number of vouchers to process in each batch')

def handle(self, *args, **options):
batch_size = options['batch_size']

total_vouchers = Voucher.objects.count()
processed_vouchers = 0

logger.info("Total number of vouchers: %d", total_vouchers)

while processed_vouchers < total_vouchers:
vouchers = Voucher.objects.all()[processed_vouchers:processed_vouchers + batch_size]
try:
# Call the Celery task asynchronously for each batch
update_voucher_names.delay(vouchers)
except Exception as exc: # pylint: disable=broad-except
logger.exception("Error updating voucher names: %s", exc)

processed_vouchers += len(vouchers)
logger.info("Processed %d out of %d vouchers", processed_vouchers, total_vouchers)
25 changes: 25 additions & 0 deletions ecommerce/extensions/voucher/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

from celery import shared_task
from celery.utils.log import get_task_logger
from django.conf import settings # lint-amnesty, pylint: disable=unused-import

from ecommerce.extensions.voucher.models import Voucher

logger = logging.getLogger(__name__)


@shared_task(bind=True, ignore_result=True)
def update_voucher_names(self, vouchers):
for voucher in vouchers:
if not f"{voucher.id} -" in voucher.name:
updated_name = f"{voucher.id} - {voucher.name}"
try:
if len(updated_name) > 128:
logger.warning("Name length exceeds 128 characters for voucher id %d. Truncating...", voucher.id)
updated_name = updated_name[:128]

voucher.name = updated_name
voucher.save()
except Exception as exc: # pylint: disable=broad-except
logger.exception("Error updating voucher name %d: %s", voucher.id, exc)

0 comments on commit b405ec9

Please sign in to comment.