From 613e2cc50ae75c8c7629d39c508f2e18b8448166 Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Thu, 29 Jul 2021 11:31:29 +0200 Subject: [PATCH] Only create pending txn when proceeding to payment Otherwise someone may go to the checkout page not proceed, and then no longer see the checkout link due to the pending transaction. While this can still happen now if the user simply interrupts the payment process instead of finishing or cancelling, it's much less likely to happen (and an event manager can always "mark unpaid" to reset the pending transaction in case a user ends up in this state) --- .../indico_payment_sixpay/blueprint.py | 9 +- .../indico_payment_sixpay/controllers.py | 101 ++++++++++++++- .../indico_payment_sixpay/plugin.py | 116 +----------------- .../templates/event_payment_form.html | 11 +- .../templates/transaction_details.html | 2 + 5 files changed, 114 insertions(+), 125 deletions(-) diff --git a/payment_sixpay/indico_payment_sixpay/blueprint.py b/payment_sixpay/indico_payment_sixpay/blueprint.py index e12a9a0..0019b59 100644 --- a/payment_sixpay/indico_payment_sixpay/blueprint.py +++ b/payment_sixpay/indico_payment_sixpay/blueprint.py @@ -7,16 +7,17 @@ from indico.core.plugins import IndicoPluginBlueprint -from indico_payment_sixpay.controllers import (SixPayNotificationHandler, UserCancelHandler, UserFailureHandler, - UserSuccessHandler) +from indico_payment_sixpay.controllers import (RHInitSixpayPayment, SixPayNotificationHandler, UserCancelHandler, + UserFailureHandler, UserSuccessHandler) blueprint = IndicoPluginBlueprint( 'payment_sixpay', __name__, - url_prefix='/event//registrations//payment/response/sixpay' + url_prefix='/event//registrations//payment/sixpay' ) +blueprint.add_url_rule('/init', 'init', RHInitSixpayPayment, methods=('GET', 'POST')) blueprint.add_url_rule('/failure', 'failure', UserCancelHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/cancel', 'cancel', UserFailureHandler, methods=('GET', 'POST')) blueprint.add_url_rule('/success', 'success', UserSuccessHandler, methods=('GET', 'POST')) -blueprint.add_url_rule('/ipn', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) +blueprint.add_url_rule('/notify', 'notify', SixPayNotificationHandler, methods=('Get', 'POST')) diff --git a/payment_sixpay/indico_payment_sixpay/controllers.py b/payment_sixpay/indico_payment_sixpay/controllers.py index 168641e..2beba3f 100644 --- a/payment_sixpay/indico_payment_sixpay/controllers.py +++ b/payment_sixpay/indico_payment_sixpay/controllers.py @@ -9,11 +9,14 @@ from urllib.parse import urljoin import requests from flask import flash, redirect, request -from werkzeug.exceptions import BadRequest +from requests import RequestException +from werkzeug.exceptions import BadRequest, NotFound +from indico.core.plugins import url_for_plugin +from indico.modules.events.payment.controllers import RHPaymentBase from indico.modules.events.payment.models.transactions import TransactionAction from indico.modules.events.payment.notifications import notify_amount_inconsistency -from indico.modules.events.payment.util import register_transaction +from indico.modules.events.payment.util import get_active_payment_plugins, register_transaction from indico.modules.events.registration.models.registrations import Registration from indico.web.flask.util import url_for from indico.web.rh import RH @@ -21,8 +24,9 @@ from indico.web.rh import RH from indico_payment_sixpay import _ from indico_payment_sixpay.plugin import SixpayPaymentPlugin from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_ASSERT_URL, - SIXPAY_PP_CANCEL_URL, SIXPAY_PP_CAPTURE_URL, get_request_header, get_setting, - to_large_currency, to_small_currency) + SIXPAY_PP_CANCEL_URL, SIXPAY_PP_CAPTURE_URL, SIXPAY_PP_INIT_URL, + get_request_header, get_setting, get_terminal_id, to_large_currency, + to_small_currency) class TransactionFailure(Exception): @@ -58,6 +62,95 @@ class RHSixpayBase(RH): return get_setting(setting, self.registration.event) +class RHInitSixpayPayment(RHPaymentBase): + def _get_transaction_parameters(self): + """Get parameters for creating a transaction request.""" + settings = SixpayPaymentPlugin.event_settings.get_all(self.event) + format_map = { + 'user_id': self.registration.user_id, + 'user_name': self.registration.full_name, + 'user_firstname': self.registration.first_name, + 'user_lastname': self.registration.last_name, + 'event_id': self.registration.event_id, + 'event_title': self.registration.event.title, + 'registration_id': self.registration.id, + 'regform_title': self.registration.registration_form.title + } + order_description = settings['order_description'].format(**format_map) + order_identifier = settings['order_identifier'].format(**format_map) + # see the SixPay Manual + # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize + # on what these things mean + transaction_parameters = { + 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, settings['account_id']), + 'TerminalId': get_terminal_id(settings['account_id']), + 'Payment': { + 'Amount': { + # indico handles price as largest currency, but six expects + # smallest. E.g. EUR: indico uses 100.2 Euro, but six + # expects 10020 Cent + 'Value': str(to_small_currency(self.registration.price, self.registration.currency)), + 'CurrencyCode': self.registration.currency, + }, + 'OrderId': order_identifier[:80], + 'DESCRIPTION': order_description[:1000], + }, + # callbacks of the transaction - where to announce success etc., when redircting the user + 'ReturnUrls': { + 'Success': url_for_plugin('payment_sixpay.success', self.registration.locator.uuid, _external=True), + 'Fail': url_for_plugin('payment_sixpay.failure', self.registration.locator.uuid, _external=True), + 'Abort': url_for_plugin('payment_sixpay.cancel', self.registration.locator.uuid, _external=True) + }, + 'Notification': { + # where to asynchronously call back from SixPay + 'NotifyUrl': url_for_plugin('payment_sixpay.notify', self.registration.locator.uuid, _external=True) + } + } + if settings['notification_mail']: + transaction_parameters['Notification']['MerchantEmails'] = [settings['notification_mail']] + return transaction_parameters + + def _init_payment_page(self, transaction_data): + """Initialize payment page.""" + endpoint = urljoin(SixpayPaymentPlugin.settings.get('url'), SIXPAY_PP_INIT_URL) + credentials = (SixpayPaymentPlugin.settings.get('username'), SixpayPaymentPlugin.settings.get('password')) + resp = requests.post(endpoint, json=transaction_data, auth=credentials) + try: + resp.raise_for_status() + except RequestException as exc: + self.logger.error('Could not initialize payment: %s', exc.response.text) + raise Exception('Could not initialize payment') + return resp.json() + + def _process_args(self): + RHPaymentBase._process_args(self) + if 'sixpay' not in get_active_payment_plugins(self.event): + raise NotFound + if not SixpayPaymentPlugin.instance.supports_currency(self.registration.currency): + raise BadRequest + + def _process(self): + transaction_params = self._get_transaction_parameters() + init_response = self._init_payment_page(transaction_params) + payment_url = init_response['RedirectUrl'] + + # create pending transaction and store Saferpay transaction token + new_indico_txn = register_transaction( + self.registration, + self.registration.price, + self.registration.currency, + TransactionAction.pending, + PROVIDER_SIXPAY, + {'Init_PP_response': init_response} + ) + if not new_indico_txn: + # set it on the current transaction if we could not create a next one + # this happens if we already have a pending transaction and it's incredibly + # ugly... + self.registration.transaction.data = {'Init_PP_response': init_response} + return redirect(payment_url) + + class SixPayNotificationHandler(RHSixpayBase): """Handler for notification from SixPay service.""" diff --git a/payment_sixpay/indico_payment_sixpay/plugin.py b/payment_sixpay/indico_payment_sixpay/plugin.py index 959ba4b..3f2ec96 100644 --- a/payment_sixpay/indico_payment_sixpay/plugin.py +++ b/payment_sixpay/indico_payment_sixpay/plugin.py @@ -5,19 +5,10 @@ # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -from urllib.parse import urljoin - -import requests -from requests import RequestException - -from indico.core.plugins import IndicoPlugin, url_for_plugin +from indico.core.plugins import IndicoPlugin from indico.modules.events.payment import PaymentPluginMixin -from indico.modules.events.payment.models.transactions import TransactionAction -from indico.modules.events.payment.util import register_transaction from indico_payment_sixpay.forms import EventSettingsForm, PluginSettingsForm -from indico_payment_sixpay.util import (PROVIDER_SIXPAY, SIXPAY_JSON_API_SPEC, SIXPAY_PP_INIT_URL, get_request_header, - get_terminal_id, to_small_currency) class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): @@ -56,108 +47,3 @@ class SixpayPaymentPlugin(PaymentPluginMixin, IndicoPlugin): """Blueprint for URL endpoints with callbacks.""" from indico_payment_sixpay.blueprint import blueprint return blueprint - - # Dear future Maintainer, - # - the business logic is here! - # - see PaymentPluginMixin.render_payment_form for what `data` provides - # - What happens here - # - We add `success`, `cancel` and `failure` for *sixpay* to redirect the - # user back to us AFTER his request - # - We add `notify` for *sixpay* to inform us asynchronously about - # the result - # - We send a request to initialize the pyment page to SixPay to get a - # request url for this transaction - # - We put the payment page URL and token we got into `data` - # - Return uses `indico_payment_sixpay/templates/event_payment_form.html`, - # presenting a trigger button to the user - def adjust_payment_form_data(self, data): - """Prepare the payment form shown to registrants.""" - global_settings = data['settings'] - transaction = self._get_transaction_parameters(data) - init_response = self._init_payment_page( - sixpay_url=global_settings['url'], - transaction_data=transaction, - credentials=(global_settings['username'], global_settings['password']) - ) - data['payment_url'] = init_response['RedirectUrl'] - - # create pending transaction and store Saferpay transaction token - new_indico_txn = register_transaction( - data['registration'], - data['amount'], - data['currency'], - TransactionAction.pending, - PROVIDER_SIXPAY, - {'Init_PP_response': init_response} - ) - if not new_indico_txn: - # set it on the current transaction if we could not create a next one - # this happens if we already have a pending transaction and it's incredibly - # ugly... - data['registration'].transaction.data = {'Init_PP_response': init_response} - return data - - @staticmethod - def get_field_format_map(registration): - """Generate dict which provides registration information.""" - return { - 'user_id': registration.user_id, - 'user_name': registration.full_name, - 'user_firstname': registration.first_name, - 'user_lastname': registration.last_name, - 'event_id': registration.event_id, - 'event_title': registration.event.title, - 'registration_id': registration.id, - 'regform_title': registration.registration_form.title - } - - def _get_transaction_parameters(self, payment_data): - """Parameters for formulating a transaction request.""" - settings = payment_data['event_settings'] - registration = payment_data['registration'] - format_map = self.get_field_format_map(registration) - for format_field in ('order_description', 'order_identifier'): - payment_data[format_field] = settings[format_field].format(**format_map) - - # see the SixPay Manual - # https://saferpay.github.io/jsonapi/#Payment_v1_PaymentPage_Initialize - # on what these things mean - transaction_parameters = { - 'RequestHeader': get_request_header(SIXPAY_JSON_API_SPEC, settings['account_id']), - 'TerminalId': get_terminal_id(settings['account_id']), - 'Payment': { - 'Amount': { - # indico handles price as largest currency, but six expects - # smallest. E.g. EUR: indico uses 100.2 Euro, but six - # expects 10020 Cent - 'Value': str(to_small_currency(payment_data['amount'], payment_data['currency'])), - 'CurrencyCode': payment_data['currency'], - }, - 'OrderId': payment_data['order_identifier'][:80], - 'DESCRIPTION': payment_data['order_description'][:1000], - }, - # callbacks of the transaction - where to announce success etc., when redircting the user - 'ReturnUrls': { - 'Success': url_for_plugin('payment_sixpay.success', registration.locator.uuid, _external=True), - 'Fail': url_for_plugin('payment_sixpay.failure', registration.locator.uuid, _external=True), - 'Abort': url_for_plugin('payment_sixpay.cancel', registration.locator.uuid, _external=True) - }, - 'Notification': { - # where to asynchronously call back from SixPay - 'NotifyUrl': url_for_plugin('payment_sixpay.notify', registration.locator.uuid, _external=True) - } - } - if settings['notification_mail']: - transaction_parameters['Notification']['MerchantEmails'] = [settings['notification_mail']] - return transaction_parameters - - def _init_payment_page(self, sixpay_url, transaction_data, credentials): - """Initialize payment page.""" - endpoint = urljoin(sixpay_url, SIXPAY_PP_INIT_URL) - resp = requests.post(endpoint, json=transaction_data, auth=credentials) - try: - resp.raise_for_status() - except RequestException as exc: - self.logger.error('Could not initialize payment: %s', exc.response.text) - raise Exception('Could not initialize payment') - return resp.json() diff --git a/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html index e6d6afb..8d04287 100644 --- a/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html +++ b/payment_sixpay/indico_payment_sixpay/templates/event_payment_form.html @@ -1,4 +1,7 @@ -Clicking on the {% trans %}Pay Now{% endtrans %} button you will get redirected to the SixPay site in order to complete your transaction. +{% trans %} + Clicking on the Pay Now button will redirect you + to the SIXPay Saferpay site in order to complete your payment. +{% endtrans %}
{% trans %}First name{% endtrans %}
@@ -8,5 +11,9 @@ Clicking on the {% trans %}Pay Now{% endtrans %} button you wil
{% trans %}Total amount{% endtrans %}
{{ format_currency(amount, currency, locale=session.lang) }}
-
{% trans %}Pay Now{% endtrans %}
+
+ + {%- trans %}Pay Now{% endtrans -%} + +
diff --git a/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html index a24f431..14c3c31 100644 --- a/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html +++ b/payment_sixpay/indico_payment_sixpay/templates/transaction_details.html @@ -1,6 +1,8 @@ {% extends 'events/payment/transaction_details.html' %} {% block details %} {% if transaction.data.Transaction %} +
{% trans %}Transaction ID{% endtrans %}
+
{{ transaction.data.Transaction.Id }}
{% trans %}Order ID{% endtrans %}
{{ transaction.data.Transaction.OrderId }}
{% endif %}