- ID
- f2ed1b3c-bd0b-4d00-af31-87e42cb6234b
BE-7819 Exp 242 - Create sharable Favorites Cookbook shell on share
- source :: https://hungryroot.atlassian.net/browse/BE-7819
- tags :: Hungryroot API Project
- epic :: EP-650 Exp 242: Favorites + User Cookbooks
- sprint :: HR BE Sprint 111
- status :: Status - Complete
- priority :: Priority - Medium
- points :: 1
Summary
When a user shares their favorites, create (or retrieve) a persistent ~Cookbook~ row with ~cookbook_type=FAVORITES~ and a unique, unguessable slug. The shell Cookbook is then accessible via the existing v3 endpoint ~/api/v3/cookbooks/<slug>/~.
~CookbookType.FAVORITES~ already exists. The current ~FavoritesService~ returns a virtual ~FavoritesEnvelope~ (id=None, slug computed inline) — this ticket makes it persistent with a real slug.
Slug generation
Use a *Base62-encoded random string* (characters =[A-Za-z0-9]=), 8 characters long. Statistically unlikely to collide over 13 million+ generations.
#+begin_src python
import os
import string
ALPHABET = string.ascii_letters + string.digits # 62 chars
def generate_slug(length: int = 8) -> str:
return "".join(ALPHABET[b % 62] for b in os.urandom(length))
#+end_src
On ~get_or_create~, generate a candidate slug and retry on the (rare) collision. Do *not* use =favorites-{customer.pk}= — slug should be opaque and unguessable so sharing is opt-in.
Scope
- ~cookbooks/services.py~ — add ~get_or_create_favorites_cookbook(customer)~ on ~FavoritesService~: look up existing ~Cookbook~ for customer with ~cookbook_type=FAVORITES~; if absent, generate Base62 slug and create with ~name="My Favorites"~, ~is_active=True~.
- ~cookbooks/views.py~ — call the above when share action is triggered; return slug in response so FE can build the share URL.
- Shell Cookbook from ~/api/v3/cookbooks/<slug>/~ need not carry items — items still served by favorites GET endpoint (BE-7818).
Out of scope
- New CookbookType value (already exists).
- The favorites GET endpoint (BE-7818) and FavoritesService core logic (BE-7817).
- Any additional cookbook API changes beyond slug being accessible.
Acceptance criteria
- [ ] ~get_or_create_favorites_cookbook(customer)~ creates a ~Cookbook~ row with Base62 slug and ~cookbook_type=FAVORITES~ on first call; returns same row on subsequent calls (idempotent).
- [ ] Slug is 8 Base62 characters (=[A-Za-z0-9]=), not derived from customer PK or any PII.
- [ ] Collision retry: if generated slug already exists, new one is generated (loop with max retries).
- [ ] ~GET /api/v3/cookbooks/<slug>/~ returns 200 for the created shell.
- [ ] Slug returned to FE when share action is triggered.
- [ ] Unit tests: first-call creates row, second-call is idempotent, slug is 8 chars, collision retry works.
- [ ] ~prek run ruff~ and ~prek run ruff-format~ clean on touched files.