Compare commits

...

2 Commits

Author SHA1 Message Date
Mira Weller
8a1e1a7de1 refactoring 2023-09-12 18:40:40 +02:00
Raphael Michel
c246a46b15 Payment providers: Allow to set an availability start date per method (Z#23126769) 2023-09-04 16:59:59 +02:00
3 changed files with 104 additions and 54 deletions

View File

@@ -336,6 +336,12 @@ class BasePaymentProvider:
help_text=_('Users will not be able to choose this payment provider after the given date.'), help_text=_('Users will not be able to choose this payment provider after the given date.'),
required=False, required=False,
)), )),
('_availability_start',
RelativeDateField(
label=_('Available from'),
help_text=_('Users will not be able to choose this payment provider before the given date.'),
required=False,
)),
('_total_min', ('_total_min',
forms.DecimalField( forms.DecimalField(
label=_('Minimum order total'), label=_('Minimum order total'),
@@ -539,40 +545,57 @@ class BasePaymentProvider:
return form return form
def _is_still_available(self, now_dt=None, cart_id=None, order=None): def _convert_availability_date_to_absolute(self, rel_date, cart_id=None, order=None):
if not rel_date:
return None
# In an event series, we use min() here, which makes it less restrictive than max() and thus makes
# it harder to put one self into a situation where no payment provider is available.
if self.event.has_subevents and cart_id:
dates = [
rel_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=CartPosition.objects.filter(
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
]
return min(dates) if dates else None
elif self.event.has_subevents and order:
dates = [
rel_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
]
return min(dates) if dates else None
elif self.event.has_subevents:
raise NotImplementedError('Payment provider is not subevent-ready.')
else:
return rel_date.datetime(self.event).date()
def _is_available_by_time(self, now_dt=None, cart_id=None, order=None):
now_dt = now_dt or now() now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone) tz = ZoneInfo(self.event.settings.timezone)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper) try:
if availability_date: availability_start = self._convert_availability_date_to_absolute(
if self.event.has_subevents and cart_id: self.settings.get('_availability_start', as_type=RelativeDateWrapper), cart_id, order)
dates = [
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=CartPosition.objects.filter(
cart_id=cart_id, event=self.event
).values_list('subevent', flat=True)
)
]
availability_date = min(dates) if dates else None
elif self.event.has_subevents and order:
dates = [
availability_date.datetime(se).date()
for se in self.event.subevents.filter(
id__in=order.positions.values_list('subevent', flat=True)
)
]
availability_date = min(dates) if dates else None
elif self.event.has_subevents:
logger.error('Payment provider is not subevent-ready.')
return False
else:
availability_date = availability_date.datetime(self.event).date()
if availability_date: if availability_start:
return availability_date >= now_dt.astimezone(tz).date() if availability_start > now_dt.astimezone(tz).date():
return False
return True availability_end = self._convert_availability_date_to_absolute(
self.settings.get('_availability_date', as_type=RelativeDateWrapper), cart_id, order)
if availability_end:
if availability_end < now_dt.astimezone(tz).date():
return False
return True
except NotImplementedError:
logger.exception('Unable to check availability')
return False
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
""" """
@@ -581,9 +604,9 @@ class BasePaymentProvider:
user will not be able to select this payment method. This will only be called user will not be able to select this payment method. This will only be called
during checkout, not on retrying. during checkout, not on retrying.
The default implementation checks for the _availability_date setting to be either unset or in the future The default implementation checks for the ``_availability_date`` setting to be either unset or in the future
and for the _total_max and _total_min requirements to be met. It also checks the ``_restrict_countries`` and for the ``_availability_from``, ``_total_max``, and ``_total_min`` requirements to be met. It also checks
and ``_restrict_to_sales_channels`` setting. the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
:param total: The total value without the payment method fee, after taxes. :param total: The total value without the payment method fee, after taxes.
@@ -592,7 +615,7 @@ class BasePaymentProvider:
The ``total`` parameter has been added. For backwards compatibility, this method is called again The ``total`` parameter has been added. For backwards compatibility, this method is called again
without this parameter if it raises a ``TypeError`` on first try. without this parameter if it raises a ``TypeError`` on first try.
""" """
timing = self._is_still_available(cart_id=get_or_create_cart_id(request)) timing = self._is_available_by_time(cart_id=get_or_create_cart_id(request))
pricing = True pricing = True
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None: if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
@@ -776,8 +799,8 @@ class BasePaymentProvider:
Will be called to check whether it is allowed to change the payment method of Will be called to check whether it is allowed to change the payment method of
an order to this one. an order to this one.
The default implementation checks for the _availability_date setting to be either unset or in the future, The default implementation checks for the ``_availability_date`` setting to be either unset or in the future,
as well as for the _total_max, _total_min and _restricted_countries settings. as well as for the ``_availabilty_from``, ``_total_max``, ``_total_min``, and ``_restricted_countries`` settings.
:param order: The order object :param order: The order object
""" """
@@ -804,7 +827,7 @@ class BasePaymentProvider:
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']): if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
return False return False
return self._is_still_available(order=order) return self._is_available_by_time(order=order)
def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]: def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
""" """

View File

@@ -554,9 +554,6 @@ class StripeMethod(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self} ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self}
return template.render(ctx) return template.render(ctx)
def payment_can_retry(self, payment):
return self._is_still_available(order=payment.order)
def _charge_source(self, request, source, payment): def _charge_source(self, request, source, payment):
try: try:
params = {} params = {}
@@ -1581,9 +1578,6 @@ class StripeSofort(StripeMethod):
return True return True
return False return False
def payment_can_retry(self, payment):
return payment.state != OrderPayment.PAYMENT_STATE_PENDING and self._is_still_available(order=payment.order)
def payment_presale_render(self, payment: OrderPayment) -> str: def payment_presale_render(self, payment: OrderPayment) -> str:
pi = payment.info_data or {} pi = payment.info_data or {}
try: try:

View File

@@ -97,7 +97,15 @@ def test_payment_fee_reverse_percent_and_abs_default(event):
def test_availability_date_available(event): def test_availability_date_available(event):
prov = DummyPaymentProvider(event) prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', datetime.date.today() + datetime.timedelta(days=1)) prov.settings.set('_availability_date', datetime.date.today() + datetime.timedelta(days=1))
result = prov._is_still_available() result = prov._is_available_by_time()
assert result
@pytest.mark.django_db
def test_availability_start_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', datetime.date.today() - datetime.timedelta(days=1))
result = prov._is_available_by_time()
assert result assert result
@@ -105,7 +113,15 @@ def test_availability_date_available(event):
def test_availability_date_not_available(event): def test_availability_date_not_available(event):
prov = DummyPaymentProvider(event) prov = DummyPaymentProvider(event)
prov.settings.set('_availability_date', datetime.date.today() - datetime.timedelta(days=1)) prov.settings.set('_availability_date', datetime.date.today() - datetime.timedelta(days=1))
result = prov._is_still_available() result = prov._is_available_by_time()
assert not result
@pytest.mark.django_db
def test_availability_start_not_available(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', datetime.date.today() + datetime.timedelta(days=1))
result = prov._is_available_by_time()
assert not result assert not result
@@ -121,9 +137,26 @@ def test_availability_date_relative(event):
)) ))
utc = datetime.timezone.utc utc = datetime.timezone.utc
assert prov._is_still_available(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc)) assert prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc)) assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_still_available(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc)) assert not prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db
def test_availability_start_relative(event):
event.settings.set('timezone', 'US/Pacific')
tz = ZoneInfo('US/Pacific')
event.date_from = datetime.datetime(2016, 12, 3, 12, 0, 0, tzinfo=tz)
event.save()
prov = DummyPaymentProvider(event)
prov.settings.set('_availability_start', RelativeDateWrapper(
RelativeDate(days_before=2, time=datetime.time(12, 0), base_date_name='date_from', minutes_before=None)
))
utc = datetime.timezone.utc
assert not prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db @pytest.mark.django_db
@@ -134,9 +167,9 @@ def test_availability_date_timezones(event):
tz = ZoneInfo('US/Pacific') tz = ZoneInfo('US/Pacific')
utc = ZoneInfo('UTC') utc = ZoneInfo('UTC')
assert prov._is_still_available(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc)) assert prov._is_available_by_time(datetime.datetime(2016, 11, 30, 23, 0, 0, tzinfo=tz).astimezone(utc))
assert prov._is_still_available(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc)) assert prov._is_available_by_time(datetime.datetime(2016, 12, 1, 23, 59, 0, tzinfo=tz).astimezone(utc))
assert not prov._is_still_available(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc)) assert not prov._is_available_by_time(datetime.datetime(2016, 12, 2, 0, 0, 1, tzinfo=tz).astimezone(utc))
@pytest.mark.django_db @pytest.mark.django_db
@@ -162,12 +195,12 @@ def test_availability_date_cart_relative_subevents(event):
prov.settings.set('_availability_date', RelativeDateWrapper( prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None) RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None)
)) ))
assert prov._is_still_available(cart_id="123") assert prov._is_available_by_time(cart_id="123")
prov.settings.set('_availability_date', RelativeDateWrapper( prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None) RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None)
)) ))
assert not prov._is_still_available(cart_id="123") assert not prov._is_available_by_time(cart_id="123")
@pytest.mark.django_db @pytest.mark.django_db
@@ -201,9 +234,9 @@ def test_availability_date_order_relative_subevents(event):
prov.settings.set('_availability_date', RelativeDateWrapper( prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None) RelativeDate(days_before=3, time=None, base_date_name='date_from', minutes_before=None)
)) ))
assert prov._is_still_available(order=order) assert prov._is_available_by_time(order=order)
prov.settings.set('_availability_date', RelativeDateWrapper( prov.settings.set('_availability_date', RelativeDateWrapper(
RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None) RelativeDate(days_before=4, time=None, base_date_name='date_from', minutes_before=None)
)) ))
assert not prov._is_still_available(order=order) assert not prov._is_available_by_time(order=order)