anonymous user handling - #3805
Open
gumaerc wants to merge 21 commits into
Open
Conversation
…ckconstraint that forces a basket to have either one, not both
OpenAPI ChangesShow/hide changesUnexpected changes? Ensure your branch is up-to-date with |
…cross domain without keycloak
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What are the relevant tickets?
https://github.com/mitodl/hq/issues/12530
Description (What does it do?)
Allows anonymous (logged-out) users to add items to a cart and begin checkout on MITx Online, converting the anonymous cart to a real account when the user authenticates - deferring account creation/onboarding until after they've already committed to checking out, rather than up front.
Basket model (
ecommerce/models.py)Basket.useris now nullable; addedBasket.anonymous_id(UUID, unique).CheckConstraintenforces exactly one ofuser/anonymous_idis set, so a half-converted basket is unrepresentable at the DB level.Basket.is_anonymousproperty.Anonymous identity & basket lifecycle (
ecommerce/api.py)get_anonymous_basket_id(request, create=False)- reads/mints a UUID4 intorequest.session["anonymous_basket_id"]. Only mints whencreate=True, so a plain anonymous page view never gets a session cookie written (preserves CDN/cache-friendliness for logged-out traffic).establish_basket_for_request(request, for_update=False)- single entry point for cart endpoints; dispatches to the existingestablish_basket()for authenticated users, or get-or-creates byanonymous_idotherwise.for_update=Truere-fetches withselect_for_update()for mutating callers.claim_anonymous_basket(request)- the conversion step. Reassigns the anonymous basket to the now-authenticated user, pops the session key, and applies any user discount (so an approved financial-assistance discount lands immediately, per the flow described in the ticket). If the user already had a basket, it's discarded in favor of the anonymous one (see Additional Context below - this is a judgment call, not something the ticket specifies).cull_anonymous_baskets()- deletes anonymous baskets older thanANONYMOUS_BASKET_CULL_AGE(defaults toSESSION_COOKIE_AGE), wired intoCELERY_BEAT_SCHEDULEto run daily.Endpoints (
ecommerce/views/legacy/__init__.py,ecommerce/urls.py)CheckoutApiViewSetnow uses per-action permissions (get_permissions()):add_to_cart,cart, andbasket_items_countareAllowAny;redeem_discountstaysIsAuthenticated(no discount-code entry for anonymous users, per the ticket).cartandbasket_items_countskip discount auto-apply for anonymous requests and never leak another user's basket when no anonymous session exists.CheckoutProductView(/cart/add) no longer requires login.AnonymousCheckoutViewat/checkout/anonymous/-LoginRequiredMixinas defense-in-depth behind the APISIX auth requirement (see below); claims the anonymous basket and redirects to the existing/checkout/to_paymentflow unchanged.APISIX routing (
config/apisix/apisix.yaml)/cartauth-required route so cart pages fall through to the general "pass" route.unauth_action: authroute for/checkout/anonymous, so an unauthenticated hit is redirected straight into Keycloak by APISIX and returns directly to/checkout/anonymous/(not through/login, which would otherwise trigger the new-user onboarding redirect before payment - onboarding is meant to happen after payment per the ticket).Frontend (
frontend/public/src)OrderSummaryCard.js- new requiredisAuthenticatedprop. Sends the user to/checkout/anonymous/instead of/checkout/to_paymentwhen logged out; the coupon-code form is not rendered at all for logged-out users.CartPage.js- passesisAuthenticatedthrough; suppresses the financial-assistance offer link when logged out.OrderReceiptPage.js- passesisAuthenticated={true}(receipts only exist for completed, already-converted orders).App.js- the cart-count badge and its query are no longer gated on auth, so it works for anonymous carts too (safe becausebasket_items_countnever mints a session for a read-only count check).How can this be tested?
Automated:
docker compose exec web pytest ecommerce/(699 passing) - covers the model constraint, all three newapi.pyfunctions (including the login-survives-the-session, and basket-collision-on-claim scenarios), the anonymous-reachable endpoints, and the cleanup task.yarn testfromfrontend/public(466 passing) - coversOrderSummaryCard,CartPage, andApp's badge-query change.Manual, with mitxonline's own self-contained
docker compose+ local APISIX/Keycloak:/cart/addor the API./cart- items should be visible, "Place your order" shown, no coupon field./checkout/anonymous/, which APISIX should force through Keycloak login (new or existing account)./checkout/to_paymentand the basket now belongs to your account (any approved FA discount should be applied).Note: if your local dev setup instead points at an external Keycloak/APISIX (e.g. a
mit-learndocker-compose stack), that repo'sconfig/apisix/apisix.yamlneeds the equivalent routing addition - this PR's diff only covers mitxonline's own routes.Additional Context
mit-learn: the "Enroll in X" button currently gates on auth before hitting/cart/add. That's frontend code in a different repository and isn't touched here.ol-infrastructure(src/ol_infrastructure/applications/mitxonline/__main__.py). This PR's/checkout/anonymous/route will not be auth-protected in production until that PR lands - see checklist below./api/v0/baskets/JSON APIs for anonymous baskets - future work for when the cart/checkout UI moves into Learn.ecommerce/views/v0/__init__.pycallsBasket.establish_basket(request), but no such classmethod exists on the model - this would raise on every call. Not touched here; worth its own ticket.Checklist:
ol-infrastructurePulumi PR (production APISIX routing for/checkout/anonymous/) before or alongside deploying this to production - without it, the new route falls under the general "pass" rule and the onboarding-bypass behavior described above won't hold in prod.mit-learnissue/PR for the "Enroll in X" button change. ([WIP] anonymous checkout mit-learn#3709)