From 54ae881dd99653927ac0df5556bbf560beeb6082 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 9 Sep 2020 22:47:58 +0200 Subject: [PATCH] add Saga payment method (#46502) --- eopayment/__init__.py | 5 +- eopayment/saga.py | 246 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 eopayment/saga.py diff --git a/eopayment/__init__.py b/eopayment/__init__.py index 245c5e2..cfd84ec 100644 --- a/eopayment/__init__.py +++ b/eopayment/__init__.py @@ -31,7 +31,7 @@ from .common import ( # noqa: F401 __all__ = ['Payment', 'URL', 'HTML', 'FORM', 'SIPS', 'SYSTEMPAY', 'SPPLUS', 'TIPI', 'DUMMY', 'get_backend', 'RECEIVED', 'ACCEPTED', 'PAID', 'DENIED', 'CANCELED', 'CANCELLED', 'ERROR', 'WAITING', - 'get_backends', 'PAYFIP_WS'] + 'get_backends', 'PAYFIP_WS', 'SAGA'] if six.PY3: __all__.extend(['KEYWARE', 'MOLLIE']) @@ -48,6 +48,7 @@ PAYZEN = 'payzen' PAYFIP_WS = 'payfip_ws' KEYWARE = 'keyware' MOLLIE = 'mollie' +SAGA = 'saga' logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ def get_backend(kind): return module.Payment __BACKENDS = [DUMMY, SIPS, SIPS2, SYSTEMPAY, SPPLUS, OGONE, PAYBOX, PAYZEN, - TIPI, PAYFIP_WS, KEYWARE, MOLLIE] + TIPI, PAYFIP_WS, KEYWARE, MOLLIE, SAGA] def get_backends(): diff --git a/eopayment/saga.py b/eopayment/saga.py new file mode 100644 index 0000000..672c888 --- /dev/null +++ b/eopayment/saga.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +# eopayment - online payment library +# Copyright (C) 2011-2020 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 __future__ import unicode_literals, print_function + +import functools + +from six.moves.urllib.parse import urljoin, parse_qs + +import lxml.etree as ET +import zeep +import zeep.exceptions + +from .common import (PaymentException, PaymentCommon, ResponseError, URL, PAID, + DENIED, CANCELLED, ERROR, PaymentResponse) + + +class SagaError(PaymentException): + pass + + +class Saga(object): + def __init__(self, wsdl_url, service_url=None, zeep_client_kwargs=None): + self.wsdl_url = wsdl_url + self.client = zeep.Client(wsdl_url, **(zeep_client_kwargs or {})) + # distribued WSDL is wrong :/ + if service_url: + self.client.service._binding_options['address'] = service_url + + def soap_call(self, operation, content_tag, **kwargs): + content = getattr(self.client.service, operation)(**kwargs) + if 'ISO-8859-1' in content: + encoded_content = content.encode('latin1') + else: + encoded_content = content.encode('utf-8') + try: + tree = ET.fromstring(encoded_content) + except Exception: + raise SagaError('Invalid SAGA response "%s"' % content[:1024]) + if tree.tag == 'erreur': + raise SagaError(tree.text) + if tree.tag != content_tag: + raise SagaError('Invalid SAGA response "%s"' % content[:1024]) + return tree + + def transaction(self, num_service, id_tiers, compte, lib_ecriture, montant, + urlretour_asynchrone, email, urlretour_synchrone): + tree = self.soap_call( + 'Transaction', + 'url', + num_service=num_service, + id_tiers=id_tiers, + compte=compte, + lib_ecriture=lib_ecriture, + montant=montant, + urlretour_asynchrone=urlretour_asynchrone, + email=email, + urlretour_synchrone=urlretour_synchrone) + # tree == ... + return tree.text + + def page_retour(self, operation, idop): + tree = self.soap_call( + operation, + 'ok', + idop=idop) + # tree == + return tree.attrib + + def page_retour_synchrone(self, idop): + return self.page_retou('PageRetourSynchrone', idop) + + def page_retour_asynchrone(self, idop): + return self.page_retou('PageRetourAsynchrone', idop) + + +class Payment(PaymentCommon): + description = { + 'caption': 'Système de paiement Saga / SAGA', + 'parameters': [ + { + 'name': 'base_url', + 'caption': 'URL de base du WSDL', + 'help_text': 'Sans la partie /paiement_internet_ws_ministere?wsdl', + 'required': True, + }, + { + 'name': 'num_service', + 'caption': 'Numéro du service', + 'required': True, + }, + { + 'name': 'compte', + 'caption': 'Compte de recettes', + 'required': True, + }, + { + 'name': 'normal_return_url', + 'caption': 'Normal return URL', + 'default': '', + 'required': True, + }, + { + 'name': 'automatic_return_url', + 'caption': 'Automatic return URL (ignored, must be set in Ogone backoffice)', + 'required': False, + }, + ] + } + + @property + def saga(self): + return Saga( + wsdl_url=urljoin(self.base_url, 'paiement_internet_ws_ministere?wsdl')) + + def request(self, amount, email, description, **kwargs): + num_service = self.num_service + id_tiers = '-1' + compte = self.compte + lib_ecriture = description + montant = self.clean_amount(amount, max_amount=100000, cents=False) + urlretour_synchrone = self.normal_return_url + # email + urlretour_asynchrone = self.automatic_return_url + + url = self.saga.transaction( + num_service=num_service, + id_tiers=id_tiers, + compte=compte, + lib_ecriture=lib_ecriture, + montant=montant, + urlretour_asynchrone=urlretour_asynchrone, + email=email, + urlretour_synchrone=urlretour_synchrone) + + try: + idop = parse_qs(url.split('?', 1)[-1])['idop'][0] + except Exception: + raise SagaError('Invalid payment URL, no idop: %s' % url) + + return str(idop), URL, url + + def response(self, query_string, **kwargs): + fields = parse_qs(query_string, True) + idop = (fields.get('idop') or [None])[0] + + if not idop: + raise ResponseError('missing idop parameter in query') + + redirect = kwargs.get('redirect', False) + + if redirect: + response = self.saga.page_retour_synchrone(idop=idop) + else: + response = self.saga.page_retour_asynchrone(idop=idop) + + etat = response['etat'] + if etat == 'paye': + result = PAID + bank_status = 'paid' + elif etat == 'refus': + result = DENIED + bank_status = 'refused' + elif etat == 'abandon': + result = CANCELLED + bank_status = 'cancelled' + else: + result = ERROR + bank_status = 'unknown result code: etat=%r' % etat + + return PaymentResponse( + result=result, + bank_status=bank_status, + signed=True, + bank_data=response, + order_id=idop, + transaction_id=idop, + test=False) + + +if __name__ == '__main__': + import click + + def show_payfip_error(func): + @functools.wraps(func) + def f(*args, **kwargs): + try: + return func(*args, **kwargs) + except SagaError as e: + click.echo(click.style('SAGA ERROR : %s' % e, fg='red')) + return f + + @click.group() + @click.option('--wsdl-url') + @click.option('--service-url', default=None) + @click.pass_context + def main(ctx, wsdl_url, service_url): + import logging + logging.basicConfig(level=logging.INFO) + # hide warning from zeep + logging.getLogger('zeep.wsdl.bindings.soap').level = logging.ERROR + + ctx.obj = Saga(wsdl_url=wsdl_url, service_url=service_url) + + @main.command() + @click.option('--num-service', type=str, required=True) + @click.option('--id-tiers', type=str, required=True) + @click.option('--compte', type=str, required=True) + @click.option('--lib-ecriture', type=str, required=True) + @click.option('--montant', type=str, required=True) + @click.option('--urlretour-asynchrone', type=str, required=True) + @click.option('--email', type=str, required=True) + @click.option('--urlretour-synchrone', type=str, required=True) + @click.pass_obj + @show_payfip_error + def transaction(saga, num_service, id_tiers, compte, lib_ecriture, montant, + urlretour_asynchrone, email, urlretour_synchrone): + url = saga.transaction( + num_service=num_service, + id_tiers=id_tiers, + compte=compte, + lib_ecriture=lib_ecriture, + montant=montant, + urlretour_asynchrone=urlretour_asynchrone, + email=email, + urlretour_synchrone=urlretour_synchrone) + print('url:', url) + + main() -- 2.28.0