forked from CGM_Public/pretix_original
Improved voucher interface with new methods (#284)
* Check that a voucher's variation matches its item * Add method to check applicability of a voucher * Add method to check if a voucher can be used * Add tests for new voucher methods * Test for ValidationErrors in Voucher.clean() * Test for voucher state during ordering process
This commit is contained in:
committed by
Raphael Michel
parent
853510a375
commit
aa63a4cded
@@ -2,6 +2,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .base import LoggedModel
|
from .base import LoggedModel
|
||||||
@@ -150,6 +151,8 @@ class Voucher(LoggedModel):
|
|||||||
if self.variation and (not self.item or not self.item.has_variations):
|
if self.variation and (not self.item or not self.item.has_variations):
|
||||||
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
|
raise ValidationError(_('You cannot select a variation without having selected a product that provides '
|
||||||
'variations.'))
|
'variations.'))
|
||||||
|
if self.variation and not self.item.variations.filter(pk=self.variation.pk).exists():
|
||||||
|
raise ValidationError(_('This variation does not belong to this product.'))
|
||||||
if self.item.has_variations and not self.variation and self.block_quota:
|
if self.item.has_variations and not self.variation and self.block_quota:
|
||||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||||
'Otherwise it might be unclear which quotas to block.'))
|
'Otherwise it might be unclear which quotas to block.'))
|
||||||
@@ -176,3 +179,25 @@ class Voucher(LoggedModel):
|
|||||||
Returns whether an order position exists that uses this voucher.
|
Returns whether an order position exists that uses this voucher.
|
||||||
"""
|
"""
|
||||||
return self.orderposition_set.exists()
|
return self.orderposition_set.exists()
|
||||||
|
|
||||||
|
def applies_to(self, item: Item, variation: ItemVariation=None) -> bool:
|
||||||
|
"""
|
||||||
|
Returns whether this voucher applies to a given item (and optionally
|
||||||
|
a variation).
|
||||||
|
"""
|
||||||
|
if self.quota:
|
||||||
|
return item.quotas.filter(pk=self.quota.pk).exists()
|
||||||
|
if self.item and not self.variation:
|
||||||
|
return self.item == item
|
||||||
|
return (self.item == item) and (self.variation == variation)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
"""
|
||||||
|
Returns True if a voucher has not yet been redeemed, but is still
|
||||||
|
within its validity (if valid_until is set).
|
||||||
|
"""
|
||||||
|
if self.redeemed:
|
||||||
|
return False
|
||||||
|
if self.valid_until and self.valid_until < now():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|||||||
@@ -117,9 +117,7 @@ def _add_new_items(event: Event, items: List[dict],
|
|||||||
return error_messages['voucher_redeemed']
|
return error_messages['voucher_redeemed']
|
||||||
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
if voucher.valid_until is not None and voucher.valid_until < now_dt:
|
||||||
return error_messages['voucher_expired']
|
return error_messages['voucher_expired']
|
||||||
if voucher.item and voucher.item.pk != item.pk:
|
if not voucher.applies_to(item, variation):
|
||||||
return error_messages['voucher_invalid_item']
|
|
||||||
if voucher.variation and (not variation or variation.pk != voucher.variation.pk):
|
|
||||||
return error_messages['voucher_invalid_item']
|
return error_messages['voucher_invalid_item']
|
||||||
doubleuse = CartPosition.objects.filter(voucher=voucher, cart_id=cart_id, event=event)
|
doubleuse = CartPosition.objects.filter(voucher=voucher, cart_id=cart_id, event=event)
|
||||||
if 'cp' in i:
|
if 'cp' in i:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -53,7 +54,10 @@ class BaseQuotaTestCase(TestCase):
|
|||||||
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
|
||||||
admission=True)
|
admission=True)
|
||||||
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
|
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
|
||||||
|
self.item3 = Item.objects.create(event=self.event, name="Goodie", default_price=23)
|
||||||
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')
|
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')
|
||||||
|
self.var2 = ItemVariation.objects.create(item=self.item2, value='M')
|
||||||
|
self.var3 = ItemVariation.objects.create(item=self.item3, value='Fancy')
|
||||||
|
|
||||||
|
|
||||||
class QuotaTestCase(BaseQuotaTestCase):
|
class QuotaTestCase(BaseQuotaTestCase):
|
||||||
@@ -197,6 +201,7 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
|
|
||||||
v = Voucher.objects.create(item=self.item1, event=self.event)
|
v = Voucher.objects.create(item=self.item1, event=self.event)
|
||||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
|
||||||
v.block_quota = True
|
v.block_quota = True
|
||||||
v.save()
|
v.save()
|
||||||
@@ -209,6 +214,7 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
|
|
||||||
v = Voucher.objects.create(item=self.item2, variation=self.var1, event=self.event)
|
v = Voucher.objects.create(item=self.item2, variation=self.var1, event=self.event)
|
||||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
|
||||||
v.block_quota = True
|
v.block_quota = True
|
||||||
v.save()
|
v.save()
|
||||||
@@ -221,6 +227,7 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
|
|
||||||
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
|
||||||
v.block_quota = True
|
v.block_quota = True
|
||||||
v.save()
|
v.save()
|
||||||
@@ -238,9 +245,10 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
self.quota.variations.add(self.var1)
|
self.quota.variations.add(self.var1)
|
||||||
self.quota.size = 1
|
self.quota.size = 1
|
||||||
self.quota.save()
|
self.quota.save()
|
||||||
Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() - timedelta(days=5),
|
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() - timedelta(days=5),
|
||||||
block_quota=True)
|
block_quota=True)
|
||||||
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
self.assertEqual(self.var1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
|
self.assertFalse(v.is_active())
|
||||||
|
|
||||||
def test_blocking_voucher_in_cart(self):
|
def test_blocking_voucher_in_cart(self):
|
||||||
self.quota.items.add(self.item1)
|
self.quota.items.add(self.item1)
|
||||||
@@ -248,6 +256,7 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
block_quota=True)
|
block_quota=True)
|
||||||
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
CartPosition.objects.create(event=self.event, item=self.item1, price=2,
|
||||||
expires=now() + timedelta(days=3), voucher=v)
|
expires=now() + timedelta(days=3), voucher=v)
|
||||||
|
self.assertTrue(v.is_in_cart())
|
||||||
self.assertEqual(self.quota.count_blocking_vouchers(), 1)
|
self.assertEqual(self.quota.count_blocking_vouchers(), 1)
|
||||||
self.assertEqual(self.quota.count_in_cart(), 0)
|
self.assertEqual(self.quota.count_in_cart(), 0)
|
||||||
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_OK, 1))
|
||||||
@@ -283,11 +292,22 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
def test_voucher_reuse(self):
|
def test_voucher_reuse(self):
|
||||||
self.quota.items.add(self.item1)
|
self.quota.items.add(self.item1)
|
||||||
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5))
|
v = Voucher.objects.create(quota=self.quota, event=self.event, valid_until=now() + timedelta(days=5))
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
self.assertFalse(v.is_in_cart())
|
||||||
|
self.assertFalse(v.is_ordered())
|
||||||
|
|
||||||
# use a voucher normally
|
# use a voucher normally
|
||||||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||||||
expires=now() + timedelta(days=3), voucher=v)
|
expires=now() + timedelta(days=3), voucher=v)
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
self.assertTrue(v.is_in_cart())
|
||||||
|
self.assertFalse(v.is_ordered())
|
||||||
|
|
||||||
order = perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
order = perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
||||||
|
v.refresh_from_db()
|
||||||
|
self.assertFalse(v.is_active())
|
||||||
|
self.assertFalse(v.is_in_cart())
|
||||||
|
self.assertTrue(v.is_ordered())
|
||||||
|
|
||||||
# assert that the voucher cannot be reused
|
# assert that the voucher cannot be reused
|
||||||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||||||
@@ -296,10 +316,59 @@ class QuotaTestCase(BaseQuotaTestCase):
|
|||||||
|
|
||||||
# assert that the voucher can be re-used after cancelling the successful order
|
# assert that the voucher can be re-used after cancelling the successful order
|
||||||
cancel_order(order)
|
cancel_order(order)
|
||||||
|
v.refresh_from_db()
|
||||||
|
self.assertTrue(v.is_active())
|
||||||
|
self.assertFalse(v.is_in_cart())
|
||||||
|
self.assertTrue(v.is_ordered())
|
||||||
|
|
||||||
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
cart = CartPosition.objects.create(event=self.event, item=self.item1, price=self.item1.default_price,
|
||||||
expires=now() + timedelta(days=3), voucher=v)
|
expires=now() + timedelta(days=3), voucher=v)
|
||||||
perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
perform_order(event=self.event.id, payment_provider='free', positions=[cart.id])
|
||||||
|
|
||||||
|
def test_voucher_applicability_quota(self):
|
||||||
|
self.quota.items.add(self.item1)
|
||||||
|
v = Voucher.objects.create(quota=self.quota, event=self.event)
|
||||||
|
self.assertTrue(v.applies_to(self.item1))
|
||||||
|
self.assertFalse(v.applies_to(self.item2))
|
||||||
|
|
||||||
|
def test_voucher_applicability_item(self):
|
||||||
|
v = Voucher.objects.create(item=self.var1.item, event=self.event)
|
||||||
|
self.assertFalse(v.applies_to(self.item1))
|
||||||
|
self.assertTrue(v.applies_to(self.var1.item))
|
||||||
|
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||||||
|
|
||||||
|
def test_voucher_applicability_variation(self):
|
||||||
|
v = Voucher.objects.create(item=self.var1.item, variation=self.var1, event=self.event)
|
||||||
|
self.assertFalse(v.applies_to(self.item1))
|
||||||
|
self.assertFalse(v.applies_to(self.var1.item))
|
||||||
|
self.assertTrue(v.applies_to(self.var1.item, self.var1))
|
||||||
|
self.assertFalse(v.applies_to(self.var1.item, self.var2))
|
||||||
|
|
||||||
|
def test_voucher_no_item_with_quota(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v = Voucher(quota=self.quota, item=self.item1, event=self.event)
|
||||||
|
v.clean()
|
||||||
|
|
||||||
|
def test_voucher_item_with_no_variation(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v = Voucher(item=self.item1, variation=self.var1, event=self.event)
|
||||||
|
v.clean()
|
||||||
|
|
||||||
|
def test_voucher_item_does_not_match_variation(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v = Voucher(item=self.item2, variation=self.var3, event=self.event)
|
||||||
|
v.clean()
|
||||||
|
|
||||||
|
def test_voucher_specify_variation_for_block_quota(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v = Voucher(item=self.item2, block_quota=True, event=self.event)
|
||||||
|
v.clean()
|
||||||
|
|
||||||
|
def test_voucher_no_item_but_variation(self):
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
v = Voucher(variation=self.var1, event=self.event)
|
||||||
|
v.clean()
|
||||||
|
|
||||||
|
|
||||||
class OrderTestCase(BaseQuotaTestCase):
|
class OrderTestCase(BaseQuotaTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user