# Product reviews system Status: In Progress (Phase 1-3 complete) ## Overview A review system for verified purchasers without requiring customer accounts. Uses email verification with a persistent browser session token (similar to "remember me") so customers can leave reviews, view orders, and edit reviews without re-verifying each time. Customers can upload photos with their reviews (1-3 images). Photos display inline as thumbnails and open in the existing lightbox component. ## Design principles 1. **No customer accounts** — email verification only, stored as a browser session 2. **One review per email+product** — prevents spam, simplifies UX 3. **Verified purchases only** — must have a paid order containing the product 4. **Edit forever** — via new magic link if session expired, resets to pending for re-moderation 5. **Moderation by default** — reviews start as pending, admin approves 6. **Photos optional** — 1-3 images per review, no video (yet) --- ## Email session token Replaces the old short-lived session-based order lookup with a persistent cookie. ### Flow (30 days, multi-purpose) - User enters email on `/contact` → system sends verification link with 1-hour token - On verify, sets a signed cookie: `email_session` containing email - Cookie lasts 30 days, httponly, secure, same-site lax - Also set on checkout completion (via `/checkout/complete` controller) - Cookie checked by: order lookup, order detail, review submission, review editing ### Implementation **New module:** `lib/berrypod/email_session.ex` ```elixir defmodule Berrypod.EmailSession do @cookie_name "email_session" @max_age 30 * 24 * 60 * 60 # 30 days @salt "email_session_v1" def put_session(conn, email) do token = Phoenix.Token.sign(BerrypodWeb.Endpoint, @salt, email) Plug.Conn.put_resp_cookie(conn, @cookie_name, token, max_age: @max_age, http_only: true, secure: Mix.env() == :prod, same_site: "Lax" ) end def get_email(conn) do with token when is_binary(token) <- conn.cookies[@cookie_name], {:ok, email} <- Phoenix.Token.verify(BerrypodWeb.Endpoint, @salt, token, max_age: @max_age) do {:ok, email} else _ -> :error end end def clear_session(conn) do Plug.Conn.delete_resp_cookie(conn, @cookie_name) end end ``` **Plug for LiveView access:** `lib/berrypod_web/plugs/email_session.ex` Adds `email_session` to conn assigns so LiveViews can access via `@email_session`. **Update existing flows:** - `OrderLookupController.verify/2` — call `EmailSession.put_session/2` - `Shop.Pages.Orders` — check `EmailSession` cookie instead of session - Checkout success — call `EmailSession.put_session/2` --- ## Reviews schema **Migration:** `priv/repo/migrations/xxx_create_reviews.exs` ```elixir create table(:reviews, primary_key: false) do add :id, :binary_id, primary_key: true add :product_id, references(:products, type: :binary_id, on_delete: :delete_all), null: false add :order_id, references(:orders, type: :binary_id, on_delete: :nilify_all) add :email, :string, null: false add :author_name, :string, null: false add :rating, :integer, null: false add :title, :string add :body, :text add :status, :string, default: "pending" # pending, approved, rejected add :image_ids, {:array, :binary_id}, default: [] # references to images table timestamps() end create unique_index(:reviews, [:email, :product_id]) create index(:reviews, [:product_id, :status]) create index(:reviews, [:status]) ``` **Schema:** `lib/berrypod/reviews/review.ex` Fields: - `id` — binary_id - `product_id` — required, references products - `order_id` — optional, for "verified purchase" badge (first matching order) - `email` — required, normalised lowercase - `author_name` — required, display name - `rating` — required, 1-5 - `title` — optional - `body` — optional - `status` — pending/approved/rejected - `image_ids` — array of binary_ids referencing images table (max 3) **Context:** `lib/berrypod/reviews.ex` Functions: - `get_review(id)` - `get_review_by_email_and_product(email, product_id)` — for edit check - `list_reviews_for_product(product_id, status: :approved)` — public display - `list_pending_reviews()` — admin queue - `create_review(attrs)` — validates purchase, uniqueness - `update_review(review, attrs)` — resets status to pending - `approve_review(review)` - `reject_review(review)` - `average_rating_for_product(product_id)` — returns {avg, count} - `can_review?(email, product_id)` — checks if email has purchased this product - `get_review_images(review)` — preloads image records for a review --- ## Review photos ### Storage approach Review photos stored in existing `images` table, referenced by `image_ids` array in the review. This reuses the existing media/image infrastructure: - Same upload pipeline - Same optimisation (Oban jobs) - Same serving (blob storage) Not added to the main media library (no `media_items` record) — just raw images for reviews. ### Upload flow 1. User selects photos in review form (standard file input, accepts jpg/png/webp/heic) 2. Photos uploaded via LiveView `allow_upload` with 3 file limit 3. On submit, photos processed through existing image pipeline 4. Image IDs stored in review's `image_ids` array 5. EXIF data stripped for privacy ### Display Extract a generic `image_lightbox` component from the existing `product_lightbox`, then create a simpler `review_photos` component for inline thumbnails. **New component:** `review_photos` in `shop_components/content.ex` ```elixir attr :images, :list, required: true # list of image URLs attr :review_id, :string, required: true def review_photos(assigns) do ~H"""
<.image_lightbox id={"review-#{@review_id}-lightbox"} images={@images} alt="Customer photo" />
""" end ``` The existing `Lightbox` JS hook works unchanged — it's already generic. ### Moderation Photos shown to admin alongside review text during moderation. Admin approves/rejects the whole review (no separate photo moderation). --- ## Review submission flows ### 1. From product page (email verification) **UI on product page:** ``` +-------------------------------------+ | Write a review | | | | [email@example.com ] [Continue] | | | | We'll send a link to verify your | | purchase and let you leave a review | +-------------------------------------+ ``` **Flow:** 1. User enters email 2. System checks: has this email purchased this product? - No -> "We couldn't find a matching order for this product" - Yes -> send verification email with review link 3. Email contains link: `/reviews/new?token=xxx` 4. Token encodes: `{email, product_id}` 5. On click -> set email session cookie -> show review form **If they already have a valid email session:** - Skip email entry, show review form directly - Or show "Leave a review as email@example.com" with form **If they've already reviewed this product:** - Show their existing review with "Edit" option ### 2. From order lookup page On `/orders`, for each delivered order line item: ``` +-------------------------------------+ | Mountain Sunrise Canvas | | Delivered 15 Jan 2024 | | | | [Write a review] or [Edit review] | +-------------------------------------+ ``` Since they're already verified (have email session), clicking goes straight to review form. ### 3. Via email request (post-delivery) **Oban job:** `lib/berrypod/reviews/review_request_job.ex` Scheduled X days after order marked delivered: - Check if customer has already reviewed all products in order - Send email with links to review each unreviewed product - Each link is a signed token for that email+product **Email template:** ``` Subject: How was your order from {shop_name}? Hi {name}, Your order #{ref} was delivered {days} days ago. We'd love to hear what you think! {for each product} [{product_title}] - [Leave a review] {end} Thanks for shopping with us! ``` **Admin setting:** Days after delivery to send request (default: 7) --- ## Review form **Route:** `GET /reviews/new?token=xxx` or `GET /products/:slug/review` **LiveView:** `lib/berrypod_web/live/shop/review_form.ex` **Form fields:** - Rating (required) — 5 star selector - Title (optional) — text input, max 100 chars - Body (optional) — textarea, max 2000 chars - Name (required) — pre-filled from order if available, max 50 chars - Photos (optional) — file upload, max 3 images **Photo upload UI:** - "Add photos (optional)" button with camera icon - Shows thumbnail previews after selection - Easy remove button on each thumbnail - Accepts jpg, png, webp, heic **Validation:** - Rating 1-5 - Title max 100 chars - Body max 2000 chars - Name max 50 chars - Max 3 photos, max 10MB each **On submit:** - Upload and process photos via existing image pipeline - Create review with status: pending - Show confirmation: "Thanks! Your review will appear once approved." - Redirect to product page **Edit mode:** - Same form, pre-filled with existing review - Existing photos shown with remove option - Submit updates review, resets status to pending - Show: "Your updated review will appear once approved." --- ## Reviews display ### Product page **Update `reviews_section` component** to accept real data: ```elixir # In product page init reviews = Reviews.list_reviews_for_product(product.id, status: :approved) {avg, count} = Reviews.average_rating_for_product(product.id) assign(socket, reviews: reviews, average_rating: avg, review_count: count ) ``` **Component changes:** - Show "Verified purchase" badge if `review.order_id` present - Show review date as relative time - Show review photos as clickable thumbnails (using `review_photos` component) - Pagination if > 10 reviews ### Product cards (optional enhancement) Show average rating + count on collection/home pages: ``` ****- (24) ``` Requires preloading aggregate data — consider caching in products table: - `rating_avg` decimal - `rating_count` integer - Updated via Oban job or on review approval --- ## Admin moderation **Route:** `/admin/reviews` **LiveView:** `lib/berrypod_web/live/admin/reviews.ex` **Features:** - Tabs: Pending | Approved | Rejected - List with: product, author, rating, excerpt, photos (thumbnails), date - Click to expand full review with full-size photos - Approve / Reject buttons - Bulk actions: approve selected, reject selected **Counts in nav:** - Show pending count badge on Reviews nav item **Optional:** Email notification to admin on new review --- ## Schema markup **Update product JSON-LD** in `lib/berrypod_web/components/seo_components.ex`: ```json { "@type": "Product", "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.5", "reviewCount": "24", "bestRating": "5", "worstRating": "1" }, "review": [ { "@type": "Review", "author": {"@type": "Person", "name": "Jane D."}, "datePublished": "2024-01-15", "reviewRating": { "@type": "Rating", "ratingValue": "5" }, "reviewBody": "..." } ] } ``` Only include if product has approved reviews. --- ## Task breakdown ### Phase 1: Email session foundation (~2h) ✓ 1. **Email session module** (1h) ✓ - Create `Berrypod.EmailSession` module - Create plug to load into assigns - Add to endpoint/router 2. **Update existing flows** (1h) ✓ - Order lookup -> set email session on verify - Orders page -> read from email session - Checkout success -> set email session (via /checkout/complete controller) ### Phase 2: Reviews schema and context (~2.5h) ✓ 3. **Reviews migration and schema** (1h) ✓ - Create migration with image_ids array - Create `Review` schema with changeset 4. **Reviews context** (1.5h) ✓ - CRUD functions - Query helpers (by product, by email, pending) - Purchase verification - Image preloading helpers ### Phase 3: Review submission (~4h) ✓ 5. **Product page review section** (1.5h) ✓ - Email entry form (if no session) - Review form (if session + can review) - Edit existing review - Verification email sending 6. **Review form LiveView** (2h) ✓ - Token verification - Form with star rating - Photo upload with LiveView uploads (max 3) - deferred to Phase 4 - Create/update handling - Photo processing via existing pipeline 7. **Orders page integration** (0.5h) ✓ - "Write a review" / "Edit review" buttons - Link to review form ### Phase 4: Display and components (~2.5h) 8. **Extract image_lightbox component** (0.5h) - Generic lightbox from product_lightbox - Same JS hook, just cleaner component interface 9. **Review photos component** (0.5h) - Inline thumbnails - Opens image_lightbox on click 10. **Product page display** (1.5h) - Update reviews_section to use real data - Verified purchase badge - Review photos display - Pagination ### Phase 5: Admin moderation (~2h) 11. **Admin reviews list** (1.5h) - Reviews list with tabs (pending/approved/rejected) - Photo thumbnails in list - Expand to see full review + photos - Approve/reject actions 12. **Nav and notifications** (0.5h) - Pending count badge in admin nav - Optional: email to admin on new review ### Phase 6: Automation and SEO (~2h) 13. **Review request emails** (1h) - Oban job for post-delivery requests - Email template - Admin setting for delay 14. **Schema markup** (0.5h) - AggregateRating on product pages - Individual Review markup 15. **Rating cache on products** (0.5h) - Add rating_avg, rating_count to products - Update on review approval - Display on product cards --- ## Files to create - `lib/berrypod/email_session.ex` — email session token handling - `lib/berrypod_web/plugs/email_session.ex` — plug to load session into assigns - `priv/repo/migrations/xxx_create_reviews.exs` — reviews table - `lib/berrypod/reviews/review.ex` — Review schema - `lib/berrypod/reviews.ex` — Reviews context - `lib/berrypod/reviews/review_notifier.ex` — review verification emails - `lib/berrypod/reviews/review_request_job.ex` — Oban job for post-delivery emails - `lib/berrypod_web/live/shop/review_form.ex` — review submission form - `lib/berrypod_web/live/admin/reviews.ex` — admin moderation ## Files to modify - `lib/berrypod_web/router.ex` — new routes - `lib/berrypod_web/controllers/order_lookup_controller.ex` — set email session - `lib/berrypod_web/live/shop/pages/orders.ex` — review buttons - `lib/berrypod_web/live/shop/pages/product.ex` — review section with real data - `lib/berrypod_web/components/shop_components/product.ex` — extract image_lightbox - `lib/berrypod_web/components/shop_components/content.ex` — reviews_section updates, review_photos component - `lib/berrypod_web/components/seo_components.ex` — review schema markup - `lib/berrypod/pages/block_types.ex` — real data loader for reviews - `lib/berrypod_web/components/admin_components.ex` — nav badge for pending reviews - `lib/berrypod/products/product.ex` — optional rating_avg, rating_count fields --- ## Open questions 1. **Delay for review request email** — 7 days after delivery? Make configurable in admin settings? 2. **Review replies** — can shop owner reply to reviews publicly? (later feature) 3. **Import reviews** — any need to import from other platforms like Etsy? 4. **Incentives** — discount code for leaving a review? (later feature) 5. **Photo size limit** — 10MB per photo reasonable? Or smaller? --- ## Total estimate ~15 hours across 6 phases. Can be broken into sessions: - Session 1: Phases 1-2 (email session + schema) ~4.5h - Session 2: Phase 3 (submission flows) ~4h - Session 3: Phases 4-5 (display + admin) ~4.5h - Session 4: Phase 6 (automation + SEO) ~2h