Template
Clients can run in-house retail without Shopify while keeping the same Docker/Django/Postgres stack and data-ownership promise. Closes #5.
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
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)
|