Payment providers: Allow to set an availability start date per method (Z#23126769) (#3569)

Co-authored-by: Mira <weller@rami.io>
This commit is contained in:
Raphael Michel
2023-09-14 11:08:37 +02:00
committed by GitHub
parent 75abab403a
commit ecb4c34e08
3 changed files with 112 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.'),
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',
forms.DecimalField(
label=_('Minimum order total'),
@@ -554,40 +560,65 @@ class BasePaymentProvider:
return form
def _is_still_available(self, now_dt=None, cart_id=None, order=None):
def _absolute_availability_date(self, rel_date, cart_id=None, order=None, aggregate_fn=min):
if not rel_date:
return None
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 aggregate_fn(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 aggregate_fn(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()
tz = ZoneInfo(self.event.settings.timezone)
availability_date = self.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date:
if self.event.has_subevents and cart_id:
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()
try:
availability_start = self._absolute_availability_date(
self.settings.get('_availability_start', as_type=RelativeDateWrapper),
cart_id,
order,
# In an event series, we use min() for the start as well. This might be inconsistent with using min() for
# for the end, but makes it harder to put one self into a situation where no payment provider is available.
aggregate_fn=min
)
if availability_date:
return availability_date >= now_dt.astimezone(tz).date()
if availability_start:
if availability_start > now_dt.astimezone(tz).date():
return False
return True
availability_end = self._absolute_availability_date(
self.settings.get('_availability_date', as_type=RelativeDateWrapper),
cart_id,
order,
aggregate_fn=min
)
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:
"""
@@ -596,9 +627,9 @@ class BasePaymentProvider:
user will not be able to select this payment method. This will only be called
during checkout, not on retrying.
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 ``_restrict_to_sales_channels`` setting.
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future
and for the ``_availability_from``, ``_total_max``, and ``_total_min`` requirements to be met. It also checks
the ``_restrict_countries`` and ``_restrict_to_sales_channels`` setting.
:param total: The total value without the payment method fee, after taxes.
@@ -607,7 +638,7 @@ class BasePaymentProvider:
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.
"""
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
if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
@@ -791,8 +822,8 @@ class BasePaymentProvider:
Will be called to check whether it is allowed to change the payment method of
an order to this one.
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.
The default implementation checks for the ``_availability_date`` setting to be either unset or in the future,
as well as for the ``_availability_from``, ``_total_max``, ``_total_min``, and ``_restricted_countries`` settings.
:param order: The order object
"""
@@ -819,7 +850,7 @@ class BasePaymentProvider:
if order.sales_channel not in self.settings.get('_restrict_to_sales_channels', as_type=list, default=['web']):
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]:
"""