mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Event cancellation: Add safety and security checks (#5565)
* Event cancellation: Add safety and security checks When cancelling an event, a large sum of money might be refunded instantly. This PR adds safety features around this by - doing a dry-run first that shows a preview of the expected refund sum - sending a confirmation mode via email for any automatic refunds of more than 100 currency units - keeping a more detailed log of the settings this was executed with * Update src/pretix/control/views/orders.py Co-authored-by: luelista <weller@rami.io> --------- Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -63,6 +63,15 @@ class EventCancelTests(TestCase):
|
||||
generate_invoice(self.order)
|
||||
djmail.outbox = []
|
||||
|
||||
def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs):
|
||||
dry_run = cancel_event(
|
||||
*args, **kwargs, dry_run=True
|
||||
)
|
||||
assert dry_run["refund_total"] == expected_refunds
|
||||
cancel_event(
|
||||
*args, **kwargs,
|
||||
)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_send_mail(self):
|
||||
gc = self.o.issued_gift_cards.create(currency="EUR")
|
||||
@@ -74,11 +83,11 @@ class EventCancelTests(TestCase):
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
assert len(djmail.outbox) == 1
|
||||
self.order.refresh_from_db()
|
||||
@@ -114,11 +123,11 @@ class EventCancelTests(TestCase):
|
||||
self.op1.blocked = ["admin"]
|
||||
self.op1.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("23.00")
|
||||
)
|
||||
|
||||
self.op1.refresh_from_db()
|
||||
@@ -147,11 +156,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
@@ -175,11 +184,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
|
||||
self.order.refresh_from_db()
|
||||
@@ -198,11 +207,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("42.00")
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
@@ -226,11 +235,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("44.00")
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
@@ -252,11 +261,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("44.00")
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
@@ -276,11 +285,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("31.40")
|
||||
)
|
||||
|
||||
r = self.order.refunds.get()
|
||||
@@ -304,11 +313,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PENDING
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("12.00")
|
||||
)
|
||||
|
||||
assert not self.order.refunds.exists()
|
||||
@@ -335,10 +344,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None,
|
||||
expected_refunds=Decimal("36.90")
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
@@ -371,11 +381,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("39.40")
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
assert r.amount == Decimal('39.40')
|
||||
@@ -400,11 +410,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, manual_refund=True,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
|
||||
assert self.order.refunds.count() == 2
|
||||
@@ -436,11 +446,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, manual_refund=False,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
|
||||
assert self.order.refunds.count() == 1
|
||||
@@ -467,11 +477,11 @@ class EventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, manual_refund=True,
|
||||
auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("46.00")
|
||||
)
|
||||
|
||||
assert self.order.refunds.count() == 1
|
||||
@@ -511,17 +521,26 @@ class SubEventCancelTests(TestCase):
|
||||
generate_invoice(self.order)
|
||||
djmail.outbox = []
|
||||
|
||||
def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs):
|
||||
dry_run = cancel_event(
|
||||
*args, **kwargs, dry_run=True
|
||||
)
|
||||
assert dry_run["refund_total"] == expected_refunds
|
||||
cancel_event(
|
||||
*args, **kwargs,
|
||||
)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_partially_send_mail_attendees(self):
|
||||
self.op1.attendee_email = 'foo@example.com'
|
||||
self.op1.save()
|
||||
self.op2.attendee_email = 'foo@example.org'
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
assert len(djmail.outbox) == 2
|
||||
self.order.refresh_from_db()
|
||||
@@ -532,19 +551,19 @@ class SubEventCancelTests(TestCase):
|
||||
def test_cancel_subevent_range(self):
|
||||
self.op2.subevent = self.se1
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
@@ -553,11 +572,11 @@ class SubEventCancelTests(TestCase):
|
||||
def test_cancel_simple_order(self):
|
||||
self.op2.subevent = self.se1
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
@@ -567,11 +586,11 @@ class SubEventCancelTests(TestCase):
|
||||
self.op2.subevent = self.se1
|
||||
self.op2.blocked = ["admin"]
|
||||
self.op2.save()
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
@@ -582,11 +601,11 @@ class SubEventCancelTests(TestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_all_subevents(self):
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
@@ -602,11 +621,12 @@ class SubEventCancelTests(TestCase):
|
||||
)
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
cancel_event(
|
||||
self.order.refresh_from_db()
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("23.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
@@ -614,20 +634,20 @@ class SubEventCancelTests(TestCase):
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_cancel_mixed_order_range(self):
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert self.order.positions.count() == 2
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2),
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="",
|
||||
send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("0.00")
|
||||
)
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
@@ -651,11 +671,11 @@ class SubEventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fee_per_ticket="",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("16.20")
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
@@ -682,11 +702,11 @@ class SubEventCancelTests(TestCase):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
cancel_event(
|
||||
self._cancel_with_dryrun(
|
||||
self.event.pk, subevent=self.se1.pk,
|
||||
auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00",
|
||||
send=False, send_subject="Event canceled", send_message="Event canceled :-(",
|
||||
user=None
|
||||
user=None, expected_refunds=Decimal("21.00")
|
||||
)
|
||||
r = self.order.refunds.get()
|
||||
assert r.state == OrderRefund.REFUND_STATE_DONE
|
||||
|
||||
Reference in New Issue
Block a user