# Collaborative Wishlists A wishlist in most stores is a private list of products. Here it is a **collaborative, shareable object** with followers, co-editors, gift purchases, and price-drop alerts — a small social layer on top of the catalog. ## What a shopper can do - **Save products** to one or more wishlists, with an optional target price. - **Share a wishlist** publicly via a per-list token — no account needed to view it. - **Invite collaborators** as editors or viewers. - **Follow** someone else's shared wishlist and get notified when they add items. - **Buy a gift** from a wishlist; the purchase is hidden from the owner so the surprise holds. - **Set price alerts** — get emailed when a saved item drops to a target price. ## How sharing works Sharing is implemented as a `Wishlist::Sharable` concern. Each wishlist carries its own `share_token`, and the public view is served from an unauthenticated route: ``` GET /shared_wishlist/:token # public, no login required ``` Every other wishlist route lives inside an `authenticate :user` block, so the public surface is exactly one deliberate endpoint. The public page also emits Open Graph meta tags, so a shared link unfurls nicely in chat apps and social feeds. ## Collaboration model Three tables carry the social features: | Table | Purpose | |-------|---------| | `wishlist_collaborators` | Roles — owner, editor, viewer — with invitation tokens | | `wishlist_follows` | Follow a shared wishlist, get notified on new items | | `gift_purchases` | A purchaser marks an item bought; it is hidden from the owner | Authorization for each action runs through Pundit policies, so a viewer can never mutate a list they were only invited to read. ## Price alerts & re-engagement Wishlists are also a marketing surface, driven by background jobs: - **Price alerts** — `wishlist_items` store a `target_price`. When a product goes on sale, `WishlistSaleNotificationJob` checks watched items and emails matches, recording `price_alert_notified_at` so a shopper is not spammed. - **Restock alerts** notify shoppers when an out-of-stock saved item returns. - **Abandoned-wishlist reminders** — a daily job nudges shoppers who saved items but never came back, tracked by `wishlist_last_visited_at` and `wishlist_reminder_sent_at`. ## Performance Wishlist counts appear on every page, so they cannot trigger a query each time: - **Counter caches** keep `wishlist_items_count` current on both `User` and `Wishlist`. - **`WishlistCacheService`** caches rendered fragments and invalidates them per user on any change via `WishlistCacheService.invalidate_for_user`. - A live count is broadcast over Turbo Streams so the header badge updates without a refresh. ## Key files | Concern | Files | |---------|-------| | Models | `Wishlist`, `WishlistItem`, `WishlistCollaborator`, `WishlistFollow`, `GiftPurchase` | | Services | `WishlistService`, `WishlistSharingService`, `WishlistCollaborationService`, `WishlistCacheService`, `WishlistNotificationService` | | Jobs | `WishlistSaleNotificationJob`, `AbandonedWishlistReminderJob` | | UI | `app/components/wishlist/`, `WishlistMailer` |