Refactor validation of cart contents, fix purchase of inactive subevent (Z#23217806) (#5715)

* Refactor validation of cart contents, fix purchase of inactive subevent (Z#23217806)

* Apply suggestions from code review

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Review notes

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
This commit is contained in:
Raphael Michel
2025-12-17 16:59:26 +01:00
committed by GitHub
parent d0d7670ca5
commit e466c4fb72
6 changed files with 340 additions and 187 deletions

View File

@@ -745,6 +745,8 @@ def test_use_membership(event, customer, membership, requiring_ticket):
item=requiring_ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123",
used_membership=membership
)
q = event.quotas.create(size=None, name="foo")
q.items.add(requiring_ticket)
order = _create_order(event, email='dummy@example.org', positions=[cp1],
now_dt=now(),
sales_channel=event.organizer.sales_channels.get(identifier="web"),
@@ -767,6 +769,8 @@ def test_use_membership_invalid(event, customer, membership, requiring_ticket):
membership.date_start -= timedelta(days=100)
membership.date_end -= timedelta(days=100)
membership.save()
q = event.quotas.create(size=None, name="foo")
q.items.add(requiring_ticket)
cp1 = CartPosition.objects.create(
item=requiring_ticket, price=23, expires=now() + timedelta(days=1), event=event, cart_id="123",
used_membership=membership

View File

@@ -1428,6 +1428,27 @@ class CartTest(CartTestMixin, TestCase):
self.assertEqual(cp2.expires, now() + self.cart_reservation_time)
self.assertEqual(cp2.max_extend, now() + 11 * self.cart_reservation_time)
def test_expired_cart_extend_price_change_note(self):
start_time = datetime.datetime(2024, 1, 1, 10, 00, 00, tzinfo=datetime.timezone.utc)
max_extend = start_time + 11 * self.cart_reservation_time
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=max_extend, max_extend=max_extend
)
cp1.update_listed_price_and_voucher()
self.ticket.default_price = Decimal("25.00")
self.ticket.save()
with freezegun.freeze_time(max_extend + timedelta(hours=1)):
response = self.client.post('/%s/%s/cart/extend' % (self.orga.slug, self.event.slug), {
}, follow=True)
doc = BeautifulSoup(response.rendered_content, "lxml")
self.assertIn('some of the prices in your cart changed', doc.select('.alert-success')[0].text)
with scopes_disabled():
cp1.refresh_from_db()
self.assertEqual(cp1.price, Decimal("25.00"))
self.assertEqual(cp1.expires, now() + self.cart_reservation_time)
def test_expired_cart_extend_fails_partially_on_bundled(self):
start_time = datetime.datetime(2024, 1, 1, 10, 00, 00, tzinfo=datetime.timezone.utc)
max_extend = start_time + 11 * self.cart_reservation_time
@@ -3408,6 +3429,22 @@ class CartAddonTest(CartTestMixin, TestCase):
assert cp2.expires > now()
assert cp2.addon_to_id == cp1.pk
@classscope(attr='orga')
def test_expand_expired_price_change(self):
cp1 = CartPosition.objects.create(
expires=now() - timedelta(minutes=10), max_extend=now() + 10 * self.cart_reservation_time,
item=self.ticket, price=Decimal('23.00'),
event=self.event, cart_id=self.session_key
)
self.ticket.default_price = Decimal("25.00")
self.ticket.save()
self.cm.extend_expired_positions()
self.cm.commit()
cp1.refresh_from_db()
assert cp1.expires > now()
assert cp1.listed_price == Decimal("25.00")
assert cp1.price == Decimal("25.00")
@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',
@@ -4080,6 +4117,8 @@ class CartBundleTest(CartTestMixin, TestCase):
@classscope(attr='orga')
def test_extend_bundled_and_addon(self):
self.trans.require_bundling = False
self.trans.save()
cp = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=21.5, expires=now() - timedelta(minutes=10), max_extend=now() + 10 * self.cart_reservation_time

View File

@@ -2669,7 +2669,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
cr1 = CartPosition.objects.create(
@@ -2839,8 +2839,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now())
se2 = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
self.quota_tickets.size = 0
self.quota_tickets.subevent = se2
self.quota_tickets.save()
@@ -2880,7 +2880,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24)
@@ -2901,7 +2901,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24, disabled=True)
@@ -2919,7 +2919,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.workshop2)
q.variations.add(self.workshop2b)
@@ -2938,7 +2938,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
SubEventItem.objects.create(subevent=se, item=self.ticket, price=24, available_until=now() - timedelta(days=1))
@@ -2956,7 +2956,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.workshop2)
q.variations.add(self.workshop2b)
@@ -3735,8 +3735,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now())
se2 = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
self.quota_tickets.size = 10
self.quota_tickets.subevent = se2
self.quota_tickets.save()
@@ -4165,7 +4165,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
with scopes_disabled():
self.event.has_subevents = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
self.workshopquota.size = 1
self.workshopquota.subevent = se
self.workshopquota.save()
@@ -4214,7 +4214,10 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), presale_start=now() + datetime.timedelta(days=1))
se = self.event.subevents.create(name='Foo', date_from=now(), presale_start=now() + datetime.timedelta(days=1),
active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4224,7 +4227,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'booking period for one of the events in your cart has not yet started.' in response.content.decode()
assert 'booking period for this event has not yet started.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4233,7 +4236,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() - datetime.timedelta(days=1))
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() - datetime.timedelta(days=1), active=True)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4243,7 +4246,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'booking period for one of the events in your cart has ended.' in response.content.decode()
assert 'The booking period for this event has ended.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4253,7 +4256,9 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.settings.display_net_prices = True
self.event.save()
self.event.settings.payment_term_last = 'RELDATE/1/23:59:59/date_from/'
se = self.event.subevents.create(name='Foo', date_from=now())
se = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4263,7 +4268,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'booking period for one of the events in your cart has ended.' in response.content.decode()
assert 'All payments for this event need to be confirmed already, so no new orders can be created.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
@@ -4272,7 +4277,10 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.event.date_to = now() - datetime.timedelta(days=1)
self.event.save()
with scopes_disabled():
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() + datetime.timedelta(days=1))
se = self.event.subevents.create(name='Foo', date_from=now(), presale_end=now() + datetime.timedelta(days=1),
active=True)
q = se.quotas.create(name="foo", size=None, event=self.event)
q.items.add(self.ticket)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
@@ -4283,6 +4291,25 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
def test_confirm_subevent_disabled(self):
with scopes_disabled():
self.event.has_subevents = True
self.event.settings.display_net_prices = True
self.event.save()
se = self.event.subevents.create(name='Foo', date_from=now(), active=False)
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10), subevent=se
)
self._set_payment()
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select(".alert-danger")), 1)
assert 'selected event date is not active.' in response.content.decode()
with scopes_disabled():
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
def test_before_presale_timemachine(self):
self._login_with_permission(self.orga)
self._enable_test_mode()
@@ -4497,6 +4524,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.assertEqual(Order.objects.first().locale, 'de')
def test_variation_require_approval(self):
self.workshop2.category = None
self.workshop2.save()
self.workshop2a.require_approval = True
self.workshop2a.save()
with scopes_disabled():
@@ -4518,6 +4547,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
def test_item_with_variations_require_approval(self):
self.workshop2.require_approval = True
self.workshop2.category = None
self.workshop2.save()
with scopes_disabled():
cr1 = CartPosition.objects.create(