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 %}