Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
8 changes: 4 additions & 4 deletions config/apisix/apisix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ routes:
- "/admin/login*"

- id: 3
name: "app-cart"
desc: "Require login for cart so session is established."
name: "app-checkout-anonymous"
desc: "Require login for the anonymous basket claim/checkout endpoint so a session is established."
priority: 5
upstream_id: 1
plugins:
Expand All @@ -88,8 +88,8 @@ routes:
set:
Content-Security-Policy: frame-ancestors 'self' ${{OPENEDX_API_BASE_URL}}
uris:
- "/cart"
- "/cart/"
- "/checkout/anonymous"
- "/checkout/anonymous/"


#END
44 changes: 16 additions & 28 deletions drf_lint_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
"courses/serializers/v1/programs.py:346:17:ORM001",
"courses/serializers/v1/programs.py:368:16:ORM001",
"courses/serializers/v2/courses.py:272:17:ORM002",
"courses/serializers/v2/courses.py:337:18:ORM001",
"courses/serializers/v2/departments.py:35:40:ORM002",
"courses/serializers/v2/departments.py:49:42:ORM002",
"courses/serializers/v2/programs.py:385:53:ORM002",
"courses/serializers/v2/programs.py:495:12:ORM002",
"courses/serializers/v2/programs.py:596:12:ORM002",
"courses/serializers/v3/courses.py:111:14:ORM001",
"courses/serializers/v3/courses.py:55:12:ORM002",
"courses/serializers/v3/programs.py:55:22:ORM001",
Expand All @@ -37,14 +37,14 @@
"ecommerce/serializers/__init__.py:326:20:ORM002",
"ecommerce/serializers/__init__.py:331:31:ORM002",
"ecommerce/serializers/__init__.py:346:31:ORM002",
"ecommerce/serializers/__init__.py:417:24:ORM002",
"ecommerce/serializers/__init__.py:436:12:ORM001",
"ecommerce/serializers/__init__.py:460:22:ORM002",
"ecommerce/serializers/__init__.py:507:22:ORM002",
"ecommerce/serializers/__init__.py:571:20:ORM002",
"ecommerce/serializers/__init__.py:704:22:ORM002",
"ecommerce/serializers/__init__.py:821:28:ORM002",
"ecommerce/serializers/__init__.py:891:28:ORM002",
"ecommerce/serializers/__init__.py:418:24:ORM002",
"ecommerce/serializers/__init__.py:437:12:ORM001",
"ecommerce/serializers/__init__.py:461:22:ORM002",
"ecommerce/serializers/__init__.py:508:22:ORM002",
"ecommerce/serializers/__init__.py:572:20:ORM002",
"ecommerce/serializers/__init__.py:705:22:ORM002",
"ecommerce/serializers/__init__.py:822:28:ORM002",
"ecommerce/serializers/__init__.py:892:28:ORM002",
"ecommerce/serializers/v0/__init__.py:279:17:ORM001",
"ecommerce/serializers/v0/__init__.py:281:18:ORM001",
"ecommerce/serializers/v0/__init__.py:282:18:ORM001",
Expand All @@ -54,14 +54,14 @@
"ecommerce/serializers/v0/__init__.py:408:20:ORM002",
"ecommerce/serializers/v0/__init__.py:414:35:ORM002",
"ecommerce/serializers/v0/__init__.py:430:31:ORM002",
"ecommerce/serializers/v0/__init__.py:502:24:ORM002",
"ecommerce/serializers/v0/__init__.py:521:12:ORM001",
"ecommerce/serializers/v0/__init__.py:545:22:ORM002",
"ecommerce/serializers/v0/__init__.py:592:22:ORM002",
"ecommerce/serializers/v0/__init__.py:657:20:ORM002",
"ecommerce/serializers/v0/__init__.py:807:22:ORM002",
"ecommerce/serializers/v0/__init__.py:503:24:ORM002",
"ecommerce/serializers/v0/__init__.py:522:12:ORM001",
"ecommerce/serializers/v0/__init__.py:546:22:ORM002",
"ecommerce/serializers/v0/__init__.py:593:22:ORM002",
"ecommerce/serializers/v0/__init__.py:658:20:ORM002",
"ecommerce/serializers/v0/__init__.py:808:22:ORM002",
"ecommerce/serializers/v0/__init__.py:83:28:ORM002",
"ecommerce/serializers/v0/__init__.py:951:28:ORM002",
"ecommerce/serializers/v0/__init__.py:952:28:ORM002",
"flexiblepricing/serializers.py:129:38:ORM001",
"flexiblepricing/serializers.py:132:34:ORM001",
"flexiblepricing/serializers.py:147:34:ORM001",
Expand All @@ -71,18 +71,6 @@
"flexiblepricing/serializers.py:207:31:ORM001",
"flexiblepricing/serializers.py:212:16:ORM001",
"flexiblepricing/serializers.py:216:16:ORM001",
"hubspot_sync/serializers.py:170:22:ORM002",
"hubspot_sync/serializers.py:171:22:ORM001",
"hubspot_sync/serializers.py:183:25:ORM002",
"hubspot_sync/serializers.py:186:33:ORM002",
"hubspot_sync/serializers.py:193:21:ORM002",
"hubspot_sync/serializers.py:312:33:ORM001",
"hubspot_sync/serializers.py:323:36:ORM001",
"hubspot_sync/serializers.py:324:33:ORM001",
"hubspot_sync/serializers.py:335:36:ORM001",
"hubspot_sync/serializers.py:64:22:ORM002",
"hubspot_sync/serializers.py:76:31:ORM002",
"hubspot_sync/serializers.py:80:31:ORM002",
"users/serializers.py:209:16:ORM001",
"users/serializers.py:254:20:ORM001",
"users/serializers.py:301:19:ORM001",
Expand Down
95 changes: 95 additions & 0 deletions ecommerce/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import uuid
from datetime import timedelta
from decimal import Decimal
from urllib.parse import urljoin

Expand Down Expand Up @@ -451,6 +452,100 @@ def establish_basket(request, *, no_delay=False):
return basket


ANONYMOUS_BASKET_SESSION_KEY = "anonymous_basket_id"


def get_anonymous_basket_id(request, *, create=False):
"""
Get the anonymous basket id stored in the request's session, minting one
if requested and none exists yet.

Kwargs:
create (bool): mint and store a new id in the session if one isn't
already present. Only pass True from call sites that are about to
write to the basket - minting an id writes to the session, which
forces a Set-Cookie header and defeats caching for anonymous page
views that don't need one.
"""
anonymous_id = request.session.get(ANONYMOUS_BASKET_SESSION_KEY)

if anonymous_id is None and create:
anonymous_id = str(uuid.uuid4())
request.session[ANONYMOUS_BASKET_SESSION_KEY] = anonymous_id

return anonymous_id


def establish_basket_for_request(request, *, for_update=False):
"""
Get or create the basket for the current request, whether the requester
is authenticated or anonymous.

Kwargs:
for_update (bool): re-fetch the basket with select_for_update() so it's
locked for the remainder of the caller's transaction. Pass True
when the caller is about to mutate basket contents.
"""
if request.user.is_authenticated:
basket = establish_basket(request)
else:
anonymous_id = get_anonymous_basket_id(request, create=True)
basket, _ = Basket.objects.get_or_create(anonymous_id=anonymous_id)

if for_update:
basket = Basket.objects.select_for_update().get(pk=basket.pk)

return basket


def claim_anonymous_basket(request):
"""
Convert the anonymous basket identified by the current session into a
basket for the now-authenticated request.user.

If request.user already has a basket, it is discarded in favor of the
anonymous basket - the anonymous basket reflects what was just shown on
the cart page, and merging would silently change the price the user saw.

Returns the claimed basket, or None if there's no anonymous basket to
claim (e.g. an expired session).
"""
anonymous_id = get_anonymous_basket_id(request, create=False)
if anonymous_id is None:
return None

with transaction.atomic():
try:
anon_basket = Basket.objects.select_for_update().get(
anonymous_id=anonymous_id
)
except Basket.DoesNotExist:
return None

Basket.objects.filter(user=request.user).exclude(pk=anon_basket.pk).delete()

anon_basket.user = request.user
anon_basket.anonymous_id = None
anon_basket.save(update_fields=["user", "anonymous_id"])

del request.session[ANONYMOUS_BASKET_SESSION_KEY]
apply_user_discounts(request)

return anon_basket


def cull_anonymous_baskets():
"""
Delete anonymous baskets that haven't been touched in a while (abandoned
carts). A basket's anonymous_id is only reachable via its session cookie,
so once that cookie could plausibly have expired there's no way for a
basket to ever be claimed - it's safe to remove.
"""
cutoff = now_in_utc() - timedelta(seconds=settings.ANONYMOUS_BASKET_CULL_AGE)

Basket.objects.filter(anonymous_id__isnull=False, updated_on__lt=cutoff).delete()


def refund_order(*, order_id: int = None, reference_number: str = None, **kwargs): # noqa: RUF013
"""
A function that performs refund for a given order id
Expand Down
Loading
Loading