from decimal import Decimal 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 shop.models import Order, Product from shop.services import ( ShopError, add_to_cart, adjust_stock, available_qty, create_order_from_cart, mark_paid, next_order_number, ) 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") 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)