Add shopper accounts, reviews, tracking, and seed_demo (#9)

Closes #9. Shop-gated buyer accounts, purchase reviews, Stripe customer ids, shipment tracking, slim public contact form, and a template-neutral seed_demo command.
This commit is contained in:
2026-09-07 08:35:55 -05:00
parent 9cdce7a897
commit 5b11cc18c7
66 changed files with 3051 additions and 285 deletions
+103 -2
View File
@@ -1,5 +1,6 @@
from datetime import timedelta
from decimal import Decimal
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.core import mail
@@ -13,6 +14,7 @@ from shop.services import (
add_to_cart,
adjust_stock,
available_qty,
create_checkout_session,
create_order_from_cart,
mark_paid,
next_order_number,
@@ -116,7 +118,9 @@ class ShopInventoryTests(TestCase):
class ShopPortalTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user("merchant", password="test-pass-123")
self.user = User.objects.create_user(
"merchant", password="test-pass-123", is_staff=True
)
self.client = Client()
self.client.login(username="merchant", password="test-pass-123")
@@ -175,7 +179,9 @@ def _sold_order(product, *, qty=1, paid_at=None, status=None, number=None):
class ShopSalesDashboardTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user("merchant", password="test-pass-123")
self.user = User.objects.create_user(
"merchant", password="test-pass-123", is_staff=True
)
self.client = Client()
self.client.login(username="merchant", password="test-pass-123")
self.dragon = _product(name="Dragon", sku="DRAGON", price=Decimal("18.00"))
@@ -252,3 +258,98 @@ class ShopSalesDashboardTests(TestCase):
self.assertContains(products, "Sales")
sales = self.client.get(reverse("shop_portal:sales"))
self.assertContains(sales, 'class="active"')
class ShopCustomerAccountTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(
username="buyer@example.com",
email="buyer@example.com",
password="s3cure-pass-123",
)
self.product = _product()
self.client = Client()
self.client.login(username="buyer@example.com", password="s3cure-pass-123")
def test_checkout_attaches_user_and_uses_stripe_customer(self):
session = self.client.session
add_to_cart(session, self.product, 1)
session.save()
captured = {}
class FakeCustomer:
id = "cus_abc"
class FakeCheckout:
id = "cs_abc"
url = "https://stripe.test/pay"
class FakeStripe:
class Customer:
@staticmethod
def create(**kwargs):
captured["customer"] = kwargs
return FakeCustomer()
class checkout:
class Session:
@staticmethod
def create(**kwargs):
captured["session"] = kwargs
return FakeCheckout()
with patch("shop.services._stripe", return_value=FakeStripe):
order = create_order_from_cart(
self.client.session,
email="buyer@example.com",
user=self.user,
)
url = create_checkout_session(
order,
success_url="https://example.test/ok",
cancel_url="https://example.test/no",
)
self.assertEqual(url, "https://stripe.test/pay")
self.assertEqual(order.user, self.user)
self.assertEqual(captured["session"]["customer"], "cus_abc")
self.assertNotIn("customer_email", captured["session"])
self.assertEqual(
captured["session"]["payment_intent_data"]["setup_future_usage"],
"on_session",
)
self.user.customer_profile.refresh_from_db()
self.assertEqual(self.user.customer_profile.stripe_customer_id, "cus_abc")
def test_review_requires_purchase(self):
blocked = self.client.post(
reverse("shop:review", kwargs={"slug": self.product.slug}),
{"rating": "5", "title": "Nope", "body": "Did not buy"},
)
self.assertEqual(blocked.status_code, 302)
self.assertEqual(self.product.reviews.count(), 0)
order = _sold_order(self.product)
order.user = self.user
order.email = self.user.email
order.save(update_fields=["user", "email"])
ok = self.client.post(
reverse("shop:review", kwargs={"slug": self.product.slug}),
{"rating": "5", "title": "Great", "body": "Loved it"},
)
self.assertEqual(ok.status_code, 302)
review = self.product.reviews.get()
self.assertEqual(review.rating, 5)
self.assertEqual(review.user, self.user)
detail = self.client.get(self.product.get_absolute_url())
self.assertContains(detail, "Great")
self.assertContains(detail, "You already reviewed")
def test_order_history_hides_other_users(self):
mine = _sold_order(self.product, number="ORD-MINE")
mine.user = self.user
mine.save(update_fields=["user"])
_sold_order(self.product, number="ORD-THEIRS")
page = self.client.get(reverse("account:orders"))
self.assertContains(page, "ORD-MINE")
self.assertNotContains(page, "ORD-THEIRS")