add reviews schema and context (phase 2)
All checks were successful
deploy / deploy (push) Successful in 1m4s

- Create reviews table with product/order associations
- Add Review schema with create/update/moderation changesets
- Add Reviews context with CRUD, purchase verification, aggregates
- Add get_images/1 to Media, get_variant/1 to Products
- 23 tests covering all context functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-04-01 10:34:07 +01:00
parent 34822254e3
commit 8dc17a6f4d
7 changed files with 766 additions and 4 deletions

View File

@@ -0,0 +1,385 @@
defmodule Berrypod.ReviewsTest do
use Berrypod.DataCase, async: true
alias Berrypod.Reviews
alias Berrypod.Reviews.Review
import Berrypod.ProductsFixtures
import Berrypod.OrdersFixtures
# ── Create ─────────────────────────────────────────────────────────
describe "create_review/1" do
test "creates a review with valid attrs" do
product = product_fixture()
attrs = %{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
}
assert {:ok, %Review{} = review} = Reviews.create_review(attrs)
assert review.product_id == product.id
assert review.email == "buyer@example.com"
assert review.author_name == "Jane"
assert review.rating == 5
assert review.status == "pending"
end
test "normalises email to lowercase" do
product = product_fixture()
attrs = %{
product_id: product.id,
email: "BUYER@EXAMPLE.COM",
author_name: "Jane",
rating: 5
}
{:ok, review} = Reviews.create_review(attrs)
assert review.email == "buyer@example.com"
end
test "auto-links to matching order for verified purchase badge" do
conn = provider_connection_fixture(%{provider_type: "printify"})
product = product_fixture(%{provider_connection: conn})
variant = product_variant_fixture(%{product: product})
order =
order_fixture(%{
customer_email: "buyer@example.com",
payment_status: "paid",
variant_id: variant.id
})
attrs = %{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
}
{:ok, review} = Reviews.create_review(attrs)
assert review.order_id == order.id
end
test "allows optional title and body" do
product = product_fixture()
attrs = %{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 4,
title: "Great product!",
body: "Would buy again."
}
{:ok, review} = Reviews.create_review(attrs)
assert review.title == "Great product!"
assert review.body == "Would buy again."
end
test "validates rating is 1-5" do
product = product_fixture()
attrs = %{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 6
}
assert {:error, changeset} = Reviews.create_review(attrs)
assert "is invalid" in errors_on(changeset).rating
end
test "enforces one review per email+product" do
product = product_fixture()
attrs = %{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
}
{:ok, _} = Reviews.create_review(attrs)
{:error, changeset} = Reviews.create_review(attrs)
assert "you have already reviewed this product" in errors_on(changeset).email
end
end
# ── Read ───────────────────────────────────────────────────────────
describe "get_review/1" do
test "returns the review" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
assert Reviews.get_review(review.id) == review
end
test "returns nil for unknown id" do
assert Reviews.get_review(Ecto.UUID.generate()) == nil
end
end
describe "get_review_by_email_and_product/2" do
test "returns the review for the email+product" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
assert Reviews.get_review_by_email_and_product("buyer@example.com", product.id) == review
end
test "returns nil when no review exists" do
product = product_fixture()
assert Reviews.get_review_by_email_and_product("nobody@example.com", product.id) == nil
end
end
describe "list_reviews_for_product/2" do
test "lists only approved reviews by default" do
product = product_fixture()
{:ok, pending} =
Reviews.create_review(%{
product_id: product.id,
email: "pending@example.com",
author_name: "Pending",
rating: 3
})
{:ok, approved} =
Reviews.create_review(%{
product_id: product.id,
email: "approved@example.com",
author_name: "Approved",
rating: 5
})
Reviews.approve_review(approved)
reviews = Reviews.list_reviews_for_product(product.id)
assert length(reviews) == 1
assert hd(reviews).email == "approved@example.com"
refute Enum.any?(reviews, &(&1.id == pending.id))
end
test "can filter by status" do
product = product_fixture()
{:ok, _} =
Reviews.create_review(%{
product_id: product.id,
email: "pending@example.com",
author_name: "Pending",
rating: 3
})
reviews = Reviews.list_reviews_for_product(product.id, status: "pending")
assert length(reviews) == 1
end
end
describe "list_pending_reviews/0" do
test "lists pending reviews with preloaded product" do
product = product_fixture()
{:ok, _} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
reviews = Reviews.list_pending_reviews()
assert length(reviews) == 1
assert hd(reviews).product.id == product.id
end
end
# ── Update ─────────────────────────────────────────────────────────
describe "update_review/2" do
test "updates the review and resets status to pending" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
Reviews.approve_review(review)
{:ok, updated} = Reviews.update_review(review, %{rating: 4, body: "Updated!"})
assert updated.rating == 4
assert updated.body == "Updated!"
assert updated.status == "pending"
end
end
# ── Moderation ─────────────────────────────────────────────────────
describe "approve_review/1" do
test "sets status to approved" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
{:ok, approved} = Reviews.approve_review(review)
assert approved.status == "approved"
end
end
describe "reject_review/1" do
test "sets status to rejected" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
{:ok, rejected} = Reviews.reject_review(review)
assert rejected.status == "rejected"
end
end
# ── Aggregates ─────────────────────────────────────────────────────
describe "average_rating_for_product/1" do
test "returns average and count for approved reviews" do
product = product_fixture()
{:ok, r1} =
Reviews.create_review(%{
product_id: product.id,
email: "a@example.com",
author_name: "A",
rating: 5
})
{:ok, r2} =
Reviews.create_review(%{
product_id: product.id,
email: "b@example.com",
author_name: "B",
rating: 4
})
Reviews.approve_review(r1)
Reviews.approve_review(r2)
{avg, count} = Reviews.average_rating_for_product(product.id)
assert Decimal.equal?(avg, Decimal.new("4.5"))
assert count == 2
end
test "returns {nil, 0} when no approved reviews" do
product = product_fixture()
assert {nil, 0} = Reviews.average_rating_for_product(product.id)
end
test "excludes pending and rejected reviews" do
product = product_fixture()
{:ok, _} =
Reviews.create_review(%{
product_id: product.id,
email: "pending@example.com",
author_name: "Pending",
rating: 1
})
assert {nil, 0} = Reviews.average_rating_for_product(product.id)
end
end
# ── Purchase verification ──────────────────────────────────────────
describe "can_review?/2" do
test "returns true when email has purchased the product" do
conn = provider_connection_fixture(%{provider_type: "printify"})
product = product_fixture(%{provider_connection: conn})
variant = product_variant_fixture(%{product: product})
order_fixture(%{
customer_email: "buyer@example.com",
payment_status: "paid",
variant_id: variant.id
})
assert Reviews.can_review?("buyer@example.com", product.id)
end
test "returns false when email has not purchased the product" do
product = product_fixture()
refute Reviews.can_review?("nobody@example.com", product.id)
end
test "returns false when order exists but for different product" do
conn = provider_connection_fixture(%{provider_type: "printify"})
product1 = product_fixture(%{provider_connection: conn})
product2 = product_fixture(%{provider_connection: conn})
variant1 = product_variant_fixture(%{product: product1})
order_fixture(%{
customer_email: "buyer@example.com",
payment_status: "paid",
variant_id: variant1.id
})
refute Reviews.can_review?("buyer@example.com", product2.id)
end
end
# ── Delete ─────────────────────────────────────────────────────────
describe "delete_review/1" do
test "deletes the review" do
product = product_fixture()
{:ok, review} =
Reviews.create_review(%{
product_id: product.id,
email: "buyer@example.com",
author_name: "Jane",
rating: 5
})
{:ok, _} = Reviews.delete_review(review)
assert Reviews.get_review(review.id) == nil
end
end
end