Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 1.145.2
---------------

- Indicate order status on redirect via query params for MIT Learn (#3464)
- Fix flakey discount-related tests (#3472)
- Fix Cart -> Learn dashboard for UAI industry verticals (#3466)

Version 1.145.1 (Released April 08, 2026)
---------------

Expand Down
40 changes: 2 additions & 38 deletions ecommerce/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,9 @@
USER_MSG_TYPE_DISCOUNT_INVALID,
USER_MSG_TYPE_ENROLL_BLOCKED,
USER_MSG_TYPE_ENROLL_DUPLICATED,
USER_MSG_TYPE_PAYMENT_ACCEPTED_NOVALUE,
USER_MSG_TYPE_REQUIRED_ENROLLMENT_CODE_EMPTY,
)
from main.settings import ECOMMERCE_DEFAULT_PAYMENT_GATEWAY
from main.utils import parse_supplied_date, redirect_with_user_message
from main.utils import parse_supplied_date
from openedx.api import create_user
from openedx.constants import EDX_ENROLLMENT_AUDIT_MODE, EDX_ENROLLMENT_VERIFIED_MODE
from openedx.tasks import create_user_from_id
Expand Down Expand Up @@ -103,30 +101,18 @@ def generate_checkout_payload( # noqa: PLR0911
return {
"country_blocked": True,
"error": USER_MSG_TYPE_ENROLL_BLOCKED,
"response": redirect_with_user_message(
reverse("user-dashboard"),
{"type": USER_MSG_TYPE_ENROLL_BLOCKED},
),
}

if basket.has_user_purchased_same_courserun(request.user):
return {
"purchased_same_courserun": True,
"error": USER_MSG_TYPE_ENROLL_DUPLICATED,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_ENROLL_DUPLICATED},
),
}

if basket.has_user_purchased_non_upgradable_courserun():
return {
"purchased_non_upgradeable_courserun": True,
"error": USER_MSG_TYPE_COURSE_NON_UPGRADABLE,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_COURSE_NON_UPGRADABLE},
),
}

if not skip_discount_check and not check_basket_discounts_for_validity(request):
Expand All @@ -136,10 +122,6 @@ def generate_checkout_payload( # noqa: PLR0911
return {
"invalid_discounts": True,
"error": USER_MSG_TYPE_DISCOUNT_INVALID,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_DISCOUNT_INVALID},
),
}

active_contracts = get_active_contracts_from_basket_items(basket)
Expand All @@ -148,30 +130,18 @@ def generate_checkout_payload( # noqa: PLR0911
return {
"invalid_discounts": True,
"error": USER_MSG_TYPE_B2B_ERROR_MISSING_ENROLLMENT_CODE,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_REQUIRED_ENROLLMENT_CODE_EMPTY},
),
}

if not validate_basket_for_b2b_purchase(request, active_contracts):
return {
"invalid_discounts": True,
"error": USER_MSG_TYPE_B2B_INVALID_BASKET,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_DISCOUNT_INVALID},
),
}

if not basket.basket_items.count():
return {
"basket_empty": True,
"error": USER_MSG_TYPE_BASKET_EMPTY,
"response": redirect_with_user_message(
reverse("cart"),
{"type": USER_MSG_TYPE_BASKET_EMPTY},
),
}

order = PendingOrder.create_from_basket(basket)
Expand Down Expand Up @@ -213,13 +183,7 @@ def generate_checkout_payload( # noqa: PLR0911

return {
"no_checkout": True,
"response": redirect_with_user_message(
reverse("user-dashboard"),
{
"type": USER_MSG_TYPE_PAYMENT_ACCEPTED_NOVALUE,
"run": order.lines.first().courseware,
},
),
"order": order,
"order_id": order.id,
}

Expand Down
36 changes: 24 additions & 12 deletions ecommerce/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
create_verified_program_course_run_enrollment,
create_verified_program_discount,
establish_basket,
generate_checkout_payload,
get_auto_apply_discounts_for_basket,
process_cybersource_payment_response,
refund_order,
Expand Down Expand Up @@ -217,6 +218,18 @@ def create_basket(user, products):
return basket


def create_pending_order(user):
"""
Call generate_checkout_payload to create a PendingOrder with a realistic
CyberSource payload.
"""
rf = RequestFactory()
request = rf.get("/")
request.user = user
request.session = {}
return generate_checkout_payload(request)


@pytest.mark.parametrize(
"order_state",
[
Expand Down Expand Up @@ -489,9 +502,7 @@ def test_unenrollment_unenrolls_learner(mocker, user):


@pytest.mark.skip_nplusone_check
def test_process_cybersource_payment_response( # noqa: PLR0913
settings, rf, mocker, user_client, user, products
):
def test_process_cybersource_payment_response(settings, rf, mocker, user, products):
"""Test that ensures the response from Cybersource for an ACCEPTed payment updates the orders state"""

settings.OPENEDX_SERVICE_WORKER_API_TOKEN = "mock_api_token" # noqa: S105
Expand All @@ -501,9 +512,9 @@ def test_process_cybersource_payment_response( # noqa: PLR0913
)
create_basket(user, products)

resp = user_client.post(reverse("checkout_api-start_checkout"))
checkout_payload = create_pending_order(user)

payload = resp.json()["payload"]
payload = checkout_payload["payload"]
payload = {
**{f"req_{key}": value for key, value in payload.items()},
"decision": "ACCEPT",
Expand All @@ -527,8 +538,8 @@ def test_process_cybersource_payment_response( # noqa: PLR0913

@pytest.mark.skip_nplusone_check
@pytest.mark.parametrize("include_discount", [True, False])
def test_process_cybersource_payment_decline_response( # noqa: PLR0913
rf, mocker, user_client, user, products, include_discount
def test_process_cybersource_payment_decline_response(
rf, mocker, user, products, include_discount
):
"""Test that ensures the response from Cybersource for an DECLINEd payment updates the orders state"""

Expand All @@ -538,9 +549,9 @@ def test_process_cybersource_payment_decline_response( # noqa: PLR0913
)
create_basket(user, products)

resp = user_client.post(reverse("checkout_api-start_checkout"))
checkout_payload = create_pending_order(user)

payload = resp.json()["payload"]
payload = checkout_payload["payload"]
payload = {
**{f"req_{key}": value for key, value in payload.items()},
"decision": "DECLINE",
Expand Down Expand Up @@ -868,7 +879,8 @@ def mock_cre_side_effect(
False,
],
)
def test_apply_discount_to_basket(user, better_discount, is_valid):
@pytest.mark.parametrize("_count", range(100))
def test_apply_discount_to_basket(user, better_discount, is_valid, _count):
"""
Test that applying a discount to a basket works as expected.

Expand All @@ -884,7 +896,7 @@ def test_apply_discount_to_basket(user, better_discount, is_valid):
BasketItem.objects.create(basket=basket, product=product, quantity=1)

existing_discount = UnlimitedUseDiscountFactory.create(
amount=100, discount_type="fixed-price"
amount=50, discount_type="percent-off"
)
BasketDiscount.objects.create(
redeemed_by=user,
Expand All @@ -894,7 +906,7 @@ def test_apply_discount_to_basket(user, better_discount, is_valid):
)

new_discount = UnlimitedUseDiscountFactory.create(
amount=(50 if better_discount else 150), discount_type="fixed-price"
amount=(60 if better_discount else 10), discount_type="percent-off"
)

if not is_valid:
Expand Down
15 changes: 11 additions & 4 deletions ecommerce/serializers/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
import reversion
from dateutil.parser import parse
from django.test import RequestFactory
from django.urls import reverse
from mitol.common.utils import now_in_utc

Expand Down Expand Up @@ -310,15 +311,21 @@ def create_order_receipt(mocker, user, products, user_client):
"""
Sets up an order for use with the receipt serializer tests.
"""
from ecommerce.api import generate_checkout_payload # noqa: PLC0415

mocker.patch(
"mitol.payment_gateway.api.PaymentGateway.validate_processor_response",
return_value=True,
)
basket = create_basket(user, products) # noqa: F841
create_basket(user, products)

resp = user_client.post(reverse("checkout_api-start_checkout"))
rf = RequestFactory()
request = rf.get("/")
request.user = user
request.session = {}
checkout_payload = generate_checkout_payload(request)

payload = resp.json()["payload"]
payload = checkout_payload["payload"]
payload = {
**{f"req_{key}": value for key, value in payload.items()},
"decision": "ACCEPT",
Expand All @@ -328,7 +335,7 @@ def create_order_receipt(mocker, user, products, user_client):

order = Order.objects.get(state=OrderStatus.PENDING, purchaser=user)

resp = user_client.post(reverse("checkout-result-callback"), payload)
user_client.post(reverse("checkout-result-callback"), payload)

order.refresh_from_db()
return order
Expand Down
Loading
Loading