Repository: pretix/pretix

Issue:
Add time limits to payment providers
**Hey!** If you ever wanted to contribute to an Open Source project, but didn't know how, you just found a perfect place to start. We try to write this issue as instructive as possible, but as we're humans we almost certainly forgot to write something important in here. If you want to try to tackle this issue and get stuck at _any_ point, please do not hesitate and ask us, either here on GitHub or via email at support@pretix.eu. We'll be very happy to help! 

We think this issue is very suitable to people who are just starting with Python or Django development or who never contributed to Open Source before. We'd like to ask people with wide experience in these areas to leave this issue to beginners. Thanks! (If you want something more advanced, just drop us a line, we'll find something :wink:)

## How to get started

First of all, you need pretix running locally on your machine. We have a guide on the developer installation [over here](https://docs.pretix.eu/en/latest/development/setup.html). Play around with it and get a feeling for what pretix is and how it works :) Try to find the places in the interface mentioned in this issue where something needs to change. Maybe try to find the same places in the code? Just searching for characteristic words can be a powerful tool to find your way around.

## What this is about

In pretix, users often can choose between different payment providers. Some payment providers (most notably bank transfer) are known to take some time. If your event comes very close, you probably want to only enable those that go dast. It would be nice if we had a setting that defines the latest date this payment provider is available, so you could do something like "allow to select credit card payments until 20.12. and bank transfer until 10.12.".

## How to do this

This can be implemented by adding a field to the default implementation of ``BasePaymentProvider.settings_form_fields`` and changing the default implementation of ``BasePaymentProvider.is_allowed()`` and ``BasePaymentProvider.order_change_allowed()`` to check for that date.

## I want to give it a try!

You're still reading, nice! If you want to try working on this, please comment below, so everyone else knows this is being looked at and can search for a different place to contribute. 

First, [fork](https://github.com/pretix/pretix/fork) the repository to your own GitHub account. Then clone it locally (or [set it as your remote](https://help.github.com/articles/configuring-a-remote-for-a-fork/) if you already cloned it earlier) and prepare your pull requests. There are a number of tutorials on the Pull Request workflow online, for example this [short version](https://guides.github.com/introduction/flow/) by GitHub itself and this [more in-depth tutorial](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github). We're excited to see your pull request! 

And don't forget: If you get stuck, don't hestitate to ask. We bound ourselves to a [Code of Conduct](https://docs.pretix.eu/en/latest/development/contribution/codeofconduct.html) and we're committed to providing a welcoming experience and giving constructive, friendly and useful feedback on your contributions.


Discussion:
Looking to pick this up if that's ok ;)
Sure, go ahead!
Just a quick one, what would be the ideal label and help text for this field and is this a required field or optional? This is roughly what came to mind: label = 'Latest payment date', help_text = 'The latest date this payment is available'?
Maybe something more descriptive for the help text? Such as 'The latest date this payment can be chosen by users.'?
I'd suggest
* Label: "Available until"
* Help text: "Users will not be able to choose this payment provider after the given date."

Resolving patch:
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -1,13 +1,16 @@
 from collections import OrderedDict
+from datetime import date
 from decimal import Decimal
 from typing import Any, Dict
 
+import pytz
 from django import forms
 from django.contrib import messages
 from django.dispatch import receiver
 from django.forms import Form
 from django.http import HttpRequest
 from django.template.loader import get_template
+from django.utils.timezone import now
 from django.utils.translation import ugettext_lazy as _
 
 from pretix.base.decimal import round_decimal
@@ -86,7 +89,7 @@ def settings_form_fields(self) -> dict:
         settings keys and the values should be corresponding Django form fields.
 
         The default implementation returns the appropriate fields for the ``_enabled``,
-        ``_fee_abs`` and ``_fee_percent`` settings mentioned above.
+        ``_fee_abs``, ``_fee_percent`` and ``_availability_date`` settings mentioned above.
 
         We suggest that you return an ``OrderedDict`` object instead of a dictionary
         and make use of the default implementation. Your implementation could look
@@ -126,6 +129,13 @@ def settings_form_fields(self):
                  help_text=_('Percentage'),
                  required=False
              )),
+            ('_availability_date',
+             forms.DateField(
+                 label=_('Available until'),
+                 help_text=_('Users will not be able to choose this payment provider after the given date.'),
+                 required=False,
+                 widget=forms.DateInput(attrs={'class': 'datepickerfield'})
+             )),
             ('_fee_reverse_calc',
              forms.BooleanField(
                  label=_('Calculate the fee from the total value including the fee.'),
@@ -190,6 +200,14 @@ def payment_form(self, request: HttpRequest) -> Form:
         form.fields = self.payment_form_fields
         return form
 
+    def _is_still_available(self, now_dt=None):
+        now_dt = now_dt or now()
+        tz = pytz.timezone(self.event.settings.timezone)
+        availability_date = self.settings.get('_availability_date', as_type=date)
+        if availability_date:
+            return availability_date >= now_dt.astimezone(tz).date()
+        return True
+
     def is_allowed(self, request: HttpRequest) -> bool:
         """
         You can use this method to disable this payment provider for certain groups
@@ -197,9 +215,9 @@ def is_allowed(self, request: HttpRequest) -> bool:
         user will not be able to select this payment method. This will only be called
         during checkout, not on retrying.
 
-        The default implementation always returns ``True``.
+        The default implementation checks for the _availability_date setting to be either unset or in the future.
         """
-        return True
+        return self._is_still_available()
 
     def payment_form_render(self, request: HttpRequest) -> str:
         """
@@ -335,11 +353,11 @@ def order_change_allowed(self, order: Order) -> bool:
         Will be called to check whether it is allowed to change the payment method of
         an order to this one.
 
-        The default implementation always returns ``True``.
+        The default implementation checks for the _availability_date setting to be either unset or in the future.
 
         :param order: The order object
         """
-        return True
+        return self._is_still_available()
 
     def order_can_retry(self, order: Order) -> bool:
         """


Test patch:
diff --git a/src/tests/base/test_payment.py b/src/tests/base/test_payment.py
--- a/src/tests/base/test_payment.py
+++ b/src/tests/base/test_payment.py
@@ -1,6 +1,8 @@
+import datetime
 from decimal import Decimal
 
 import pytest
+import pytz
 from django.utils.timezone import now
 from tests.testdummy.payment import DummyPaymentProvider
 
@@ -50,3 +52,32 @@ def test_payment_fee_reverse_percent_and_abs_default(event):
     prov.settings.set('_fee_abs', Decimal('0.30'))
     prov.settings.set('_fee_percent', Decimal('2.90'))
     assert prov.calculate_fee(Decimal('100.00')) == Decimal('3.30')
+
+
+@pytest.mark.django_db
+def test_availability_date_available(event):
+    prov = DummyPaymentProvider(event)
+    prov.settings.set('_availability_date', datetime.date.today() + datetime.timedelta(days=1))
+    result = prov._is_still_available()
+    assert result
+
+
+@pytest.mark.django_db
+def test_availability_date_not_available(event):
+    prov = DummyPaymentProvider(event)
+    prov.settings.set('_availability_date', datetime.date.today() - datetime.timedelta(days=1))
+    result = prov._is_still_available()
+    assert not result
+
+
+@pytest.mark.django_db
+def test_availability_date_timezones(event):
+    event.settings.set('timezone', 'US/Pacific')
+    prov = DummyPaymentProvider(event)
+    prov.settings.set('_availability_date', '2016-12-01')
+
+    tz = pytz.timezone('US/Pacific')
+    utc = pytz.timezone('UTC')
+    assert prov._is_still_available(tz.localize(datetime.datetime(2016, 11, 30, 23, 0, 0)).astimezone(utc))
+    assert prov._is_still_available(tz.localize(datetime.datetime(2016, 12, 1, 23, 59, 0)).astimezone(utc))
+    assert not prov._is_still_available(tz.localize(datetime.datetime(2016, 12, 2, 0, 0, 1)).astimezone(utc))