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 from django.test import Client, TestCase, override_settings from django.urls import reverse from django.utils import timezone from shop.models import Order, OrderItem, Product from shop.services import ( ShopError, add_to_cart, adjust_stock, available_qty, create_checkout_session, create_order_from_cart, mark_paid, next_order_number, ) from shop.stats import sales_dashboard def _product(**kwargs): defaults = dict( name="Dragon figurine", sku="TOY-001", price=Decimal("18.00"), stock_qty=5, is_published=True, fulfillment=Product.Fulfillment.STOCKED, ) defaults.update(kwargs) return Product.objects.create(**defaults) class ShopPublicTests(TestCase): def test_list_hides_unpublished(self): _product(name="Live", sku="LIVE-1") _product(name="Draft", sku="DRAFT-1", is_published=False) response = Client().get(reverse("shop:list")) self.assertEqual(response.status_code, 200) self.assertContains(response, "Live") self.assertNotContains(response, "Draft") def test_add_to_cart_then_checkout_form(self): product = _product() client = Client() response = client.post( reverse("shop:cart_add", kwargs={"slug": product.slug}), {"quantity": "2"}, ) self.assertEqual(response.status_code, 302) cart = client.get(reverse("shop:cart")) self.assertContains(cart, "Dragon figurine") self.assertContains(cart, "36.00") checkout = client.get(reverse("shop:checkout")) self.assertEqual(checkout.status_code, 200) self.assertContains(checkout, "Pay with Stripe") class ShopInventoryTests(TestCase): def test_stocked_availability_and_adjust(self): product = _product(stock_qty=4) self.assertEqual(available_qty(product), 4) adjust_stock(product, -2) product.refresh_from_db() self.assertEqual(product.stock_qty, 2) with self.assertRaises(ShopError): adjust_stock(product, -5) def test_mark_paid_decrements_stock_and_emails(self): product = _product(stock_qty=3) session = self.client.session add_to_cart(session, product, 2) session.save() order = create_order_from_cart( self.client.session, email="buyer@example.com", customer_name="Pat" ) mark_paid(order) product.refresh_from_db() self.assertEqual(product.stock_qty, 1) order.refresh_from_db() self.assertEqual(order.status, Order.Status.PAID) self.assertEqual(len(mail.outbox), 1) self.assertIn(order.number, mail.outbox[0].subject) def test_insufficient_stock_blocks_order(self): product = _product(stock_qty=1) session = self.client.session add_to_cart(session, product, 3) session.save() with self.assertRaises(ShopError): create_order_from_cart(self.client.session, email="buyer@example.com") @override_settings(SHOP_PRINT_QUEUE_LIMIT_MINUTES=60) def test_made_to_order_queue_limit(self): product = _product( sku="MTO-1", fulfillment=Product.Fulfillment.MADE_TO_ORDER, print_minutes=30, stock_qty=0, ) self.assertEqual(available_qty(product), 2) def test_next_number_increments(self): n1 = next_order_number() Order.objects.create( number=n1, email="a@example.com", amount=Decimal("1.00"), ) n2 = next_order_number() self.assertNotEqual(n1, n2) class ShopPortalTests(TestCase): def setUp(self): User = get_user_model() 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") def test_list_requires_login(self): anon = Client() self.assertEqual(anon.get(reverse("shop_portal:product_list")).status_code, 302) def test_create_and_adjust_stock(self): response = self.client.post( reverse("shop_portal:product_new"), { "name": "Booster box", "sku": "TCG-BOX", "price": "89.99", "stock_qty": "10", "fulfillment": "stocked", "is_published": "on", "track_inventory": "on", }, ) self.assertEqual(response.status_code, 302) product = Product.objects.get(sku="TCG-BOX") self.assertTrue(product.is_published) self.assertEqual(product.slug, "booster-box") adjust = self.client.post( reverse("shop_portal:product_stock", kwargs={"pk": product.pk}), {"delta": "-3"}, ) self.assertEqual(adjust.status_code, 302) product.refresh_from_db() self.assertEqual(product.stock_qty, 7) def _sold_order(product, *, qty=1, paid_at=None, status=None, number=None): n = Order.objects.count() + 1 order = Order.objects.create( number=number or f"ORD-TEST-{n:04d}", email=f"buyer{n}@example.com", customer_name="Pat", status=status or Order.Status.PAID, amount=product.price * qty, currency="usd", paid_at=paid_at if paid_at is not None else timezone.now(), ) OrderItem.objects.create( order=order, product=product, name=product.name, sku=product.sku, quantity=qty, unit_price=product.price, ) return order class ShopSalesDashboardTests(TestCase): def setUp(self): User = get_user_model() 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")) self.fox = _product(name="Fox", sku="FOX", price=Decimal("12.00")) def test_sales_requires_login(self): anon = Client() self.assertEqual(anon.get(reverse("shop_portal:sales")).status_code, 302) def test_empty_dashboard(self): data = sales_dashboard() self.assertEqual(data["order_count"], 0) self.assertEqual(data["revenue"], Decimal("0.00")) self.assertEqual(len(data["daily_sales"]), 30) self.assertFalse(data["has_sales"]) response = self.client.get(reverse("shop_portal:sales")) self.assertEqual(response.status_code, 200) self.assertContains(response, "No paid orders in this window") self.assertContains(response, "Sales") self.assertContains(response, "Orders per day") self.assertContains(response, "Top products") def test_counts_paid_and_fulfilled_in_window(self): now = timezone.now() _sold_order(self.dragon, qty=2, paid_at=now) _sold_order(self.fox, qty=5, paid_at=now - timedelta(days=2)) _sold_order( self.dragon, qty=1, paid_at=now - timedelta(days=1), status=Order.Status.FULFILLED, ) _sold_order( self.fox, qty=9, paid_at=now - timedelta(days=40), ) open_order = _sold_order(self.dragon, qty=3, paid_at=None, status=Order.Status.OPEN) open_order.paid_at = None open_order.save(update_fields=["paid_at"]) cancelled = _sold_order( self.fox, qty=4, paid_at=now, status=Order.Status.CANCELLED ) self.assertEqual(cancelled.status, Order.Status.CANCELLED) data = sales_dashboard() self.assertEqual(data["order_count"], 3) self.assertEqual(data["units_sold"], 8) self.assertEqual(data["revenue"], Decimal("114.00")) self.assertEqual(data["aov"], Decimal("38.00")) self.assertEqual([row["sku"] for row in data["top_products"]], ["FOX", "DRAGON"]) self.assertEqual(data["top_products"][0]["units"], 5) self.assertEqual(data["top_products"][1]["units"], 3) today_bar = data["daily_sales"][-1] self.assertEqual(today_bar["count"], 1) response = self.client.get(reverse("shop_portal:sales")) self.assertEqual(response.status_code, 200) self.assertContains(response, "Dragon") self.assertContains(response, "Fox") self.assertContains(response, "114.00") self.assertContains(response, "Orders per day") self.assertContains(response, "Revenue per day") def test_portal_nav_and_home_stats(self): _sold_order(self.dragon, qty=1) home = self.client.get(reverse("dashboard:home")) self.assertEqual(home.status_code, 200) self.assertEqual(home.context["shop_sales_30d"], 1) self.assertEqual(home.context["shop_revenue_30d"], Decimal("18.00")) self.assertContains(home, reverse("shop_portal:sales")) self.assertContains(home, "Sales (30 days)") products = self.client.get(reverse("shop_portal:product_list")) 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")