- ID
- f3b8e6a2-7c1d-4e9a-9b3f-2a6d5c8e1f47
HomeFeed APIs: supporting unauthenticated users
Context
~home_feed~ exposes two GET endpoints, both hard-requiring an authenticated, account-holding
customer:
- ~HomeFeedView~ (~home_feed/views/home_feed_view.py:42~) — calls SmartCart RecSys for a
personalized, ranked feed.
- ~MarketingItemsView~ (~home_feed/views/marketing_items_view.py:32~) — filters the
~HomeFeedConfig.MARKETING_ITEMS~ dynamic config list down to items eligible for the customer.
Both use ~permission_classes = [IsStaff | IsCustomerPathAuthorized]~, and
~IsCustomerPathAuthorized~ / ~CustomerModelMixin.parse_customer_id()~ unconditionally reject
unauthenticated requests (~app/rest/permissions.py:85~, ~app/rest/mixins.py:24~). The ask: let
logged-out visitors get a home feed too (e.g. a pre-signup browsing experience) — scoped to
*both endpoints*, serving a *generic/default (non-personalized) feed* to anonymous visitors, with
no new guest-identity or session model.
Key constraint discovered
~MarketingItemsService~ (~home_feed/services/marketing_items_service.py:27~) was already built
with a seam for this: its ~__init__~ takes explicit ~serving_size~/~diet~/exclusion params, and
~from_customer()~ is just one way to populate them — "Filter inputs are explicit, so eligibility
can be evaluated either for a stored customer... or for a synthetic set of preferences with no
customer" (line 36). Sensible no-customer defaults already exist elsewhere in the codebase:
~DEFAULT_SERVING_SIZE = 2~ (~app/const.py:1888~), and ~{}~ is a valid "no restrictions"
~DietaryPreferenceDict~ (empty dict skips all restriction filters in
~customer_dietary_service.filter_by_dietary_dict~).
~HomeFeedService~, by contrast, calls SmartCart's ~/recommendations/home-feed~ endpoint, whose
request schema (~home_feed/shared/schema_dtos/sc_home_feed_request.py:29~) declares
~customer_id: int~ as *required*. There's no legitimate customer_id to send for a real anonymous
visitor, and SmartCart/RecSys is owned by another team — this repo can't unilaterally make that
call support anonymous requests. A true SmartCart-personalized feed for logged-out visitors is
out of scope for this repo alone.
*Recommendation:* for anonymous requests, don't call SmartCart at all. Serve the same
non-personalized item set ~MarketingItemsService~ already produces (curated dynamic-config items,
filtered only by default serving size — no dietary/tag/favorite/cart exclusions since there's no
customer data to exclude) as the anonymous "home feed" content too. Flag true SmartCart-backed
anonymous recs as a follow-up dependency on the RecSys team if deeper personalization is wanted
later.
Design
Follow the existing public-endpoint convention from ~discovery_home~
(~V3PublicCollectionListView~ / ~V3PublicCollectionDetailView~ in
~discovery_home/views/v3/collection.py~, routed without a ~customers/<id>/~ prefix and
~permission_classes = [AllowAny]~), rather than overloading ~customers/self/...~ (which
~CustomerModelMixin.parse_customer_id()~ explicitly rejects for unauthenticated users by design).
1. ~MarketingItemsService~ — add a no-customer constructor
In ~home_feed/services/marketing_items_service.py~, add a classmethod alongside
~from_customer()~:
#+begin_src python
@classmethod
def from_defaults(cls) -> MarketingItemsService:
"""Build the service with no customer context, using default serving size and no restrictions."""
return cls(serving_size=DEFAULT_SERVING_SIZE, diet={})
#+end_src
(~DEFAULT_SERVING_SIZE~ from ~app.const~.) All other params (~never_product_ids~,
~disliked_tag_ids~, ~excluded_*_ids~) already default to empty sets in ~__init__~.
2. New public views
Add to ~home_feed/views/~:
- ~PublicMarketingItemsView~ — mirrors ~MarketingItemsView~ but no ~CustomerModelMixin~,
~permission_classes = [AllowAny]~, calls
~MarketingItemsService.from_defaults().get_marketing_items()~.
- ~PublicHomeFeedView~ — same shape as ~HomeFeedView~ (same param/response serializers,
pagination), ~permission_classes = [AllowAny]~, but delegates to
~MarketingItemsService.from_defaults().get_marketing_items()~ instead of ~HomeFeedService~
(no SmartCart call). Update its docstring/~extend_schema~ description to state it returns
non-personalized items, not SmartCart-ranked recs.
3. Routing
In ~home_feed/urls.py~, add routes without the ~customers/<str:customer_id>/~ prefix, e.g.:
#+begin_src python
path("home_feed/", PublicHomeFeedView.as_view(), name="v3-public-home-feed-list"),
path("home_feed/marketing_items/", PublicMarketingItemsView.as_view(), name="v3-public-home-feed-marketing-items-list"),
#+end_src
Exact path segments to be confirmed against any existing frontend/API-contract expectations —
check with whoever owns the client integration before finalizing the paths.
4. Tests
- ~home_feed/tests/services/test_marketing_items_service.py~ — add coverage for
~from_defaults()~ (default serving size applied, no exclusions, dietary filter is a no-op).
- ~home_feed/tests/views/~ — add ~test_public_home_feed_view.py~ / extend
~test_marketing_items_view.py~ with an unauthenticated-request test hitting the new public
routes and asserting a 200 with the marketing-items-derived payload, plus a regression test that
the existing customer-scoped routes still 401/403 for anonymous requests.
Verification
- ~python manage.py test home_feed~ for the new/updated test files.
- Manually hit the new public URLs with no session cookie (e.g. via ~curl~ or an incognito
browser tab against local dev) and confirm a 200 with paginated items; confirm the existing
~customers/<id>/home_feed/~ and ~customers/<id>/marketing_items/~ routes still require auth.
- ~prek run ruff-format~ / ~prek run ruff~ on all modified files.