diff --git a/combo_plugin_nanterre/__init__.py b/combo_plugin_nanterre/__init__.py
new file mode 100644
index 0000000..23e556a
--- /dev/null
+++ b/combo_plugin_nanterre/__init__.py
@@ -0,0 +1,33 @@
+# combo-plugin-nanterre - Combo Nanterre plugin
+# Copyright (C) 2017 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import django.apps
+from django.utils.translation import ugettext_lazy as _
+
+
+class Plugin(object):
+ def get_apps(self):
+ return [__name__]
+
+class AppConfig(django.apps.AppConfig):
+ name = __name__
+ verbose_name = _('Nanterre extension')
+
+ def get_after_urls(self):
+ from . import urls
+ return urls.urlpatterns
+
+default_app_config = __name__ + '.AppConfig'
diff --git a/combo_plugin_nanterre/urls.py b/combo_plugin_nanterre/urls.py
new file mode 100644
index 0000000..d1597de
--- /dev/null
+++ b/combo_plugin_nanterre/urls.py
@@ -0,0 +1,28 @@
+# combo-plugin-nanterre - Combo Nanterre plugin
+# Copyright (C) 2017 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from django.conf.urls import url
+
+from .views import saga_transaction, saga_retour_asynchrone, saga_retour_synchrone
+
+urlpatterns = [
+ url('^_plugin/nanterre/saga-transaction/*$', saga_transaction,
+ name='nanterre-saga-transaction'),
+ url('^_plugin/nanterre/saga-retour-asynchrone/*$', saga_retour_asynchrone,
+ name='nanterre-saga-retour-asynchrone'),
+ url('^_plugin/nanterre/saga-retour-synchrone/*$', saga_retour_synchrone,
+ name='nanterre-saga-retour-synchrone'),
+]
diff --git a/combo_plugin_nanterre/views.py b/combo_plugin_nanterre/views.py
new file mode 100644
index 0000000..a79a1c5
--- /dev/null
+++ b/combo_plugin_nanterre/views.py
@@ -0,0 +1,163 @@
+# combo-plugin-nanterre - Combo Nanterre plugin
+# -*- coding: utf-8 -*-
+# Copyright (C) 2017 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import json
+import logging
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.template import RequestContext
+from django.views.decorators.csrf import csrf_exempt
+
+from combo.utils import requests, get_templated_url
+
+
+ERROR_MESSAGE = ("Le système de paiement n'est pas disponible, "
+ "veuillez essayer ultérieurement.")
+MESSAGE_BY_STATE = {
+ 'paye': (messages.SUCCESS, "Le paiement a bien été effectué."),
+ 'abandon': (messages.WARNING, "Le paiement a été annulé."),
+ 'refus': (messages.WARNING, "Le paiement a été refusé."),
+}
+
+
+def rsu_post(request, endpoint, payload):
+ timeout = getattr(settings, 'NANTERRE_PAYMENT_TIMEOUT', 10)
+ url = '[zoo_url]rsu/' + endpoint
+ context = RequestContext(request, {'request': request})
+ url = get_templated_url(url, context=context)
+ return requests.post(url, json=payload, timeout=timeout).json()
+
+
+@csrf_exempt
+@login_required
+def saga_transaction(request):
+ logger = logging.getLogger('combo_plugin_nanterre.saga_transaction')
+ num_factures = request.POST.getlist('num_factures')
+ email = request.POST.get('email') or request.user.email
+ error_url = request.POST.get('error_url') or '/'
+
+ urlretour_asynchrone = request.build_absolute_uri(
+ reverse('nanterre-saga-retour-asynchrone')).rstrip('/')
+ urlretour_synchrone = request.build_absolute_uri(
+ reverse('nanterre-saga-retour-synchrone')).rstrip('/')
+
+ payload = {
+ 'num_factures': num_factures,
+ 'email': email,
+ 'urlretour_asynchrone': urlretour_asynchrone,
+ 'urlretour_synchrone': urlretour_synchrone,
+ }
+ try:
+ saga = rsu_post(request, 'saga/[user_nameid]/transaction/', payload)
+ except:
+ logger.error('[rsu/saga] failed to create transaction '
+ 'for num_factures=%s', num_factures)
+ messages.error(request, ERROR_MESSAGE)
+ return HttpResponseRedirect(error_url)
+ if not isinstance(saga, dict) or not saga.get('data', {}).get('url'):
+ logger.error('[rsu/saga] failed to create transaction '
+ 'for num_factures=%s, received bad response=%r',
+ num_factures, saga)
+ messages.error(request, ERROR_MESSAGE)
+ return HttpResponseRedirect(error_url)
+ if saga.get('errors'):
+ logger.warning('[rsu/saga] failed to create transaction '
+ 'for num_factures=%s, errors=%r',
+ num_factures, saga['errors'])
+ for error in saga['errors']:
+ messages.error(request, error)
+ return HttpResponseRedirect(error_url)
+ if saga.get('err') != 0:
+ logger.warning('[rsu/saga] failed to create transaction '
+ 'for num_factures=%s, unknown error, code=%r',
+ num_factures, saga['err'])
+ messages.error(request, ERROR_MESSAGE)
+ return HttpResponseRedirect(error_url)
+
+ # finally, response seems good! redirect to payment system URL
+ logger.info('[rsu/saga] new transaction created '
+ 'for num_factures=%s, redirect to %s',
+ num_factures, saga['data']['url'])
+ return HttpResponseRedirect(saga['data']['url'])
+
+
+@csrf_exempt
+@login_required
+def saga_retour_synchrone(request):
+ logger = logging.getLogger('combo_plugin_nanterre.saga_retour_synchrone')
+ next_url = getattr(settings, 'NANTERRE_PAYMENT_RESULT_PAGE', '/')
+ idop = request.GET.get('idop')
+ payload = {'idop': idop}
+ try:
+ saga = rsu_post(request, 'saga/retour-synchrone/', payload)
+ except:
+ logger.error('[rsu/saga] retour-synchrone: cannot post idop=%s', idop)
+ messages.error(request, ERROR_MESSAGE)
+ return HttpResponseRedirect(next_url)
+
+ # add a result message and redirect
+ if (isinstance(saga, dict) and saga.get('err') == 0
+ and saga.get('data', {}).get('etat')):
+ etat = saga['data']['etat']
+ if etat in MESSAGE_BY_STATE:
+ logger.info('[rsu/saga] retour-synchrone: idop=%s etat=%s',
+ idop, etat)
+ messages.add_message(request, *MESSAGE_BY_STATE[etat])
+ else:
+ logger.error('[rsu/saga] retour-synchrone: idop=%s '
+ 'receive unknown etat=%s', idop, etat)
+ messages.error(request, ERROR_MESSAGE)
+ else:
+ logger.error('[rsu/saga] retour-synchrone: idop=%s '
+ 'receive bad response=%r', idop, saga)
+ messages.error(request, ERROR_MESSAGE)
+ return HttpResponseRedirect(next_url)
+
+
+@csrf_exempt
+def saga_retour_asynchrone(request):
+ logger = logging.getLogger('combo_plugin_nanterre.saga_retour_asynchrone')
+ idop = request.GET.get('idop')
+ payload = {'idop': idop}
+ err = 0
+ try:
+ saga = rsu_post(request, 'saga/retour-asynchrone/', payload)
+ except:
+ err = 1
+ logger.error('[rsu/saga] retour-asynchrone: cannot post idop=%s', idop)
+ else:
+ if (isinstance(saga, dict) and saga.get('err') == 0
+ and saga.get('data', {}).get('etat')):
+ etat = saga['data']['etat']
+ if etat in MESSAGE_BY_STATE:
+ logger.info('[rsu/saga] retour-asynchrone: idop=%s etat=%s',
+ idop, etat)
+ else:
+ err = 1
+ logger.error('[rsu/saga] retour-asynchrone: idop=%s '
+ 'receive unknown etat=%s', idop, etat)
+ else:
+ err = 1
+ logger.error('[rsu/saga] retour-asynchrone: idop=%s '
+ 'receive bad response=%r', idop, saga)
+ response = HttpResponse(content_type='application/json')
+ response.write(json.dumps({'err': err}))
+ return response