mirror of
https://github.com/lucaspalomodevelop/indico-plugins.git
synced 2026-03-13 07:29:39 +00:00
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)
This commit is contained in:
parent
2565ad5f78
commit
613e2cc50a
@ -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/<int:event_id>/registrations/<int:reg_form_id>/payment/response/sixpay'
|
||||
url_prefix='/event/<int:event_id>/registrations/<int:reg_form_id>/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'))
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
Clicking on the <strong>{% trans %}Pay Now{% endtrans %}</strong> button you will get redirected to the SixPay site in order to complete your transaction.
|
||||
{% trans %}
|
||||
Clicking on the <strong>Pay Now</strong> button will redirect you
|
||||
to the SIXPay Saferpay site in order to complete your payment.
|
||||
{% endtrans %}
|
||||
|
||||
<dl class="i-data-list">
|
||||
<dt>{% trans %}First name{% endtrans %}</dt>
|
||||
@ -8,5 +11,9 @@ Clicking on the <strong>{% trans %}Pay Now{% endtrans %}</strong> button you wil
|
||||
<dt>{% trans %}Total amount{% endtrans %}</dt>
|
||||
<dd>{{ format_currency(amount, currency, locale=session.lang) }}</dd>
|
||||
<dt></dt>
|
||||
<dd><a class="i-button" href="{{ payment_url }}">{% trans %}Pay Now{% endtrans %}</a></dd>
|
||||
<dd>
|
||||
<a class="i-button" href="{{ url_for_plugin('payment_sixpay.init', registration.locator.registrant) }}">
|
||||
{%- trans %}Pay Now{% endtrans -%}
|
||||
</a>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{% extends 'events/payment/transaction_details.html' %}
|
||||
{% block details %}
|
||||
{% if transaction.data.Transaction %}
|
||||
<dt>{% trans %}Transaction ID{% endtrans %}</dt>
|
||||
<dd>{{ transaction.data.Transaction.Id }}</dd>
|
||||
<dt>{% trans %}Order ID{% endtrans %}</dt>
|
||||
<dd>{{ transaction.data.Transaction.OrderId }}</dd>
|
||||
{% endif %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user