Example #1
0
class QPayProSettingsHolder(BasePaymentProvider):
    identifier = 'qpaypro'
    verbose_name = _('QPayPro')
    is_enabled = False
    is_meta = True
    url_onlinemetrix = 'https://h.online-metrix.net'

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'qpaypro', event)

    @property
    def were_general_settings_provided(self):
        return bool(self.settings.general_x_login
                    and self.settings.general_x_private_key
                    and self.settings.general_x_api_secret
                    and self.settings.general_x_endpoint
                    and self.settings.general_x_org_id
                    and self.settings.general_x_country
                    and self.settings.general_x_state
                    and self.settings.general_x_city
                    and self.settings.general_x_address)

    @property
    def settings_form_fields(self):
        if (self.were_general_settings_provided):
            fields = []
        else:
            fields = get_settings_form_fields('', True)
        d = OrderedDict(fields + [
            ('method_creditcard',
             forms.BooleanField(
                 label=_('Credit card'),
                 required=False,
             )),
            ('method_visaencuotas',
             forms.BooleanField(
                 label=_('Monthly payments'),
                 required=False,
             )),
        ] + list(super().settings_form_fields.items()))
        d.move_to_end('_enabled', last=False)
        return d

    def get_settings_key(self, key):
        if (self.were_general_settings_provided):
            key = 'general_{0}'.format(key)
        return self.settings.get(key)
Example #2
0
    def test_sandbox(self):
        sandbox = SettingsSandbox('testing', 'foo', self.event)
        sandbox.set('foo', 'bar')
        self.assertEqual(sandbox.get('foo'), 'bar')
        self.assertEqual(self.event.settings.get('testing_foo_foo'), 'bar')
        self.assertIsNone(self.event.settings.get('foo'), 'bar')

        sandbox['bar'] = 'baz'
        sandbox.baz = 42

        self.event = Event.objects.get(id=self.event.id)
        sandbox = SettingsSandbox('testing', 'foo', self.event)
        self.assertEqual(sandbox['bar'], 'baz')
        self.assertEqual(sandbox.baz, '42')

        del sandbox.baz
        del sandbox['bar']

        self.assertIsNone(sandbox.bar)
        self.assertIsNone(sandbox['baz'])
Example #3
0
    def test_sandbox(self):
        sandbox = SettingsSandbox('testing', 'foo', self.event)
        sandbox.set('foo', 'bar')
        self.assertEqual(sandbox.get('foo'), 'bar')
        self.assertEqual(self.event.settings.get('testing_foo_foo'), 'bar')
        self.assertIsNone(self.event.settings.get('foo'), 'bar')

        sandbox['bar'] = 'baz'
        sandbox.baz = 42

        self.event = Event.objects.get(id=self.event.id)
        sandbox = SettingsSandbox('testing', 'foo', self.event)
        self.assertEqual(sandbox['bar'], 'baz')
        self.assertEqual(sandbox.baz, '42')

        del sandbox.baz
        del sandbox['bar']

        self.assertIsNone(sandbox.bar)
        self.assertIsNone(sandbox['baz'])
Example #4
0
    def test_sandbox(self):
        sandbox = SettingsSandbox("testing", "foo", self.event)
        sandbox.set("foo", "bar")
        self.assertEqual(sandbox.get("foo"), "bar")
        self.assertEqual(self.event.settings.get("testing_foo_foo"), "bar")
        self.assertIsNone(self.event.settings.get("foo"), "bar")

        sandbox["bar"] = "baz"
        sandbox.baz = 42

        self.event = Event.objects.get(identity=self.event.identity)
        sandbox = SettingsSandbox("testing", "foo", self.event)
        self.assertEqual(sandbox["bar"], "baz")
        self.assertEqual(sandbox.baz, "42")

        del sandbox.baz
        del sandbox["bar"]

        self.assertIsNone(sandbox.bar)
        self.assertIsNone(sandbox["baz"])
Example #5
0
class BaseTicketOutput:
    """
    This is the base class for all ticket outputs.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('ticketoutput', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this output is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def generate(self, order: Order) -> Tuple[str, str, str]:
        """
        This method should generate the download file and return a tuple consisting of a
        filename, a file type and file content.
        """
        raise NotImplementedError()

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this ticket output. This should
        be short but self-explaining. Good examples include 'PDF tickets'
        and 'Passbook'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this ticket output.
        This should only contain lowercase letters and in most
        cases will be the same as your packagename.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``
        setting 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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('paper_size',
                         forms.CharField(
                             label=_('Paper size'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable output'),
                 required=False,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def download_button_text(self) -> str:
        """
        The text on the download button in the frontend.
        """
        return _('Download ticket')

    @property
    def download_button_icon(self) -> str:
        """
        The name of the icon on the download button in the frontend
        """
        return None
Example #6
0
 def __init__(self, event: Event):
     self.event = event
     self.settings = SettingsSandbox('payment', self.identifier, event)
     # Default values
     if self.settings.get('_fee_reverse_calc') is None:
         self.settings.set('_fee_reverse_calc', True)
Example #7
0
 def __init__(self, event: Event):
     self.event = event
     self.settings = SettingsSandbox('payment', self.identifier, event)
     # Default values
     if self.settings.get('_fee_reverse_calc') is None:
         self.settings.set('_fee_reverse_calc', True)
Example #8
0
class MollieMethod(BasePaymentProvider):
    method = ''
    abort_pending_allowed = False
    refunds_allowed = True

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'mollie', event)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def identifier(self):
        return 'mollie_{}'.format(self.method)

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return self.refunds_allowed

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        return self.refunds_allowed

    def payment_prepare(self, request, payment):
        return self.checkout_prepare(request, None)

    def payment_is_valid_session(self, request: HttpRequest):
        return True

    @property
    def request_headers(self):
        headers = {}
        if self.settings.connect_client_id and self.settings.access_token:
            headers['Authorization'] = 'Bearer %s' % self.settings.access_token
        else:
            headers['Authorization'] = 'Bearer %s' % self.settings.api_key
        return headers

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_mollie/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretix_mollie/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self
        }
        return template.render(ctx)

    def payment_can_retry(self, payment):
        return self._is_still_available(order=payment.order)

    def payment_pending_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretix_mollie/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self,
            'order': payment.order,
            'payment': payment,
            'payment_info': payment_info,
        }
        return template.render(ctx)

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretix_mollie/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def execute_refund(self, refund: OrderRefund):
        payment = refund.payment.info_data.get('id')
        body = {
            'amount': {
                'currency': self.event.currency,
                'value': str(refund.amount)
            },
        }
        if self.settings.connect_client_id and self.settings.access_token:
            body['testmode'] = refund.payment.info_data.get('mode',
                                                            'live') == 'test'
        try:
            print(self.request_headers, body)
            req = requests.post(
                'https://api.mollie.com/v2/payments/{}/refunds'.format(
                    payment),
                json=body,
                headers=self.request_headers)
            req.raise_for_status()
            req.json()
        except HTTPError:
            logger.exception('Mollie error: %s' % req.text)
            try:
                refund.info_data = req.json()
            except:
                refund.info_data = {'error': True, 'detail': req.text}
            raise PaymentException(
                _('Mollie reported an error: {}').format(
                    refund.info_data.get('detail')))
        else:
            refund.done()

    def get_locale(self, language):
        pretix_to_mollie_locales = {
            'en': 'en_US',
            'nl': 'nl_NL',
            'nl_BE': 'nl_BE',
            'fr': 'fr_FR',
            'de': 'de_DE',
            'es': 'es_ES',
            'ca': 'ca_ES',
            'pt': 'pt_PT',
            'it': 'it_IT',
            'nb': 'nb_NO',
            'sv': 'sv_SE',
            'fi': 'fi_FI',
            'da': 'da_DK',
            'is': 'is_IS',
            'hu': 'hu_HU',
            'pl': 'pl_PL',
            'lv': 'lv_LV',
            'lt': 'lt_LT'
        }
        return pretix_to_mollie_locales.get(
            language,
            pretix_to_mollie_locales.get(
                language.split('-')[0],
                pretix_to_mollie_locales.get(language.split('_')[0], 'en')))

    def _get_payment_body(self, payment):
        b = {
            'amount': {
                'currency': self.event.currency,
                'value': str(payment.amount),
            },
            'description':
            'Order {}-{}'.format(self.event.slug.upper(), payment.full_id),
            'redirectUrl':
            build_absolute_uri(
                self.event,
                'plugins:pretix_mollie:return',
                kwargs={
                    'order':
                    payment.order.code,
                    'payment':
                    payment.pk,
                    'hash':
                    hashlib.sha1(
                        payment.order.secret.lower().encode()).hexdigest(),
                }),
            'webhookUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_mollie:webhook',
                               kwargs={'payment': payment.pk}),
            'locale':
            self.get_locale(payment.order.locale),
            'method':
            self.method,
            'metadata': {
                'organizer': self.event.organizer.slug,
                'event': self.event.slug,
                'order': payment.order.code,
                'payment': payment.local_id,
            }
        }
        if self.settings.connect_client_id and self.settings.access_token:
            b['profileId'] = self.settings.connect_profile
            b['testmode'] = self.settings.endpoint == 'test' or self.event.testmode
        return b

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        try:
            req = requests.post('https://api.mollie.com/v2/payments',
                                json=self._get_payment_body(payment),
                                headers=self.request_headers)
            req.raise_for_status()
        except HTTPError:
            logger.exception('Mollie error: %s' % req.text)
            try:
                payment.info_data = req.json()
            except:
                payment.info_data = {'error': True, 'detail': req.text}
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'data': payment.info_data
                })
            raise PaymentException(
                _('We had trouble communicating with Mollie. Please try again and get in touch '
                  'with us if this problem persists.'))

        data = req.json()
        payment.info = json.dumps(data)
        payment.state = OrderPayment.PAYMENT_STATE_CREATED
        payment.save()
        request.session['payment_mollie_order_secret'] = payment.order.secret
        return self.redirect(request,
                             data.get('_links').get('checkout').get('href'))

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (build_absolute_uri(
                request.event, 'plugins:pretix_mollie:redirect') + '?url=' +
                    urllib.parse.quote(signer.sign(url)))
        else:
            return str(url)

    def shred_payment_info(self, obj: OrderPayment):
        if not obj.info:
            return
        d = json.loads(obj.info)
        if 'details' in d:
            d['details'] = {
                k: 'â–ˆ'
                for k in d['details'].keys() if k not in ('bitcoinAmount', )
            }

        d['_shredded'] = True
        obj.info = json.dumps(d)
        obj.save(update_fields=['info'])
Example #9
0
 def settings(self):
     return SettingsSandbox('payment', 'banktransfer', getattr(self.request, 'event', self.request.organizer))
Example #10
0
class BasePaymentProvider:
    """
    This is the base class for all payment providers.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('payment', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns, whether or whether not this payment provider is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def calculate_fee(self, price: Decimal) -> Decimal:
        """
        Calculate the fee for this payment provider which will be added to
        final price before fees (but after taxes). It should include any taxes.
        The default implementation makes use of the setting ``_fee_abs`` for an
        absolute fee and ``_fee_percent`` for a percentage.

        :param price: The total value without the payment method fee, after taxes.
        """
        fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
        fee_percent = self.settings.get('_fee_percent',
                                        as_type=Decimal,
                                        default=0)
        return (price * fee_percent / 100).quantize(Decimal('.01')) + fee_abs

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this payment provider. This should
        be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card via Stripe'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this payment provider.
        This should only contain lowercase letters and in most
        cases will be the same as your packagename.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        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.

        We suggest that you return an ``OrderedDict`` object instead of a dictionary
        and make use of the default implementation. Your implementation could look
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('bank_details',
                         forms.CharField(
                             widget=forms.Textarea,
                             label=_('Bank account details'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable payment method'),
                 required=False,
             )),
            ('_fee_abs',
             forms.DecimalField(label=_('Additional fee'),
                                help_text=_('Absolute value'),
                                required=False)),
            ('_fee_percent',
             forms.DecimalField(label=_('Additional fee'),
                                help_text=_('Percentage'),
                                required=False)),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def payment_form_fields(self) -> dict:
        """
        This is used by the default implementation of :py:meth:`checkout_form`.
        It should return an object similar to :py:attr:`settings_form_fields`.

        The default implementation returns an empty dictionary.
        """
        return {}

    def payment_form(self, request: HttpRequest) -> Form:
        """
        This is called by the default implementation of :py:meth:`checkout_form_render`
        to obtain the form that is displayed to the user during the checkout
        process. The default implementation constructs the form using
        :py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
        and all fields and fills the form with data form the user's session.
        """
        form = Form(data=(request.POST if request.method == 'POST' else None),
                    prefix='payment_%s' % self.identifier,
                    initial={
                        k.replace('payment_%s_' % self.identifier, ''): v
                        for k, v in request.session.items()
                        if k.startswith('payment_%s_' % self.identifier)
                    })
        form.fields = self.payment_form_fields
        return form

    def is_allowed(self, request: HttpRequest) -> bool:
        """
        You can use this method to disable this payment provider for certain groups
        of users, products or other criteria. If this method returns ``False``, the
        user will not be able to select this payment method.

        The default implementation always returns ``True``.
        """
        return True

    def payment_form_render(self, request: HttpRequest) -> str:
        """
        When the user selects this provider as his prefered payment method,
        he will be shown the HTML you return from this method.

        The default implementation will call :py:meth:`checkout_form`
        and render the returned form. If your payment method doesn't require
        the user to fill out form fields, you should just return a paragraph
        of explainatory text.
        """
        form = self.payment_form(request)
        template = get_template(
            'pretixpresale/event/checkout_payment_form_default.html')
        ctx = {'request': request, 'form': form}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        """
        If the user successfully filled in his payment data, he will be redirected
        to a confirmation page which lists all details of his order for a final review.
        This method should return the HTML which should be displayed inside the
        'Payment' box on this page.

        In most cases, this should include a short summary of the user's input and
        a short explaination on how the payment process will continue.
        """
        raise NotImplementedError()  # NOQA

    def checkout_prepare(self, request: HttpRequest,
                         cart: Dict[str, Any]) -> "bool|str":
        """
        Will be called after the user selected this provider as his payment method.
        If you provided a form to the user to enter payment data, this method should
        at least store the user's input into his session.

        This method should return ``False``, if the user's input was invalid, ``True``
        if the input was valid and the frontend should continue with default behaviour
        or a string containing an URL, if the user should be redirected somewhere else.

        On errors, you should use Django's message framework to display an error message
        to the user (or the normal form validation error messages).

        The default implementation stores the input into the form returned by
        :py:meth:`payment_form` in the user's session.

        If your payment method requires you to redirect the user to an external provider,
        this might be the place to do so.

        .. IMPORTANT:: If this is called, the user has not yet confirmed his or her order.
           You may NOT do anything which actually moves money.

        :param cart: This dictionary contains at least the following keys:

            positions:
               A list of ``CartPosition`` objects that are annotated with the special
               attributes ``count`` and ``total`` because multiple objects of the
               same content are grouped into one.

            raw:
                The raw list of ``CartPosition`` objects in the users cart

            total:
                The overall total *including* the fee for the payment method.

            payment_fee:
                The fee for the payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_is_valid_session(self, request: HttpRequest) -> bool:
        """
        This is called at the time the user tries to place the order. It should return
        ``True``, if the user's session is valid and all data your payment provider requires
        in future steps is present.
        """
        raise NotImplementedError()  # NOQA

    def payment_perform(self, request: HttpRequest, order: Order) -> str:
        """
        After the user confirmed his purchase, this method will be called to complete
        the payment process. This is the place to actually move the money, if applicable.
        If you need any special  behaviour,  you can return a string
        containing an URL the user will be redirected to. If you are done with your process
        you should return the user to the order's detail page.

        If the payment is completed, you should call ``pretix.base.services.orders.mark_order_paid(order, provider, info)``
        with ``provider`` being your :py:attr:`identifier` and ``info`` being any string
        you might want to store for later usage. Please note that ``mark_order_paid`` might
        raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this
        order is over and some of the items are sold out. You should use the exception message
        to display a meaningful error to the user.

        The default implementation just returns ``None`` and therefore leaves the
        order unpaid. The user will be redirected to the order's detail page by default.

        On errors, you should use Django's message framework to display an error message
        to the user.

        :param order: The order object
        """
        return None

    def order_pending_mail_render(self, order: Order) -> str:
        """
        After the user submitted his order, he or she will receive a confirmation
        e-mail. You can return a string from this method if you want to add additional
        information to this e-mail.

        :param order: The order object
        """
        return ""

    def order_pending_render(self, request: HttpRequest, order: Order) -> str:
        """
        If the user visits a detail page of an order which has not yet been paid but
        this payment method was selected during checkout, this method will be called
        to provide HTML content for the 'payment' box on the page.

        It should contain instructions on how to continue with the payment process,
        either in form of text or buttons/links/etc.

        :param order: The order object
        """
        raise NotImplementedError()  # NOQA

    def order_can_retry(self, order: Order) -> bool:
        """
        Will be called if the user views the detail page of an unpaid order to determine
        whether the user should be presented with an option to retry the payment. The default
        implementation always returns False.

        :param order: The order object
        """
        return False

    def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
        """
        Will be called if the user retries to pay an unpaid order (after the user filled in
        e.g. the form returned by :py:meth:`payment_form`).

        It should return and report errors the same way as :py:meth:`checkout_prepare`, but
        receives an ``Order`` object instead of a cart object.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def order_paid_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the user views the detail page of an paid order which is
        associated with this payment provider.

        It should return HTML code which should be displayed to the user or None,
        if there is nothing to say (like the default implementation does).

        :param order: The order object
        """
        return None

    def order_control_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the *event administrator* views the detail page of an order
        which is associated with this payment provider.

        It should return HTML code containing information regarding the current payment
        status and, if applicable, next steps.

        The default implementation returns the verbose name of the payment provider.

        :param order: The order object
        """
        return _('Payment provider: %s' % self.verbose_name)

    def order_control_refund_render(self, order: Order) -> str:
        """
        Will be called if the event administrator clicks an order's 'refund' button.
        This can be used to display information *before* the order is being refunded.

        It should return HTML code which should be displayed to the user. It should
        contain information about to which extend the money will be refunded
        automatically.

        :param order: The order object
        """
        self.order.log_action('pretix.base.order.refunded')
        return '<div class="alert alert-warning">%s</div>' % _(
            'The money can not be automatically refunded, '
            'please transfer the money back manually.')

    def order_control_refund_perform(self, request: HttpRequest,
                                     order: Order) -> "bool|str":
        """
        Will be called if the event administrator confirms the refund.

        This should transfer the money back (if possible). You can return an URL the
        user should be redirected to if you need special behaviour or None to continue
        with default behaviour.

        On failure, you should use Django's message framework to display an error message
        to the user.

        The default implementation sets the Orders state to refunded and shows a success
        message.

        :param request: The HTTP request
        :param order: The order object
        """
        from pretix.base.services.orders import mark_order_refunded

        mark_order_refunded(order, user=request.user)
        messages.success(
            request,
            _('The order has been marked as refunded. Please transfer the money '
              'back to the buyer manually.'))
Example #11
0
 def __init__(self, event):
     self.event = event
     self.settings = SettingsSandbox('payment', self.identifier, event)
Example #12
0
class StripeMethod(BasePaymentProvider):
    identifier = ''
    method = ''

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'stripe', event)

    @property
    def test_mode_message(self):
        if self.settings.connect_client_id and not self.settings.secret_key:
            is_testmode = True
        else:
            is_testmode = '_test_' in self.settings.secret_key
        if is_testmode:
            return mark_safe(
                _('The Stripe plugin is operating in test mode. You can use one of <a {args}>many test '
                  'cards</a> to perform a transaction. No money will actually be transferred.'
                  ).
                format(
                    args=
                    'href="https://stripe.com/docs/testing#cards" target="_blank"'
                ))
        return None

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_prepare(self, request, payment):
        return self.checkout_prepare(request, None)

    def _amount_to_decimal(self, cents):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return round_decimal(float(cents) / (10**places), self.event.currency)

    def _decimal_to_int(self, amount):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return int(amount * 10**places)

    def _get_amount(self, payment):
        return self._decimal_to_int(payment.amount)

    @property
    def api_kwargs(self):
        if self.settings.connect_client_id and self.settings.connect_user_id:
            if self.settings.get('endpoint',
                                 'live') == 'live' and not self.event.testmode:
                kwargs = {
                    'api_key': self.settings.connect_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
            else:
                kwargs = {
                    'api_key': self.settings.connect_test_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
        else:
            kwargs = {
                'api_key': self.settings.secret_key,
            }
        return kwargs

    def _init_api(self):
        stripe.api_version = '2019-05-16'
        stripe.set_app_info("pretix",
                            partner_id="pp_partner_FSaz4PpKIur7Ox",
                            version=__version__,
                            url="https://pretix.eu")

    def checkout_confirm_render(self, request) -> str:
        template = get_template(
            'pretixplugins/stripe/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self
        }
        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):
        try:
            params = {}
            if not source.startswith('src_'):
                params['statement_descriptor'] = ugettext(
                    '{event}-{code}').format(event=self.event.slug.upper(),
                                             code=payment.order.code)[:22]
            params.update(self.api_kwargs)
            charge = stripe.Charge.create(
                amount=self._get_amount(payment),
                currency=self.event.currency.lower(),
                source=source,
                description='{event}-{code}'.format(
                    event=self.event.slug.upper(), code=payment.order.code),
                metadata={
                    'order': str(payment.order.id),
                    'event': self.event.id,
                    'code': payment.order.code
                },
                # TODO: Is this sufficient?
                idempotency_key=str(self.event.id) + payment.order.code +
                source,
                **params)
        except stripe.error.CardError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            logger.info('Stripe card error: %s' % str(err))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'message': err['message']
                })
            raise PaymentException(
                _('Stripe reported an error with your card: %s') %
                err['message'])

        except stripe.error.StripeError as e:
            if e.json_body and 'error' in e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'message': err['message']
                })
            raise PaymentException(
                _('We had trouble communicating with Stripe. Please try again and get in touch '
                  'with us if this problem persists.'))
        else:
            ReferencedStripeObject.objects.get_or_create(reference=charge.id,
                                                         defaults={
                                                             'order':
                                                             payment.order,
                                                             'payment': payment
                                                         })
            if charge.status == 'succeeded' and charge.paid:
                try:
                    payment.info = str(charge)
                    payment.confirm()
                except Quota.QuotaExceededException as e:
                    raise PaymentException(str(e))

                except SendMailException:
                    raise PaymentException(
                        _('There was an error sending the confirmation mail.'))
            elif charge.status == 'pending':
                if request:
                    messages.warning(
                        request,
                        _('Your payment is pending completion. We will inform you as soon as the '
                          'payment completed.'))
                payment.info = str(charge)
                payment.state = OrderPayment.PAYMENT_STATE_PENDING
                payment.save()
                return
            else:
                logger.info('Charge failed: %s' % str(charge))
                payment.info = str(charge)
                payment.state = OrderPayment.PAYMENT_STATE_FAILED
                payment.save()
                payment.order.log_action(
                    'pretix.event.order.payment.failed', {
                        'local_id': payment.local_id,
                        'provider': payment.provider,
                        'info': str(charge)
                    })
                raise PaymentException(
                    _('Stripe reported an error: %s') % charge.failure_message)

    def payment_pending_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/pending.html')
        ctx = {
            'request':
            request,
            'event':
            self.event,
            'settings':
            self.settings,
            'provider':
            self,
            'order':
            payment.order,
            'payment':
            payment,
            'payment_info':
            payment_info,
            'payment_hash':
            hashlib.sha1(payment.order.secret.lower().encode()).hexdigest()
        }
        return template.render(ctx)

    def api_payment_details(self, payment: OrderPayment):
        return {
            "id": payment.info_data.get("id", None),
            "payment_method": payment.info_data.get("payment_method", None)
        }

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if 'amount' in payment_info:
                payment_info['amount'] /= 10**settings.CURRENCY_PLACES.get(
                    self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def execute_refund(self, refund: OrderRefund):
        self._init_api()

        payment_info = refund.payment.info_data

        if not payment_info:
            raise PaymentException(_('No payment information found.'))

        try:
            if payment_info['id'].startswith('pi_'):
                chargeid = payment_info['charges']['data'][0]['id']
            else:
                chargeid = payment_info['id']

            ch = stripe.Charge.retrieve(chargeid, **self.api_kwargs)
            r = ch.refunds.create(amount=self._get_amount(refund), )
            ch.refresh()
        except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \
                as e:
            if e.json_body and 'error' in e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            raise PaymentException(
                _('We had trouble communicating with Stripe. Please try again and contact '
                  'support if the problem persists.'))
        except stripe.error.StripeError as err:
            logger.error('Stripe error: %s' % str(err))
            raise PaymentException(_('Stripe returned an error'))
        else:
            refund.info = str(r)
            if r.status in ('succeeded', 'pending'):
                refund.done()
            elif r.status in ('failed', 'canceled'):
                refund.state = OrderRefund.REFUND_STATE_FAILED
                refund.execution_date = now()
                refund.save()

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        self._init_api()
        try:
            source = self._create_source(request, payment)
        except stripe.error.StripeError as e:
            if e.json_body and 'err' in e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'message': err['message']
                })
            raise PaymentException(
                _('We had trouble communicating with Stripe. Please try again and get in touch '
                  'with us if this problem persists.'))

        ReferencedStripeObject.objects.get_or_create(reference=source.id,
                                                     defaults={
                                                         'order':
                                                         payment.order,
                                                         'payment': payment
                                                     })
        payment.info = str(source)
        payment.state = OrderPayment.PAYMENT_STATE_PENDING
        payment.save()
        request.session['payment_stripe_order_secret'] = payment.order.secret
        return self.redirect(request, source.redirect.url)

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (
                build_absolute_uri(request.event, 'plugins:stripe:redirect') +
                '?url=' + urllib.parse.quote(signer.sign(url)))
        else:
            return str(url)

    def shred_payment_info(self, obj: OrderPayment):
        if not obj.info:
            return
        d = json.loads(obj.info)
        new = {}
        if 'source' in d:
            new['source'] = {
                'id': d['source'].get('id'),
                'type': d['source'].get('type'),
                'brand': d['source'].get('brand'),
                'last4': d['source'].get('last4'),
                'bank_name': d['source'].get('bank_name'),
                'bank': d['source'].get('bank'),
                'bic': d['source'].get('bic'),
                'card': {
                    'brand': d['source'].get('card', {}).get('brand'),
                    'country': d['source'].get('card', {}).get('cuntry'),
                    'last4': d['source'].get('card', {}).get('last4'),
                }
            }
        if 'amount' in d:
            new['amount'] = d['amount']
        if 'currency' in d:
            new['currency'] = d['currency']
        if 'status' in d:
            new['status'] = d['status']
        if 'id' in d:
            new['id'] = d['id']

        new['_shredded'] = True
        obj.info = json.dumps(new)
        obj.save(update_fields=['info'])

        for le in obj.order.all_logentries().filter(
                action_type="pretix.plugins.stripe.event").exclude(
                    data="", shredded=True):
            d = le.parsed_data
            if 'data' in d:
                for k, v in list(d['data']['object'].items()):
                    if v not in ('reason', 'status', 'failure_message',
                                 'object', 'id'):
                        d['data']['object'][k] = 'â–ˆ'
                le.data = json.dumps(d)
                le.shredded = True
                le.save(update_fields=['data', 'shredded'])
Example #13
0
class StripeMethod(BasePaymentProvider):
    identifier = ''
    method = ''

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'stripe', event)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method),
                                                                                 as_type=bool)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_prepare(self, request, payment):
        return self.checkout_prepare(request, None)

    def _amount_to_decimal(self, cents):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return round_decimal(float(cents) / (10 ** places), self.event.currency)

    def _decimal_to_int(self, amount):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return int(amount * 10 ** places)

    def _get_amount(self, payment):
        return self._decimal_to_int(payment.amount)

    @property
    def api_kwargs(self):
        if self.settings.connect_client_id and self.settings.connect_user_id:
            if self.settings.get('endpoint', 'live') == 'live':
                kwargs = {
                    'api_key': self.settings.connect_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
            else:
                kwargs = {
                    'api_key': self.settings.connect_test_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
        else:
            kwargs = {
                'api_key': self.settings.secret_key,
            }
        return kwargs

    def _init_api(self):
        stripe.api_version = '2018-02-28'
        stripe.set_app_info("pretix", version=__version__, url="https://pretix.eu")

    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretixplugins/stripe/checkout_payment_confirm.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self}
        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):
        try:
            params = {}
            if not source.startswith('src_'):
                params['statement_descriptor'] = ugettext('{event}-{code}').format(
                    event=self.event.slug.upper(),
                    code=payment.order.code
                )[:22]
            params.update(self.api_kwargs)
            charge = stripe.Charge.create(
                amount=self._get_amount(payment),
                currency=self.event.currency.lower(),
                source=source,
                metadata={
                    'order': str(payment.order.id),
                    'event': self.event.id,
                    'code': payment.order.code
                },
                # TODO: Is this sufficient?
                idempotency_key=str(self.event.id) + payment.order.code + source,
                **params
            )
        except stripe.error.CardError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            logger.info('Stripe card error: %s' % str(err))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider,
                'message': err['message']
            })
            raise PaymentException(_('Stripe reported an error with your card: %s') % err['message'])

        except stripe.error.StripeError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider,
                'message': err['message']
            })
            raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
                                     'with us if this problem persists.'))
        else:
            ReferencedStripeObject.objects.get_or_create(
                reference=charge.id,
                defaults={'order': payment.order, 'payment': payment}
            )
            if charge.status == 'succeeded' and charge.paid:
                try:
                    payment.info = str(charge)
                    payment.confirm()
                except Quota.QuotaExceededException as e:
                    raise PaymentException(str(e))

                except SendMailException:
                    raise PaymentException(_('There was an error sending the confirmation mail.'))
            elif charge.status == 'pending':
                if request:
                    messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the '
                                                'payment completed.'))
                payment.info = str(charge)
                payment.state = OrderPayment.PAYMENT_STATE_PENDING
                payment.save()
                return
            else:
                logger.info('Charge failed: %s' % str(charge))
                payment.info = str(charge)
                payment.state = OrderPayment.PAYMENT_STATE_FAILED
                payment.save()
                payment.order.log_action('pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'info': str(charge)
                })
                raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message)

    def payment_pending_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self,
            'order': payment.order,
            'payment': payment,
            'payment_info': payment_info,
        }
        return template.render(ctx)

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if 'amount' in payment_info:
                payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def execute_refund(self, refund: OrderRefund):
        self._init_api()

        payment_info = refund.payment.info_data

        if not payment_info:
            raise PaymentException(_('No payment information found.'))

        try:
            ch = stripe.Charge.retrieve(payment_info['id'], **self.api_kwargs)
            r = ch.refunds.create(
                amount=self._get_amount(refund),
            )
            ch.refresh()
        except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \
                as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            raise PaymentException(_('We had trouble communicating with Stripe. Please try again and contact '
                                     'support if the problem persists.'))
        except stripe.error.StripeError as err:
            logger.error('Stripe error: %s' % str(err))
            raise PaymentException(_('Stripe returned an error'))
        else:
            refund.info = str(r)
            if r.status in ('succeeded', 'pending'):
                refund.done()
            elif r.status in ('failed', 'canceled'):
                refund.state = OrderRefund.REFUND_STATE_FAILED
                refund.execution_date = now()
                refund.save()

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        self._init_api()
        try:
            source = self._create_source(request, payment)
        except stripe.error.StripeError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            payment.info_data = {
                'error': True,
                'message': err['message'],
            }
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider,
                'message': err['message']
            })
            raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
                                     'with us if this problem persists.'))

        ReferencedStripeObject.objects.get_or_create(
            reference=source.id,
            defaults={'order': payment.order, 'payment': payment}
        )
        payment.info = str(source)
        payment.state = OrderPayment.PAYMENT_STATE_PENDING
        payment.save()
        request.session['payment_stripe_order_secret'] = payment.order.secret
        return self.redirect(request, source.redirect.url)

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (
                build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?url=' +
                urllib.parse.quote(signer.sign(url))
            )
        else:
            return str(url)

    def shred_payment_info(self, obj: OrderPayment):
        if not obj.info:
            return
        d = json.loads(obj.info)
        new = {}
        if 'source' in d:
            new['source'] = {
                'id': d['source'].get('id'),
                'type': d['source'].get('type'),
                'brand': d['source'].get('brand'),
                'last4': d['source'].get('last4'),
                'bank_name': d['source'].get('bank_name'),
                'bank': d['source'].get('bank'),
                'bic': d['source'].get('bic'),
                'card': {
                    'brand': d['source'].get('card', {}).get('brand'),
                    'country': d['source'].get('card', {}).get('cuntry'),
                    'last4': d['source'].get('card', {}).get('last4'),
                }
            }
        if 'amount' in d:
            new['amount'] = d['amount']
        if 'currency' in d:
            new['currency'] = d['currency']
        if 'status' in d:
            new['status'] = d['status']
        if 'id' in d:
            new['id'] = d['id']

        new['_shredded'] = True
        obj.info = json.dumps(new)
        obj.save(update_fields=['info'])

        for le in obj.order.all_logentries().filter(
                action_type="pretix.plugins.stripe.event"
        ).exclude(data="", shredded=True):
            d = le.parsed_data
            if 'data' in d:
                for k, v in list(d['data']['object'].items()):
                    if v not in ('reason', 'status', 'failure_message', 'object', 'id'):
                        d['data']['object'][k] = 'â–ˆ'
                le.data = json.dumps(d)
                le.shredded = True
                le.save(update_fields=['data', 'shredded'])
class SaferpayMethod(BasePaymentProvider):
    method = ''
    abort_pending_allowed = False
    refunds_allowed = True
    cancel_flow = True
    payment_methods = []

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'saferpay', event)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def identifier(self):
        return 'saferpay_{}'.format(self.method)

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return self.refunds_allowed

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        return self.refunds_allowed

    def payment_prepare(self, request, payment):
        return self.checkout_prepare(request, None)

    def payment_is_valid_session(self, request: HttpRequest):
        return True

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_saferpay/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template(
            'pretix_saferpay/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self
        }
        return template.render(ctx)

    def payment_can_retry(self, payment):
        return self._is_still_available(order=payment.order)

    def payment_pending_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretix_saferpay/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self,
            'order': payment.order,
            'payment': payment,
            'payment_info': payment_info,
        }
        return template.render(ctx)

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if 'amount' in payment_info:
                payment_info['amount'] /= 10**settings.CURRENCY_PLACES.get(
                    self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretix_saferpay/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def execute_refund(self, refund: OrderRefund):
        d = refund.payment.info_data

        try:
            if self.cancel_flow and refund.amount == refund.payment.amount:
                if 'Id' not in d:
                    raise PaymentException(
                        _('The payment has not been captured successfully and can therefore not be '
                          'refunded.'))

                req = self._post('Payment/v1/Transaction/Cancel',
                                 json={
                                     "RequestHeader": {
                                         "SpecVersion": "1.10",
                                         "CustomerId":
                                         self.settings.customer_id,
                                         "RequestId": str(uuid.uuid4()),
                                         "RetryIndicator": 0
                                     },
                                     "TransactionReference": {
                                         "TransactionId": d.get('Id')
                                     }
                                 })
                if req.status_code == 200:
                    refund.info = req.text
                    refund.save(update_fields=['info'])
                    refund.done()

                try:
                    err = req.json()
                except:
                    req.raise_for_status()
                else:
                    if err['ErrorName'] not in ('ACTION_NOT_SUPPORTED',
                                                'TRANSACTION_ALREADY_CAPTURED',
                                                'TRANSACTION_IN_WRONG_STATE'):
                        req.raise_for_status()

            if 'CaptureId' not in d:
                raise PaymentException(
                    _('The payment has not been captured successfully and can therefore not be '
                      'refunded.'))

            req = self._post(
                'Payment/v1/Transaction/Refund',
                json={
                    "RequestHeader": {
                        "SpecVersion": "1.10",
                        "CustomerId": self.settings.customer_id,
                        "RequestId": str(uuid.uuid4()),
                        "RetryIndicator": 0
                    },
                    "Refund": {
                        "Amount": {
                            "Value": str(self._decimal_to_int(refund.amount)),
                            "CurrencyCode": self.event.currency
                        },
                        "OrderId":
                        "{}-{}-R-{}".format(self.event.slug.upper(),
                                            refund.order.code,
                                            refund.local_id),
                        "Description":
                        "Order {}-{}".format(self.event.slug.upper(),
                                             refund.order.code),
                    },
                    "CaptureReference": {
                        "CaptureId": d.get('CaptureId')
                    }
                })
            req.raise_for_status()
            refund.info_data = req.json()
            refund.save(update_fields=['info'])

            if refund.info_data['Transaction'].get('Status') == 'AUTHORIZED':
                req = self._post(
                    'Payment/v1/Transaction/Capture',
                    json={
                        "RequestHeader": {
                            "SpecVersion": "1.10",
                            "CustomerId": self.settings.customer_id,
                            "RequestId": str(uuid.uuid4()),
                            "RetryIndicator": 0
                        },
                        "TransactionReference": {
                            "TransactionId":
                            refund.info_data['Transaction'].get('Id')
                        }
                    })
                req.raise_for_status()
                data = req.json()
                if data['Status'] == 'CAPTURED':
                    refund.order.log_action('pretix_saferpay.event.paid')
                    trans = refund.info_data
                    trans['Transaction']['Status'] = 'CAPTURED'
                    trans['Transaction']['CaptureId'] = data['CaptureId']
                    refund.info = json.dumps(trans)
                    refund.save(update_fields=['info'])
                    refund.done()

        except HTTPError:
            logger.exception('Saferpay error: %s' % req.text)
            try:
                refund.info_data = req.json()
            except:
                refund.info_data = {'error': True, 'detail': req.text}
            refund.state = OrderRefund.REFUND_STATE_FAILED
            refund.save()
            refund.order.log_action(
                'pretix.event.order.refund.failed', {
                    'local_id': refund.local_id,
                    'provider': refund.provider,
                    'data': refund.info_data
                })
            raise PaymentException(
                _('We had trouble communicating with Saferpay. Please try again and get in touch '
                  'with us if this problem persists.'))

    @property
    def test_mode_message(self):
        if self.settings.endpoint == 'test':
            return _(
                'The Saferpay plugin is operating in test mode. No money will actually be transferred.'
            )
        return None

    def _post(self, endpoint, *args, **kwargs):
        r = requests.post('https://{env}.saferpay.com/api/{ep}'.format(
            env='www' if self.settings.get('endpoint') == 'live' else 'test',
            ep=endpoint,
        ),
                          auth=(self.settings.get('api_user'),
                                self.settings.get('api_pass')),
                          *args,
                          **kwargs)
        return r

    def _get(self, endpoint, *args, **kwargs):
        r = requests.get('https://{env}.saferpay.com/api/{ep}'.format(
            env='www' if self.settings.get('endpoint') == 'live' else 'test',
            ep=endpoint,
        ),
                         auth=(self.settings.get('api_user'),
                               self.settings.get('api_pass')),
                         *args,
                         **kwargs)
        return r

    def get_locale(self, language):
        saferpay_locales = {
            'de', 'en', 'fr', 'da', 'cs', 'es', 'et', 'hr', 'it', 'hu', 'lv',
            'lt', 'nl', 'nn', 'pl', 'pt', 'ru', 'ro', 'sk', 'sl', 'fi', 'sv',
            'tr', 'el', 'ja', 'zh'
        }
        if language[:2] in saferpay_locales:
            return language[:2]
        return 'en'

    def _amount_to_decimal(self, cents):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return round_decimal(float(cents) / (10**places), self.event.currency)

    def _decimal_to_int(self, amount):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return int(amount * 10**places)

    def _get_payment_page_init_body(self, payment):
        b = {
            "RequestHeader": {
                "SpecVersion": "1.10",
                "CustomerId": self.settings.customer_id,
                "RequestId": str(uuid.uuid4()),
                "RetryIndicator": 0,
                "ClientInfo": {
                    "ShopInfo": "pretix",
                }
            },
            "TerminalId": self.settings.terminal_id,
            "Payment": {
                "Amount": {
                    "Value": str(self._decimal_to_int(payment.amount)),
                    "CurrencyCode": self.event.currency
                },
                "OrderId":
                "{}-{}-P-{}".format(self.event.slug.upper(),
                                    payment.order.code, payment.local_id),
                "Description":
                "Order {}-{}".format(self.event.slug.upper(),
                                     payment.order.code),
                "PayerNote":
                "{}-{}".format(self.event.slug.upper(), payment.order.code),
            },
            "PaymentMethods": self.payment_methods,
            "Payer": {
                "LanguageCode": self.get_locale(payment.order.locale),
            },
            "ReturnUrls": {
                "Success":
                build_absolute_uri(
                    self.event,
                    'plugins:pretix_saferpay:return',
                    kwargs={
                        'order':
                        payment.order.code,
                        'payment':
                        payment.pk,
                        'hash':
                        hashlib.sha1(
                            payment.order.secret.lower().encode()).hexdigest(),
                        'action':
                        'success'
                    }),
                "Fail":
                build_absolute_uri(
                    self.event,
                    'plugins:pretix_saferpay:return',
                    kwargs={
                        'order':
                        payment.order.code,
                        'payment':
                        payment.pk,
                        'hash':
                        hashlib.sha1(
                            payment.order.secret.lower().encode()).hexdigest(),
                        'action':
                        'fail'
                    }),
                "Abort":
                build_absolute_uri(
                    self.event,
                    'plugins:pretix_saferpay:return',
                    kwargs={
                        'order':
                        payment.order.code,
                        'payment':
                        payment.pk,
                        'hash':
                        hashlib.sha1(
                            payment.order.secret.lower().encode()).hexdigest(),
                        'action':
                        'abort'
                    }),
            },
            "Notification": {
                "NotifyUrl":
                build_absolute_uri(self.event,
                                   'plugins:pretix_saferpay:webhook',
                                   kwargs={
                                       'payment': payment.pk,
                                   }),
            },
            "BillingAddressForm": {
                "Display": False
            },
            "DeliveryAddressForm": {
                "Display": False
            }
        }
        return b

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        try:
            req = self._post('Payment/v1/PaymentPage/Initialize',
                             json=self._get_payment_page_init_body(payment))
            req.raise_for_status()
        except HTTPError:
            logger.exception('Saferpay error: %s' % req.text)
            try:
                payment.info_data = req.json()
            except:
                payment.info_data = {'error': True, 'detail': req.text}
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'data': payment.info_data
                })
            raise PaymentException(
                _('We had trouble communicating with Saferpay. Please try again and get in touch '
                  'with us if this problem persists.'))

        data = req.json()
        payment.info = json.dumps(data)
        payment.state = OrderPayment.PAYMENT_STATE_CREATED
        payment.save()
        request.session['payment_saferpay_order_secret'] = payment.order.secret
        return self.redirect(request, data.get('RedirectUrl'))

    def redirect(self, request, url):
        if request.session.get('iframe_session', False) and self.method in (
                'paypal', 'sofort', 'giropay', 'paydirekt'):
            signer = signing.Signer(salt='safe-redirect')
            return (build_absolute_uri(
                request.event, 'plugins:pretix_saferpay:redirect') + '?url=' +
                    urllib.parse.quote(signer.sign(url)))
        else:
            return str(url)

    def shred_payment_info(self, obj: OrderPayment):
        if not obj.info:
            return
        d = json.loads(obj.info)
        if 'details' in d:
            d['details'] = {k: 'â–ˆ' for k in d['details'].keys()}

        d['_shredded'] = True
        obj.info = json.dumps(d)
        obj.save(update_fields=['info'])
Example #15
0
class WirecardMethod(BasePaymentProvider):
    method = ''
    wc_payment_type = 'SELECT'
    statement_length = 253
    order_ref_length = 32

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'wirecard', event)

    @property
    def identifier(self):
        return 'wirecard_{}'.format(self.method)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_wirecard/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template(
            'pretix_wirecard/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_prepare(self, request, total):
        return True

    def payment_is_valid_session(self, request):
        return True

    def payment_perform(self, request, order) -> str:
        request.session['wirecard_nonce'] = get_random_string(length=12)
        request.session['wirecard_order_secret'] = order.secret
        return eventreverse(self.event,
                            'plugins:pretix_wirecard:redirect',
                            kwargs={
                                'order':
                                order.code,
                                'hash':
                                hashlib.sha1(
                                    order.secret.lower().encode()).hexdigest(),
                            })

    def sign_parameters(self, params: dict, order: list = None) -> dict:
        keys = order or (list(params.keys()) +
                         ['requestFingerprintOrder', 'secret'])
        params['requestFingerprintOrder'] = ','.join(keys)
        payload = ''.join(
            self.settings.get('secret') if k == 'secret' else params[k]
            for k in keys)
        params['requestFingerprint'] = hmac.new(
            self.settings.get('secret').encode(), payload.encode(),
            hashlib.sha512).hexdigest().upper()
        return params

    def params_for_order(self, order, request):
        if not request.session.get('wirecard_nonce'):
            request.session['wirecard_nonce'] = get_random_string(length=12)
            request.session['wirecard_order_secret'] = order.secret
        hash = hashlib.sha1(order.secret.lower().encode()).hexdigest()
        # TODO: imageURL, cssURL?
        return {
            'customerId':
            self.settings.get('customer_id'),
            'shopId':
            self.settings.get('shop_id', ''),
            'language':
            order.locale[:2],
            'paymentType':
            self.wc_payment_type,
            'amount':
            str(order.total),
            'currency':
            self.event.currency,
            'orderDescription':
            _('Order {event}-{code}').format(event=self.event.slug.upper(),
                                             code=order.code),
            'successUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': order.code,
                                   'hash': hash,
                               }),
            'cancelUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': order.code,
                                   'hash': hash,
                               }),
            'failureUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': order.code,
                                   'hash': hash,
                               }),
            'confirmUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:confirm',
                               kwargs={
                                   'order': order.code,
                                   'hash': hash,
                               }).replace(':8000', ''),  # TODO: Remove
            'pendingUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:confirm',
                               kwargs={
                                   'order': order.code,
                                   'hash': hash,
                               }),
            'duplicateRequestCheck':
            'yes',
            'serviceUrl':
            self.event.settings.imprint_url,
            'customerStatement':
            _('ORDER {order} EVENT {event} BY {organizer}').format(
                event=self.event.slug.upper(),
                order=order.code,
                organizer=self.event.organizer.name)[:self.statement_length],
            'orderReference':
            '{code}{id}'.format(code=order.code,
                                id=request.session.get('wirecard_nonce'))
            [:self.order_ref_length],
            'displayText':
            _('Order {} for event {} by {}').format(order.code,
                                                    self.event.name,
                                                    self.event.organizer.name),
            'pretix_orderCode':
            order.code,
            'pretix_eventSlug':
            self.event.slug,
            'pretix_organizerSlug':
            self.event.organizer.slug,
            'pretix_nonce':
            request.session.get('wirecard_nonce'),
        }

    def order_pending_render(self, request, order) -> str:
        retry = True
        try:
            if order.payment_info and json.loads(
                    order.payment_info)['paymentState'] == 'PENDING':
                retry = False
        except KeyError:
            pass
        template = get_template('pretix_wirecard/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'retry': retry,
            'order': order
        }
        return template.render(ctx)

    def order_control_render(self, request, order) -> str:
        if order.payment_info:
            payment_info = json.loads(order.payment_info)
        else:
            payment_info = None
        template = get_template('pretix_wirecard/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'order': order,
            'provname': self.verbose_name
        }
        return template.render(ctx)

    def order_can_retry(self, order):
        return True

    @property
    def refund_available(self):
        return bool(self.settings.get('toolkit_password'))

    def order_control_refund_render(self, order, request) -> str:
        if self.refund_available:
            template = get_template('pretix_wirecard/control_refund.html')
            ctx = {
                'request': request,
                'form': self._refund_form(request),
            }
            return template.render(ctx)
        else:
            return super().order_control_refund_render(order, request)

    def _refund(self, order_number, amount, currency, language):
        params = {
            'customerId': self.settings.get('customer_id'),
            'shopId': self.settings.get('shop_id', ''),
            'toolkitPassword': self.settings.get('toolkit_password'),
            'command': 'refund',
            'language': language,
            'orderNumber': order_number,
            'amount': str(amount),
            'currency': currency
        }
        r = requests.post(
            'https://checkout.wirecard.com/page/toolkit.php',
            data=self.sign_parameters(params, [
                'customerId', 'shopId', 'toolkitPassword', 'secret', 'command',
                'language', 'orderNumber', 'amount', 'currency'
            ]))
        retvals = parse_qs(r.text)
        if retvals['status'][0] != '0':
            logger.error('Wirecard error during refund: %s' % r.text)
            raise PaymentException(
                _('Wirecard reported an error: {msg}').format(
                    msg=retvals['message'][0]))

    def _refund_form(self, request):
        return RefundForm(
            data=request.POST if request.method == "POST" else None)

    def order_control_refund_perform(self, request, order) -> "bool|str":
        if order.payment_info:
            payment_info = json.loads(order.payment_info)
        else:
            payment_info = None

        if not payment_info or not self.refund_available:
            mark_order_refunded(order, user=request.user)
            messages.warning(
                request,
                _('We were unable to transfer the money back automatically. '
                  'Please get in touch with the customer and transfer it back manually.'
                  ))
            return

        f = self._refund_form(request)
        if not f.is_valid():
            messages.error(request,
                           _('Your input was invalid, please try again.'))
            return
        elif f.cleaned_data.get('auto_refund') == 'manual':
            order = mark_order_refunded(order, user=request.user)
            order.payment_manual = True
            order.save()
            return

        try:
            self._refund(payment_info['orderNumber'], order.total,
                         self.event.currency, order.locale[:2])
        except PaymentException as e:
            messages.error(request, str(e))
        except requests.exceptions.RequestException as e:
            logger.exception('Wirecard error: %s' % str(e))
            messages.error(
                request,
                _('We had trouble communicating with Wirecard. Please try again and contact '
                  'support if the problem persists.'))
        else:
            mark_order_refunded(order, user=request.user)

    def shred_payment_info(self, order: Order):
        if not order.payment_info:
            return
        d = json.loads(order.payment_info)
        new = {'_shreded': True}
        for k in ('paymentState', 'amount', 'authenticated', 'paymentType',
                  'pretix_orderCode', 'currency', 'orderNumber',
                  'financialInstitution', 'message', 'mandateId', 'dueDate'):
            if k in d:
                new[k] = d[k]
        order.payment_info = json.dumps(new)
        order.save(update_fields=['payment_info'])
Example #16
0
class QPayProMethod(QPayProSettingsHolder):
    method = ''
    abort_pending_allowed = False
    refunds_allowed = True

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'qpaypro', event)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def identifier(self):
        return 'qpaypro_{}'.format(self.method)

    @property
    def is_enabled(self) -> bool:
        return (self.settings.get('_enabled', as_type=bool)
                and self.settings.get('method_{}'.format(self.method),
                                      as_type=bool))

    def _fingerprint_prepare(self, request, url_next):
        if not super().checkout_prepare(request, None):
            return False

        # Device fingerprint session id
        session_onlinemetrix_key = self.get_payment_key_prefix(
        ) + 'session_onlinemetrix'
        if not request.session.get(session_onlinemetrix_key, False):
            request.session[session_onlinemetrix_key] = get_random_string(32)

        # Device fingerprint URLs
        params = 'org_id={x_org_id}&session_id={x_login}{session_id}'.format(
            x_org_id=self.get_settings_key('x_org_id'),
            x_login=self.get_settings_key('x_login'),
            session_id=request.session.get(session_onlinemetrix_key, ''))
        url_script = '{url}/fp/tags.js?{params}'.format(
            url=self.url_onlinemetrix,
            params=params,
        )
        url_iframe = '{url}/fp/tags?{params}'.format(
            url=self.url_onlinemetrix,
            params=params,
        )

        # Final URL using the result of all the previous steps
        signer = signing.Signer(salt='safe-redirect')
        url_final = (
            eventreverse(self.event, 'plugins:pretix_qpaypro:onlinemetrix') +
            '?' + 'url_script=' + urllib.parse.quote(signer.sign(url_script)) +
            '&' + 'url_iframe=' + urllib.parse.quote(signer.sign(url_iframe)) +
            '&' + 'url_next=' + urllib.parse.quote(signer.sign(url_next)))
        return url_final

    def payment_prepare(self, request, payment):
        url_next = eventreverse(self.event,
                                'presale:event.order.pay.confirm',
                                kwargs={
                                    'order': payment.order.code,
                                    'secret': payment.order.secret,
                                    'payment': payment.pk
                                })
        return self._fingerprint_prepare(request, url_next)

    def checkout_prepare(self, request, cart):
        url_next = eventreverse(self.event,
                                'presale:event.checkout',
                                kwargs={
                                    'step': 'confirm',
                                })
        return self._fingerprint_prepare(request, url_next)

    def get_payment_key_prefix(self):
        return 'payment_{0}_'.format(self.identifier)

    def payment_is_valid_session(self, request: HttpRequest):
        key_prefix = self.get_payment_key_prefix()
        return (request.session.get(key_prefix + 'cc_type', '') != ''
                and request.session.get(key_prefix + 'cc_number', '') != ''
                and request.session.get(key_prefix + 'cc_exp_month', '') != ''
                and request.session.get(key_prefix + 'cc_exp_year', '') != ''
                and request.session.get(key_prefix + 'cc_cvv2', '') != ''
                and request.session.get(key_prefix + 'cc_first_name', '') != ''
                and request.session.get(key_prefix + 'cc_last_name', '') != ''
                and request.session.get(key_prefix + 'session_onlinemetrix',
                                        '') != '')

    @property
    def payment_form_fields(self):
        return OrderedDict(get_payment_form_fields())

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_qpaypro/checkout_payment_form.html')
        ctx = {
            'form': self.payment_form(request),
        }
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretix_qpaypro/checkout_payment_confirm.html')
        key_prefix = self.get_payment_key_prefix()
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self,
            'cc_type': request.session[key_prefix + 'cc_type'].upper(),
            'cc_number':
            mask_cc_number(request.session[key_prefix + 'cc_number']),
            'cc_exp_month': request.session[key_prefix + 'cc_exp_month'],
            'cc_exp_year': request.session[key_prefix + 'cc_exp_year'],
            'cc_first_name': request.session[key_prefix + 'cc_first_name'],
            'cc_last_name': request.session[key_prefix + 'cc_last_name'],
        }
        return template.render(ctx)

    def _get_payment_body(self, request: HttpRequest, payment: OrderPayment):
        key_prefix = self.get_payment_key_prefix()

        # Get a complete list of the cart contents
        x_line_item = ''
        for line in payment.order.positions.all():
            x_line_item += '{description}<|>{code}<|>{quantity}<|>{value}<|>'.format(
                description=line.item.name,
                code=line.item.name,
                quantity='1',
                value=line.price,
            )

        # Get the order page for relay URL
        x_relay_url = build_absolute_uri(self.event,
                                         'presale:event.order',
                                         kwargs={
                                             'order': payment.order.code,
                                             'secret': payment.order.secret
                                         }) + '?paid=yes'

        # Generate all the transaction body
        b = {
            'x_login':
            self.get_settings_key('x_login'),
            'x_private_key':
            self.get_settings_key('x_private_key'),
            'x_api_secret':
            self.get_settings_key('x_api_secret'),
            'x_description':
            'Order {} - {}'.format(self.event.slug.upper(), payment.full_id),
            'x_amount':
            str(payment.amount),
            'x_currency_code':
            self.event.currency,
            'x_product_id':
            payment.order.code,
            'x_audit_number':
            payment.order.code,
            'x_line_item':
            x_line_item,
            'x_email':
            payment.order.email,
            'x_fp_sequence':
            payment.order.code,
            'x_fp_timestamp':
            str(datetime.now()),
            'x_invoice_num':
            payment.order.code,
            'x_first_name':
            request.session.get(key_prefix + 'cc_first_name', ''),
            'x_last_name':
            request.session.get(key_prefix + 'cc_last_name', ''),
            'x_company':
            'C/F',
            'x_address':
            self.get_settings_key('x_address'),
            'x_city':
            self.get_settings_key('x_city'),
            'x_state':
            self.get_settings_key('x_state'),
            'x_zip':
            self.get_settings_key('x_zip'),
            'x_country':
            self.get_settings_key('x_country'),
            'x_relay_response':
            'TRUE',
            'x_relay_url':
            x_relay_url,
            'x_type':
            'AUTH_ONLY',
            'x_method':
            'CC',
            'visaencuotas':
            0,
            'cc_number':
            request.session.get(key_prefix + 'cc_number', ''),
            'cc_exp':
            '{}/{}'.format(
                request.session.get(key_prefix + 'cc_exp_month', ''),
                str(request.session.get(key_prefix + 'cc_exp_year', ''))[-2:]),
            'cc_cvv2':
            request.session.get(key_prefix + 'cc_cvv2', ''),
            'cc_name':
            '{} {}'.format(
                request.session.get(key_prefix + 'cc_first_name', ''),
                request.session.get(key_prefix + 'cc_last_name', ''),
            ),
            'cc_type':
            request.session.get(key_prefix + 'cc_type', ''),
            'device_fingerprint_id':
            request.session.get(key_prefix + 'session_onlinemetrix', ''),
        }
        return b

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        try:
            # Get the correct endpoint to consume
            x_endpoint = self.get_settings_key('x_endpoint')
            if x_endpoint == 'live':
                url = 'https://payments.qpaypro.com/checkout/api_v1'
            else:
                url = 'https://sandbox.qpaypro.com/payment/api_v1'

            # Get the message body
            payment_body = self._get_payment_body(request, payment)

            # # To save the information befor send
            # # TO DO: to delete this action because of security issues
            # payment.order.log_action('pretix.event.order.payment.started', {
            #     'local_id': payment.local_id,
            #     'provider': payment.provider,
            #     'data': payment_body
            # })

            # Perform the call to the endpoint
            req = requests.post(
                url,
                json=payment_body,
            )
            req.raise_for_status()

            # Load the response to be read
            data = req.json()

            # The result is evaluated to determine the next step
            if not (data['result'] == 1 and data['responseCode'] == 100):
                raise PaymentException(data['responseText'])

            # To save the result
            payment.info = req.json()
            payment.confirm()
        except (HTTPError, PaymentException, Quota.QuotaExceededException):
            logger.exception('QPayPro error: %s' % req.text)
            try:
                payment.info_data = req.json()
            except Exception:
                payment.info_data = {'error': True, 'detail': req.text}
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action(
                'pretix.event.order.payment.failed', {
                    'local_id': payment.local_id,
                    'provider': payment.provider,
                    'data': payment.info_data
                })
            raise PaymentException(
                _('We had trouble communicating with QPayPro. Please try again and get in touch '
                  'with us if this problem persists.'))

        return None
Example #17
0
 def __init__(self, event: Event):
     self.event = event
     self.settings = SettingsSandbox('payment', self.identifier, event)
Example #18
0
class BaseTicketOutput:
    """
    This is the base class for all ticket outputs.
    """

    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('ticketoutput', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this output is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    @property
    def multi_download_enabled(self) -> bool:
        """
        Returns whether or not the ``generate_order`` method may be called. Returns
        ``True`` by default.
        """
        return True

    def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
        """
        This method should generate the download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.

        .. note:: If the event uses the event series feature (internally called subevents)
                  and your generated ticket contains information like the event name or date,
                  you probably want to display the properties of the subevent. A common pattern
                  to do this would be a declaration ``ev = position.subevent or position.order.event``
                  and then access properties that are present on both classes like ``ev.name`` or
                  ``ev.date_from``.
        """
        raise NotImplementedError()

    def generate_order(self, order: Order) -> Tuple[str, str, str]:
        """
        This method is the same as order() but should not generate one file per order position
        but instead one file for the full order.

        This method is optional to implement. If you don't implement it, the default
        implementation will offer a zip file of the generate() results for the order positions.

        This method should generate a download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.

        If you override this method, make sure that positions that are addons (i.e. ``addon_to``
        is set) are only outputted if the event setting ``ticket_download_addons`` is active.
        Do the same for positions that are non-admission without ``ticket_download_nonadm`` active.
        If you want, you can just iterate over ``order.positions_with_tickets`` which applies the
        appropriate filters for you.
        """
        with tempfile.TemporaryDirectory() as d:
            with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
                for pos in order.positions_with_tickets:
                    fname, __, content = self.generate(pos)
                    zipf.writestr('{}-{}{}'.format(
                        order.code, pos.positionid, os.path.splitext(fname)[1]
                    ), content)

            with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
                return '{}-{}.zip'.format(order.code, self.identifier), 'application/zip', zipf.read()

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this ticket output. This should be short but
        self-explanatory. Good examples include 'PDF tickets' and 'Passbook'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this ticket output.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``
        setting 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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('paper_size',
                         forms.CharField(
                             label=_('Paper size'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable output'),
                 required=False,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def download_button_text(self) -> str:
        """
        The text on the download button in the frontend.
        """
        return _('Download ticket')

    @property
    def download_button_icon(self) -> str:
        """
        The Font Awesome icon on the download button in the frontend.
        """
        return 'fa-download'
Example #19
0
class BaseTicketOutput:
    """
    This is the base class for all ticket outputs.
    """

    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('ticketoutput', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this output is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def generate(self, order: Order) -> Tuple[str, str, str]:
        """
        This method should generate the download file and return a tuple consisting of a
        filename, a file type and file content.
        """
        raise NotImplementedError()

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this ticket output. This should
        be short but self-explaining. Good examples include 'PDF tickets'
        and 'Passbook'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this ticket output.
        This should only contain lowercase letters and in most
        cases will be the same as your packagename.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``
        setting 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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('paper_size',
                         forms.CharField(
                             label=_('Paper size'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable output'),
                 required=False,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def download_button_text(self) -> str:
        """
        The text on the download button in the frontend.
        """
        return _('Download ticket')

    @property
    def download_button_icon(self) -> str:
        """
        The name of the icon on the download button in the frontend
        """
        return None
Example #20
0
class Paypal(BasePaymentProvider):
    identifier = 'paypal'
    verbose_name = _('PayPal')
    payment_form_fields = OrderedDict([
    ])

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'paypal', event)

    @property
    def settings_form_fields(self):
        if self.settings.connect_client_id and not self.settings.secret:
            # PayPal connect
            if self.settings.connect_user_id:
                fields = [
                    ('connect_user_id',
                     forms.CharField(
                         label=_('PayPal account'),
                         disabled=True
                     )),
                ]
            else:
                return {}
        else:
            fields = [
                ('client_id',
                 forms.CharField(
                     label=_('Client ID'),
                     max_length=80,
                     min_length=80,
                     help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
                         text=_('Click here for a tutorial on how to obtain the required keys'),
                         docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
                     )
                 )),
                ('secret',
                 forms.CharField(
                     label=_('Secret'),
                     max_length=80,
                     min_length=80,
                 )),
                ('endpoint',
                 forms.ChoiceField(
                     label=_('Endpoint'),
                     initial='live',
                     choices=(
                         ('live', 'Live'),
                         ('sandbox', 'Sandbox'),
                     ),
                 )),
            ]

        d = OrderedDict(
            fields + list(super().settings_form_fields.items())
        )

        d.move_to_end('_enabled', False)
        return d

    def get_connect_url(self, request):
        request.session['payment_paypal_oauth_event'] = request.event.pk

        self.init_api()
        return Tokeninfo.authorize_url({'scope': 'openid profile email'})

    def settings_content_render(self, request):
        if self.settings.connect_client_id and not self.settings.secret:
            # Use PayPal connect
            if not self.settings.connect_user_id:
                return (
                    "<p>{}</p>"
                    "<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
                ).format(
                    _('To accept payments via PayPal, you will need an account at PayPal. By clicking on the '
                      'following button, you can either create a new PayPal account connect pretix to an existing '
                      'one.'),
                    self.get_connect_url(request),
                    _('Connect with {icon} PayPal').format(icon='<i class="fa fa-paypal"></i>')
                )
            else:
                return (
                    "<button formaction='{}' class='btn btn-danger'>{}</button>"
                ).format(
                    reverse('plugins:paypal:oauth.disconnect', kwargs={
                        'organizer': self.event.organizer.slug,
                        'event': self.event.slug,
                    }),
                    _('Disconnect from PayPal')
                )
        else:
            return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
                _('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
                  'when payments are refunded externally.'),
                build_global_uri('plugins:paypal:webhook')
            )

    def init_api(self):
        if self.settings.connect_client_id:
            paypalrestsdk.set_config(
                mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
                client_id=self.settings.connect_client_id,
                client_secret=self.settings.connect_secret_key,
                openid_client_id=self.settings.connect_client_id,
                openid_client_secret=self.settings.connect_secret_key,
                openid_redirect_uri=urlquote(build_global_uri('plugins:paypal:oauth.return')))
        else:
            paypalrestsdk.set_config(
                mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
                client_id=self.settings.get('client_id'),
                client_secret=self.settings.get('secret'))

    def payment_is_valid_session(self, request):
        return (request.session.get('payment_paypal_id', '') != ''
                and request.session.get('payment_paypal_payer', '') != '')

    def payment_form_render(self, request) -> str:
        template = get_template('pretixplugins/paypal/checkout_payment_form.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings}
        return template.render(ctx)

    def checkout_prepare(self, request, cart):
        self.init_api()
        kwargs = {}
        if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
            kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']

        if request.event.settings.payment_paypal_connect_user_id:
            userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
            request.event.settings.payment_paypal_connect_user_id = userinfo.email
            payee = {
                "email": request.event.settings.payment_paypal_connect_user_id,
                # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
                # we should use it instead of the merchant's eMail-address
                # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
            }
        else:
            payee = {}

        payment = paypalrestsdk.Payment({
            'intent': 'sale',
            'payer': {
                "payment_method": "paypal",
            },
            "redirect_urls": {
                "return_url": build_absolute_uri(request.event, 'plugins:paypal:return', kwargs=kwargs),
                "cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort', kwargs=kwargs),
            },
            "transactions": [
                {
                    "item_list": {
                        "items": [
                            {
                                "name": __('Order for %s') % str(request.event),
                                "quantity": 1,
                                "price": self.format_price(cart['total']),
                                "currency": request.event.currency
                            }
                        ]
                    },
                    "amount": {
                        "currency": request.event.currency,
                        "total": self.format_price(cart['total'])
                    },
                    "description": __('Event tickets for {event}').format(event=request.event.name),
                    "payee": payee
                }
            ]
        })
        request.session['payment_paypal_order'] = None
        return self._create_payment(request, payment)

    def format_price(self, value):
        return str(round_decimal(value, self.event.currency, {
            # PayPal behaves differently than Stripe in deciding what currencies have decimal places
            # Source https://developer.paypal.com/docs/classic/api/currency_codes/
            'HUF': 0,
            'JPY': 0,
            'MYR': 0,
            'TWD': 0,
            # However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
            'CLP': 0,
            # Let's just guess that the ones listed here are 0-based as well
            # https://developers.braintreepayments.com/reference/general/currencies
            'BIF': 0,
            'DJF': 0,
            'GNF': 0,
            'KMF': 0,
            'KRW': 0,
            'LAK': 0,
            'PYG': 0,
            'RWF': 0,
            'UGX': 0,
            'VND': 0,
            'VUV': 0,
            'XAF': 0,
            'XOF': 0,
            'XPF': 0,
        }))

    @property
    def abort_pending_allowed(self):
        return False

    def _create_payment(self, request, payment):
        try:
            if payment.create():
                if payment.state not in ('created', 'approved', 'pending'):
                    messages.error(request, _('We had trouble communicating with PayPal'))
                    logger.error('Invalid payment state: ' + str(payment))
                    return
                request.session['payment_paypal_id'] = payment.id
                for link in payment.links:
                    if link.method == "REDIRECT" and link.rel == "approval_url":
                        if request.session.get('iframe_session', False):
                            signer = signing.Signer(salt='safe-redirect')
                            return (
                                build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
                                urllib.parse.quote(signer.sign(link.href))
                            )
                        else:
                            return str(link.href)
            else:
                messages.error(request, _('We had trouble communicating with PayPal'))
                logger.error('Error on creating payment: ' + str(payment.error))
        except Exception as e:
            messages.error(request, _('We had trouble communicating with PayPal'))
            logger.exception('Error on creating payment: ' + str(e))

    def checkout_confirm_render(self, request) -> str:
        """
        Returns the HTML that should be displayed when the user selected this provider
        on the 'confirm order' page.
        """
        template = get_template('pretixplugins/paypal/checkout_payment_confirm.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings}
        return template.render(ctx)

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
            raise PaymentException(_('We were unable to process your payment. See below for details on how to '
                                     'proceed.'))

        self.init_api()
        pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
        ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
        if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
                != self.event.currency:
            logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
            raise PaymentException(_('We were unable to process your payment. See below for details on how to '
                                     'proceed.'))

        return self._execute_payment(pp_payment, request, payment)

    def _execute_payment(self, payment, request, payment_obj):
        if payment.state == 'created':
            payment.replace([
                {
                    "op": "replace",
                    "path": "/transactions/0/item_list",
                    "value": {
                        "items": [
                            {
                                "name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(),
                                                                         code=payment_obj.order.code),
                                "quantity": 1,
                                "price": self.format_price(payment_obj.amount),
                                "currency": payment_obj.order.event.currency
                            }
                        ]
                    }
                },
                {
                    "op": "replace",
                    "path": "/transactions/0/description",
                    "value": __('Order {order} for {event}').format(
                        event=request.event.name,
                        order=payment_obj.order.code
                    )
                }
            ])
            try:
                payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
            except Exception as e:
                messages.error(request, _('We had trouble communicating with PayPal'))
                logger.exception('Error on creating payment: ' + str(e))

        for trans in payment.transactions:
            for rr in trans.related_resources:
                if hasattr(rr, 'sale') and rr.sale:
                    if rr.sale.state == 'pending':
                        messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
                                                    'soon as the payment completed.'))
                        payment_obj.info = json.dumps(payment.to_dict())
                        payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
                        payment_obj.save()
                        return

        payment_obj.refresh_from_db()
        if payment.state == 'pending':
            messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
                                        'payment completed.'))
            payment_obj.info = json.dumps(payment.to_dict())
            payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
            payment_obj.save()
            return

        if payment.state != 'approved':
            payment_obj.state = OrderPayment.PAYMENT_STATE_FAILED
            payment_obj.save()
            payment_obj.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider,
            })
            logger.error('Invalid state: %s' % str(payment))
            raise PaymentException(_('We were unable to process your payment. See below for details on how to '
                                     'proceed.'))

        if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
            logger.warning('PayPal success event even though order is already marked as paid')
            return

        try:
            payment_obj.info = json.dumps(payment.to_dict())
            payment_obj.save(update_fields=['info'])
            payment_obj.confirm()
        except Quota.QuotaExceededException as e:
            raise PaymentException(str(e))

        except SendMailException:
            messages.warning(request, _('There was an error sending the confirmation mail.'))
        return None

    def payment_pending_render(self, request, payment) -> str:
        retry = True
        try:
            if payment.info and payment.info_data['state'] == 'pending':
                retry = False
        except KeyError:
            pass
        template = get_template('pretixplugins/paypal/pending.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings,
               'retry': retry, 'order': payment.order}
        return template.render(ctx)

    def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
        template = get_template('pretixplugins/paypal/control.html')
        ctx = {'request': request, 'event': self.event, 'settings': self.settings,
               'payment_info': payment.info_data, 'order': payment.order}
        return template.render(ctx)

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return True

    def payment_refund_supported(self, payment: OrderPayment):
        return True

    def execute_refund(self, refund: OrderRefund):
        self.init_api()

        sale = None
        for res in refund.payment.info_data['transactions'][0]['related_resources']:
            for k, v in res.items():
                if k == 'sale':
                    sale = paypalrestsdk.Sale.find(v['id'])
                    break

        pp_refund = sale.refund({
            "amount": {
                "total": self.format_price(refund.amount),
                "currency": refund.order.event.currency
            }
        })
        if not pp_refund.success():
            raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(pp_refund.error))
        else:
            sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
            refund.payment.info = json.dumps(sale.to_dict())
            refund.info = json.dumps(pp_refund.to_dict())
            refund.done()

    def payment_prepare(self, request, payment_obj):
        self.init_api()

        if request.event.settings.payment_paypal_connect_user_id:
            userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
            request.event.settings.payment_paypal_connect_user_id = userinfo.email
            payee = {
                "email": request.event.settings.payment_paypal_connect_user_id,
                # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
                # we should use it instead of the merchant's eMail-address
                # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
            }
        else:
            payee = {}

        payment = paypalrestsdk.Payment({
            'intent': 'sale',
            'payer': {
                "payment_method": "paypal",
            },
            "redirect_urls": {
                "return_url": build_absolute_uri(request.event, 'plugins:paypal:return'),
                "cancel_url": build_absolute_uri(request.event, 'plugins:paypal:abort'),
            },
            "transactions": [
                {
                    "item_list": {
                        "items": [
                            {
                                "name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(),
                                                                         code=payment_obj.order.code),
                                "quantity": 1,
                                "price": self.format_price(payment_obj.amount),
                                "currency": payment_obj.order.event.currency
                            }
                        ]
                    },
                    "amount": {
                        "currency": request.event.currency,
                        "total": self.format_price(payment_obj.amount)
                    },
                    "description": __('Order {order} for {event}').format(
                        event=request.event.name,
                        order=payment_obj.order.code
                    ),
                    "payee": payee
                }
            ]
        })
        request.session['payment_paypal_order'] = payment_obj.order.pk
        request.session['payment_paypal_payment'] = payment_obj.pk
        return self._create_payment(request, payment)

    def shred_payment_info(self, obj):
        if obj.info:
            d = json.loads(obj.info)
            new = {
                'id': d.get('id'),
                'payer': {
                    'payer_info': {
                        'email': 'â–ˆ'
                    }
                },
                'update_time': d.get('update_time'),
                'transactions': [
                    {
                        'amount': t.get('amount')
                    } for t in d.get('transactions', [])
                ],
                '_shredded': True
            }
            obj.info = json.dumps(new)
            obj.save(update_fields=['info'])

        for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
            d = le.parsed_data
            if 'resource' in d:
                d['resource'] = {
                    'id': d['resource'].get('id'),
                    'sale_id': d['resource'].get('sale_id'),
                    'parent_payment': d['resource'].get('parent_payment'),
                }
            le.data = json.dumps(d)
            le.shredded = True
            le.save(update_fields=['data', 'shredded'])
Example #21
0
 def settings(self):
     return SettingsSandbox('payment', 'banktransfer', self.request.event)
Example #22
0
 def __init__(self, event: Event):
     super().__init__(event)
     self.settings = SettingsSandbox('payment', 'paypal', event)
Example #23
0
class BasePaymentProvider:
    """
    This is the base class for all payment providers.
    """

    def __init__(self, event):
        self.event = event
        self.settings = SettingsSandbox('payment', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns, whether or whether not this payment provider is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def calculate_fee(self, price: Decimal) -> Decimal:
        """
        Calculate the fee for this payment provider which will be added to
        final price before fees (but after taxes). It should include any taxes.
        The default implementation makes use of the setting ``_fee_abs`` for an
        absolute fee and ``_fee_percent`` for a percentage.

        :param price: The total value without the payment method fee, after taxes.
        """
        fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
        fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
        return Decimal(price * fee_percent / 100 + fee_abs)

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this payment provider. This should
        be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card via Stripe'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this payment provider.
        This should only contain lowercase letters and in most
        cases will be the same as your packagename.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        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.

        We suggest that you return an ``OrderedDict`` object instead of a dictionary
        and make use of the default implementation. Your implementation could look
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('bank_details',
                         forms.CharField(
                             widget=forms.Textarea,
                             label=_('Bank account details'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable payment method'),
                 required=False,
             )),
            ('_fee_abs',
             forms.DecimalField(
                 label=_('Additional fee'),
                 help_text=_('Absolute value'),
                 required=False
             )),
            ('_fee_percent',
             forms.DecimalField(
                 label=_('Additional fee'),
                 help_text=_('Percentage'),
                 required=False
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def payment_form_fields(self) -> dict:
        """
        This is used by the default implementation of :py:meth:`checkout_form`.
        It should return an object similar to :py:attr:`settings_form_fields`.

        The default implementation returns an empty dictionary.
        """
        return {}

    def payment_form(self, request: HttpRequest) -> Form:
        """
        This is called by the default implementation of :py:meth:`checkout_form_render`
        to obtain the form that is displayed to the user during the checkout
        process. The default implementation constructs the form using
        :py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
        and all fields and fills the form with data form the user's session.
        """
        form = Form(
            data=(request.POST if request.method == 'POST' else None),
            prefix='payment_%s' % self.identifier,
            initial={
                k.replace('payment_%s_' % self.identifier, ''): v
                for k, v in request.session.items()
                if k.startswith('payment_%s_' % self.identifier)
            }
        )
        form.fields = self.payment_form_fields
        return form

    def is_allowed(self, request: HttpRequest) -> bool:
        """
        You can use this method to disable this payment provider for certain groups
        of users, products or other criteria. If this method returns ``False``, the
        user will not be able to select this payment method.

        The default implementation always returns ``True``.
        """
        return True

    def payment_form_render(self, request: HttpRequest) -> str:
        """
        When the user selects this provider as his prefered payment method,
        he will be shown the HTML you return from this method.

        The default implementation will call :py:meth:`checkout_form`
        and render the returned form. If your payment method doesn't require
        the user to fill out form fields, you should just return a paragraph
        of explainatory text.
        """
        form = self.payment_form(request)
        template = get_template('pretixpresale/event/checkout_payment_form_default.html')
        ctx = {'request': request, 'form': form}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        """
        If the user successfully filled in his payment data, he will be redirected
        to a confirmation page which lists all details of his order for a final review.
        This method should return the HTML which should be displayed inside the
        'Payment' box on this page.

        In most cases, this should include a short summary of the user's input and
        a short explaination on how the payment process will continue.
        """
        raise NotImplementedError()  # NOQA

    def checkout_prepare(self, request: HttpRequest, cart: dict) -> "bool|str":
        """
        Will be called after the user selected this provider as his payment method.
        If you provided a form to the user to enter payment data, this method should
        at least store the user's input into his session.

        This method should return ``False``, if the user's input was invalid, ``True``
        if the input was valid and the frontend should continue with default behaviour
        or a string containing an URL, if the user should be redirected somewhere else.

        On errors, you should use Django's message framework to display an error message
        to the user (or the normal form validation error messages).

        The default implementation stores the input into the form returned by
        :py:meth:`payment_form` in the user's session.

        If your payment method requires you to redirect the user to an external provider,
        this might be the place to do so.

        .. IMPORTANT:: If this is called, the user has not yet confirmed his or her order.
           You may NOT do anything which actually moves money.

        :param cart: This dictionary contains at least the following keys:

            positions:
               A list of ``CartPosition`` objects that are annotated with the special
               attributes ``count`` and ``total`` because multiple objects of the
               same content are grouped into one.

            raw:
                The raw list of ``CartPosition`` objects in the users cart

            total:
                The overall total *including* the fee for the payment method.

            payment_fee:
                The fee for the payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_is_valid_session(self, request: HttpRequest) -> bool:
        """
        This is called at the time the user tries to place the order. It should return
        ``True``, if the user's session is valid and all data your payment provider requires
        in future steps is present.
        """
        raise NotImplementedError()  # NOQA

    def payment_perform(self, request: HttpRequest, order: Order) -> str:
        """
        After the user confirmed his purchase, this method will be called to complete
        the payment process. This is the place to actually move the money, if applicable.
        If you need any speical  behaviour,  you can return a string
        containing an URL the user will be redirected to. If you are done with your process
        you should return the user to the order's detail page.

        If the payment is completed, you should call ``pretix.base.services.orders.mark_order_paid(order, provider, info)``
        with ``provider`` being your :py:attr:`identifier` and ``info`` being any string
        you might want to store for later usage. Please note, that if you want to store
        something inside ``order.payment_info``, please do it after the ``mark_order_paid`` call,
        as this call does a object clone for you. Please also note that ``mark_order_paid`` might
        raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this
        order is over and some of the items are sold out. You should use the exception message
        to display a meaningful error to the user.

        The default implementation just returns ``None`` and therefore leaves the
        order unpaid. The user will be redirected to the order's detail page by default.

        On errors, you should use Django's message framework to display an error message
        to the user.

        :param order: The order object
        """
        return None

    def order_pending_mail_render(self, order: Order) -> str:
        """
        After the user submitted his order, he or she will receive a confirmation
        e-mail. You can return a string from this method if you want to add additional
        information to this e-mail.

        :param order: The order object
        """
        return ""

    def order_pending_render(self, request: HttpRequest, order: Order) -> str:
        """
        If the user visits a detail page of an order which has not yet been paid but
        this payment method was selected during checkout, this method will be called
        to provide HTML content for the 'payment' box on the page.

        It should contain instructions on how to continue with the payment process,
        either in form of text or buttons/links/etc.

        :param order: The order object
        """
        raise NotImplementedError()  # NOQA

    def order_can_retry(self, order: Order) -> bool:
        """
        Will be called if the user views the detail page of an unpaid order to determine
        whether the user should be presented with an option to retry the payment. The default
        implementation always returns False.

        :param order: The order object
        """
        return False

    def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
        """
        Will be called if the user retries to pay an unpaid order (after the user filled in
        e.g. the form returned by :py:meth:`payment_form`).

        It should return and report errors the same way as :py:meth:`checkout_prepare`, but
        receives an ``Order`` object instead of a cart object.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def order_paid_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the user views the detail page of an paid order which is
        associated with this payment provider.

        It should return HTML code which should be displayed to the user or None,
        if there is nothing to say (like the default implementation does).

        :param order: The order object
        """
        return None

    def order_control_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the *event administrator* views the detail page of an order
        which is associated with this payment provider.

        It should return HTML code containing information regarding the current payment
        status and, if applicable, next steps.

        The default implementation returns the verbose name of the payment provider.

        :param order: The order object
        """
        return _('Payment provider: %s' % self.verbose_name)

    def order_control_refund_render(self, order: Order) -> str:
        """
        Will be called if the event administrator clicks an order's 'refund' button.
        This can be used to display information *before* the order is being refunded.

        It should return HTML code which should be displayed to the user. It should
        contain information about to which extend the money will be refunded
        automatically.

        :param order: The order object
        """
        return '<div class="alert alert-warning">%s</div>' % _('The money can not be automatically refunded, '
                                                               'please transfer the money back manually.')

    def order_control_refund_perform(self, request: HttpRequest, order: Order) -> "bool|str":
        """
        Will be called if the event administrator confirms the refund.

        This should transfer the money back (if possible). You can return an URL the
        user should be redirected to if you need special behaviour or None to continue
        with default behaviour.

        On failure, you should use Django's message framework to display an error message
        to the user.

        The default implementation sets the Orders state to refunded and shows a success
        message.

        :param request: The HTTP request
        :param order: The order object
        """
        order.mark_refunded()
        messages.success(request, _('The order has been marked as refunded. Please transfer the money '
                                    'back to the buyer manually.'))
Example #24
0
class Mercadopago(BasePaymentProvider):
    identifier = 'pretix_mercadopago'
    verbose_name = _('MercadoPago')

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'mercadopago', event)

    @property
    def test_mode_message(self):
        if self.settings.connect_client_id and not self.settings.secret:
            # in OAuth mode, sandbox mode needs to be set global
            is_sandbox = self.settings.connect_endpoint == 'sandbox'
        else:
            is_sandbox = self.settings.get('endpoint') == 'sandbox'
        if is_sandbox:
            return _(
                'The MercadoPago sandbox is being used, you can test without '
                'actually sending money but you will need a '
                'MercadoPago sandbox user to log in.')
        return None

    ####################################################################
    #                           No Refunds                             #
    ####################################################################

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return False

    def payment_refund_supported(self, payment: OrderPayment):
        return False

    def execute_refund(self, refund: OrderRefund):
        raise PaymentException(_('Refunding is not supported.'))

    ####################################################################
    #                       Plugin Settings                            #
    ####################################################################

    @property
    def settings_form_fields(self):
        fields = [
            ('client_id',
             forms.CharField(
                 label=_('Client ID'),
                 max_length=71,
                 min_length=10,
                 help_text=_('{token}<a target="_blank" rel="noopener" '
                             'href="{docs_url}">{text}</a>').
                 format(
                     token=_('puede usar un token el lugar del client_id o '),
                     text=
                     _('Click here for a tutorial on how to obtain the required keys'
                       ),
                     docs_url=
                     'https://www.mercadopago.com.ar/developers/es/guides/faqs/credentials'
                 ))),
            ('secret',
             forms.CharField(label=_('Secret'),
                             max_length=71,
                             min_length=10,
                             required=False)),
            ('endpoint',
             forms.ChoiceField(
                 label=_('Endpoint'),
                 initial='live',
                 required=True,
                 choices=(
                     ('live', 'Live'),
                     ('sandbox', 'Sandbox'),
                 ),
             )),
            ('currency',
             forms.ChoiceField(
                 label=_('Currency'),
                 initial='ARS',
                 required=True,
                 choices=(
                     ('ARS', 'ARS'),
                     ('BRL', 'BRL'),
                     ('CLP', 'CLP'),
                     ('MXN', 'MXN'),
                     ('COP', 'COP'),
                     ('PEN', 'PEN'),
                     ('UYU', 'UYU'),
                 ),
             )),
            ('exchange_rate',
             forms.DecimalField(
                 label=_('Exchange Rate'),
                 required=True,
                 min_value=0,
                 decimal_places=2,
                 help_text=
                 _('Exchange rate to apply to the event currency. Use "1" to not apply any exchange rate.'
                   ))),
        ]

        d = OrderedDict(fields + list(super().settings_form_fields.items()))

        return d

    def settings_content_render(self, request):
        settings_content = ""
        if not self.settings.get('client_id'):
            settings_content = (
                "<p>{}</p>"
                "<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
            ).format(
                _('To accept payments via MercadoPagp, you will need an account at MercadoPago. '
                  'By clicking on the following button, you can either create a new MercadoPago '
                  'account connect pretix to an existing one.'),
                self.get_connect_url(request),
                _('Connect with {icon} MercadoPago').format(
                    icon='<i class="fa fa-mercadopago"></i>'))
        else:
            settings_content = "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
                _('Please configure a MercadoPago Webhook to the following endpoint in order '
                  'to automatically cancel orders when payments are refunded externally.'
                  ),
                build_absolute_uri(request.event,
                                   'plugins:pretix_mercadopago:webhook'))

        if self.event.currency is not self.settings.get('currency'):
            settings_content += (
                '<br><br><div class="alert alert-warning">%s '
                '<a href="ihttps://www.mercadopago.com.ar/developers/es/reference/merchant_orders/resource/">%s</a>'
                '</div>'
            ) % (_(
                "MercadoPago does not process payments in your event's currency."
            ), _("Please make sure you are using the proper exchange rate."))

        return settings_content

    def init_api(self) -> mercadopago.MP:
        if self.settings.get('client_id') and not self.settings.get('secret'):
            mp = mercadopago.MP(self.settings.get('client_id'))
        else:
            mp = mercadopago.MP(self.settings.get('client_id'),
                                self.settings.get('secret'))
        return mp

    ####################################################################
    #                       MercadoPago Interaction                    #
    ####################################################################
    def payment_form_render(self, request) -> str:
        # When the user selects this provider
        # as their preferred payment method,
        # they will be shown the HTML you return from this method.
        return "You will be redirected to MercadoPago now."

    def payment_is_valid_session(self, request):
        # This is called at the time the user tries to place the order.
        # It should return True if the user’s session is valid and all data
        # your payment provider requires in future steps is present.
        return True

    def execute_payment(self, request: HttpRequest, payment_obj: OrderPayment):
        try:
            # After the user has confirmed their purchase,
            # this method will be called to complete the payment process.
            mp = self.init_api()
            order = payment_obj.order
            meta_info = json.loads(order.meta_info)
            form_data = meta_info.get('contact_form_data', {})

            address = {}
            company = ''
            name = ''
            if hasattr(Order, 'invoice_address'):
                address = {
                    "zip_code": order.invoice_address.zipcode,
                    "street_name": order.invoice_address.street
                }
                company = order.invoice_address.company
                name = str(order.invoice_address.name_parts)

            identification_type = form_data.get('invoicing_type_tax_id', '')

            if identification_type == 'PASS':
                identification_number = form_data.get('invoicing_tax_id_pass',
                                                      '')
            elif identification_type == 'VAT':
                identification_number = form_data.get('invoicing_tax_id_vat',
                                                      '')
            else:
                identification_number = form_data.get('invoicing_tax_id_dni',
                                                      '')

            price = float(payment_obj.amount)
            if self.settings.get('currency') is not order.event.currency:
                price = price * float(self.settings.get('exchange_rate'))
            price = round(price, 2)

            order_url = build_absolute_uri(request.event,
                                           'presale:event.order',
                                           kwargs={
                                               'order': order.code,
                                               'secret': order.secret
                                           })

            preference = {
                "items": [{
                    "title":
                    __('Order {slug}-{code}').format(slug=self.event.slug,
                                                     code=order.code),
                    "quantity":
                    1,
                    "unit_price":
                    price,
                    "currency_id":
                    self.settings.get('currency')
                }],
                "auto_return":
                'all',
                "back_urls": {
                    "failure":
                    order_url,
                    "pending":
                    build_absolute_uri(request.event,
                                       'plugins:pretix_mercadopago:return'),
                    "success":
                    build_absolute_uri(request.event,
                                       'plugins:pretix_mercadopago:return')
                },
                "notification_url":
                build_absolute_uri(request.event,
                                   'plugins:pretix_mercadopago:return'),
                "statement_descriptor":
                __('Order {slug}-{code}').format(slug=self.event.slug,
                                                 code=order.code),
                "external_reference":
                str(payment_obj.id),
                #          "additional_info": json.dumps(order.invoice_address)[:600],
                "payer": {
                    "name": name,
                    "surname": company,
                    "email": form_data.get('email', ''),
                    "identification": {
                        "type": identification_type,
                        "number": identification_number
                    },
                    "address": address
                },
                "payment_methods": {
                    "installments": 1
                }
            }

            # Get the payment reported by the IPN.
            # Glossary of attributes response in https://developers.mercadopago.com
            #        paymentInfo = mp.get_payment(kwargs["id"])

            preferenceResult = mp.create_preference(preference)
            payment_obj.info = json.dumps(preferenceResult, indent=4)
            payment_obj.save()
            request.session['payment_mercadopago_preferece_id'] = str(
                preferenceResult['response']['id'])
            request.session['payment_mercadopago_collector_id'] = str(
                preferenceResult['response']['collector_id'])
            request.session['payment_mercadopago_order'] = order.pk
            request.session['payment_mercadopago_payment'] = payment_obj.pk

            try:
                if preferenceResult:
                    if preferenceResult["status"] not in (
                            200, 201
                    ):  # ate not in ('created', 'approved', 'pending'):
                        messages.error(
                            request,
                            _('We had trouble communicating with MercadoPago' +
                              str(preferenceResult["response"]["message"])))
                        logger.error('Invalid payment state: ' +
                                     str(preferenceResult["response"]))
                        return
                    request.session['payment_mercadopago_id'] = str(
                        preferenceResult["response"]["id"])
                    if (self.test_mode_message == None):
                        link = preferenceResult["response"]["init_point"]
                    else:
                        link = preferenceResult["response"][
                            "sandbox_init_point"]
                    return link
                else:
                    messages.error(
                        request,
                        _('We had trouble communicating with MercadoPago' +
                          str(preferenceResult["response"])))
                    logger.error('Error on creating payment: ' +
                                 str(preferenceResult["response"]))
            except Exception as e:
                messages.error(
                    request,
                    _('We had trouble communicating with ' + 'MercadoPago ' +
                      str(e) + str(preferenceResult["response"])))
                logger.exception('Error on creating payment: ' + str(e))

        except Exception as e:
            messages.error(
                request,
                _('We had trouble preparing the order for ' + 'MercadoPago ' +
                  str(e)))
            logger.exception('Error on creating payment: ' + str(e))

    def checkout_confirm_render(self, request) -> str:
        # Returns the HTML that should be displayed when the user selected this provider
        # on the 'confirm order' page.

        try:
            # TODO weird error that doesn't include templates on our path folder
            template = get_template(
                '../../pretix_mercadopago/templates/pretix_mercadopago/checkout_payment_confirm.html'
            )
        except Exception as e:
            template = get_template(
                'pretixplugins/paypal/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
        if order.status == Order.STATUS_PAID:
            if payment.info_data.get('id', None):
                try:
                    return '{}\r\n{}: {}'.format(
                        _('The payment for this invoice has already been received.'
                          ),
                        _('Payment ID'),
                        payment.info_data['response']['id'],
                    )
                except (KeyError, IndexError):
                    return '{}\r\n{}: {}'.format(
                        _('The payment for this invoice has already been received.'
                          ), _('Payment ID'),
                        payment.info_data['response']['id'])
            else:
                return super().render_invoice_text(order, payment)

        return self.settings.get('_invoice_text',
                                 as_type=LazyI18nString,
                                 default='')

    def matching_id(self, payment: OrderPayment):
        # Will be called to get an ID for a matching this payment when comparing
        # pretix records with records of an external source.
        # This should return the main transaction ID for your API.
        return payment.info_data.get('external_reference', None)

    def api_payment_details(self, payment: OrderPayment):
        # Will be called to populate the details parameter
        # of the payment in the REST API.
        res = {"payment_info": payment.info}

        try:
            res = json.loads(payment.info)
        except Exception as e:
            logger.exception('Could not parse json payment.info')

        return res

    ####################################################################
    #                          Utility functions                       #
    ####################################################################
    def get_connect_url(self, request):
        request.session['payment_mercadopago_oauth_event'] = request.event.pk

        self.init_api()
        return Tokeninfo.authorize_url({'scope': 'openid profile email'})
Example #25
0
class StripeMethod(BasePaymentProvider):
    identifier = ''
    method = ''

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'stripe', event)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def order_prepare(self, request, order):
        return self.checkout_prepare(request, None)

    def _get_amount(self, order):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return int(order.total * 10**places)

    @property
    def api_kwargs(self):
        if self.settings.connect_client_id and self.settings.connect_user_id:
            if self.settings.get('endpoint', 'live') == 'live':
                kwargs = {
                    'api_key': self.settings.connect_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
            else:
                kwargs = {
                    'api_key': self.settings.connect_test_secret_key,
                    'stripe_account': self.settings.connect_user_id
                }
        else:
            kwargs = {
                'api_key': self.settings.secret_key,
            }
        return kwargs

    def _init_api(self):
        stripe.api_version = '2018-02-28'
        stripe.set_app_info("pretix",
                            version=__version__,
                            url="https://pretix.eu")

    def checkout_confirm_render(self, request) -> str:
        template = get_template(
            'pretixplugins/stripe/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self
        }
        return template.render(ctx)

    def order_can_retry(self, order):
        return self._is_still_available(order=order)

    def _charge_source(self, request, source, order):
        try:
            charge = stripe.Charge.create(
                amount=self._get_amount(order),
                currency=self.event.currency.lower(),
                source=source,
                metadata={
                    'order': str(order.id),
                    'event': self.event.id,
                    'code': order.code
                },
                # TODO: Is this sufficient?
                idempotency_key=str(self.event.id) + order.code + source,
                **self.api_kwargs)
        except stripe.error.CardError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            logger.info('Stripe card error: %s' % str(err))
            order.payment_info = json.dumps({
                'error': True,
                'message': err['message'],
            })
            order.save(update_fields=['payment_info'])
            raise PaymentException(
                _('Stripe reported an error with your card: %s') %
                err['message'])

        except stripe.error.StripeError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            order.payment_info = json.dumps({
                'error': True,
                'message': err['message'],
            })
            order.save(update_fields=['payment_info'])
            raise PaymentException(
                _('We had trouble communicating with Stripe. Please try again and get in touch '
                  'with us if this problem persists.'))
        else:
            ReferencedStripeObject.objects.get_or_create(order=order,
                                                         reference=charge.id)
            if charge.status == 'succeeded' and charge.paid:
                try:
                    mark_order_paid(order, self.identifier, str(charge))
                except Quota.QuotaExceededException as e:
                    RequiredAction.objects.create(
                        event=self.event,
                        action_type='pretix.plugins.stripe.overpaid',
                        data=json.dumps({
                            'order': order.code,
                            'charge': charge.id
                        }))
                    raise PaymentException(str(e))

                except SendMailException:
                    raise PaymentException(
                        _('There was an error sending the confirmation mail.'))
            elif charge.status == 'pending':
                if request:
                    messages.warning(
                        request,
                        _('Your payment is pending completion. We will inform you as soon as the '
                          'payment completed.'))
                order.payment_info = str(charge)
                order.save(update_fields=['payment_info'])
                return
            else:
                logger.info('Charge failed: %s' % str(charge))
                order.payment_info = str(charge)
                order.save(update_fields=['payment_info'])
                raise PaymentException(
                    _('Stripe reported an error: %s') % charge.failure_message)

    def order_pending_render(self, request, order) -> str:
        if order.payment_info:
            payment_info = json.loads(order.payment_info)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self,
            'order': order,
            'payment_info': payment_info,
        }
        return template.render(ctx)

    def order_control_render(self, request, order) -> str:
        if order.payment_info:
            payment_info = json.loads(order.payment_info)
            if 'amount' in payment_info:
                payment_info['amount'] /= 10**settings.CURRENCY_PLACES.get(
                    self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretixplugins/stripe/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'order': order,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def _refund_form(self, request):
        return RefundForm(
            data=request.POST if request.method == "POST" else None)

    def order_control_refund_render(self, order, request) -> str:
        template = get_template('pretixplugins/stripe/control_refund.html')
        ctx = {
            'request': request,
            'form': self._refund_form(request),
        }
        return template.render(ctx)

    def order_control_refund_perform(self, request, order) -> "bool|str":
        self._init_api()

        f = self._refund_form(request)
        if not f.is_valid():
            messages.error(request,
                           _('Your input was invalid, please try again.'))
            return
        elif f.cleaned_data.get('auto_refund') == 'manual':
            order = mark_order_refunded(order, user=request.user)
            order.payment_manual = True
            order.save()
            return

        if order.payment_info:
            payment_info = json.loads(order.payment_info)
        else:
            payment_info = None

        if not payment_info:
            mark_order_refunded(order, user=request.user)
            messages.warning(
                request,
                _('We were unable to transfer the money back automatically. '
                  'Please get in touch with the customer and transfer it back manually.'
                  ))
            return

        try:
            ch = stripe.Charge.retrieve(payment_info['id'], **self.api_kwargs)
            ch.refunds.create()
            ch.refresh()
        except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \
                as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            messages.error(
                request,
                _('We had trouble communicating with Stripe. Please try again and contact '
                  'support if the problem persists.'))
            logger.error('Stripe error: %s' % str(err))
        except stripe.error.StripeError:
            mark_order_refunded(order, user=request.user)
            messages.warning(
                request,
                _('We were unable to transfer the money back automatically. '
                  'Please get in touch with the customer and transfer it back manually.'
                  ))
        else:
            order = mark_order_refunded(order, user=request.user)
            order.payment_info = str(ch)
            order.save()

    def payment_perform(self, request, order) -> str:
        self._init_api()
        try:
            source = self._create_source(request, order)
        except stripe.error.StripeError as e:
            if e.json_body:
                err = e.json_body['error']
                logger.exception('Stripe error: %s' % str(err))
            else:
                err = {'message': str(e)}
                logger.exception('Stripe error: %s' % str(e))
            order.payment_info = json.dumps({
                'error': True,
                'message': err['message'],
            })
            order.save(update_fields=['payment_info'])
            raise PaymentException(
                _('We had trouble communicating with Stripe. Please try again and get in touch '
                  'with us if this problem persists.'))

        ReferencedStripeObject.objects.get_or_create(order=order,
                                                     reference=source.id)
        order.payment_info = str(source)
        order.save(update_fields=['payment_info'])
        request.session['payment_stripe_order_secret'] = order.secret
        return self.redirect(request, source.redirect.url)

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (
                build_absolute_uri(request.event, 'plugins:stripe:redirect') +
                '?url=' + urllib.parse.quote(signer.sign(url)))
        else:
            return str(url)
Example #26
0
 def __init__(self, event: Event):
     super().__init__(event)
     self.settings = SettingsSandbox('payment', 'mercadopago', event)
Example #27
0
class MollieSettingsHolder(BasePaymentProvider):
    identifier = 'mollie'
    verbose_name = _('Mollie')
    is_enabled = False
    is_meta = True

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'mollie', event)

    def get_connect_url(self, request):
        request.session['payment_mollie_oauth_event'] = request.event.pk
        if 'payment_mollie_oauth_token' not in request.session:
            request.session['payment_mollie_oauth_token'] = get_random_string(
                32)
        return (
            "https://www.mollie.com/oauth2/authorize?client_id={}&redirect_uri={}"
            "&state={}&scope=payments.read+payments.write+refunds.read+refunds.write+profiles.read+organizations.read"
            "&response_type=code&approval_prompt=auto").format(
                self.settings.connect_client_id,
                urlquote(
                    build_global_uri('plugins:pretix_mollie:oauth.return')),
                request.session['payment_mollie_oauth_token'],
            )

    def settings_content_render(self, request):
        if self.settings.connect_client_id and not self.settings.api_key:
            # Use Mollie Connect
            if not self.settings.access_token:
                return (
                    "<p>{}</p>"
                    "<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
                ).format(
                    _('To accept payments via Mollie, you will need an account at Mollie. By clicking on the '
                      'following button, you can either create a new Mollie account connect pretix to an existing '
                      'one.'), self.get_connect_url(request),
                    _('Connect with Mollie'))
            else:
                return (
                    "<button formaction='{}' class='btn btn-danger'>{}</button>"
                ).format(
                    reverse('plugins:pretix_mollie:oauth.disconnect',
                            kwargs={
                                'organizer': self.event.organizer.slug,
                                'event': self.event.slug,
                            }), _('Disconnect from Mollie'))

    @property
    def test_mode_message(self):
        if self.settings.connect_client_id and not self.settings.api_key:
            is_testmode = True
        else:
            is_testmode = 'test_' in self.settings.secret_key
        if is_testmode:
            return _(
                'The Mollie plugin is operating in test mode. No money will actually be transferred.'
            )
        return None

    @property
    def settings_form_fields(self):
        if self.settings.connect_client_id and not self.settings.api_key:
            # Mollie Connect
            if self.settings.access_token:
                fields = [
                    ('connect_org_name',
                     forms.CharField(label=_('Mollie account'),
                                     disabled=True)),
                    ('connect_profile',
                     forms.ChoiceField(label=_('Website profile'),
                                       choices=self.settings.get(
                                           'connect_profiles', as_type=list)
                                       or [])),
                    ('endpoint',
                     forms.ChoiceField(
                         label=_('Endpoint'),
                         initial='live',
                         choices=(
                             ('live', pgettext('mollie', 'Live')),
                             ('test', pgettext('mollie', 'Testing')),
                         ),
                     )),
                ]
            else:
                return {}
        else:
            fields = [
                ('api_key',
                 forms.CharField(
                     label=_('Secret key'),
                     validators=(MollieKeyValidator(['live_', 'test_']), ),
                 )),
            ]
        d = OrderedDict(fields + [
            ('method_creditcard',
             forms.BooleanField(
                 label=_('Credit card'),
                 required=False,
             )),
            ('method_bancontact',
             forms.BooleanField(
                 label=_('Bancontact'),
                 required=False,
             )),
            ('method_banktransfer',
             forms.BooleanField(
                 label=_('Bank transfer'),
                 required=False,
             )),
            ('method_belfius',
             forms.BooleanField(
                 label=_('Belfius Pay Button'),
                 required=False,
             )),
            ('method_bitcoin',
             forms.BooleanField(
                 label=_('Bitcoin'),
                 required=False,
             )),
            ('method_eps', forms.BooleanField(
                label=_('EPS'),
                required=False,
            )),
            ('method_giropay',
             forms.BooleanField(
                 label=_('giropay'),
                 required=False,
             )),
            ('method_ideal',
             forms.BooleanField(
                 label=_('iDEAL'),
                 required=False,
             )),
            ('method_inghomepay',
             forms.BooleanField(
                 label=_('ING Home’Pay'),
                 required=False,
             )),
            ('method_kbc',
             forms.BooleanField(
                 label=_('KBC/CBC Payment Button'),
                 required=False,
             )),
            ('method_paysafecard',
             forms.BooleanField(
                 label=_('paysafecard'),
                 required=False,
             )),
            ('method_sofort',
             forms.BooleanField(
                 label=_('Sofort'),
                 required=False,
             )),
        ] + list(super().settings_form_fields.items()))
        d.move_to_end('_enabled', last=False)
        return d
Example #28
0
class BaseTicketOutput:
    """
    This is the base class for all ticket outputs.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('ticketoutput', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this output is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
        """
        This method should generate the download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.
        """
        raise NotImplementedError()

    def generate_order(self, order: Order) -> Tuple[str, str, str]:
        """
        This method is the same as order() but should not generate one file per order position
        but instead one file for the full order.

        This method is optional to implement. If you don't implement it, the default
        implementation will offer a zip file of the generate() results for the order positions.

        This method should generate a download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.

        If you override this method, make sure that positions that are addons (i.e. ``addon_to``
        is set) are only outputted if the event setting ``ticket_download_addons`` is active.
        """
        with tempfile.TemporaryDirectory() as d:
            with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
                for pos in order.positions.all():
                    if pos.addon_to_id and not self.event.settings.ticket_download_addons:
                        continue
                    fname, __, content = self.generate(pos)
                    zipf.writestr(
                        '{}-{}{}'.format(order.code, pos.positionid,
                                         os.path.splitext(fname)[1]), content)

            with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
                return '{}-{}.zip'.format(
                    order.code,
                    self.identifier), 'application/zip', zipf.read()

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this ticket output. This should be short but
        self-explanatory. Good examples include 'PDF tickets' and 'Passbook'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this ticket output.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``
        setting 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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('paper_size',
                         forms.CharField(
                             label=_('Paper size'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable output'),
                 required=False,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def download_button_text(self) -> str:
        """
        The text on the download button in the frontend.
        """
        return _('Download ticket')
Example #29
0
class BasePaymentProvider:
    """
    This is the base class for all payment providers.
    """

    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('payment', self.identifier, event)
        # Default values
        if self.settings.get('_fee_reverse_calc') is None:
            self.settings.set('_fee_reverse_calc', True)

    def __str__(self):
        return self.identifier

    @property
    def is_implicit(self) -> bool:
        """
        Returns whether or whether not this payment provider is an "implicit" payment provider that will
        *always* and unconditionally be used if is_allowed() returns True and does not require any input.
        This is  intended to be used by the FreePaymentProvider, which skips the payment choice page.
        By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
        """
        return False

    @property
    def is_meta(self) -> bool:
        """
        Returns whether or whether not this payment provider is a "meta" payment provider that only
        works as a settings holder for other payment providers and should never be used directly. This
        is a trick to implement payment gateways with multiple payment methods but unified payment settings.
        Take a look at the built-in stripe provider to see how this might be used.
        By default, this returns ``False``.
        """
        return False

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this payment provider is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    @property
    def test_mode_message(self) -> str:
        """
        If this property is set to a string, this will be displayed when this payment provider is selected
        while the event is in test mode. You should use it to explain to your user how your plugin behaves,
        e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.

        If you do not set this (or, return ``None``), pretix will show a default message warning the user
        that this plugin does not support test mode payments.
        """
        return None

    def calculate_fee(self, price: Decimal) -> Decimal:
        """
        Calculate the fee for this payment provider which will be added to
        final price before fees (but after taxes). It should include any taxes.
        The default implementation makes use of the setting ``_fee_abs`` for an
        absolute fee and ``_fee_percent`` for a percentage.

        :param price: The total value without the payment method fee, after taxes.
        """
        fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
        fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
        fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True)
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        if fee_reverse_calc:
            return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price).quantize(
                Decimal('1') / 10 ** places, ROUND_HALF_UP
            )
        else:
            return (price * fee_percent / 100 + fee_abs).quantize(
                Decimal('1') / 10 ** places, ROUND_HALF_UP
            )

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this payment provider. This should
        be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card via Stripe'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def public_name(self) -> str:
        """
        A human-readable name for this payment provider to be shown to the public.
        This should be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card', but 'Credit card via Stripe' might be to explicit. By default,
        this is the same as ``verbose_name``
        """
        return self.verbose_name

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this payment provider.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def abort_pending_allowed(self) -> bool:
        """
        Whether or not a user can abort a payment in pending start to switch to another
        payment method. This returns ``False`` by default which is no guarantee that
        aborting a pending payment can never happen, it just hides the frontend button
        to avoid users accidentally committing double payments.
        """
        return False

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``,
        ``_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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('bank_details',
                         forms.CharField(
                             widget=forms.Textarea,
                             label=_('Bank account details'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        d = OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable payment method'),
                 required=False,
             )),
            ('_availability_date',
             RelativeDateField(
                 label=_('Available until'),
                 help_text=_('Users will not be able to choose this payment provider after the given date.'),
                 required=False,
             )),
            ('_invoice_text',
             I18nFormField(
                 label=_('Text on invoices'),
                 help_text=_('Will be printed just below the payment figures and above the closing text on invoices. '
                             'This will only be used if the invoice is generated before the order is paid. If the '
                             'invoice is generated later, it will show a text stating that it has already been paid.'),
                 required=False,
                 widget=I18nTextarea,
                 widget_kwargs={'attrs': {'rows': '2'}}
             )),
            ('_total_min',
             forms.DecimalField(
                 label=_('Minimum order total'),
                 help_text=_('This payment will be available only if the order total is equal to or exceeds the given '
                             'value. The order total for this purpose may be computed without taking the fees imposed '
                             'by this payment method into account.'),
                 localize=True,
                 required=False,
                 decimal_places=places,
                 widget=DecimalTextInput(places=places)
             )),
            ('_total_max',
             forms.DecimalField(
                 label=_('Maximum order total'),
                 help_text=_('This payment will be available only if the order total is equal to or below the given '
                             'value. The order total for this purpose may be computed without taking the fees imposed '
                             'by this payment method into account.'),
                 localize=True,
                 required=False,
                 decimal_places=places,
                 widget=DecimalTextInput(places=places)
             )),
            ('_fee_abs',
             forms.DecimalField(
                 label=_('Additional fee'),
                 help_text=_('Absolute value'),
                 localize=True,
                 required=False,
                 decimal_places=places,
                 widget=DecimalTextInput(places=places)
             )),
            ('_fee_percent',
             forms.DecimalField(
                 label=_('Additional fee'),
                 help_text=_('Percentage of the order total. Note that this percentage will currently only '
                             'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
                             'fees, if there are any.'),
                 localize=True,
                 required=False,
             )),
            ('_fee_reverse_calc',
             forms.BooleanField(
                 label=_('Calculate the fee from the total value including the fee.'),
                 help_text=_('We recommend to enable this if you want your users to pay the payment fees of your '
                             'payment provider. <a href="{docs_url}" target="_blank" rel="noopener">Click here '
                             'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
                             'above!').format(docs_url='https://docs.pretix.eu/en/latest/user/payments/fees.html'),
                 required=False
             )),
            ('_restricted_countries',
             forms.MultipleChoiceField(
                 label=_('Restrict to countries'),
                 choices=Countries(),
                 help_text=_('Only allow choosing this payment provider for invoice addresses in the selected '
                             'countries. If you don\'t select any country, all countries are allowed. This is only '
                             'enabled if the invoice address is required.'),
                 widget=forms.CheckboxSelectMultiple(
                     attrs={'class': 'scrolling-multiple-choice'}
                 ),
                 required=False,
                 disabled=not self.event.settings.invoice_address_required
             )),
        ])
        d['_restricted_countries']._as_type = list
        return d

    def settings_form_clean(self, cleaned_data):
        """
        Overriding this method allows you to inject custom validation into the settings form.

        :param cleaned_data: Form data as per previous validations.
        :return: Please return the modified cleaned_data
        """
        return cleaned_data

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        return ""

    def render_invoice_text(self, order: Order) -> str:
        """
        This is called when an invoice for an order with this payment provider is generated.
        The default implementation returns the content of the _invoice_text configuration
        variable (an I18nString), or an empty string if unconfigured.
        """
        if order.status == Order.STATUS_PAID:
            return pgettext_lazy('invoice', 'The payment for this invoice has already been received.')
        return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')

    @property
    def payment_form_fields(self) -> dict:
        """
        This is used by the default implementation of :py:meth:`payment_form`.
        It should return an object similar to :py:attr:`settings_form_fields`.

        The default implementation returns an empty dictionary.
        """
        return {}

    def payment_form(self, request: HttpRequest) -> Form:
        """
        This is called by the default implementation of :py:meth:`payment_form_render`
        to obtain the form that is displayed to the user during the checkout
        process. The default implementation constructs the form using
        :py:attr:`payment_form_fields` and sets appropriate prefixes for the form
        and all fields and fills the form with data form the user's session.

        If you overwrite this, we strongly suggest that you inherit from
        ``PaymentProviderForm`` (from this module) that handles some nasty issues about
        required fields for you.
        """
        form = PaymentProviderForm(
            data=(request.POST if request.method == 'POST' and request.POST.get("payment") == self.identifier else None),
            prefix='payment_%s' % self.identifier,
            initial={
                k.replace('payment_%s_' % self.identifier, ''): v
                for k, v in request.session.items()
                if k.startswith('payment_%s_' % self.identifier)
            }
        )
        form.fields = self.payment_form_fields

        for k, v in form.fields.items():
            v._required = v.required
            v.required = False
            v.widget.is_required = False

        return form

    def _is_still_available(self, now_dt=None, cart_id=None, order=None):
        now_dt = now_dt or now()
        tz = pytz.timezone(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:
                availability_date = min([
                    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)
                    )
                ])
            elif self.event.has_subevents and order:
                availability_date = min([
                    availability_date.datetime(se).date()
                    for se in self.event.subevents.filter(
                        id__in=order.positions.values_list('subevent', flat=True)
                    )
                ])
            elif self.event.has_subevents:
                logger.error('Payment provider is not subevent-ready.')
                return False
            else:
                availability_date = availability_date.datetime(self.event).date()

            return availability_date >= now_dt.astimezone(tz).date()

        return True

    def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
        """
        You can use this method to disable this payment provider for certain groups
        of users, products or other criteria. If this method returns ``False``, the
        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``
        setting.

        :param total: The total value without the payment method fee, after taxes.

        .. versionchanged:: 1.17.0

           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))
        pricing = True

        if (self.settings._total_max is not None or self.settings._total_min is not None) and total is None:
            raise ImproperlyConfigured('This payment provider does not support maximum or minimum amounts.')

        if self.settings._total_max is not None:
            pricing = pricing and total <= Decimal(self.settings._total_max)

        if self.settings._total_min is not None:
            pricing = pricing and total >= Decimal(self.settings._total_min)

        def get_invoice_address():
            if not hasattr(request, '_checkout_flow_invoice_address'):
                cs = cart_session(request)
                iapk = cs.get('invoice_address')
                if not iapk:
                    request._checkout_flow_invoice_address = InvoiceAddress()
                else:
                    try:
                        request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
                    except InvoiceAddress.DoesNotExist:
                        request._checkout_flow_invoice_address = InvoiceAddress()
            return request._checkout_flow_invoice_address

        if self.event.settings.invoice_address_required:
            restricted_countries = self.settings.get('_restricted_countries', as_type=list)
            if restricted_countries:
                ia = get_invoice_address()
                if str(ia.country) not in restricted_countries:
                    return False

        return timing and pricing

    def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
        """
        When the user selects this provider as their preferred payment method,
        they will be shown the HTML you return from this method.

        The default implementation will call :py:meth:`payment_form`
        and render the returned form. If your payment method doesn't require
        the user to fill out form fields, you should just return a paragraph
        of explanatory text.
        """
        form = self.payment_form(request)
        template = get_template('pretixpresale/event/checkout_payment_form_default.html')
        ctx = {'request': request, 'form': form}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        """
        If the user has successfully filled in their payment data, they will be redirected
        to a confirmation page which lists all details of their order for a final review.
        This method should return the HTML which should be displayed inside the
        'Payment' box on this page.

        In most cases, this should include a short summary of the user's input and
        a short explanation on how the payment process will continue.
        """
        raise NotImplementedError()  # NOQA

    def payment_pending_render(self, request: HttpRequest, payment: OrderPayment) -> str:
        """
        Render customer-facing instructions on how to proceed with a pending payment

        :return: HTML
        """
        return ""

    def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str]:
        """
        Will be called after the user selects this provider as their payment method.
        If you provided a form to the user to enter payment data, this method should
        at least store the user's input into their session.

        This method should return ``False`` if the user's input was invalid, ``True``
        if the input was valid and the frontend should continue with default behavior
        or a string containing a URL if the user should be redirected somewhere else.

        On errors, you should use Django's message framework to display an error message
        to the user (or the normal form validation error messages).

        The default implementation stores the input into the form returned by
        :py:meth:`payment_form` in the user's session.

        If your payment method requires you to redirect the user to an external provider,
        this might be the place to do so.

        .. IMPORTANT:: If this is called, the user has not yet confirmed their order.
           You may NOT do anything which actually moves money.

        :param cart: This dictionary contains at least the following keys:

            positions:
               A list of ``CartPosition`` objects that are annotated with the special
               attributes ``count`` and ``total`` because multiple objects of the
               same content are grouped into one.

            raw:
                The raw list of ``CartPosition`` objects in the users cart

            total:
                The overall total *including* the fee for the payment method.

            payment_fee:
                The fee for the payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_is_valid_session(self, request: HttpRequest) -> bool:
        """
        This is called at the time the user tries to place the order. It should return
        ``True`` if the user's session is valid and all data your payment provider requires
        in future steps is present.
        """
        raise NotImplementedError()  # NOQA

    def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
        """
        After the user has confirmed their purchase, this method will be called to complete
        the payment process. This is the place to actually move the money if applicable.
        You will be passed an :py:class:`pretix.base.models.OrderPayment` object that contains
        the amount of money that should be paid.

        If you need any special behavior, you can return a string
        containing the URL the user will be redirected to. If you are done with your process
        you should return the user to the order's detail page.

        If the payment is completed, you should call ``payment.confirm()``. Please note that ``this`` might
        raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
        some of the items are sold out. You should use the exception message to display a meaningful error
        to the user.

        The default implementation just returns ``None`` and therefore leaves the
        order unpaid. The user will be redirected to the order's detail page by default.

        On errors, you should raise a ``PaymentException``.

        :param order: The order object
        :param payment: An ``OrderPayment`` instance
        """
        return None

    def order_pending_mail_render(self, order: Order) -> str:
        """
        After the user has submitted their order, they will receive a confirmation
        email. You can return a string from this method if you want to add additional
        information to this email.

        :param order: The order object
        """
        return ""

    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 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.

        :param order: The order object
        """
        ps = order.pending_sum
        if self.settings._total_max is not None and ps > Decimal(self.settings._total_max):
            return False

        if self.settings._total_min is not None and ps < Decimal(self.settings._total_min):
            return False

        restricted_countries = self.settings.get('_restricted_countries', as_type=list)
        if restricted_countries:
            try:
                ia = order.invoice_address
            except InvoiceAddress.DoesNotExist:
                return True
            else:
                if str(ia.country) not in restricted_countries:
                    return False

        return self._is_still_available(order=order)

    def payment_prepare(self, request: HttpRequest, payment: OrderPayment) -> Union[bool, str]:
        """
        Will be called if the user retries to pay an unpaid order (after the user filled in
        e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
        method.

        It should return and report errors the same way as :py:meth:`checkout_prepare`, but
        receives an ``Order`` object instead of a cart object.

        Note: The ``Order`` object given to this method might be different from the version
        stored in the database as it's total will already contain the payment fee for the
        new payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_control_render(self, request: HttpRequest, payment: OrderPayment) -> str:
        """
        Will be called if the *event administrator* views the details of a payment.

        It should return HTML code containing information regarding the current payment
        status and, if applicable, next steps.

        The default implementation returns the verbose name of the payment provider.

        :param order: The order object
        """
        return ''

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        """
        Will be called to check if the provider supports automatic refunding for this
        payment.
        """
        return False

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        """
        Will be called to check if the provider supports automatic partial refunding for this
        payment.
        """
        return False

    def execute_refund(self, refund: OrderRefund):
        """
        Will be called to execute an refund. Note that refunds have an amount property and can be partial.

        This should transfer the money back (if possible).
        On success, you should call ``refund.done()``.
        On failure, you should raise a PaymentException.
        """
        raise PaymentException(_('Automatic refunds are not supported by this payment provider.'))

    def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
        """
        When personal data is removed from an event, this method is called to scrub payment-related data
        from a payment or refund. By default, it removes all info from the ``info`` attribute. You can override
        this behavior if you want to retain attributes that are not personal data on their own, i.e. a
        reference to a transaction in an external system. You can also override this to scrub more data, e.g.
        data from external sources that is saved in LogEntry objects or other places.

        :param order: An order
        """
        obj.info = '{}'
        obj.save(update_fields=['info'])
Example #30
0
class Tinkoff(BasePaymentProvider):
    identifier = 'tinkoff'
    verbose_name = _('Credit card')
    abort_pending_allowed = False  
    refunds_allowed = True

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'tinkoff', event)

    @property
    def is_enabled(self) -> bool:
        return self.settings.get('_enabled', as_type=bool)

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_tinkoff/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'form': self.payment_form(request)
        }
        return template.render(ctx)

    def checkout_prepare(self, request, total):
        return True

    def payment_is_valid_session(self, request):
        return True
    
    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretix_tinkoff/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'form': self.payment_form(request)
        }
        return template.render(ctx)

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if payment.state == OrderPayment.PAYMENT_STATE_FAILED:
                data = self.status(payment_info)
                if data.get('Status') == 'CONFIRMED':
                    payment.order.log_action('pretix_tinkoff.event.CONFIRMED')
                    payment.confirm()
                logger.info("Failed Payment check status: {}".format(data))
        else:
            payment_info = None

        template = get_template('pretix_tinkoff/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'provider': self,
        }
        return template.render(ctx)

    def payment_can_retry(self, payment):
        return self._is_still_available(order=payment.order)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return self.refunds_allowed

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return False

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        pay_data = {
            "NotificationURL": build_absolute_uri(self.event, 'plugins:pretix_tinkoff:webhook', kwargs={
                'payment': payment.pk
            }),
            "SuccessURL": build_absolute_uri(self.event, 'plugins:pretix_tinkoff:return', kwargs={
                'order': payment.order.code,
                'payment': payment.pk,
                'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
                'action': 'success'
            }),
            "FailURL": build_absolute_uri(self.event, 'plugins:pretix_tinkoff:return', kwargs={
                'order': payment.order.code,
                'payment': payment.pk,
                'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
                'action': 'fail'
            }),
            'Amount': int(payment.amount * 100),
            'OrderId': "{}-{}".format(self.event.slug.upper(), payment.order.code),
            'Description': "Order {}-{}".format(self.event.slug.upper(), payment.order.code),
            'DATA': {
                'organizer': self.event.organizer.slug,
                'event': self.event.slug,
                'order': payment.order.code,
                'payment': payment.local_id,
                'order-full-code': payment.order.full_code
            },
            'PayType': "O"
        }
        try:
            req = self._init(pay_data)
        except HTTPError:
            logger.exception('Tinkoff error: %s' % req)
        
        if req['Success'] == False:
            logger.exception('Tinkoff error: %s' % req)

            payment.info_data = {
                'error': True,
                'detail': req
            }
            
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save()
            payment.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider,
                'data': payment.info_data
            })
            raise PaymentException(_('We had trouble communicating with Tinkoff. Please try again and get in touch '
                                     'with us if this problem persists. Detail: {}'.format(req['Details'])))
        ReferencedTinkoffTransaction.objects.get_or_create(order=payment.order, payment=payment, reference=req['PaymentId'])
        LogTransaction(paymentid=req['PaymentId'], order=payment.order, payment=payment, method='get', meta_info=json.dumps(req))
        payment.info = json.dumps(req)
        payment.state = OrderPayment.PAYMENT_STATE_CREATED
        logger.info('pay_request: {}'.format(pay_data))
        logger.info('pay_response: {}'.format(payment.info))
        payment.save(update_fields=['info'])
        return self.redirect(request, req['PaymentURL'])

    def redirect(self, request, url):
        if request.session.get('iframe_session', False):
            signer = signing.Signer(salt='safe-redirect')
            return (
                build_absolute_uri(request.event, 'plugins:pretix_tinkoff:redirect') + '?url=' +
                urllib.parse.quote(signer.sign(url))
            )
        else:
            return str(url)

    def execute_refund(self, refund: OrderRefund):
        payment_data = refund.payment.info_data
        data = {
            'PaymentId': payment_data['PaymentId'],
            'Amount':  int(refund.amount * 100)
        }

        logger.info('refund_request:{}'.format(data))
        req = self.cancel(data)
        refund.info_data = json.dumps(req)
        refund.save(update_fields=['info'])
        logger.info('refund_response:{}'.format(req))
        if req['Success'] == False:
            raise PaymentException(_('Refunding the amount via Tinkoff failed: {}'.format(req['Details'])))
        else:
            if req['Status'] == 'REFUNDED':
                refund.done()

    @property
    def secret_key(self):
        return self.settings.terminal_password

    @property
    def terminal_key(self):
        return self.settings.terminal_key

    def _init(self, payment):
        response = self._request('INIT', requests.post, payment).json()
        return response


    def _request(self, url, method, data):
        url = get_config()['URLS'][url]

        data.update({
            'TerminalKey': self.settings.terminal_key,
            'Token': self._token(data),
        })
        pay_request = method(url, data=json.dumps(data, cls=Encoder), headers={'Content-Type': 'application/json'})
        pay_request.raise_for_status()
        if pay_request.status_code != 200:
            raise logger.exception('Tinkoff bad status code')
        return pay_request
    
    def _token(self, data):
        base = [
            ['Password', self.settings.terminal_password],
        ]

        if 'TerminalKey' not in data:
            base.append(['TerminalKey', self.settings.terminal_key])

        for name_token, value_token in data.items():
            if name_token == 'Token':
                continue
            if isinstance(value_token, bool):
                base.append([name_token, str(value_token).lower()])
            elif not isinstance(value_token, list) or not isinstance(value_token, dict):
                base.append([name_token, value_token])

        base.sort(key=lambda i: i[0])
        values = ''.join(map(lambda i: str(i[1]), base))

        m = hashlib.sha256()
        m.update(values.encode())
        return m.hexdigest()
    
    def token_correct(self, token, data):
        return token == self._token(data)

    def resend(self, payment):
        response = self._request('RESEND', requests.post, {}).json()
        return response

    def status(self, payment):
        response = self._request('GET_STATE', requests.post, {'PaymentId': payment['PaymentId']}).json()
        return response

    def cancel(self, payment):
        response = self._request('CANCEL', requests.post, payment).json()
        return response
Example #31
0
class BasePaymentProvider:
    """
    This is the base class for all payment providers.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('payment', self.identifier, event)
        # Default values
        if self.settings.get('_fee_reverse_calc') is None:
            self.settings.set('_fee_reverse_calc', True)

    def __str__(self):
        return self.identifier

    @property
    def is_implicit(self) -> bool:
        """
        Returns whether or whether not this payment provider is an "implicit" payment provider that will
        *always* and unconditionally be used if is_allowed() returns True and does not require any input.
        This is  intended to be used by the FreePaymentProvider, which skips the payment choice page.
        By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
        """
        return False

    @property
    def is_meta(self) -> bool:
        """
        Returns whether or whether not this payment provider is a "meta" payment provider that only
        works as a settings holder for other payment providers and should never be used directly. This
        is a trick to implement payment gateways with multiple payment methods but unified payment settings.
        Take a look at the built-in stripe provider to see how this might be used.
        By default, this returns ``False``.
        """
        return False

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this payment provider is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    @property
    def test_mode_message(self) -> str:
        """
        If this property is set to a string, this will be displayed when this payment provider is selected
        while the event is in test mode. You should use it to explain to your user how your plugin behaves,
        e.g. if it falls back to a test mode automatically as well or if actual payments will be performed.

        If you do not set this (or, return ``None``), pretix will show a default message warning the user
        that this plugin does not support test mode payments.
        """
        return None

    def calculate_fee(self, price: Decimal) -> Decimal:
        """
        Calculate the fee for this payment provider which will be added to
        final price before fees (but after taxes). It should include any taxes.
        The default implementation makes use of the setting ``_fee_abs`` for an
        absolute fee and ``_fee_percent`` for a percentage.

        :param price: The total value without the payment method fee, after taxes.
        """
        fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
        fee_percent = self.settings.get('_fee_percent',
                                        as_type=Decimal,
                                        default=0)
        fee_reverse_calc = self.settings.get('_fee_reverse_calc',
                                             as_type=bool,
                                             default=True)
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        if fee_reverse_calc:
            return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) -
                    price).quantize(Decimal('1') / 10**places, ROUND_HALF_UP)
        else:
            return (price * fee_percent / 100 + fee_abs).quantize(
                Decimal('1') / 10**places, ROUND_HALF_UP)

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this payment provider. This should
        be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card via Stripe'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def public_name(self) -> str:
        """
        A human-readable name for this payment provider to be shown to the public.
        This should be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card', but 'Credit card via Stripe' might be to explicit. By default,
        this is the same as ``verbose_name``
        """
        return self.verbose_name

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this payment provider.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def abort_pending_allowed(self) -> bool:
        """
        Whether or not a user can abort a payment in pending start to switch to another
        payment method. This returns ``False`` by default which is no guarantee that
        aborting a pending payment can never happen, it just hides the frontend button
        to avoid users accidentally committing double payments.
        """
        return False

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``,
        ``_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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('bank_details',
                         forms.CharField(
                             widget=forms.Textarea,
                             label=_('Bank account details'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        d = OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable payment method'),
                 required=False,
             )),
            ('_availability_date',
             RelativeDateField(
                 label=_('Available until'),
                 help_text=
                 _('Users will not be able to choose this payment provider after the given date.'
                   ),
                 required=False,
             )),
            ('_invoice_text',
             I18nFormField(
                 label=_('Text on invoices'),
                 help_text=
                 _('Will be printed just below the payment figures and above the closing text on invoices. '
                   'This will only be used if the invoice is generated before the order is paid. If the '
                   'invoice is generated later, it will show a text stating that it has already been paid.'
                   ),
                 required=False,
                 widget=I18nTextarea,
                 widget_kwargs={'attrs': {
                     'rows': '2'
                 }})),
            ('_total_min',
             forms.DecimalField(
                 label=_('Minimum order total'),
                 help_text=
                 _('This payment will be available only if the order total is equal to or exceeds the given '
                   'value. The order total for this purpose may be computed without taking the fees imposed '
                   'by this payment method into account.'),
                 localize=True,
                 required=False,
                 decimal_places=places,
                 widget=DecimalTextInput(places=places))),
            ('_total_max',
             forms.DecimalField(
                 label=_('Maximum order total'),
                 help_text=
                 _('This payment will be available only if the order total is equal to or below the given '
                   'value. The order total for this purpose may be computed without taking the fees imposed '
                   'by this payment method into account.'),
                 localize=True,
                 required=False,
                 decimal_places=places,
                 widget=DecimalTextInput(places=places))),
            ('_fee_abs',
             forms.DecimalField(label=_('Additional fee'),
                                help_text=_('Absolute value'),
                                localize=True,
                                required=False,
                                decimal_places=places,
                                widget=DecimalTextInput(places=places))),
            ('_fee_percent',
             forms.DecimalField(
                 label=_('Additional fee'),
                 help_text=_('Percentage of the order total.'),
                 localize=True,
                 required=False,
             )),
            ('_fee_reverse_calc',
             forms.BooleanField(
                 label=_(
                     'Calculate the fee from the total value including the fee.'
                 ),
                 help_text=
                 _('We recommend to enable this if you want your users to pay the payment fees of your '
                   'payment provider. <a href="{docs_url}" target="_blank" rel="noopener">Click here '
                   'for detailed information on what this does.</a> Don\'t forget to set the correct fees '
                   'above!').
                 format(
                     docs_url=
                     'https://docs.pretix.eu/en/latest/user/payments/fees.html'
                 ),
                 required=False)),
            ('_restricted_countries',
             forms.MultipleChoiceField(
                 label=_('Restrict to countries'),
                 choices=Countries(),
                 help_text=
                 _('Only allow choosing this payment provider for invoice addresses in the selected '
                   'countries. If you don\'t select any country, all countries are allowed. This is only '
                   'enabled if the invoice address is required.'),
                 widget=forms.CheckboxSelectMultiple(
                     attrs={'class': 'scrolling-multiple-choice'}),
                 required=False,
                 disabled=not self.event.settings.invoice_address_required)),
        ])
        d['_restricted_countries']._as_type = list
        return d

    def settings_form_clean(self, cleaned_data):
        """
        Overriding this method allows you to inject custom validation into the settings form.

        :param cleaned_data: Form data as per previous validations.
        :return: Please return the modified cleaned_data
        """
        return cleaned_data

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        return ""

    def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
        """
        This is called when an invoice for an order with this payment provider is generated.
        The default implementation returns the content of the _invoice_text configuration
        variable (an I18nString), or an empty string if unconfigured. For paid orders, the
        default implementation always renders a string stating that the invoice is already paid.
        """
        if order.status == Order.STATUS_PAID:
            return pgettext_lazy(
                'invoice',
                'The payment for this invoice has already been received.')
        return self.settings.get('_invoice_text',
                                 as_type=LazyI18nString,
                                 default='')

    @property
    def payment_form_fields(self) -> dict:
        """
        This is used by the default implementation of :py:meth:`payment_form`.
        It should return an object similar to :py:attr:`settings_form_fields`.

        The default implementation returns an empty dictionary.
        """
        return {}

    def payment_form(self, request: HttpRequest) -> Form:
        """
        This is called by the default implementation of :py:meth:`payment_form_render`
        to obtain the form that is displayed to the user during the checkout
        process. The default implementation constructs the form using
        :py:attr:`payment_form_fields` and sets appropriate prefixes for the form
        and all fields and fills the form with data form the user's session.

        If you overwrite this, we strongly suggest that you inherit from
        ``PaymentProviderForm`` (from this module) that handles some nasty issues about
        required fields for you.
        """
        form = PaymentProviderForm(
            data=(request.POST if request.method == 'POST'
                  and request.POST.get("payment") == self.identifier else
                  None),
            prefix='payment_%s' % self.identifier,
            initial={
                k.replace('payment_%s_' % self.identifier, ''): v
                for k, v in request.session.items()
                if k.startswith('payment_%s_' % self.identifier)
            })
        form.fields = self.payment_form_fields

        for k, v in form.fields.items():
            v._required = v.required
            v.required = False
            v.widget.is_required = False

        return form

    def _is_still_available(self, now_dt=None, cart_id=None, order=None):
        now_dt = now_dt or now()
        tz = pytz.timezone(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:
                availability_date = min([
                    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))
                ])
            elif self.event.has_subevents and order:
                availability_date = min([
                    availability_date.datetime(se).date()
                    for se in self.event.subevents.filter(
                        id__in=order.positions.values_list('subevent',
                                                           flat=True))
                ])
            elif self.event.has_subevents:
                logger.error('Payment provider is not subevent-ready.')
                return False
            else:
                availability_date = availability_date.datetime(
                    self.event).date()

            return availability_date >= now_dt.astimezone(tz).date()

        return True

    def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
        """
        You can use this method to disable this payment provider for certain groups
        of users, products or other criteria. If this method returns ``False``, the
        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``
        setting.

        :param total: The total value without the payment method fee, after taxes.

        .. versionchanged:: 1.17.0

           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))
        pricing = True

        if (self.settings._total_max is not None
                or self.settings._total_min is not None) and total is None:
            raise ImproperlyConfigured(
                'This payment provider does not support maximum or minimum amounts.'
            )

        if self.settings._total_max is not None:
            pricing = pricing and total <= Decimal(self.settings._total_max)

        if self.settings._total_min is not None:
            pricing = pricing and total >= Decimal(self.settings._total_min)

        def get_invoice_address():
            if not hasattr(request, '_checkout_flow_invoice_address'):
                cs = cart_session(request)
                iapk = cs.get('invoice_address')
                if not iapk:
                    request._checkout_flow_invoice_address = InvoiceAddress()
                else:
                    try:
                        request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
                            pk=iapk, order__isnull=True)
                    except InvoiceAddress.DoesNotExist:
                        request._checkout_flow_invoice_address = InvoiceAddress(
                        )
            return request._checkout_flow_invoice_address

        if self.event.settings.invoice_address_required:
            restricted_countries = self.settings.get('_restricted_countries',
                                                     as_type=list)
            if restricted_countries:
                ia = get_invoice_address()
                if str(ia.country) not in restricted_countries:
                    return False

        return timing and pricing

    def payment_form_render(self, request: HttpRequest, total: Decimal) -> str:
        """
        When the user selects this provider as their preferred payment method,
        they will be shown the HTML you return from this method.

        The default implementation will call :py:meth:`payment_form`
        and render the returned form. If your payment method doesn't require
        the user to fill out form fields, you should just return a paragraph
        of explanatory text.
        """
        form = self.payment_form(request)
        template = get_template(
            'pretixpresale/event/checkout_payment_form_default.html')
        ctx = {'request': request, 'form': form}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        """
        If the user has successfully filled in their payment data, they will be redirected
        to a confirmation page which lists all details of their order for a final review.
        This method should return the HTML which should be displayed inside the
        'Payment' box on this page.

        In most cases, this should include a short summary of the user's input and
        a short explanation on how the payment process will continue.
        """
        raise NotImplementedError()  # NOQA

    def payment_pending_render(self, request: HttpRequest,
                               payment: OrderPayment) -> str:
        """
        Render customer-facing instructions on how to proceed with a pending payment

        :return: HTML
        """
        return ""

    def checkout_prepare(self, request: HttpRequest,
                         cart: Dict[str, Any]) -> Union[bool, str]:
        """
        Will be called after the user selects this provider as their payment method.
        If you provided a form to the user to enter payment data, this method should
        at least store the user's input into their session.

        This method should return ``False`` if the user's input was invalid, ``True``
        if the input was valid and the frontend should continue with default behavior
        or a string containing a URL if the user should be redirected somewhere else.

        On errors, you should use Django's message framework to display an error message
        to the user (or the normal form validation error messages).

        The default implementation stores the input into the form returned by
        :py:meth:`payment_form` in the user's session.

        If your payment method requires you to redirect the user to an external provider,
        this might be the place to do so.

        .. IMPORTANT:: If this is called, the user has not yet confirmed their order.
           You may NOT do anything which actually moves money.

        :param cart: This dictionary contains at least the following keys:

            positions:
               A list of ``CartPosition`` objects that are annotated with the special
               attributes ``count`` and ``total`` because multiple objects of the
               same content are grouped into one.

            raw:
                The raw list of ``CartPosition`` objects in the users cart

            total:
                The overall total *including* the fee for the payment method.

            payment_fee:
                The fee for the payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_is_valid_session(self, request: HttpRequest) -> bool:
        """
        This is called at the time the user tries to place the order. It should return
        ``True`` if the user's session is valid and all data your payment provider requires
        in future steps is present.
        """
        raise NotImplementedError()  # NOQA

    def execute_payment(self, request: HttpRequest,
                        payment: OrderPayment) -> str:
        """
        After the user has confirmed their purchase, this method will be called to complete
        the payment process. This is the place to actually move the money if applicable.
        You will be passed an :py:class:`pretix.base.models.OrderPayment` object that contains
        the amount of money that should be paid.

        If you need any special behavior, you can return a string
        containing the URL the user will be redirected to. If you are done with your process
        you should return the user to the order's detail page.

        If the payment is completed, you should call ``payment.confirm()``. Please note that this might
        raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this order is over and
        some of the items are sold out. You should use the exception message to display a meaningful error
        to the user.

        The default implementation just returns ``None`` and therefore leaves the
        order unpaid. The user will be redirected to the order's detail page by default.

        On errors, you should raise a ``PaymentException``.

        :param order: The order object
        :param payment: An ``OrderPayment`` instance
        """
        return None

    def order_pending_mail_render(self, order: Order,
                                  payment: OrderPayment) -> str:
        """
        After the user has submitted their order, they will receive a confirmation
        email. You can return a string from this method if you want to add additional
        information to this email.

        :param order: The order object
        :param payment: The payment object
        """
        return ""

    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 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.

        :param order: The order object
        """
        ps = order.pending_sum
        if self.settings._total_max is not None and ps > Decimal(
                self.settings._total_max):
            return False

        if self.settings._total_min is not None and ps < Decimal(
                self.settings._total_min):
            return False

        restricted_countries = self.settings.get('_restricted_countries',
                                                 as_type=list)
        if restricted_countries:
            try:
                ia = order.invoice_address
            except InvoiceAddress.DoesNotExist:
                return True
            else:
                if str(ia.country) not in restricted_countries:
                    return False

        return self._is_still_available(order=order)

    def payment_prepare(self, request: HttpRequest,
                        payment: OrderPayment) -> Union[bool, str]:
        """
        Will be called if the user retries to pay an unpaid order (after the user filled in
        e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
        method.

        It should return and report errors the same way as :py:meth:`checkout_prepare`, but
        receives an ``Order`` object instead of a cart object.

        Note: The ``Order`` object given to this method might be different from the version
        stored in the database as it's total will already contain the payment fee for the
        new payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_control_render(self, request: HttpRequest,
                               payment: OrderPayment) -> str:
        """
        Will be called if the *event administrator* views the details of a payment.

        It should return HTML code containing information regarding the current payment
        status and, if applicable, next steps.

        The default implementation returns the verbose name of the payment provider.

        :param order: The order object
        """
        return ''

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        """
        Will be called to check if the provider supports automatic refunding for this
        payment.
        """
        return False

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        """
        Will be called to check if the provider supports automatic partial refunding for this
        payment.
        """
        return False

    def execute_refund(self, refund: OrderRefund):
        """
        Will be called to execute an refund. Note that refunds have an amount property and can be partial.

        This should transfer the money back (if possible).
        On success, you should call ``refund.done()``.
        On failure, you should raise a PaymentException.
        """
        raise PaymentException(
            _('Automatic refunds are not supported by this payment provider.'))

    def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
        """
        When personal data is removed from an event, this method is called to scrub payment-related data
        from a payment or refund. By default, it removes all info from the ``info`` attribute. You can override
        this behavior if you want to retain attributes that are not personal data on their own, i.e. a
        reference to a transaction in an external system. You can also override this to scrub more data, e.g.
        data from external sources that is saved in LogEntry objects or other places.

        :param order: An order
        """
        obj.info = '{}'
        obj.save(update_fields=['info'])

    def api_payment_details(self, payment: OrderPayment):
        """
        Will be called to populate the ``details`` parameter of the payment in the REST API.

        :param payment: The payment in question.
        :return: A serializable dictionary
        """
        return {}
Example #32
0
class WirecardMethod(BasePaymentProvider):
    method = ''
    wc_payment_type = 'SELECT'
    statement_length = 32
    order_ref_length = 32
    abort_pending_allowed = True

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'wirecard', event)

    @property
    def identifier(self):
        return 'wirecard_{}'.format(self.method)

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_form_render(self, request) -> str:
        template = get_template('pretix_wirecard/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        template = get_template(
            'pretix_wirecard/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_prepare(self, request, total):
        return True

    def payment_is_valid_session(self, request):
        return True

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        request.session['wirecard_nonce'] = get_random_string(length=12)
        request.session['wirecard_order_secret'] = payment.order.secret
        request.session['wirecard_payment'] = payment.pk
        return eventreverse(
            self.event,
            'plugins:pretix_wirecard:redirect',
            kwargs={
                'order':
                payment.order.code,
                'payment':
                payment.pk,
                'hash':
                hashlib.sha1(
                    payment.order.secret.lower().encode()).hexdigest(),
            })

    def sign_parameters(self, params: dict, order: list = None) -> dict:
        keys = order or (list(params.keys()) +
                         ['requestFingerprintOrder', 'secret'])
        params['requestFingerprintOrder'] = ','.join(keys)
        payload = ''.join(
            self.settings.get('secret') if k == 'secret' else params[k]
            for k in keys)
        params['requestFingerprint'] = hmac.new(
            self.settings.get('secret').encode(), payload.encode(),
            hashlib.sha512).hexdigest().upper()
        return params

    def params_for_payment(self, payment, request):
        if not request.session.get('wirecard_nonce'):
            request.session['wirecard_nonce'] = get_random_string(length=12)
            request.session['wirecard_order_secret'] = payment.order.secret
            request.session['wirecard_payment'] = payment.pk
        hash = hashlib.sha1(payment.order.secret.lower().encode()).hexdigest()
        # TODO: imageURL, cssURL?
        return {
            'customerId':
            self.settings.get('customer_id'),
            'shopId':
            self.settings.get('shop_id', ''),
            'language':
            payment.order.locale[:2],
            'paymentType':
            self.wc_payment_type,
            'amount':
            str(payment.amount),
            'currency':
            self.event.currency,
            'orderDescription':
            _('Order {event}-{code}').format(event=self.event.slug.upper(),
                                             code=payment.order.code),
            'successUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': payment.order.code,
                                   'payment': payment.pk,
                                   'hash': hash,
                               }),
            'cancelUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': payment.order.code,
                                   'payment': payment.pk,
                                   'hash': hash,
                               }),
            'failureUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:return',
                               kwargs={
                                   'order': payment.order.code,
                                   'payment': payment.pk,
                                   'hash': hash,
                               }),
            'confirmUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:confirm',
                               kwargs={
                                   'order': payment.order.code,
                                   'payment': payment.pk,
                                   'hash': hash,
                               }).replace(':8000', ''),  # TODO: Remove
            'pendingUrl':
            build_absolute_uri(self.event,
                               'plugins:pretix_wirecard:confirm',
                               kwargs={
                                   'order': payment.order.code,
                                   'payment': payment.pk,
                                   'hash': hash,
                               }),
            'duplicateRequestCheck':
            'yes',
            'serviceUrl':
            self.event.settings.imprint_url,
            'customerStatement':
            str(_('ORDER {order} EVENT {event} BY {organizer}')).format(
                event=self.event.slug.upper(),
                order=payment.order.code,
                organizer=self.event.organizer.name)[:self.statement_length -
                                                     1],
            'orderReference':
            '{code}{id}'.format(code=payment.order.code,
                                id=request.session.get('wirecard_nonce'))
            [:self.order_ref_length - 1],
            'displayText':
            _('Order {} for event {} by {}').format(payment.order.code,
                                                    self.event.name,
                                                    self.event.organizer.name),
            'pretix_orderCode':
            payment.order.code,
            'pretix_eventSlug':
            self.event.slug,
            'pretix_organizerSlug':
            self.event.organizer.slug,
            'pretix_nonce':
            request.session.get('wirecard_nonce'),
        }

    def payment_pending_render(self, request: HttpRequest,
                               payment: OrderPayment):
        retry = True
        try:
            if payment.info_data['paymentState'] == 'PENDING':
                retry = False
        except KeyError:
            pass
        template = get_template('pretix_wirecard/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'retry': retry,
            'order': payment.order
        }
        return template.render(ctx)

    def payment_control_render(self, request: HttpRequest,
                               payment: OrderPayment):
        template = get_template('pretix_wirecard/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment.info_data,
            'order': payment.order,
            'provname': self.verbose_name
        }
        return template.render(ctx)

    def order_can_retry(self, order):
        return True

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return bool(self.settings.get('toolkit_password'))

    def payment_refund_supported(self, payment: OrderPayment):
        return bool(self.settings.get('toolkit_password'))

    def _refund(self, order_number, amount, currency, language):
        params = {
            'customerId': self.settings.get('customer_id'),
            'shopId': self.settings.get('shop_id', ''),
            'toolkitPassword': self.settings.get('toolkit_password'),
            'command': 'refund',
            'language': language,
            'orderNumber': order_number,
            'amount': str(amount),
            'currency': currency
        }
        r = requests.post(
            'https://checkout.wirecard.com/page/toolkit.php',
            data=self.sign_parameters(params, [
                'customerId', 'shopId', 'toolkitPassword', 'secret', 'command',
                'language', 'orderNumber', 'amount', 'currency'
            ]))
        retvals = parse_qs(r.text)
        if retvals['status'][0] != '0':
            logger.error('Wirecard error during refund: %s' % r.text)
            raise PaymentException(
                _('Wirecard reported an error: {msg}').format(
                    msg=retvals['message'][0]))

    def execute_refund(self, refund: OrderRefund):
        try:
            self._refund(refund.payment.info_data['orderNumber'],
                         refund.amount, self.event.currency,
                         refund.order.locale[:2])
        except requests.exceptions.RequestException as e:
            logger.exception('Wirecard error: %s' % str(e))
            raise PaymentException(
                _('We had trouble communicating with Wirecard. Please try again and contact '
                  'support if the problem persists.'))
        else:
            refund.done()

    def shred_payment_info(self, obj: Union[OrderPayment, OrderRefund]):
        d = obj.info_data
        new = {'_shreded': True}
        for k in ('paymentState', 'amount', 'authenticated', 'paymentType',
                  'pretix_orderCode', 'currency', 'orderNumber',
                  'financialInstitution', 'message', 'mandateId', 'dueDate'):
            if k in d:
                new[k] = d[k]
        obj.info_data = new
        obj.save(update_fields=['info'])

        for le in obj.order.all_logentries().filter(
                action_type="pretix_wirecard.wirecard.event").exclude(data=""):
            d = le.parsed_data
            new = {'_shreded': True}
            for k in ('paymentState', 'amount', 'authenticated', 'paymentType',
                      'pretix_orderCode', 'currency', 'orderNumber',
                      'financialInstitution', 'message', 'mandateId',
                      'dueDate'):
                if k in d:
                    new[k] = d[k]
            le.data = json.dumps(new)
            le.shredded = True
            le.save(update_fields=['data', 'shredded'])
Example #33
0
class Paypal(BasePaymentProvider):
    identifier = 'paypal'
    verbose_name = _('PayPal')
    payment_form_fields = OrderedDict([])

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'paypal', event)

    @property
    def test_mode_message(self):
        if self.settings.connect_client_id and not self.settings.secret:
            # in OAuth mode, sandbox mode needs to be set global
            is_sandbox = self.settings.connect_endpoint == 'sandbox'
        else:
            is_sandbox = self.settings.get('endpoint') == 'sandbox'
        if is_sandbox:
            return _(
                'The PayPal sandbox is being used, you can test without actually sending money but you will need a '
                'PayPal sandbox user to log in.')
        return None

    @property
    def settings_form_fields(self):
        if self.settings.connect_client_id and not self.settings.secret:
            # PayPal connect
            if self.settings.connect_user_id:
                fields = [
                    ('connect_user_id',
                     forms.CharField(label=_('PayPal account'),
                                     disabled=True)),
                ]
            else:
                return {}
        else:
            fields = [
                ('client_id',
                 forms.CharField(
                     label=_('Client ID'),
                     max_length=80,
                     min_length=80,
                     help_text=
                     _('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>'
                       ).
                     format(text=_(
                         'Click here for a tutorial on how to obtain the required keys'
                     ),
                            docs_url=
                            'https://docs.pretix.eu/en/latest/user/payments/paypal.html'
                            ))),
                ('secret',
                 forms.CharField(
                     label=_('Secret'),
                     max_length=80,
                     min_length=80,
                 )),
                ('endpoint',
                 forms.ChoiceField(
                     label=_('Endpoint'),
                     initial='live',
                     choices=(
                         ('live', 'Live'),
                         ('sandbox', 'Sandbox'),
                     ),
                 )),
            ]

        d = OrderedDict(fields + list(super().settings_form_fields.items()))

        d.move_to_end('_enabled', False)
        return d

    def get_connect_url(self, request):
        request.session['payment_paypal_oauth_event'] = request.event.pk

        self.init_api()
        return Tokeninfo.authorize_url({'scope': 'openid profile email'})

    def settings_content_render(self, request):
        if self.settings.connect_client_id and not self.settings.secret:
            # Use PayPal connect
            if not self.settings.connect_user_id:
                return (
                    "<p>{}</p>"
                    "<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
                ).format(
                    _('To accept payments via PayPal, you will need an account at PayPal. By clicking on the '
                      'following button, you can either create a new PayPal account connect pretix to an existing '
                      'one.'), self.get_connect_url(request),
                    _('Connect with {icon} PayPal').format(
                        icon='<i class="fa fa-paypal"></i>'))
            else:
                return (
                    "<button formaction='{}' class='btn btn-danger'>{}</button>"
                ).format(
                    reverse('plugins:paypal:oauth.disconnect',
                            kwargs={
                                'organizer': self.event.organizer.slug,
                                'event': self.event.slug,
                            }), _('Disconnect from PayPal'))
        else:
            return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
                _('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
                  'when payments are refunded externally.'),
                build_global_uri('plugins:paypal:webhook'))

    def init_api(self):
        if self.settings.connect_client_id and not self.settings.secret:
            paypalrestsdk.set_config(
                mode="sandbox"
                if "sandbox" in self.settings.connect_endpoint else 'live',
                client_id=self.settings.connect_client_id,
                client_secret=self.settings.connect_secret_key,
                openid_client_id=self.settings.connect_client_id,
                openid_client_secret=self.settings.connect_secret_key,
                openid_redirect_uri=urlquote(
                    build_global_uri('plugins:paypal:oauth.return')))
        else:
            paypalrestsdk.set_config(
                mode="sandbox"
                if "sandbox" in self.settings.get('endpoint') else 'live',
                client_id=self.settings.get('client_id'),
                client_secret=self.settings.get('secret'))

    def payment_is_valid_session(self, request):
        return (request.session.get('payment_paypal_id', '') != ''
                and request.session.get('payment_paypal_payer', '') != '')

    def payment_form_render(self, request) -> str:
        template = get_template(
            'pretixplugins/paypal/checkout_payment_form.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def checkout_prepare(self, request, cart):
        self.init_api()
        kwargs = {}
        if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
            kwargs['cart_namespace'] = request.resolver_match.kwargs[
                'cart_namespace']

        if request.event.settings.payment_paypal_connect_user_id:
            try:
                userinfo = Tokeninfo.create_with_refresh_token(
                    request.event.settings.payment_paypal_connect_refresh_token
                ).userinfo()
            except BadRequest as ex:
                ex = json.loads(ex.content)
                messages.error(
                    request, '{}: {} ({})'.format(
                        _('We had trouble communicating with PayPal'),
                        ex['error_description'], ex['correlation_id']))
                return

            request.event.settings.payment_paypal_connect_user_id = userinfo.email
            payee = {
                "email": request.event.settings.payment_paypal_connect_user_id,
                # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
                # we should use it instead of the merchant's eMail-address
                # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
            }
        else:
            payee = {}

        payment = paypalrestsdk.Payment({
            'header': {
                'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'
            },
            'intent':
            'sale',
            'payer': {
                "payment_method": "paypal",
            },
            "redirect_urls": {
                "return_url":
                build_absolute_uri(request.event,
                                   'plugins:paypal:return',
                                   kwargs=kwargs),
                "cancel_url":
                build_absolute_uri(request.event,
                                   'plugins:paypal:abort',
                                   kwargs=kwargs),
            },
            "transactions": [{
                "item_list": {
                    "items": [{
                        "name": __('Order for %s') % str(request.event),
                        "quantity": 1,
                        "price": self.format_price(cart['total']),
                        "currency": request.event.currency
                    }]
                },
                "amount": {
                    "currency": request.event.currency,
                    "total": self.format_price(cart['total'])
                },
                "description":
                __('Event tickets for {event}').format(
                    event=request.event.name),
                "payee":
                payee
            }]
        })
        request.session['payment_paypal_order'] = None
        return self._create_payment(request, payment)

    def format_price(self, value):
        return str(
            round_decimal(
                value,
                self.event.currency,
                {
                    # PayPal behaves differently than Stripe in deciding what currencies have decimal places
                    # Source https://developer.paypal.com/docs/classic/api/currency_codes/
                    'HUF': 0,
                    'JPY': 0,
                    'MYR': 0,
                    'TWD': 0,
                    # However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
                    'CLP': 0,
                    # Let's just guess that the ones listed here are 0-based as well
                    # https://developers.braintreepayments.com/reference/general/currencies
                    'BIF': 0,
                    'DJF': 0,
                    'GNF': 0,
                    'KMF': 0,
                    'KRW': 0,
                    'LAK': 0,
                    'PYG': 0,
                    'RWF': 0,
                    'UGX': 0,
                    'VND': 0,
                    'VUV': 0,
                    'XAF': 0,
                    'XOF': 0,
                    'XPF': 0,
                }))

    @property
    def abort_pending_allowed(self):
        return False

    def _create_payment(self, request, payment):
        try:
            if payment.create():
                if payment.state not in ('created', 'approved', 'pending'):
                    messages.error(
                        request, _('We had trouble communicating with PayPal'))
                    logger.error('Invalid payment state: ' + str(payment))
                    return
                request.session['payment_paypal_id'] = payment.id
                for link in payment.links:
                    if link.method == "REDIRECT" and link.rel == "approval_url":
                        if request.session.get('iframe_session', False):
                            signer = signing.Signer(salt='safe-redirect')
                            return (build_absolute_uri(
                                request.event, 'plugins:paypal:redirect') +
                                    '?url=' +
                                    urllib.parse.quote(signer.sign(link.href)))
                        else:
                            return str(link.href)
            else:
                messages.error(request,
                               _('We had trouble communicating with PayPal'))
                logger.error('Error on creating payment: ' +
                             str(payment.error))
        except Exception as e:
            messages.error(request,
                           _('We had trouble communicating with PayPal'))
            logger.exception('Error on creating payment: ' + str(e))

    def checkout_confirm_render(self, request) -> str:
        """
        Returns the HTML that should be displayed when the user selected this provider
        on the 'confirm order' page.
        """
        template = get_template(
            'pretixplugins/paypal/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings
        }
        return template.render(ctx)

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        if (request.session.get('payment_paypal_id', '') == ''
                or request.session.get('payment_paypal_payer', '') == ''):
            raise PaymentException(
                _('We were unable to process your payment. See below for details on how to '
                  'proceed.'))

        self.init_api()
        pp_payment = paypalrestsdk.Payment.find(
            request.session.get('payment_paypal_id'))
        ReferencedPayPalObject.objects.get_or_create(order=payment.order,
                                                     payment=payment,
                                                     reference=pp_payment.id)
        if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
                != self.event.currency:
            logger.error('Value mismatch: Payment %s vs paypal trans %s' %
                         (payment.id, str(pp_payment)))
            raise PaymentException(
                _('We were unable to process your payment. See below for details on how to '
                  'proceed.'))

        return self._execute_payment(pp_payment, request, payment)

    def _execute_payment(self, payment, request, payment_obj):
        if payment.state == 'created':
            payment.replace([{
                "op": "replace",
                "path": "/transactions/0/item_list",
                "value": {
                    "items": [{
                        "name":
                        __('Order {slug}-{code}').format(
                            slug=self.event.slug.upper(),
                            code=payment_obj.order.code),
                        "quantity":
                        1,
                        "price":
                        self.format_price(payment_obj.amount),
                        "currency":
                        payment_obj.order.event.currency
                    }]
                }
            }, {
                "op":
                "replace",
                "path":
                "/transactions/0/description",
                "value":
                __('Order {order} for {event}').format(
                    event=request.event.name, order=payment_obj.order.code)
            }])
            try:
                payment.execute(
                    {"payer_id": request.session.get('payment_paypal_payer')})
            except Exception as e:
                messages.error(request,
                               _('We had trouble communicating with PayPal'))
                logger.exception('Error on creating payment: ' + str(e))

        for trans in payment.transactions:
            for rr in trans.related_resources:
                if hasattr(rr, 'sale') and rr.sale:
                    if rr.sale.state == 'pending':
                        messages.warning(
                            request,
                            _('PayPal has not yet approved the payment. We will inform you as '
                              'soon as the payment completed.'))
                        payment_obj.info = json.dumps(payment.to_dict())
                        payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
                        payment_obj.save()
                        return

        payment_obj.refresh_from_db()
        if payment.state == 'pending':
            messages.warning(
                request,
                _('PayPal has not yet approved the payment. We will inform you as soon as the '
                  'payment completed.'))
            payment_obj.info = json.dumps(payment.to_dict())
            payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
            payment_obj.save()
            return

        if payment.state != 'approved':
            payment_obj.fail(info=payment.to_dict())
            logger.error('Invalid state: %s' % str(payment))
            raise PaymentException(
                _('We were unable to process your payment. See below for details on how to '
                  'proceed.'))

        if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
            logger.warning(
                'PayPal success event even though order is already marked as paid'
            )
            return

        try:
            payment_obj.info = json.dumps(payment.to_dict())
            payment_obj.save(update_fields=['info'])
            payment_obj.confirm()
        except Quota.QuotaExceededException as e:
            raise PaymentException(str(e))

        except SendMailException:
            messages.warning(
                request,
                _('There was an error sending the confirmation mail.'))
        return None

    def payment_pending_render(self, request, payment) -> str:
        retry = True
        try:
            if payment.info and payment.info_data['state'] == 'pending':
                retry = False
        except KeyError:
            pass
        template = get_template('pretixplugins/paypal/pending.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'retry': retry,
            'order': payment.order
        }
        return template.render(ctx)

    def matching_id(self, payment: OrderPayment):
        sale_id = None
        for trans in payment.info_data.get('transactions', []):
            for res in trans.get('related_resources', []):
                if 'sale' in res and 'id' in res['sale']:
                    sale_id = res['sale']['id']
        return sale_id or payment.info_data.get('id', None)

    def api_payment_details(self, payment: OrderPayment):
        sale_id = None
        for trans in payment.info_data.get('transactions', []):
            for res in trans.get('related_resources', []):
                if 'sale' in res and 'id' in res['sale']:
                    sale_id = res['sale']['id']
        return {
            "payer_email":
            payment.info_data.get('payer', {}).get('payer_info',
                                                   {}).get('email'),
            "payer_id":
            payment.info_data.get('payer', {}).get('payer_info',
                                                   {}).get('payer_id'),
            "cart_id":
            payment.info_data.get('cart', None),
            "payment_id":
            payment.info_data.get('id', None),
            "sale_id":
            sale_id,
        }

    def payment_control_render(self, request: HttpRequest,
                               payment: OrderPayment):
        template = get_template('pretixplugins/paypal/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment.info_data,
            'order': payment.order
        }
        return template.render(ctx)

    def payment_partial_refund_supported(self, payment: OrderPayment):
        return True

    def payment_refund_supported(self, payment: OrderPayment):
        return True

    def execute_refund(self, refund: OrderRefund):
        self.init_api()

        sale = None
        for res in refund.payment.info_data['transactions'][0][
                'related_resources']:
            for k, v in res.items():
                if k == 'sale':
                    sale = paypalrestsdk.Sale.find(v['id'])
                    break

        pp_refund = sale.refund({
            "amount": {
                "total": self.format_price(refund.amount),
                "currency": refund.order.event.currency
            }
        })
        if not pp_refund.success():
            raise PaymentException(
                _('Refunding the amount via PayPal failed: {}').format(
                    pp_refund.error))
        else:
            sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
            refund.payment.info = json.dumps(sale.to_dict())
            refund.info = json.dumps(pp_refund.to_dict())
            refund.done()

    def payment_prepare(self, request, payment_obj):
        self.init_api()

        if request.event.settings.payment_paypal_connect_user_id:
            userinfo = Tokeninfo.create_with_refresh_token(
                request.event.settings.payment_paypal_connect_refresh_token
            ).userinfo()
            request.event.settings.payment_paypal_connect_user_id = userinfo.email
            payee = {
                "email": request.event.settings.payment_paypal_connect_user_id,
                # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
                # we should use it instead of the merchant's eMail-address
                # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
            }
        else:
            payee = {}

        payment = paypalrestsdk.Payment({
            'header': {
                'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'
            },
            'intent':
            'sale',
            'payer': {
                "payment_method": "paypal",
            },
            "redirect_urls": {
                "return_url":
                build_absolute_uri(request.event, 'plugins:paypal:return'),
                "cancel_url":
                build_absolute_uri(request.event, 'plugins:paypal:abort'),
            },
            "transactions": [{
                "item_list": {
                    "items": [{
                        "name":
                        __('Order {slug}-{code}').format(
                            slug=self.event.slug.upper(),
                            code=payment_obj.order.code),
                        "quantity":
                        1,
                        "price":
                        self.format_price(payment_obj.amount),
                        "currency":
                        payment_obj.order.event.currency
                    }]
                },
                "amount": {
                    "currency": request.event.currency,
                    "total": self.format_price(payment_obj.amount)
                },
                "description":
                __('Order {order} for {event}').format(
                    event=request.event.name, order=payment_obj.order.code),
                "payee":
                payee
            }]
        })
        request.session['payment_paypal_order'] = payment_obj.order.pk
        request.session['payment_paypal_payment'] = payment_obj.pk
        return self._create_payment(request, payment)

    def shred_payment_info(self, obj):
        if obj.info:
            d = json.loads(obj.info)
            new = {
                'id':
                d.get('id'),
                'payer': {
                    'payer_info': {
                        'email': 'â–ˆ'
                    }
                },
                'update_time':
                d.get('update_time'),
                'transactions': [{
                    'amount': t.get('amount')
                } for t in d.get('transactions', [])],
                '_shredded':
                True
            }
            obj.info = json.dumps(new)
            obj.save(update_fields=['info'])

        for le in obj.order.all_logentries().filter(
                action_type="pretix.plugins.paypal.event").exclude(data=""):
            d = le.parsed_data
            if 'resource' in d:
                d['resource'] = {
                    'id': d['resource'].get('id'),
                    'sale_id': d['resource'].get('sale_id'),
                    'parent_payment': d['resource'].get('parent_payment'),
                }
            le.data = json.dumps(d)
            le.shredded = True
            le.save(update_fields=['data', 'shredded'])

    def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
        if order.status == Order.STATUS_PAID:
            if payment.info_data.get('id', None):
                try:
                    return '{}\r\n{}: {}\r\n{}: {}'.format(
                        _('The payment for this invoice has already been received.'
                          ), _('PayPal payment ID'), payment.info_data['id'],
                        _('PayPal sale ID'), payment.info_data['transactions']
                        [0]['related_resources'][0]['sale']['id'])
                except (KeyError, IndexError):
                    return '{}\r\n{}: {}'.format(
                        _('The payment for this invoice has already been received.'
                          ), _('PayPal payment ID'), payment.info_data['id'])
            else:
                return super().render_invoice_text(order, payment)

        return self.settings.get('_invoice_text',
                                 as_type=LazyI18nString,
                                 default='')
Example #34
0
class BaseTicketOutput:
    """
    This is the base class for all ticket outputs.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('ticketoutput', self.identifier, event)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this output is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    @property
    def multi_download_enabled(self) -> bool:
        """
        Returns whether or not the ``generate_order`` method may be called. Returns
        ``True`` by default.
        """
        return True

    def generate(self, position: OrderPosition) -> Tuple[str, str, str]:
        """
        This method should generate the download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.

        Alternatively, you can pass a tuple consisting of an arbitrary string, ``text/uri-list``
        and a single URL. In this case, the user will be redirected to this link instead of
        being asked to download a generated file.

        .. note:: If the event uses the event series feature (internally called subevents)
                  and your generated ticket contains information like the event name or date,
                  you probably want to display the properties of the subevent. A common pattern
                  to do this would be a declaration ``ev = position.subevent or position.order.event``
                  and then access properties that are present on both classes like ``ev.name`` or
                  ``ev.date_from``.

        .. note:: Should you elect to use the URI redirection feature instead of offering downloads,
                  you should also set the ``multi_download_enabled``-property to ``False``.
        """
        raise NotImplementedError()

    def generate_order(self, order: Order) -> Tuple[str, str, str]:
        """
        This method is the same as order() but should not generate one file per order position
        but instead one file for the full order.

        This method is optional to implement. If you don't implement it, the default
        implementation will offer a zip file of the generate() results for the order positions.

        This method should generate a download file and return a tuple consisting of a
        filename, a file type and file content. The extension will be taken from the filename
        which is otherwise ignored.

        If you override this method, make sure that positions that are addons (i.e. ``addon_to``
        is set) are only outputted if the event setting ``ticket_download_addons`` is active.
        Do the same for positions that are non-admission without ``ticket_download_nonadm`` active.
        If you want, you can just iterate over ``order.positions_with_tickets`` which applies the
        appropriate filters for you.
        """
        with tempfile.TemporaryDirectory() as d:
            with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
                for pos in order.positions_with_tickets:
                    fname, __, content = self.generate(pos)
                    zipf.writestr(
                        '{}-{}{}'.format(order.code, pos.positionid,
                                         os.path.splitext(fname)[1]), content)

            with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
                return '{}-{}.zip'.format(
                    order.code,
                    self.identifier), 'application/zip', zipf.read()

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this ticket output. This should be short but
        self-explanatory. Good examples include 'PDF tickets' and 'Passbook'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this ticket output.
        This should only contain lowercase letters and in most
        cases will be the same as your package name.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``
        setting 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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('paper_size',
                         forms.CharField(
                             label=_('Paper size'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable output'),
                 required=False,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    @property
    def download_button_text(self) -> str:
        """
        The text on the download button in the frontend.
        """
        return _('Download ticket')

    @property
    def download_button_icon(self) -> str:
        """
        The Font Awesome icon on the download button in the frontend.
        """
        return 'fa-download'

    @property
    def preview_allowed(self) -> bool:
        """
        By default, the ``generate()`` method is called for generating a preview in the pretix backend.
        In case your plugin cannot generate previews for any reason, you can manually disable it here.
        """
        return True

    @property
    def javascript_required(self) -> bool:
        """
        If this property is set to true, the download-button for this ticket-type will not be displayed
        when the user's browser has JavaScript disabled.

        Defaults to ``False``
        """
        return False
Example #35
0
 def __init__(self, event: Event):
     self.event = event
     self.settings = SettingsSandbox('ticketoutput', self.identifier, event)
Example #36
0
class BasePaymentProvider:
    """
    This is the base class for all payment providers.
    """
    def __init__(self, event: Event):
        self.event = event
        self.settings = SettingsSandbox('payment', self.identifier, event)
        # Default values
        if self.settings.get('_fee_reverse_calc') is None:
            self.settings.set('_fee_reverse_calc', True)

    def __str__(self):
        return self.identifier

    @property
    def is_enabled(self) -> bool:
        """
        Returns whether or whether not this payment provider is enabled.
        By default, this is determined by the value of the ``_enabled`` setting.
        """
        return self.settings.get('_enabled', as_type=bool)

    def calculate_fee(self, price: Decimal) -> Decimal:
        """
        Calculate the fee for this payment provider which will be added to
        final price before fees (but after taxes). It should include any taxes.
        The default implementation makes use of the setting ``_fee_abs`` for an
        absolute fee and ``_fee_percent`` for a percentage.

        :param price: The total value without the payment method fee, after taxes.
        """
        fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
        fee_percent = self.settings.get('_fee_percent',
                                        as_type=Decimal,
                                        default=0)
        fee_reverse_calc = self.settings.get('_fee_reverse_calc',
                                             as_type=bool,
                                             default=True)
        if fee_reverse_calc:
            return round_decimal((price + fee_abs) *
                                 (1 / (1 - fee_percent / 100)) - price)
        else:
            return round_decimal(price * fee_percent / 100) + fee_abs

    @property
    def verbose_name(self) -> str:
        """
        A human-readable name for this payment provider. This should
        be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card via Stripe'.
        """
        raise NotImplementedError()  # NOQA

    @property
    def public_name(self) -> str:
        """
        A human-readable name for this payment provider to be shown to the public.
        This should be short but self-explaining. Good examples include 'Bank transfer'
        and 'Credit card', but 'Credit card via Stripe' might be to explicit. By default,
        this is the same as ``verbose_name``
        """
        return self.verbose_name

    @property
    def identifier(self) -> str:
        """
        A short and unique identifier for this payment provider.
        This should only contain lowercase letters and in most
        cases will be the same as your packagename.
        """
        raise NotImplementedError()  # NOQA

    @property
    def settings_form_fields(self) -> dict:
        """
        When the event's administrator visits the event configuration
        page, this method is called to return the configuration fields available.

        It should therefore return a dictionary where the keys should be (unprefixed)
        settings keys and the values should be corresponding Django form fields.

        The default implementation returns the appropriate fields for the ``_enabled``,
        ``_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
        like this::

            @property
            def settings_form_fields(self):
                return OrderedDict(
                    list(super().settings_form_fields.items()) + [
                        ('bank_details',
                         forms.CharField(
                             widget=forms.Textarea,
                             label=_('Bank account details'),
                             required=False
                         ))
                    ]
                )

        .. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
                     implementation.
        """
        return OrderedDict([
            ('_enabled',
             forms.BooleanField(
                 label=_('Enable payment method'),
                 required=False,
             )),
            ('_fee_abs',
             forms.DecimalField(label=_('Additional fee'),
                                help_text=_('Absolute value'),
                                required=False)),
            ('_fee_percent',
             forms.DecimalField(label=_('Additional fee'),
                                help_text=_('Percentage'),
                                required=False)),
            ('_availability_date',
             RelativeDateField(
                 label=_('Available until'),
                 help_text=
                 _('Users will not be able to choose this payment provider after the given date.'
                   ),
                 required=False,
             )),
            ('_fee_reverse_calc',
             forms.BooleanField(
                 label=_(
                     'Calculate the fee from the total value including the fee.'
                 ),
                 help_text=
                 _('We recommend you to enable this if you want your users to pay the payment fees of your '
                   'payment provider. <a href="{docs_url}" target="_blank">Click here '
                   'for detailled information on what this does.</a> Don\'t forget to set the correct fees '
                   'above!').
                 format(
                     docs_url=
                     'https://docs.pretix.eu/en/latest/user/payments/fees.html'
                 ),
                 required=False)),
            ('_invoice_text',
             I18nFormField(
                 label=_('Text on invoices'),
                 help_text=
                 _('Will be printed just below the payment figures and above the closing text on invoices.'
                   ),
                 required=False,
                 widget=I18nTextarea,
             )),
        ])

    def settings_content_render(self, request: HttpRequest) -> str:
        """
        When the event's administrator visits the event configuration
        page, this method is called. It may return HTML containing additional information
        that is displayed below the form fields configured in ``settings_form_fields``.
        """
        pass

    def render_invoice_text(self, order: Order) -> str:
        """
        This is called when an invoice for an order with this payment provider is generated.
        The default implementation returns the content of the _invoice_text configuration
        variable (an I18nString), or an empty string if unconfigured.
        """
        return self.settings.get('_invoice_text',
                                 as_type=LazyI18nString,
                                 default='')

    @property
    def payment_form_fields(self) -> dict:
        """
        This is used by the default implementation of :py:meth:`checkout_form`.
        It should return an object similar to :py:attr:`settings_form_fields`.

        The default implementation returns an empty dictionary.
        """
        return {}

    def payment_form(self, request: HttpRequest) -> Form:
        """
        This is called by the default implementation of :py:meth:`checkout_form_render`
        to obtain the form that is displayed to the user during the checkout
        process. The default implementation constructs the form using
        :py:attr:`checkout_form_fields` and sets appropriate prefixes for the form
        and all fields and fills the form with data form the user's session.

        If you overwrite this, we strongly suggest that you inherit from
        ``PaymentProviderForm`` (from this module) that handles some nasty issues about
        required fields for you.
        """
        form = PaymentProviderForm(
            data=(request.POST if request.method == 'POST' else None),
            prefix='payment_%s' % self.identifier,
            initial={
                k.replace('payment_%s_' % self.identifier, ''): v
                for k, v in request.session.items()
                if k.startswith('payment_%s_' % self.identifier)
            })
        form.fields = self.payment_form_fields

        for k, v in form.fields.items():
            v._required = v.required
            v.required = False
            v.widget.is_required = False

        return form

    def _is_still_available(self, now_dt=None, cart_id=None, order=None):
        now_dt = now_dt or now()
        tz = pytz.timezone(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:
                availability_date = min([
                    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))
                ])
            elif self.event.has_subevents and order:
                availability_date = min([
                    availability_date.datetime(se).date()
                    for se in self.event.subevents.filter(
                        id__in=order.positions.values_list('subevent',
                                                           flat=True))
                ])
            elif self.event.has_subevents:
                logger.error('Payment provider is not subevent-ready.')
                return False
            else:
                availability_date = availability_date.datetime(
                    self.event).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
        of users, products or other criteria. If this method returns ``False``, the
        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.
        """
        return self._is_still_available(cart_id=request.session.session_key)

    def payment_form_render(self, request: HttpRequest) -> str:
        """
        When the user selects this provider as his prefered payment method,
        they will be shown the HTML you return from this method.

        The default implementation will call :py:meth:`checkout_form`
        and render the returned form. If your payment method doesn't require
        the user to fill out form fields, you should just return a paragraph
        of explanatory text.
        """
        form = self.payment_form(request)
        template = get_template(
            'pretixpresale/event/checkout_payment_form_default.html')
        ctx = {'request': request, 'form': form}
        return template.render(ctx)

    def checkout_confirm_render(self, request) -> str:
        """
        If the user has successfully filled in his payment data, they will be redirected
        to a confirmation page which lists all details of his order for a final review.
        This method should return the HTML which should be displayed inside the
        'Payment' box on this page.

        In most cases, this should include a short summary of the user's input and
        a short explanation on how the payment process will continue.
        """
        raise NotImplementedError()  # NOQA

    def checkout_prepare(self, request: HttpRequest,
                         cart: Dict[str, Any]) -> Union[bool, str]:
        """
        Will be called after the user selects this provider as his payment method.
        If you provided a form to the user to enter payment data, this method should
        at least store the user's input into his session.

        This method should return ``False`` if the user's input was invalid, ``True``
        if the input was valid and the frontend should continue with default behaviour
        or a string containing a URL if the user should be redirected somewhere else.

        On errors, you should use Django's message framework to display an error message
        to the user (or the normal form validation error messages).

        The default implementation stores the input into the form returned by
        :py:meth:`payment_form` in the user's session.

        If your payment method requires you to redirect the user to an external provider,
        this might be the place to do so.

        .. IMPORTANT:: If this is called, the user has not yet confirmed his or her order.
           You may NOT do anything which actually moves money.

        :param cart: This dictionary contains at least the following keys:

            positions:
               A list of ``CartPosition`` objects that are annotated with the special
               attributes ``count`` and ``total`` because multiple objects of the
               same content are grouped into one.

            raw:
                The raw list of ``CartPosition`` objects in the users cart

            total:
                The overall total *including* the fee for the payment method.

            payment_fee:
                The fee for the payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def payment_is_valid_session(self, request: HttpRequest) -> bool:
        """
        This is called at the time the user tries to place the order. It should return
        ``True`` if the user's session is valid and all data your payment provider requires
        in future steps is present.
        """
        raise NotImplementedError()  # NOQA

    def payment_perform(self, request: HttpRequest, order: Order) -> str:
        """
        After the user has confirmed their purchase, this method will be called to complete
        the payment process. This is the place to actually move the money if applicable.
        If you need any special  behaviour,  you can return a string
        containing the URL the user will be redirected to. If you are done with your process
        you should return the user to the order's detail page.

        If the payment is completed, you should call ``pretix.base.services.orders.mark_order_paid(order, provider, info)``
        with ``provider`` being your :py:attr:`identifier` and ``info`` being any string
        you might want to store for later usage. Please note that ``mark_order_paid`` might
        raise a ``Quota.QuotaExceededException`` if (and only if) the payment term of this
        order is over and some of the items are sold out. You should use the exception message
        to display a meaningful error to the user.

        The default implementation just returns ``None`` and therefore leaves the
        order unpaid. The user will be redirected to the order's detail page by default.

        On errors, you should raise a ``PaymentException``.
        :param order: The order object
        """
        return None

    def order_pending_mail_render(self, order: Order) -> str:
        """
        After the user has submitted their order, they will receive a confirmation
        email. You can return a string from this method if you want to add additional
        information to this email.

        :param order: The order object
        """
        return ""

    def order_pending_render(self, request: HttpRequest, order: Order) -> str:
        """
        If the user visits a detail page of an order which has not yet been paid but
        this payment method was selected during checkout, this method will be called
        to provide HTML content for the 'payment' box on the page.

        It should contain instructions on how to continue with the payment process,
        either in form of text or buttons/links/etc.

        :param order: The order object
        """
        raise NotImplementedError()  # NOQA

    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 checks for the _availability_date setting to be either unset or in the future.

        :param order: The order object
        """
        return self._is_still_available(order=order)

    def order_can_retry(self, order: Order) -> bool:
        """
        Will be called if the user views the detail page of an unpaid order to determine
        whether the user should be presented with an option to retry the payment. The default
        implementation always returns False.

        If you want to enable retrials for your payment method, the best is to just return
        ``self._is_still_available()`` from this method to disable it as soon as the method
        gets disabled or the methods end date is reached.

        The retry workflow is also used if a user switches to this payment method for an existing
        order!

        :param order: The order object
        """
        return False

    def retry_prepare(self, request: HttpRequest,
                      order: Order) -> Union[bool, str]:
        """
        Deprecated, use order_prepare instead
        """
        raise DeprecationWarning(
            'retry_prepare is deprecated, use order_prepare instead')
        return self.order_prepare(request, order)

    def order_prepare(self, request: HttpRequest,
                      order: Order) -> Union[bool, str]:
        """
        Will be called if the user retries to pay an unpaid order (after the user filled in
        e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
        method.

        It should return and report errors the same way as :py:meth:`checkout_prepare`, but
        receives an ``Order`` object instead of a cart object.

        Note: The ``Order`` object given to this method might be different from the version
        stored in the database as it's total will already contain the payment fee for the
        new payment method.
        """
        form = self.payment_form(request)
        if form.is_valid():
            for k, v in form.cleaned_data.items():
                request.session['payment_%s_%s' % (self.identifier, k)] = v
            return True
        else:
            return False

    def order_paid_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the user views the detail page of a paid order which is
        associated with this payment provider.

        It should return HTML code which should be displayed to the user or None,
        if there is nothing to say (like the default implementation does).

        :param order: The order object
        """
        return None

    def order_control_render(self, request: HttpRequest, order: Order) -> str:
        """
        Will be called if the *event administrator* views the detail page of an order
        which is associated with this payment provider.

        It should return HTML code containing information regarding the current payment
        status and, if applicable, next steps.

        The default implementation returns the verbose name of the payment provider.

        :param order: The order object
        """
        return _('Payment provider: %s' % self.verbose_name)

    def order_control_refund_render(self,
                                    order: Order,
                                    request: HttpRequest = None) -> str:
        """
        Will be called if the event administrator clicks an order's 'refund' button.
        This can be used to display information *before* the order is being refunded.

        It should return HTML code which should be displayed to the user. It should
        contain information about to which extend the money will be refunded
        automatically.

        :param order: The order object
        :param request: The HTTP request

        .. versionchanged:: 1.6

           The parameter ``request`` has been added.
        """
        return '<div class="alert alert-warning">%s</div>' % _(
            'The money can not be automatically refunded, '
            'please transfer the money back manually.')

    def order_control_refund_perform(self, request: HttpRequest,
                                     order: Order) -> Union[bool, str]:
        """
        Will be called if the event administrator confirms the refund.

        This should transfer the money back (if possible). You can return the URL the
        user should be redirected to if you need special behaviour or None to continue
        with default behaviour.

        On failure, you should use Django's message framework to display an error message
        to the user.

        The default implementation sets the Order's state to refunded and shows a success
        message.

        :param request: The HTTP request
        :param order: The order object
        """
        from pretix.base.services.orders import mark_order_refunded

        mark_order_refunded(order, user=request.user)
        messages.success(
            request,
            _('The order has been marked as refunded. Please transfer the money '
              'back to the buyer manually.'))
Example #37
0
 def __init__(self, event: Event):
     super().__init__(event)
     self.settings = SettingsSandbox('payment', 'stripe', event)
Example #38
0
 def __init__(self, event: Event):
     super().__init__(event)
     self.settings = SettingsSandbox('payment', 'mete', event)
     self.logger = logging.getLogger("Mete-Provider")
Example #39
0
 def __init__(self, event: Event):
     self.event = event
     self.settings = SettingsSandbox('ticketoutput', self.identifier, event)
class AdyenMethod(BasePaymentProvider):
    identifier = ''
    method = ''

    def __init__(self, event: Event):
        super().__init__(event)
        self.settings = SettingsSandbox('payment', 'adyen', event)

    @property
    def test_mode_message(self):
        if self.settings.test_merchant_account and self.settings.test_api_key:
            return mark_safe(
                _('The Adyen plugin is operating in test mode. You can use one of <a {args}>many test '
                  'cards</a> to perform a transaction. No money will actually be transferred.'
                  ).
                format(
                    args=
                    'href="https://docs.adyen.com/development-resources/test-cards/test-card-numbers" '
                    'target="_blank"'))
        return None

    @property
    def settings_form_fields(self):
        return {}

    @property
    def is_enabled(self) -> bool:
        return self.settings.get(
            '_enabled', as_type=bool) and self.settings.get(
                'method_{}'.format(self.method), as_type=bool)

    def payment_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
        return True

    def payment_prepare(self, request, payment):
        return self.checkout_prepare(request, None)

    def checkout_prepare(self, request: HttpRequest,
                         cart: Dict[str, Any]) -> Union[bool, str]:
        payment_method_data = request.POST.get(
            '{}-{}'.format('adyen_paymentMethodData', self.method), '')
        request.session['{}-{}'.format('payment_adyen_paymentMethodData',
                                       self.method)] = payment_method_data

        if payment_method_data == '':
            messages.warning(
                request,
                _('You may need to enable JavaScript for Adyen payments.'))
            return False
        return True

    def payment_is_valid_session(self, request):
        return request.session.get(
            '{}-{}'.format('payment_adyen_paymentMethodData', self.method),
            '') != ''

    def _amount_to_decimal(self, cents):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return round_decimal(float(cents) / (10**places), self.event.currency)

    def _decimal_to_int(self, amount):
        places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
        return int(amount * 10**places)

    def _get_amount(self, payment):
        return self._decimal_to_int(payment.amount)

    def statement_descriptor(self, payment, length=22):
        return '{event}-{code} {eventname}'.format(
            event=self.event.slug.upper(),
            code=payment.order.code,
            eventname=re.sub('[^a-zA-Z0-9 ]', '',
                             str(self.event.name)))[:length]

    @property
    def api_kwargs(self):
        kwargs = {
            'merchantAccount':
            self.settings.test_merchant_account
            if self.event.testmode else self.settings.prod_merchant_account,
            'applicationInfo': {
                'merchantApplication': {
                    'name': 'pretix-adyen',
                    'version': PluginApp.PretixPluginMeta.version,
                },
                'externalPlatform': {
                    'name': 'pretix',
                    'version': __version__,
                    'integrator': settings.PRETIX_INSTANCE_NAME,
                }
            }
        }

        return kwargs

    def _init_api(self, env=None):
        self.adyen = Adyen.Adyen(
            app_name='pretix',
            xapikey=self.settings.test_api_key
            if self.event.testmode else self.settings.prod_api_key,
            # API-calls go only to -live in prod - not to -live-au or -live-us like in the frontend.
            platform=env if env else 'test' if self.event.testmode else 'live',
            live_endpoint_prefix=self.settings.prod_prefix)

    def checkout_confirm_render(self, request) -> str:
        template = get_template('pretix_adyen/checkout_payment_confirm.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'provider': self
        }
        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):
        pass

    def payment_pending_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
        else:
            payment_info = None
        template = get_template('pretix_adyen/pending.html')
        ctx = {
            'request':
            request,
            'event':
            self.event,
            'settings':
            self.settings,
            'provider':
            self,
            'order':
            payment.order,
            'payment':
            payment,
            'payment_info':
            payment_info,
            'payment_hash':
            hashlib.sha1(payment.order.secret.lower().encode()).hexdigest()
        }
        return template.render(ctx)

    def api_payment_details(self, payment: OrderPayment):
        return {
            "id": payment.info_data.get("id", None),
            "payment_method": payment.info_data.get("payment_method", None)
        }

    def payment_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if 'amount' in payment_info:
                payment_info['amount'][
                    'value'] /= 10**settings.CURRENCY_PLACES.get(
                        self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretix_adyen/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def refund_control_render(self, request, payment) -> str:
        if payment.info:
            payment_info = json.loads(payment.info)
            if 'amount' in payment_info:
                payment_info['amount'][
                    'value'] /= 10**settings.CURRENCY_PLACES.get(
                        self.event.currency, 2)
        else:
            payment_info = None
        template = get_template('pretix_adyen/control.html')
        ctx = {
            'request': request,
            'event': self.event,
            'settings': self.settings,
            'payment_info': payment_info,
            'payment': payment,
            'method': self.method,
            'provider': self,
        }
        return template.render(ctx)

    def execute_refund(self, refund: OrderRefund):
        self._init_api()

        payment_info = refund.payment.info_data

        if not payment_info:
            raise PaymentException(_('No payment information found.'))

        rqdata = {
            'modificationAmount': {
                'value': self._get_amount(refund),
                'currency': self.event.currency,
            },
            'originalReference':
            payment_info['pspReference'],
            'merchantOrderReference':
            '{event}-{code}'.format(event=self.event.slug.upper(),
                                    code=refund.order.code),
            'reference':
            '{event}-{code}-R-{payment}'.format(event=self.event.slug.upper(),
                                                code=refund.order.code,
                                                payment=refund.local_id),
            'shopperStatement':
            self.statement_descriptor(refund),
            'captureDelayHours':
            0,
            **self.api_kwargs
        }

        try:
            result = self.adyen.payment.refund(rqdata)
        except AdyenError as e:
            logger.exception('AdyenError: %s' % str(e))
            return

        refund.info = json.dumps(result.message)
        refund.state = OrderRefund.REFUND_STATE_TRANSIT
        refund.save()
        refund.order.log_action('pretix.event.order.refund.created', {
            'local_id': refund.local_id,
            'provider': refund.provider,
        })

    def _get_originKey(self, env):
        originkeyenv = 'originkey_{}'.format(env)

        if not self.settings[originkeyenv]:
            self._init_api(env)

            origin_domains = {'originDomains': [settings.SITE_URL]}

            try:
                result = self.adyen.checkout.origin_keys(origin_domains)
                self.settings[originkeyenv] = result.message['originKeys'][
                    settings.SITE_URL]
            except AdyenError as e:
                logger.exception('AdyenError: %s' % str(e))

        return self.settings.get(originkeyenv, '')

    def execute_payment(self, request: HttpRequest, payment: OrderPayment):
        self._init_api()
        try:
            payment_method_data = json.loads(request.session['{}-{}'.format(
                'payment_adyen_paymentMethodData', self.method)])

            rqdata = {
                'amount': {
                    'value': self._get_amount(payment),
                    'currency': self.event.currency,
                },
                'merchantOrderReference':
                '{event}-{code}'.format(event=self.event.slug.upper(),
                                        code=payment.order.code),
                'reference':
                '{event}-{code}-P-{payment}'.format(
                    event=self.event.slug.upper(),
                    code=payment.order.code,
                    payment=payment.local_id),
                'shopperStatement':
                self.statement_descriptor(payment),
                'paymentMethod':
                payment_method_data['paymentMethod'],
                'returnUrl':
                build_absolute_uri(
                    self.event,
                    'plugins:pretix_adyen:return',
                    kwargs={
                        'order':
                        payment.order.code,
                        'payment':
                        payment.pk,
                        'hash':
                        hashlib.sha1(
                            payment.order.secret.lower().encode()).hexdigest(),
                    }),
                'channel':
                'Web',
                'origin':
                settings.SITE_URL,
                'captureDelayHours':
                0,
                'shopperInteraction':
                'Ecommerce',
                **self.api_kwargs
            }

            if self.method == "scheme":
                rqdata['additionalData'] = {'allow3DS2': 'true'}
                rqdata['browserInfo'] = payment_method_data['browserInfo']
                # Since we do not have the IP-address of the customer, we cannot pass rqdata['shopperIP'].

            try:
                result = self.adyen.checkout.payments(rqdata)
            except AdyenError as e:
                logger.exception('Adyen error: %s' % str(e))
                payment.state = OrderPayment.PAYMENT_STATE_FAILED
                payment.info = json.dumps({
                    'refusalReason':
                    json.loads(e.raw_response or {}).get('message', '')
                })
                payment.save()
                payment.order.log_action(
                    'pretix.event.order.payment.failed', {
                        'local_id':
                        payment.local_id,
                        'provider':
                        payment.provider,
                        'message':
                        json.loads(e.raw_response or {}).get('message', '')
                    })
                raise PaymentException(
                    _('We had trouble communicating with Adyen. Please try again and get in touch '
                      'with us if this problem persists.'))

            if 'action' in result.message:
                payment.info = json.dumps(result.message)
                payment.state = OrderPayment.PAYMENT_STATE_CREATED
                payment.save()
                payment.order.log_action('pretix.event.order.payment.started',
                                         {
                                             'local_id': payment.local_id,
                                             'provider': payment.provider
                                         })
                return build_absolute_uri(
                    self.event,
                    'plugins:pretix_adyen:sca',
                    kwargs={
                        'order':
                        payment.order.code,
                        'payment':
                        payment.pk,
                        'hash':
                        hashlib.sha1(
                            payment.order.secret.lower().encode()).hexdigest(),
                    })

            else:
                payment.info = json.dumps(result.message)
                payment.save()
                self._handle_resultcode(payment)
        finally:
            del request.session['{}-{}'.format(
                'payment_adyen_paymentMethodData', self.method)]

    def _handle_resultcode(self, payment: OrderPayment):
        payment_info = json.loads(payment.info)

        if payment_info['resultCode'] in [
                'AuthenticationFinished',
                'ChallengeShopper',
                'IdentifyShopper',
                'PresentToShopper',
                'Received',
                'RedirectShopper',
        ]:
            # At this point, the payment has already been created - so no need to set the status or log it again
            # payment.state = OrderPayment.PAYMENT_STATE_CREATED
            pass
        elif payment_info['resultCode'] in ['Error', 'Refused']:
            payment.state = OrderPayment.PAYMENT_STATE_FAILED
            payment.save(update_fields=['state'])
            payment.order.log_action('pretix.event.order.payment.failed', {
                'local_id': payment.local_id,
                'provider': payment.provider
            })
        elif payment_info['resultCode'] == 'Cancelled':
            payment.state = OrderPayment.PAYMENT_STATE_CANCELED
            payment.save(update_fields=['state'])
            payment.order.log_action('pretix.event.order.payment.canceled', {
                'local_id': payment.local_id,
                'provider': payment.provider
            })
        elif payment_info['resultCode'] == 'Pending':
            payment.state = OrderPayment.PAYMENT_STATE_PENDING
            payment.save(update_fields=['state'])
            # Nothing we can log here...
        elif payment_info['resultCode'] == 'Authorised':
            payment.confirm()

        return payment.state

    def _handle_action(self,
                       request: HttpRequest,
                       payment: OrderPayment,
                       statedata=None,
                       payload=None,
                       md=None,
                       pares=None):
        self._init_api()

        payment_info = json.loads(payment.info)

        try:
            if statedata:
                result = self.adyen.checkout.payments_details(
                    json.loads(statedata))
            elif payload:
                result = self.adyen.checkout.payments_details({
                    'paymentData':
                    payment_info['paymentData'],
                    'details': {
                        'payload': payload,
                    },
                })
            elif md and pares:
                result = self.adyen.checkout.payments_details({
                    'paymentData':
                    payment_info['paymentData'],
                    'details': {
                        'MD': md,
                        'PaRes': pares,
                    },
                })
            else:
                messages.error(
                    request,
                    _('Sorry, there was an error in the payment process.'))
                return eventreverse(self.event,
                                    'presale:event.order',
                                    kwargs={
                                        'order': payment.order.code,
                                        'secret': payment.order.secret
                                    })
        except AdyenError as e:
            logger.exception('AdyenError: %s' % str(e))
            messages.error(
                request,
                _('Sorry, there was an error in the payment process.'))
            return eventreverse(self.event,
                                'presale:event.order',
                                kwargs={
                                    'order': payment.order.code,
                                    'secret': payment.order.secret
                                })

        payment.info = json.dumps(result.message)
        payment.save(update_fields=['info'])

        if 'action' in result.message:
            return build_absolute_uri(
                self.event,
                'plugins:pretix_adyen:sca',
                kwargs={
                    'order':
                    payment.order.code,
                    'payment':
                    payment.pk,
                    'hash':
                    hashlib.sha1(
                        payment.order.secret.lower().encode()).hexdigest(),
                })
        else:
            state = self._handle_resultcode(payment)
            return eventreverse(self.event,
                                'presale:event.order',
                                kwargs={
                                    'order': payment.order.code,
                                    'secret': payment.order.secret
                                }) + ('?paid=yes' if state in [
                                    OrderPayment.PAYMENT_STATE_CONFIRMED,
                                    OrderPayment.PAYMENT_STATE_PENDING
                                ] else '')

    def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
        global_allowed = super().is_allowed(request, total)

        if request.event.testmode:
            local_allowed = request.event.settings.payment_adyen_test_merchant_account \
                and request.event.settings.payment_adyen_test_api_key \
                and request.event.settings.payment_adyen_test_hmac_key
        else:
            local_allowed = request.event.settings.payment_adyen_prod_merchant_account \
                and request.event.settings.payment_adyen_prod_api_key \
                and request.event.settings.payment_adyen_prod_hmac_key \
                and request.event.settings.payment_adyen_prod_prefix

        if global_allowed and local_allowed:
            self._init_api()

            def get_invoice_address():
                if not hasattr(request, '_checkout_flow_invoice_address'):
                    cs = cart_session(request)
                    iapk = cs.get('invoice_address')
                    if not iapk:
                        request._checkout_flow_invoice_address = InvoiceAddress(
                        )
                    else:
                        try:
                            request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
                                pk=iapk, order__isnull=True)
                        except InvoiceAddress.DoesNotExist:
                            request._checkout_flow_invoice_address = InvoiceAddress(
                            )
                return request._checkout_flow_invoice_address

            rqdata = {
                'amount': {
                    'value': self._decimal_to_int(total),
                    'currency': self.event.currency
                },
                'channel': 'Web',
                **self.api_kwargs
            }

            ia = get_invoice_address()
            if ia.country:
                rqdata['countryCode'] = str(ia.country)

            try:
                response = self.adyen.checkout.payment_methods(rqdata)
                if any(
                        d.get('type', None) == self.method
                        for d in response.message['paymentMethods']):
                    self.payment_methods = json.dumps(response.message)
                    return True
            except AdyenError as e:
                logger.exception('AdyenError: %s' % str(e))
                return False

        return False

    def payment_form_render(self, request, total) -> str:
        template = get_template('pretix_adyen/checkout_payment_form.html')

        if not hasattr(self, 'payment_methods'):
            self.is_allowed(request, total)

        ctx = {
            'method': self.method,
            'paymentMethodsResponse': self.payment_methods,
        }

        return template.render(ctx)