mirror of
https://github.com/pretix/pretix.git
synced 2026-05-13 16:33:59 +00:00
wip
This commit is contained in:
@@ -2,16 +2,15 @@ import dataclasses
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from typing import Callable, Dict, List, Set, Union
|
||||||
from django.core.validators import MaxValueValidator
|
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.functional import _StrPromise
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
|
|
||||||
from pretix.base.models import Event
|
from pretix.base.models import Event, Order, OrderPosition
|
||||||
from pretix.base.models import OrderPosition, Order
|
|
||||||
from typing import Dict, Union, Callable, List
|
|
||||||
|
|
||||||
from pretix.base.reldate import ModelRelativeDateTimeField
|
from pretix.base.reldate import ModelRelativeDateTimeField
|
||||||
from pretix.base.timemachine import time_machine_now
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
|
||||||
@@ -28,33 +27,44 @@ class RelativeFee:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
return self.reference_price * self.percentage
|
return self.reference_price * (self.percentage/100)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Ruling:
|
class CheckResult:
|
||||||
cancellation_possible: bool
|
cancellation_possible: bool
|
||||||
reason: str
|
reason: str | _StrPromise
|
||||||
|
|
||||||
|
|
||||||
Rulings=Dict[str, Ruling]
|
@dataclass(frozen=True)
|
||||||
|
class OrderDiff:
|
||||||
|
order: Order
|
||||||
|
prev: Set[OrderPosition]
|
||||||
|
next: Set[OrderPosition]
|
||||||
|
|
||||||
|
def cancellations(self):
|
||||||
|
return self.prev.difference(self.next)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CheckResult=Dict[str, CheckResult]
|
||||||
Fee=Union[AbsoluteFee, RelativeFee]
|
Fee=Union[AbsoluteFee, RelativeFee]
|
||||||
RuleFn=Callable[[OrderPosition], Rulings]
|
CheckFn=Callable[[OrderDiff, OrderPosition], CheckResult]
|
||||||
|
|
||||||
|
|
||||||
def merge_rulings(a: Rulings, b: Rulings) -> Rulings:
|
def merge_check_results(a: CheckResult, b: CheckResult) -> CheckResult:
|
||||||
result = dict(a)
|
result = dict(a)
|
||||||
for key, b_inner in b.items():
|
for key, b_inner in b.items():
|
||||||
if key in result:
|
if key in result:
|
||||||
result[key] = result[key] | b_inner # merge inner dicts
|
result[key] = result[key] | b_inner
|
||||||
else:
|
else:
|
||||||
result[key] = b_inner
|
result[key] = b_inner
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CancellationConsequence:
|
class Ruling:
|
||||||
rule_id: int
|
rule_id: int
|
||||||
rulings: Rulings
|
check_results: CheckResult
|
||||||
order_fee: Decimal=dataclasses.field(default_factory=lambda: Decimal(0))
|
order_fee: Decimal=dataclasses.field(default_factory=lambda: Decimal(0))
|
||||||
position_fee: Fee=dataclasses.field(default_factory=lambda: AbsoluteFee(Decimal(0)))
|
position_fee: Fee=dataclasses.field(default_factory=lambda: AbsoluteFee(Decimal(0)))
|
||||||
|
|
||||||
@@ -65,7 +75,7 @@ class CancellationConsequence:
|
|||||||
self,
|
self,
|
||||||
'cancellation_possible',
|
'cancellation_possible',
|
||||||
all(ruling.cancellation_possible
|
all(ruling.cancellation_possible
|
||||||
for ruling in self.rulings.values()
|
for ruling in self.check_results.values()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,7 +84,7 @@ class CancellationConsequence:
|
|||||||
return self.position_fee.amount + self.order_fee
|
return self.position_fee.amount + self.order_fee
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
if not isinstance(other, CancellationConsequence):
|
if not isinstance(other, Ruling):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
if self.cancellation_possible == other.cancellation_possible:
|
if self.cancellation_possible == other.cancellation_possible:
|
||||||
@@ -85,12 +95,13 @@ class CancellationConsequence:
|
|||||||
|
|
||||||
class CancellationRuleQuerySet(models.QuerySet):
|
class CancellationRuleQuerySet(models.QuerySet):
|
||||||
def cancellation_possible(self, order: Order):
|
def cancellation_possible(self, order: Order):
|
||||||
return all([v[0].cancellation_possible for v in self._evaluate(order)])
|
verdicts = [v[0] for v in self._evaluate(order)]
|
||||||
|
return all(v.cancellation_possible for v in verdicts), verdicts
|
||||||
|
|
||||||
def _evaluate(self, order: Order) -> List[List[CancellationConsequence]]:
|
def _evaluate(self, order: Order) -> List[List[Ruling]]:
|
||||||
return [self._evaluate_op(position) for position in order.positions.all()]
|
return [self._evaluate_op(position) for position in order.positions.all()]
|
||||||
|
|
||||||
def _evaluate_op(self, order_position: OrderPosition) -> List[CancellationConsequence]:
|
def _evaluate_op(self, order_position: OrderPosition) -> List[Ruling]:
|
||||||
consequences=[rule.apply(order_position) for rule in self]
|
consequences=[rule.apply(order_position) for rule in self]
|
||||||
consequences.sort()
|
consequences.sort()
|
||||||
return consequences
|
return consequences
|
||||||
@@ -98,6 +109,11 @@ class CancellationRuleQuerySet(models.QuerySet):
|
|||||||
|
|
||||||
|
|
||||||
class CancellationRule(models.Model):
|
class CancellationRule(models.Model):
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
organizer=models.ForeignKey(
|
organizer=models.ForeignKey(
|
||||||
"Organizer",
|
"Organizer",
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
@@ -157,75 +173,132 @@ class CancellationRule(models.Model):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.rules: List[RuleFn]=[self._rule_order_status, self._rule_time_window,
|
self.checks: List[CheckFn]=[self._check_order_status, self._check_time_window,
|
||||||
self._system_rule_not_checked_in]
|
self._system_check_not_checked_in]
|
||||||
|
self.partial_checks: List[CheckFn]=[self._system_check_not_discounted]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _system_rule_not_checked_in(order_position: OrderPosition) -> Rulings:
|
def _system_check_not_checked_in(diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||||
|
check_id = "SYSTEM_TICKET_NOT_USED"
|
||||||
|
|
||||||
if order_position.checkins.filter(list__consider_tickets_used=True).exists():
|
if order_position.checkins.filter(list__consider_tickets_used=True).exists():
|
||||||
return {"SYSTEM_TICKET_NOT_USED": Ruling(
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=False,
|
cancellation_possible=False,
|
||||||
reason=f"Order position was used",
|
reason=f"Order position was used",
|
||||||
)}
|
)}
|
||||||
else:
|
else:
|
||||||
return {"SYSTEM_TICKET_NOT_USED": Ruling(
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=True,
|
cancellation_possible=True,
|
||||||
reason=f"Order position not yet used",
|
reason=f"Order position not yet used",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
def _rule_time_window(self, order_position: OrderPosition) -> Rulings:
|
@staticmethod
|
||||||
in_allowed_until = self.allowed_until < time_machine_now()
|
def _system_check_not_discounted(diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||||
in_exemption = self.except_after > time_machine_now()
|
"""
|
||||||
if in_allowed_until and not in_exemption:
|
Check that ensures that orders containing discounted order_positions cannot
|
||||||
return {"TIME_WINDOW": Ruling(
|
be canceled partially.
|
||||||
|
This is a stop-gap solution until the `discount_grouper` attribute for
|
||||||
|
AbstractPositions is introduced, allowing us to be more grannular
|
||||||
|
|
||||||
|
:param diff:
|
||||||
|
:param order_position:
|
||||||
|
:return CheckResults:
|
||||||
|
"""
|
||||||
|
check_id = "SYSTEM_TICKET_NOT_USED"
|
||||||
|
|
||||||
|
if order_position in diff.cancellations():
|
||||||
|
if order_position.discount is None:
|
||||||
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=True,
|
cancellation_possible=True,
|
||||||
reason=f"Cancellation in required time window between {self.allowed_until} and {self.except_after}",
|
reason=_("Order position was bought without discount"),
|
||||||
)}
|
|
||||||
elif in_allowed_until and in_exemption:
|
|
||||||
return {"TIME_WINDOW": Ruling(
|
|
||||||
cancellation_possible=False,
|
|
||||||
reason=f"Cancellation in exemption period after {self.except_after}",
|
|
||||||
)}
|
)}
|
||||||
else:
|
else:
|
||||||
return {"TIME_WINDOW": Ruling(
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=False,
|
cancellation_possible=False,
|
||||||
reason=f"Cancellation after time window ending on {self.allowed_until}",
|
reason=_("Order position was bought with a discount"),
|
||||||
|
)}
|
||||||
|
else:
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=False,
|
||||||
|
reason=_("Order position not canceled - check not applicable"),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
def _rule_order_status(self, order_position: OrderPosition) -> Rulings:
|
def _check_time_window(self, diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||||
if order_position.order.status == self.order_status:
|
check_id = "TIME_WINDOW"
|
||||||
return {"ORDER_STATUS": Ruling(
|
|
||||||
|
relevant_event = order_position.subevent or order_position.event
|
||||||
|
|
||||||
|
if not self.allowed_until and not self.allowed_until:
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=True,
|
||||||
|
reason=f"No time window specified",
|
||||||
|
)}
|
||||||
|
|
||||||
|
in_allowed_until=time_machine_now() < self.allowed_until.datetime(
|
||||||
|
relevant_event) if self.allowed_until else False
|
||||||
|
in_exemption=time_machine_now() > self.except_after.datetime(
|
||||||
|
relevant_event) if self.except_after else False
|
||||||
|
if in_allowed_until and not in_exemption:
|
||||||
|
except_after_message = f" and not after {self.except_after.datetime(relevant_event)}" if self.except_after else ""
|
||||||
|
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=True,
|
||||||
|
reason=f"Cancellation in required time window before {self.allowed_until.datetime(relevant_event)}{except_after_message}",
|
||||||
|
)}
|
||||||
|
elif in_allowed_until and in_exemption:
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=False,
|
||||||
|
reason=f"Cancellation in exemption period after {self.except_after.datetime(relevant_event)}",
|
||||||
|
)}
|
||||||
|
else:
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=False,
|
||||||
|
reason=f"Cancellation after time window ending on {self.allowed_until.datetime(relevant_event)}",
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _check_order_status(self, diff: OrderDiff, order_position: OrderPosition) -> CheckResult:
|
||||||
|
check_id = "ORDER_STATUS"
|
||||||
|
|
||||||
|
if not self.order_status:
|
||||||
|
return {check_id: CheckResult(
|
||||||
|
cancellation_possible=True,
|
||||||
|
reason=f"Orders in every status can be cancelled",
|
||||||
|
)}
|
||||||
|
elif order_position.order.status == self.order_status:
|
||||||
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=True,
|
cancellation_possible=True,
|
||||||
reason=f"Order in required status: '{order_position.order.status}'",
|
reason=f"Order in required status: '{order_position.order.status}'",
|
||||||
)}
|
)}
|
||||||
else:
|
else:
|
||||||
return {"ORDER_STATUS": Ruling(
|
return {check_id: CheckResult(
|
||||||
cancellation_possible=False,
|
cancellation_possible=False,
|
||||||
reason=f"Order in status '{order_position.order.status}' cannot be canceled",
|
reason=f"Order in status '{order_position.order.status}' cannot be canceled",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
# OrderPositions mit discount dürfen nur storniert werden, wenn alle positions mit dem gleichen discount_grouper storniert werden
|
|
||||||
# OrderPositions mit Item.min_per_order dürfen nur storniert werden, wenn genug übrig bleiben oder alle des gleichen Items storniert werden
|
# OrderPositions mit Item.min_per_order dürfen nur storniert werden, wenn genug übrig bleiben oder alle des gleichen Items storniert werden
|
||||||
# OrderPositions mit addon_to != None dürfen nur über den bestehenden Add-On-Flow storniert werden
|
# OrderPositions mit addon_to != None dürfen nur über den bestehenden Add-On-Flow storniert werden
|
||||||
# OrderPositions mit is_bundled dürfen nur mit der Parent-Position zusammen storniert werden
|
# OrderPositions mit is_bundled dürfen nur mit der Parent-Position zusammen storniert werden
|
||||||
# Shipping modul kann storno geshippter Items verhindern
|
# Shipping modul kann storno geshippter Items verhindern
|
||||||
# Backend-Anzeige "welche Regel greift da gerade" in der Order
|
# Backend-Anzeige "welche Regel greift da gerade" in der Order
|
||||||
|
|
||||||
def apply(self, order_position: OrderPosition) -> CancellationConsequence:
|
def apply(self, diff: OrderDiff, order_position: OrderPosition) -> Ruling:
|
||||||
rulings=reduce(merge_rulings,
|
check_results=reduce(merge_check_results,
|
||||||
[rule(order_position) for rule in self.rules])
|
[rule(diff, order_position) for rule in self.checks])
|
||||||
|
|
||||||
if self.fee_percentage_per_item and self.fee_absolute_per_item:
|
if self.fee_percentage_per_item and self.fee_absolute_per_item:
|
||||||
raise NotImplementedError("Should never be reached")
|
raise NotImplementedError("Should never be reached")
|
||||||
elif self.fee_absolute_per_item:
|
elif self.fee_absolute_per_item:
|
||||||
fee=AbsoluteFee(self.fee_absolute_per_item)
|
fee=AbsoluteFee(self.fee_absolute_per_item)
|
||||||
else:
|
else:
|
||||||
fee=RelativeFee(percentage=self.fee_absolute_per_item,
|
fee=RelativeFee(percentage=self.fee_percentage_per_item,
|
||||||
reference_price=order_position.price)
|
reference_price=order_position.price)
|
||||||
|
|
||||||
return CancellationConsequence(
|
return Ruling(
|
||||||
rule_id=self.id,
|
rule_id=self.id,
|
||||||
rulings=rulings,
|
check_results=check_results,
|
||||||
order_fee=self.fee_absolute_per_order,
|
order_fee=self.fee_absolute_per_order,
|
||||||
position_fee=fee
|
position_fee=fee
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,71 +1,74 @@
|
|||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from pretix.base.models.cancellation import CancellationRule
|
|
||||||
from pretix.base.models import Order, Event, OrderPosition, Organizer, Item
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
from django.utils.timezone import make_aware, now
|
|
||||||
from django_scopes import scope
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.utils.timezone import make_aware, now
|
||||||
|
from django_scopes import scope
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from pretix.base.models.cancellation import Ruling
|
from pretix.base.models import Event, Item, Order, OrderPosition, Organizer
|
||||||
|
from pretix.base.models.cancellation import CancellationRule, Ruling
|
||||||
|
|
||||||
|
NOW = now()
|
||||||
|
DAYS_UNTIL_EVENT=60
|
||||||
|
EVENT_START = NOW+timedelta(days=DAYS_UNTIL_EVENT)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
|
@pytest.fixture()
|
||||||
def event():
|
def event():
|
||||||
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
|
||||||
event = Event.objects.create(
|
event = Event.objects.create(
|
||||||
organizer=o, name='Dummy', slug='dummy',
|
organizer=o, name='Dummy', slug='dummy',
|
||||||
date_from=now(),
|
date_from=EVENT_START,
|
||||||
plugins='pretix.plugins.banktransfer'
|
plugins='pretix.plugins.banktransfer'
|
||||||
)
|
)
|
||||||
with scope(organizer=o):
|
return event
|
||||||
yield event
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture()
|
||||||
def ticket(event):
|
def item1(event):
|
||||||
ticket = Item.objects.create(event=event, name='Early-bird ticket',
|
return Item.objects.create(event=event, name='Early-bird item1',
|
||||||
default_price=Decimal('23.00'), admission=True)
|
default_price=Decimal('23.00'), admission=True)
|
||||||
|
|
||||||
return ticket
|
@pytest.fixture()
|
||||||
|
def order(event):
|
||||||
|
return Order.objects.create(
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_status_rule(event, ticket):
|
|
||||||
o = Order.objects.create(
|
|
||||||
code='FOO', event=event, email='dummy@dummy.test',
|
code='FOO', event=event, email='dummy@dummy.test',
|
||||||
status=Order.STATUS_PENDING, locale='en',
|
status=Order.STATUS_PENDING, locale='en',
|
||||||
datetime=now(), expires=now() + timedelta(days=10),
|
datetime=NOW,
|
||||||
total=0,
|
total=0,
|
||||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_status_rule(event, item1, order):
|
||||||
|
with scope(organizer=event.organizer, event=event):
|
||||||
op = OrderPosition.objects.create(
|
op = OrderPosition.objects.create(
|
||||||
order=o, item=ticket, variation=None,
|
order=order, item=item1, variation=None,
|
||||||
price=Decimal("0.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
price=Decimal("0.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||||
)
|
)
|
||||||
|
|
||||||
cancellation_rule = CancellationRule.objects.create(
|
cancellation_rule = CancellationRule.objects.create(
|
||||||
organizer=event.organizer, event=event, item=ticket,
|
organizer=event.organizer, event=event, item=item1,
|
||||||
order_status=Order.STATUS_PENDING
|
order_status=Order.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
assert cancellation_rule._rule_order_status(order_position=op) == {
|
assert cancellation_rule._rule_order_status(order_position=op) == {
|
||||||
1: Ruling(
|
'ORDER_STATUS': Ruling(
|
||||||
cancellation_possible=True,
|
cancellation_possible=True,
|
||||||
reason="Order in required status: 'n'",
|
reason="Order in required status: 'n'",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellation_rule = CancellationRule.objects.create(
|
cancellation_rule = CancellationRule.objects.create(
|
||||||
organizer=event.organizer, event=event, item=ticket,
|
organizer=event.organizer, event=event, item=item1,
|
||||||
order_status=Order.STATUS_PAID
|
order_status=Order.STATUS_PAID
|
||||||
)
|
)
|
||||||
|
|
||||||
assert cancellation_rule._rule_order_status(order_position=op) == {
|
assert cancellation_rule._rule_order_status(order_position=op) == {
|
||||||
2: Ruling(
|
'ORDER_STATUS': Ruling(
|
||||||
cancellation_possible=False,
|
cancellation_possible=False,
|
||||||
reason="Order in status 'n' cannot be canceled",
|
reason="Order in status 'n' cannot be canceled",
|
||||||
),
|
),
|
||||||
@@ -73,30 +76,113 @@ def test_status_rule(event, ticket):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_cancelation_rule_query_set(event, ticket):
|
def test_timing(event, item1, order):
|
||||||
with scope(organizer=event.organizer, event=event):
|
with scope(organizer=event.organizer, event=event):
|
||||||
o = Order.objects.create(
|
order.status = Order.STATUS_PAID
|
||||||
code='FOO', event=event, email='dummy@dummy.test',
|
order.save()
|
||||||
status=Order.STATUS_PENDING, locale='en',
|
|
||||||
datetime=now(), expires=now() + timedelta(days=10),
|
|
||||||
total=0,
|
|
||||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
|
||||||
)
|
|
||||||
|
|
||||||
op = OrderPosition.objects.create(
|
OrderPosition.objects.create(
|
||||||
order=o, item=ticket, variation=None,
|
order=order, item=item1, variation=None,
|
||||||
price=Decimal("0.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
price=Decimal("0.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||||
)
|
)
|
||||||
|
|
||||||
cr1 = CancellationRule.objects.create(
|
CancellationRule.objects.create(
|
||||||
organizer=event.organizer, event=event, item=ticket,
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=now() + timedelta(hours=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
with freeze_time(now()):
|
||||||
|
possible, verdicts = CancellationRule.objects.all().cancellation_possible(order)
|
||||||
|
assert possible == True
|
||||||
|
|
||||||
|
with freeze_time(now()+timedelta(hours=2)):
|
||||||
|
possible, verdicts=CancellationRule.objects.all().cancellation_possible(order)
|
||||||
|
assert possible == False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_multiple_limits(event, item1, order):
|
||||||
|
with (scope(organizer=event.organizer, event=event)):
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
OrderPosition.objects.create(
|
||||||
|
order=order, item=item1, variation=None,
|
||||||
|
price=Decimal("100.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# free in the first hour after booking
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=NOW + timedelta(hours=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# free until 30 days before event
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=EVENT_START - timedelta(days=30),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 50% until 14 days before event
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=EVENT_START - timedelta(days=14),
|
||||||
|
fee_percentage_per_item=Decimal(50.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 80% until 7 days before event
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=EVENT_START - timedelta(days=7),
|
||||||
|
fee_percentage_per_item=Decimal(80.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 100% until 1 day before event
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
|
allowed_until=EVENT_START - timedelta(days=1),
|
||||||
|
fee_percentage_per_item=Decimal(100)
|
||||||
|
)
|
||||||
|
|
||||||
|
possible_trace = []
|
||||||
|
cost_trace = []
|
||||||
|
|
||||||
|
for days in range(DAYS_UNTIL_EVENT):
|
||||||
|
today = NOW + timedelta(days=days)
|
||||||
|
with freeze_time(today):
|
||||||
|
possible, verdicts=CancellationRule.objects.all().cancellation_possible(
|
||||||
|
order)
|
||||||
|
possible_trace.append(possible)
|
||||||
|
cost_trace.append(verdicts[0].total_fee)
|
||||||
|
|
||||||
|
assert possible_trace == [True] * 59 + [False]
|
||||||
|
assert cost_trace == [Decimal("0.0000")] * 30 + \
|
||||||
|
[Decimal("50.0000")] * 16 + \
|
||||||
|
[Decimal("80.0000")] * 7 + \
|
||||||
|
[Decimal("100.0000")] * 6 + \
|
||||||
|
[Decimal("0.0000")]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cancellation_rule_query_set(event, item1, order):
|
||||||
|
with scope(organizer=event.organizer, event=event):
|
||||||
|
OrderPosition.objects.create(
|
||||||
|
order=order, item=item1, variation=None,
|
||||||
|
price=Decimal("0.00"), attendee_name_parts={'full_name': "Peter"}, positionid=1
|
||||||
|
)
|
||||||
|
|
||||||
|
CancellationRule.objects.create(
|
||||||
|
organizer=event.organizer, event=event, item=item1,
|
||||||
order_status=Order.STATUS_PENDING, fee_absolute_per_order=Decimal('10.00'),
|
order_status=Order.STATUS_PENDING, fee_absolute_per_order=Decimal('10.00'),
|
||||||
)
|
)
|
||||||
|
|
||||||
cr2 = CancellationRule.objects.create(
|
CancellationRule.objects.create(
|
||||||
organizer=event.organizer, event=event, item=ticket,
|
organizer=event.organizer, event=event, item=item1,
|
||||||
order_status=Order.STATUS_PAID
|
order_status=Order.STATUS_PAID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
possible, verdicts = CancellationRule.objects.all().cancellation_possible(order)
|
||||||
|
|
||||||
assert CancellationRule.objects.all().cancellation_possible(o) == True
|
assert possible == True
|
||||||
|
|||||||
Reference in New Issue
Block a user