Add a maximum budget to vouchers (#1526)

* Data model changes

* Fix test failures

* Adjustments

* Some tests and API support

* Check when extending orders

* Make things more deterministic, fix style

* Do not apply negative discounts

* Update price_before_voucher on item/subevent changes

* Add tests for price_before_voucher in combination with free price

* Fix InvoiceAddress.DoesNotExist
This commit is contained in:
Raphael Michel
2020-01-03 16:15:17 +01:00
committed by GitHub
parent b738e3bd9d
commit 8e2821b398
20 changed files with 649 additions and 23 deletions

View File

@@ -3105,6 +3105,81 @@ def test_order_create_auto_pricing_reverse_charge_require_valid_vatid(token_clie
assert p.tax_rate == Decimal('19.00')
@pytest.mark.django_db
def test_order_create_autopricing_voucher_budget_partially(token_client, organizer, event, item, quota, question,
taxrule):
with scopes_disabled():
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('2.50'),
max_usages=999)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['voucher'] = voucher.code
del res['positions'][0]['price']
del res['positions'][0]['positionid']
res['positions'].append(res['positions'][0])
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
print(resp.data)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
p = o.positions.first()
p2 = o.positions.last()
assert p.price == Decimal('21.50')
assert p2.price == Decimal('22.00')
@pytest.mark.django_db
def test_order_create_autopricing_voucher_budget_full(token_client, organizer, event, item, quota, question, taxrule):
with scopes_disabled():
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('0.50'),
max_usages=999)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['voucher'] = voucher.code
del res['positions'][0]['price']
del res['positions'][0]['positionid']
res['positions'].append(res['positions'][0])
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': [{}, {'voucher': ['The voucher has a remaining budget of 0.00, therefore a '
'discount of 1.50 can not be given.']}]}
@pytest.mark.django_db
def test_order_create_voucher_budget_exceeded(token_client, organizer, event, item, quota, question, taxrule):
with scopes_disabled():
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('3.00'),
max_usages=999)
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['voucher'] = voucher.code
res['positions'][0]['price'] = '19.00'
del res['positions'][0]['positionid']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
print(resp.data)
assert resp.status_code == 400
assert resp.data == {'positions': [{'voucher': ['The voucher has a remaining budget of 3.00, therefore a '
'discount of 4.00 can not be given.']}]}
@pytest.mark.django_db
def test_order_create_voucher_price(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)

View File

@@ -795,6 +795,31 @@ class VoucherTestCase(BaseQuotaTestCase):
v = Voucher.objects.create(event=self.event, price_mode='percent', value=Decimal('23.00'))
assert v.calculate_price(Decimal('100.00')) == Decimal('77.00')
@classscope(attr='o')
def test_calculate_price_max_discount(self):
v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
assert v.calculate_price(Decimal('23.42'), max_discount=Decimal('5.00')) == Decimal('18.42')
@classscope(attr='o')
def test_calculate_budget_used(self):
v = Voucher.objects.create(event=self.event, price_mode='sset', value=Decimal('20.00'))
order = Order.objects.create(
status=Order.STATUS_PENDING, event=self.event,
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
)
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
price_before_voucher=Decimal('23.00'))
assert v.budget_used() == Decimal('3.00')
order = Order.objects.create(
status=Order.STATUS_PAID, event=self.event,
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
)
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
price_before_voucher=Decimal('23.00'))
assert v.budget_used() == Decimal('6.00')
class OrderTestCase(BaseQuotaTestCase):
def setUp(self):

View File

@@ -878,6 +878,36 @@ class OrderChangeManagerTests(TestCase):
assert self.op1.tax_value == Decimal('3.67')
assert self.op1.tax_rule == self.shirt.tax_rule
@classscope(attr='o')
def test_change_item_change_price_before_voucher(self):
self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='5.00')
self.op1.price = Decimal('5.00')
self.op1.price_before_voucher = Decimal('23.00')
self.op1.save()
p = self.op1.price
self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.commit()
self.op1.refresh_from_db()
self.order.refresh_from_db()
assert self.op1.item == self.shirt
assert self.op1.price == p
assert self.op1.price_before_voucher == Decimal('12.00')
@classscope(attr='o')
def test_change_item_change_price_before_voucher_minimum_value(self):
self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='20.00')
self.op1.price = Decimal('20.00')
self.op1.price_before_voucher = Decimal('23.00')
self.op1.save()
p = self.op1.price
self.ocm.change_item(self.op1, self.shirt, None)
self.ocm.commit()
self.op1.refresh_from_db()
self.order.refresh_from_db()
assert self.op1.item == self.shirt
assert self.op1.price == p
assert self.op1.price_before_voucher == Decimal('20.00')
@classscope(attr='o')
def test_change_item_success(self):
self.ocm.change_item(self.op1, self.shirt, None)

View File

@@ -845,6 +845,69 @@ def test_order_extend_expired_quota_partial(client, env):
assert o.status == Order.STATUS_EXPIRED
@pytest.mark.django_db
def test_order_extend_expired_voucher_budget_ok(client, env):
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.expires = now() - timedelta(days=5)
o.status = Order.STATUS_EXPIRED
o.save()
v = env[0].vouchers.create(
code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('1.50')
)
p = o.positions.first()
p.voucher = v
p.price_before_voucher = p.price
p.price -= Decimal('1.50')
p.save()
q = Quota.objects.create(event=env[0], size=100)
q.items.add(env[3])
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
'expires': newdate
}, follow=True)
assert b'alert-success' in response.content
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert o.status == Order.STATUS_PENDING
assert v.budget_used() == Decimal('1.50')
@pytest.mark.django_db
def test_order_extend_expired_voucher_budget_fail(client, env):
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
o.expires = now() - timedelta(days=5)
o.status = Order.STATUS_EXPIRED
olddate = o.expires
o.save()
v = env[0].vouchers.create(
code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('0.00')
)
p = o.positions.first()
p.voucher = v
p.price_before_voucher = p.price
p.price -= Decimal('1.50')
p.save()
q = Quota.objects.create(event=env[0], size=100)
q.items.add(env[3])
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
'expires': newdate
}, follow=True)
assert b'alert-danger' in response.content
assert b'The voucher "FOO" no longer has sufficient budget.' in response.content
with scopes_disabled():
o = Order.objects.get(id=env[2].id)
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
assert o.status == Order.STATUS_EXPIRED
assert v.budget_used() == Decimal('0.00')
@pytest.mark.django_db
def test_order_mark_paid_overdue_quota_blocked_by_waiting_list(client, env):
with scopes_disabled():

View File

@@ -1388,6 +1388,32 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('21.00'))
self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
def test_voucher_free_price_before_voucher_cap(self):
with scopes_disabled():
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
self.ticket.free_price = True
self.ticket.save()
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1',
'price_%d' % self.ticket.id: '41.00',
'_voucher_code': v.code,
}, follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
target_status_code=200)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[1].text)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('41.00'))
self.assertEqual(objs[0].price_before_voucher, Decimal('41.00'))
def test_voucher_free_price_lower_bound(self):
with scopes_disabled():
@@ -1412,6 +1438,7 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(objs[0].item, self.ticket)
self.assertIsNone(objs[0].variation)
self.assertEqual(objs[0].price, Decimal('20.70'))
self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
def test_voucher_redemed(self):
with scopes_disabled():
@@ -2438,6 +2465,20 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp2.expires > now()
assert cp2.addon_to_id == cp1.pk
@classscope(attr='orga')
def test_expand_expired_refresh_voucher(self):
v = Voucher.objects.create(item=self.ticket, value=Decimal('20.00'), event=self.event, price_mode='set',
valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
cp1 = CartPosition.objects.create(
expires=now() - timedelta(minutes=10), item=self.ticket, price=Decimal('21.50'),
event=self.event, cart_id=self.session_key, voucher=v
)
self.cm.extend_expired_positions()
self.cm.commit()
cp1.refresh_from_db()
assert cp1.expires > now()
assert cp1.price_before_voucher == Decimal('23.00')
class CartBundleTest(CartTestMixin, TestCase):
@scopes_disabled()
@@ -2490,6 +2531,30 @@ class CartBundleTest(CartTestMixin, TestCase):
assert cp.price == 23 - 1.5
assert cp.addons.count() == 1
assert cp.voucher == v
assert cp.price_before_voucher == 23 - 1.5
a = cp.addons.get()
assert a.item == self.trans
assert a.price == 1.5
assert not a.voucher
@classscope(attr='orga')
def test_discounted_voucher_on_base_product(self):
v = self.event.vouchers.create(code="foo", item=self.ticket, price_mode='subtract', value=Decimal('1.50'))
self.cm.add_new_items([
{
'item': self.ticket.pk,
'variation': None,
'voucher': v.code,
'count': 1
}
])
self.cm.commit()
cp = CartPosition.objects.get(addon_to__isnull=True)
assert cp.item == self.ticket
assert cp.price == 23 - 1.5 - 1.5
assert cp.addons.count() == 1
assert cp.voucher == v
assert cp.price_before_voucher == 23 - 1.5
a = cp.addons.get()
assert a.item == self.trans
assert a.price == 1.5

View File

@@ -3000,3 +3000,140 @@ class CheckoutSeatingTest(BaseCheckoutTestCase, TestCase):
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
class CheckoutVoucherBudgetTest(BaseCheckoutTestCase, TestCase):
@scopes_disabled()
def setUp(self):
super().setUp()
self.v = Voucher.objects.create(item=self.ticket, value=Decimal('21.50'), event=self.event, price_mode='set',
valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
self.cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
)
self.cp2 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
)
@scopes_disabled()
def test_no_budget(self):
oid = _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
o = Order.objects.get(pk=oid)
op = o.positions.first()
assert op.item == self.ticket
assert op.price_before_voucher == Decimal('23.00')
@scopes_disabled()
def test_budget_exceeded_for_second_order(self):
self.v.budget = Decimal('1.50')
self.v.save()
oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
'web')
o = Order.objects.get(pk=oid)
op = o.positions.first()
assert op.item == self.ticket
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('23.00')
@scopes_disabled()
def test_budget_exceeded_between_positions(self):
self.v.budget = Decimal('1.50')
self.v.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp1.refresh_from_db()
assert self.cp1.price == Decimal('21.50')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('23.00')
@scopes_disabled()
def test_budget_exceeded_in_first_position(self):
self.v.budget = Decimal('1.00')
self.v.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp1.refresh_from_db()
assert self.cp1.price == Decimal('22.00')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('23.00')
@scopes_disabled()
def test_budget_exceeded_in_second_position(self):
self.v.budget = Decimal('2.50')
self.v.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp1.refresh_from_db()
assert self.cp1.price == Decimal('21.50')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('22.00')
@scopes_disabled()
def test_budget_exceeded_during_price_change(self):
self.v.budget = Decimal('2.50')
self.v.value = Decimal('21.00')
self.v.save()
self.cp1.expires = now() - timedelta(hours=1)
self.cp1.save()
self.cp2.expires = now() - timedelta(hours=1)
self.cp2.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp1.refresh_from_db()
assert self.cp1.price == Decimal('21.00')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('22.50')
@scopes_disabled()
def test_budget_exceeded_expired_cart(self):
self.v.budget = Decimal('0.00')
self.v.value = Decimal('21.00')
self.v.save()
self.cp1.expires = now() - timedelta(hours=1)
self.cp1.save()
self.cp2.expires = now() - timedelta(hours=1)
self.cp2.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp1.refresh_from_db()
assert self.cp1.price == Decimal('23.00')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('23.00')
@scopes_disabled()
def test_budget_overbooked_expired_cart(self):
self.v.budget = Decimal('1.50')
self.v.value = Decimal('21.50')
self.v.save()
self.cp1.expires = now() - timedelta(hours=1)
self.cp1.save()
self.cp2.expires = now() - timedelta(hours=1)
self.cp2.save()
oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
'web')
o = Order.objects.get(pk=oid)
op = o.positions.first()
assert op.item == self.ticket
self.v.budget = Decimal('1.00')
self.v.save()
with self.assertRaises(OrderError):
_perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
'web')
self.cp2.refresh_from_db()
assert self.cp2.price == Decimal('23.00')