generated from westfarn/web_django_template
Customer accounts, order tracking, and purchase reviews (#8)
## Summary - Slim the public contact form to email, interest, and message. Name, phone, and address live on the customer profile instead. - Customers can register, sign in, save shipping details, and view order history. Logged-in checkout creates a Stripe Customer and saves cards on Stripe (`setup_future_usage`); we only store `stripe_customer_id`. - Shipment tracking: EasyPost tracker lookup + webhook, plus paste-in numbers from Pirate Ship/Shippo. Customers see carrier status on their orders; `dispatch_due` refreshes open shipments. - Product reviews (1–5) only after a paid/fulfilled purchase of that product. Fixes #7 ## Test plan - [ ] Contact form submits with only email + message; extra name/phone/address fields are ignored - [ ] Register, sign in, save profile (name/phone/shipping) - [ ] Guest checkout still works; after signup, prior orders with that email show in history - [ ] Logged-in checkout prefills shipping and does not collect card data locally - [ ] Portal: buy label or paste a Pirate Ship tracking number, confirm status/events; customer order page shows tracking - [ ] Product page: non-buyers cannot review; buyers can leave one 1–5 star review - [ ] Non-staff users hitting `/portal/` redirect to `/account/` Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
+98
-2
@@ -24,6 +24,7 @@ from shop.services import (
|
||||
add_to_cart,
|
||||
adjust_stock,
|
||||
available_qty,
|
||||
create_checkout_session,
|
||||
create_order_from_cart,
|
||||
looks_like_stl,
|
||||
mark_paid,
|
||||
@@ -370,7 +371,7 @@ 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")
|
||||
|
||||
@@ -663,7 +664,7 @@ 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"))
|
||||
@@ -741,3 +742,98 @@ class ShopSalesDashboardTests(TestCase):
|
||||
sales = self.client.get(reverse("shop_portal:sales"))
|
||||
self.assertContains(sales, 'class="active"')
|
||||
|
||||
|
||||
class ShopAccountAndReviewTests(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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user