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

fix: fixed basket deletion issue #3102

Merged
merged 8 commits into from
Aug 19, 2024
Merged

Conversation

Anas12091101
Copy link
Contributor

What are the relevant tickets?

Followup for #3021

Description (What does it do?)

We were getting some errors while testing the functionality of this PR on RC. This PR fixes those errors.

How can this be tested?

  • Validate that the checkout page is working properly

1 In the .env file set the value BASKET_EXPIRY_DAYS=0 ( warning: this will delete all your baskets )
2 For any course, open the checkout page and don't complete the order. ( You can leave this step if you have existing basket and basket items )
3 Open the mitxpro shell by running python manage.py shell in the mitxpro directory
4 Run the following code in the shell:

>>> from ecommerce.models import Basket, BasketItem
>>> Basket.objects.all().count()
< number of existing baskets >
>>> BasketItem.objects.all().count()
< number of existing basket items >

5 Now run the following code

>>> from ecommerce.tasks import delete_expired_baskets
>>> delete_expired_baskets.delay()

6 In the celery logs, see the logs of this task. The task should be executed successfully and should show some useful logs.
7 Run the code in step 4 again. This time, the count will return 0

Comment on lines 25 to 31
with transaction.atomic():
expired_baskets = Basket.objects.select_for_update(skip_locked=True).filter(
updated_on__lte=cutoff_date
)
log.info("Found %d expired baskets to delete", len(expired_baskets))
if expired_baskets:
clear_and_delete_baskets(expired_baskets)
Copy link
Contributor

Choose a reason for hiding this comment

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

How about we make it more robust giving this single and whole responsibility to clear_and_delete_baskets to do everything needed. We can pass in a user to this method. So, If there is a user the clear_and_delete_baskets would delete all the associated baskets to that user otherwise it will delete the baskets based on the cutoff time? That way would make the code cleaner and short as well

@@ -716,42 +716,56 @@ def set_coupons_to_redeemed(redeemed_email, coupon_ids):
sheets.tasks.set_assignment_rows_to_enrolled.delay(sheet_update_map)


def clear_and_delete_baskets(baskets):
def clear_and_delete_baskets(user=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

I made some tweaks to this method for you. These changes should make it simpler. You should be able to replace this whole method with

def clear_and_delete_baskets(user=None):
    """
    Delete baskets and all the associated items.

    Args:
       user (User, optional): The user whose baskets should be deleted. If not provided, expired baskets will be deleted.
    """
    cutoff_date = now_in_utc() - timedelta(days=settings.BASKET_EXPIRY_DAYS)
    basket_filter = {"user": user} if user else {"updated_on__lte": cutoff_date}

    with transaction.atomic():
        baskets = Basket.objects.select_for_update(skip_locked=True).filter(**basket_filter)
        log.info(
            "Basket deletion requested for baskets Ids: %s",
            [basket.id for basket in baskets],
        )
        for basket in baskets:
            log.info("Clearing and deleting basket with Id: %s", basket.id)

            with transaction.atomic():
                basket_items = basket.basketitems.all()
                log.info(
                    "Deleting basket items: %s",
                    list(basket_items.values_list("id", flat=True)),
                )
                basket_items.delete()

                course_run_selections = basket.courserunselection_set.all()
                course_run_selections_ids = list(
                    course_run_selections.values_list("id", flat=True)
                )
                log.info(
                    "Deleting course run selections: %s", course_run_selections_ids
                )
                course_run_selections.delete()

                coupon_selections = basket.couponselection_set.all()
                coupon_selections_ids = list(
                    coupon_selections.values_list("id", flat=True)
                )
                log.info("Deleting coupon selections: %s", coupon_selections_ids)
                coupon_selections.delete()

                log.info("Deleting basket: %s", basket.id)
                basket.delete()

Comment on lines 1093 to 1105
def test_delete_baskets_with_user_args(baskets_with_different_users):
"""
Test to verify that the basket of the user passed in the clear_and_delete_baskets fn is deleted only
"""
user1 = baskets_with_different_users.baskets[0].user
user2 = baskets_with_different_users.baskets[1].user

assert Basket.objects.filter(user=user1).count() == 1

clear_and_delete_baskets(user1)

assert Basket.objects.filter(user=user1).count() == 0
assert (
Basket.objects.filter(user=user2).count() == 1
) # Not deleting basket of other users

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need another fixture baskets_with_different_users. I've re-written the test for you that tests exactly what we want to.

Suggested change
def test_delete_baskets_with_user_args(baskets_with_different_users):
"""
Test to verify that the basket of the user passed in the clear_and_delete_baskets fn is deleted only
"""
user1 = baskets_with_different_users.baskets[0].user
user2 = baskets_with_different_users.baskets[1].user
assert Basket.objects.filter(user=user1).count() == 1
clear_and_delete_baskets(user1)
assert Basket.objects.filter(user=user1).count() == 0
assert (
Basket.objects.filter(user=user2).count() == 1
) # Not deleting basket of other users
def test_delete_baskets_with_user_args():
"""
Test to verify that the basket of the user passed in the clear_and_delete_baskets fn is deleted only
"""
baskets = BasketFactory.create_batch(2)
assert Basket.objects.filter(user=baskets[0].user).count() == 1
clear_and_delete_baskets(baskets[0].user)
assert Basket.objects.filter(user=baskets[0].user).count() == 0
assert (
Basket.objects.filter(user=baskets[1].user).count() == 1
) # Not deleting basket of other users

Comment on lines 1110 to 1128
@pytest.mark.parametrize("is_expired", [True, False])
def test_delete_expired_basket(mocker, user, basket_and_coupons, is_expired):
"""
Test to verify that the expired baskets are deleted on calling clear_and_delete_baskets fn without user argument
"""
basket_and_coupons.basket.user = user
basket_and_coupons.basket.save()

if is_expired:
now_in_utc = mocker.patch("ecommerce.api.now_in_utc")
now_in_utc.return_value = datetime.datetime.now(
tz=datetime.timezone.utc
) + datetime.timedelta(days=settings.BASKET_EXPIRY_DAYS)
else:
mocker.patch("django.conf.settings.BASKET_EXPIRY_DAYS", 15)

assert Basket.objects.filter(user=user).count() == 1
clear_and_delete_baskets()
assert bool(Basket.objects.filter(user=user).count()) != is_expired
Copy link
Contributor

Choose a reason for hiding this comment

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

This test has a complex readability and it wasn't testing the thing in a way it should have been. I've re-written it for you

Suggested change
@pytest.mark.parametrize("is_expired", [True, False])
def test_delete_expired_basket(mocker, user, basket_and_coupons, is_expired):
"""
Test to verify that the expired baskets are deleted on calling clear_and_delete_baskets fn without user argument
"""
basket_and_coupons.basket.user = user
basket_and_coupons.basket.save()
if is_expired:
now_in_utc = mocker.patch("ecommerce.api.now_in_utc")
now_in_utc.return_value = datetime.datetime.now(
tz=datetime.timezone.utc
) + datetime.timedelta(days=settings.BASKET_EXPIRY_DAYS)
else:
mocker.patch("django.conf.settings.BASKET_EXPIRY_DAYS", 15)
assert Basket.objects.filter(user=user).count() == 1
clear_and_delete_baskets()
assert bool(Basket.objects.filter(user=user).count()) != is_expired
@patch("django.utils.timezone.now")
def test_delete_expired_basket(patch_now):
"""
Test to verify that the expired baskets are deleted on calling clear_and_delete_baskets fn without user argument
"""
patch_now.return_value = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=settings.BASKET_EXPIRY_DAYS)
BasketFactory.create_batch(3)
patch_now.return_value = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=settings.BASKET_EXPIRY_DAYS + 1)
unexpired_baskets = BasketFactory.create_batch(3)
patch_now.stop()
# Calling the clear baskets without user argument so it should delete the expired baskets
clear_and_delete_baskets()
assert Basket.objects.all().count() == 3
assert list(Basket.objects.all().values_list("id", flat=True)) == [basket.id for basket in unexpired_baskets]


CouponGroup = namedtuple( # noqa: PYI024
"CouponGroup", ["coupon", "coupon_version", "payment", "payment_version"]
)


@pytest.fixture
Copy link
Contributor

Choose a reason for hiding this comment

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

This fixture can now be removed.

@Anas12091101
Copy link
Contributor Author

@arslanashraf7 thanks for all the suggestions

Copy link
Contributor

@arslanashraf7 arslanashraf7 left a comment

Choose a reason for hiding this comment

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

Thanks for making all the changes 👍

@Anas12091101 Anas12091101 force-pushed the anas/fix-basket-deletion branch from 3db6a50 to 6e67f59 Compare August 19, 2024 12:08
@odlbot odlbot temporarily deployed to xpro-ci-pr-3102 August 19, 2024 12:08 Inactive
@Anas12091101 Anas12091101 merged commit 4623427 into master Aug 19, 2024
7 checks passed
@Anas12091101 Anas12091101 deleted the anas/fix-basket-deletion branch August 19, 2024 12:25
@odlbot odlbot mentioned this pull request Aug 19, 2024
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants