diff --git a/addons/payment/models/res_config.py b/addons/payment/models/res_config.py index 70668a296da..a74e595b52e 100644 --- a/addons/payment/models/res_config.py +++ b/addons/payment/models/res_config.py @@ -16,4 +16,7 @@ class AccountPaymentConfig(osv.TransientModel): 'module_payment_adyen': fields.boolean( 'Manage Payments Using Adyen', help='-It installs the module payment_adyen.'), + 'module_payment_buckaroo': fields.boolean( + 'Manage Payments Using Buckaroo', + help='-It installs the module payment_buckaroo.'), } diff --git a/addons/payment/views/res_config_view.xml b/addons/payment/views/res_config_view.xml index 101acb7c9ee..45c0a3d168e 100644 --- a/addons/payment/views/res_config_view.xml +++ b/addons/payment/views/res_config_view.xml @@ -20,6 +20,10 @@ diff --git a/addons/payment_buckaroo/__init__.py b/addons/payment_buckaroo/__init__.py new file mode 100644 index 00000000000..1f4b1b74e57 --- /dev/null +++ b/addons/payment_buckaroo/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# 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 models +import controllers diff --git a/addons/payment_buckaroo/__openerp__.py b/addons/payment_buckaroo/__openerp__.py new file mode 100644 index 00000000000..526f7c38612 --- /dev/null +++ b/addons/payment_buckaroo/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Buckaroo Payment Acquirer', + 'category': 'Hidden', + 'summary': 'Payment Acquirer: Buckaroo Implementation', + 'version': '1.0', + 'description': """Buckaroo Payment Acquirer""", + 'author': 'OpenERP SA', + 'depends': ['payment'], + 'data': [ + 'views/buckaroo.xml', + 'views/payment_acquirer.xml', + 'data/buckaroo.xml', + ], + 'installable': True, +} diff --git a/addons/payment_buckaroo/controllers/__init__.py b/addons/payment_buckaroo/controllers/__init__.py new file mode 100644 index 00000000000..bbd183e955b --- /dev/null +++ b/addons/payment_buckaroo/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import main diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py new file mode 100644 index 00000000000..d3fe5b196fc --- /dev/null +++ b/addons/payment_buckaroo/controllers/main.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +try: + import simplejson as json +except ImportError: + import json + +import logging +import pprint +import werkzeug + +from openerp import http, SUPERUSER_ID +from openerp.http import request + +_logger = logging.getLogger(__name__) + + +class BuckarooController(http.Controller): + _return_url = '/payment/buckaroo/return' + _cancel_url = '/payment/buckaroo/cancel' + _exception_url = '/payment/buckaroo/error' + _reject_url = '/payment/buckaroo/reject' + + @http.route([ + '/payment/buckaroo/return', + '/payment/buckaroo/cancel', + '/payment/buckaroo/error', + '/payment/buckaroo/reject', + ], type='http', auth='none') + def buckaroo_return(self, **post): + """ Buckaroo.""" + _logger.info('Buckaroo: entering form_feedback with post data %s', pprint.pformat(post)) # debug + request.registry['payment.transaction'].form_feedback(request.cr, SUPERUSER_ID, post, 'buckaroo', context=request.context) + return_url = post.pop('return_url', '') + if not return_url: + data ='' + post.pop('ADD_RETURNDATA', '{}').replace("'", "\"") + custom = json.loads(data) + return_url = custom.pop('return_url', '/') + return werkzeug.utils.redirect(return_url) diff --git a/addons/payment_buckaroo/data/buckaroo.xml b/addons/payment_buckaroo/data/buckaroo.xml new file mode 100644 index 00000000000..9d100a59191 --- /dev/null +++ b/addons/payment_buckaroo/data/buckaroo.xml @@ -0,0 +1,18 @@ + + + + + + Buckaroo + buckaroo + + + test + You will be redirected to the Buckaroo website after cliking on the payment button.

]]>
+ dummy + dummy +
+ +
+
diff --git a/addons/payment_buckaroo/models/__init__.py b/addons/payment_buckaroo/models/__init__.py new file mode 100644 index 00000000000..7e1780a0059 --- /dev/null +++ b/addons/payment_buckaroo/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import buckaroo diff --git a/addons/payment_buckaroo/models/buckaroo.py b/addons/payment_buckaroo/models/buckaroo.py new file mode 100644 index 00000000000..5b576a24c3b --- /dev/null +++ b/addons/payment_buckaroo/models/buckaroo.py @@ -0,0 +1,191 @@ +# -*- coding: utf-'8' "-*-" +from hashlib import sha1 +import logging +import urlparse + +from openerp.addons.payment.models.payment_acquirer import ValidationError +from openerp.addons.payment_buckaroo.controllers.main import BuckarooController +from openerp.osv import osv, fields +from openerp.tools.float_utils import float_compare + +_logger = logging.getLogger(__name__) + + +class AcquirerBuckaroo(osv.Model): + _inherit = 'payment.acquirer' + + def _get_buckaroo_urls(self, cr, uid, environment, context=None): + """ Buckaroo URLs + """ + if environment == 'prod': + return { + 'buckaroo_form_url': 'https://checkout.buckaroo.nl/html/', + } + else: + return { + 'buckaroo_form_url': 'https://testcheckout.buckaroo.nl/html/', + } + + def _get_providers(self, cr, uid, context=None): + providers = super(AcquirerBuckaroo, self)._get_providers(cr, uid, context=context) + providers.append(['buckaroo', 'Buckaroo']) + return providers + + _columns = { + 'brq_websitekey': fields.char('WebsiteKey', required_if_provider='buckaroo'), + 'brq_secretkey': fields.char('SecretKey', required_if_provider='buckaroo'), + } + + def _buckaroo_generate_digital_sign(self, acquirer, inout, values): + """ Generate the shasign for incoming or outgoing communications. + + :param browse acquirer: the payment.acquirer browse record. It should + have a shakey in shaky out + :param string inout: 'in' (openerp contacting buckaroo) or 'out' (buckaroo + contacting openerp). + :param dict values: transaction values + + :return string: shasign + """ + assert inout in ('in', 'out') + assert acquirer.provider == 'buckaroo' + + keys = "add_returndata Brq_amount Brq_culture Brq_currency Brq_invoicenumber Brq_return Brq_returncancel Brq_returnerror Brq_returnreject brq_test Brq_websitekey".split() + + def get_value(key): + if values.get(key): + return values[key] + return '' + + if inout == 'out': + if 'BRQ_SIGNATURE' in values: + del values['BRQ_SIGNATURE'] + items = sorted((k.upper(), v) for k, v in values.items()) + sign = ''.join('%s=%s' % (k, v) for k, v in items) + else: + sign = ''.join('%s=%s' % (k,get_value(k)) for k in keys) + #Add the pre-shared secret key at the end of the signature + sign = sign + acquirer.brq_secretkey + if isinstance(sign, str): + sign = urlparse.parse_qsl(sign) + shasign = sha1(sign).hexdigest() + return shasign + + + def buckaroo_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None): + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + acquirer = self.browse(cr, uid, id, context=context) + buckaroo_tx_values = dict(tx_values) + buckaroo_tx_values.update({ + 'Brq_websitekey': acquirer.brq_websitekey, + 'Brq_amount': tx_values['amount'], + 'Brq_currency': tx_values['currency'] and tx_values['currency'].name or '', + 'Brq_invoicenumber': tx_values['reference'], + 'brq_test' : True, + 'Brq_return': '%s' % urlparse.urljoin(base_url, BuckarooController._return_url), + 'Brq_returncancel': '%s' % urlparse.urljoin(base_url, BuckarooController._cancel_url), + 'Brq_returnerror': '%s' % urlparse.urljoin(base_url, BuckarooController._exception_url), + 'Brq_returnreject': '%s' % urlparse.urljoin(base_url, BuckarooController._reject_url), + 'Brq_culture': 'en-US', + }) + if buckaroo_tx_values.get('return_url'): + buckaroo_tx_values['add_returndata'] = {'return_url': '%s' % buckaroo_tx_values.pop('return_url')} + else: + buckaroo_tx_values['add_returndata'] = '' + buckaroo_tx_values['Brq_signature'] = self._buckaroo_generate_digital_sign(acquirer, 'in', buckaroo_tx_values) + return partner_values, buckaroo_tx_values + + def buckaroo_get_form_action_url(self, cr, uid, id, context=None): + acquirer = self.browse(cr, uid, id, context=context) + return self._get_buckaroo_urls(cr, uid, acquirer.environment, context=context)['buckaroo_form_url'] + +class TxBuckaroo(osv.Model): + _inherit = 'payment.transaction' + + # buckaroo status + _buckaroo_valid_tx_status = [190] + _buckaroo_pending_tx_status = [790, 791, 792, 793] + _buckaroo_cancel_tx_status = [890, 891] + _buckaroo_error_tx_status = [490, 491, 492] + _buckaroo_reject_tx_status = [690] + + _columns = { + 'buckaroo_txnid': fields.char('Transaction ID'), + } + + + # -------------------------------------------------- + # FORM RELATED METHODS + # -------------------------------------------------- + + def _buckaroo_form_get_tx_from_data(self, cr, uid, data, context=None): + """ Given a data dict coming from buckaroo, verify it and find the related + transaction record. """ + reference, pay_id, shasign = data.get('BRQ_INVOICENUMBER'), data.get('BRQ_PAYMENT'), data.get('BRQ_SIGNATURE') + if not reference or not pay_id or not shasign: + error_msg = 'Buckaroo: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % (reference, pay_id, shasign) + _logger.error(error_msg) + raise ValidationError(error_msg) + + tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context) + if not tx_ids or len(tx_ids) > 1: + error_msg = 'Buckaroo: received data for reference %s' % (reference) + if not tx_ids: + error_msg += '; no order found' + else: + error_msg += '; multiple order found' + _logger.error(error_msg) + raise ValidationError(error_msg) + tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context) + + #verify shasign + shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out' ,data) + if shasign_check.upper() != shasign.upper(): + error_msg = 'Buckaroo: invalid shasign, received %s, computed %s, for data %s' % (shasign, shasign_check, data) + _logger.error(error_msg) + raise ValidationError(error_msg) + + return tx + + def _buckaroo_form_get_invalid_parameters(self, cr, uid, tx, data, context=None): + invalid_parameters = [] + + if tx.acquirer_reference and data.get('BRQ_TRANSACTIONS') != tx.acquirer_reference: + invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), tx.acquirer_reference)) + # check what is buyed + if float_compare(float(data.get('BRQ_AMOUNT', '0.0')), tx.amount, 2) != 0: + invalid_parameters.append(('Amount', data.get('BRQ_AMOUNT'), '%.2f' % tx.amount)) + if data.get('BRQ_CURRENCY') != tx.currency_id.name: + invalid_parameters.append(('Currency', data.get('BRQ_CURRENCY'), tx.currency_id.name)) + + return invalid_parameters + + def _buckaroo_form_validate(self, cr, uid, tx, data, context=None): + status_code = int(data.get('BRQ_STATUSCODE','0')) + if status_code in self._buckaroo_valid_tx_status: + tx.write({ + 'state': 'done', + 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'), + }) + return True + elif status_code in self._buckaroo_pending_tx_status: + tx.write({ + 'state': 'pending', + 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'), + }) + return True + elif status_code in self._buckaroo_cancel_tx_status: + tx.write({ + 'state': 'cancel', + 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'), + }) + return True + else: + error = 'Buckaroo: feedback error' + _logger.info(error) + tx.write({ + 'state': 'error', + 'state_message': error, + 'buckaroo_txnid': data.get('BRQ_TRANSACTIONS'), + }) + return False diff --git a/addons/payment_buckaroo/static/description/icon.png b/addons/payment_buckaroo/static/description/icon.png new file mode 100644 index 00000000000..663fcad1d6d Binary files /dev/null and b/addons/payment_buckaroo/static/description/icon.png differ diff --git a/addons/payment_buckaroo/static/src/img/buckaroo_icon.png b/addons/payment_buckaroo/static/src/img/buckaroo_icon.png new file mode 100644 index 00000000000..819606db730 Binary files /dev/null and b/addons/payment_buckaroo/static/src/img/buckaroo_icon.png differ diff --git a/addons/payment_buckaroo/static/src/img/logo.png b/addons/payment_buckaroo/static/src/img/logo.png new file mode 100644 index 00000000000..663fcad1d6d Binary files /dev/null and b/addons/payment_buckaroo/static/src/img/logo.png differ diff --git a/addons/payment_buckaroo/tests/__init__.py b/addons/payment_buckaroo/tests/__init__.py new file mode 100644 index 00000000000..d245ab8339d --- /dev/null +++ b/addons/payment_buckaroo/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from openerp.addons.payment_buckaroo.tests import test_buckaroo + +checks = [ + test_buckaroo, +] diff --git a/addons/payment_buckaroo/tests/test_buckaroo.py b/addons/payment_buckaroo/tests/test_buckaroo.py new file mode 100644 index 00000000000..b826f3b920e --- /dev/null +++ b/addons/payment_buckaroo/tests/test_buckaroo.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- + +from lxml import objectify +import urlparse + +import openerp +from openerp.addons.payment.models.payment_acquirer import ValidationError +from openerp.addons.payment.tests.common import PaymentAcquirerCommon +from openerp.addons.payment_buckaroo.controllers.main import BuckarooController +from openerp.tools import mute_logger + + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(False) +class BuckarooCommon(PaymentAcquirerCommon): + + def setUp(self): + super(BuckarooCommon, self).setUp() + cr, uid = self.cr, self.uid + self.base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url') + + # get the buckaroo account + model, self.buckaroo_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_buckaroo', 'payment_acquirer_buckaroo') + + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(False) +class BuckarooForm(BuckarooCommon): + + def test_10_Buckaroo_form_render(self): + cr, uid, context = self.cr, self.uid, {} + # be sure not to do stupid things + buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None) + self.assertEqual(buckaroo.environment, 'test', 'test without test environment') + + # ---------------------------------------- + # Test: button direct rendering + # ---------------------------------------- + + form_values = { + 'add_returndata': None, + 'Brq_websitekey': buckaroo.brq_websitekey, + 'Brq_amount': '2240.0', + 'Brq_currency': 'EUR', + 'Brq_invoicenumber': 'SO004', + 'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me + 'brq_test': 'True', + 'Brq_return': '%s' % urlparse.urljoin(self.base_url, BuckarooController._return_url), + 'Brq_returncancel': '%s' % urlparse.urljoin(self.base_url, BuckarooController._cancel_url), + 'Brq_returnerror': '%s' % urlparse.urljoin(self.base_url, BuckarooController._exception_url), + 'Brq_returnreject': '%s' % urlparse.urljoin(self.base_url, BuckarooController._reject_url), + 'Brq_culture': 'en-US', + } + + # render the button + res = self.payment_acquirer.render( + cr, uid, self.buckaroo_id, + 'SO004', 2240.0, self.currency_euro_id, + partner_id=None, + partner_values=self.buyer_values, + context=context) + + # check form result + tree = objectify.fromstring(res) + self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + for form_input in tree.input: + if form_input.get('name') in ['submit']: + continue + self.assertEqual( + form_input.get('value'), + form_values[form_input.get('name')], + 'Buckaroo: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')]) + ) + + # ---------------------------------------- + # Test2: button using tx + validation + # ---------------------------------------- + + # create a new draft tx + tx_id = self.payment_transaction.create( + cr, uid, { + 'amount': 2240.0, + 'acquirer_id': self.buckaroo_id, + 'currency_id': self.currency_euro_id, + 'reference': 'SO004', + 'partner_id': self.buyer_id, + }, context=context + ) + + # render the button + res = self.payment_acquirer.render( + cr, uid, self.buckaroo_id, + 'should_be_erased', 2240.0, self.currency_euro, + tx_id=tx_id, + partner_id=None, + partner_values=self.buyer_values, + context=context) + + # check form result + tree = objectify.fromstring(res) + self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url') + for form_input in tree.input: + if form_input.get('name') in ['submit']: + continue + self.assertEqual( + form_input.get('value'), + form_values[form_input.get('name')], + 'Buckaroo: wrong value for form input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')]) + ) + + @mute_logger('openerp.addons.payment_buckaroo.models.buckaroo', 'ValidationError') + def test_20_buckaroo_form_management(self): + cr, uid, context = self.cr, self.uid, {} + # be sure not to do stupid thing + buckaroo = self.payment_acquirer.browse(self.cr, self.uid, self.buckaroo_id, None) + self.assertEqual(buckaroo.environment, 'test', 'test without test environment') + + # typical data posted by buckaroo after client has successfully paid + buckaroo_post_data = { + 'BRQ_RETURNDATA': u'', + 'BRQ_AMOUNT': u'2240.00', + 'BRQ_CURRENCY': u'EUR', + 'BRQ_CUSTOMER_NAME': u'Jan de Tester', + 'BRQ_INVOICENUMBER': u'SO004', + 'BRQ_PAYMENT': u'573311D081B04069BD6336001611DBD4', + 'BRQ_PAYMENT_METHOD': u'paypal', + 'BRQ_SERVICE_PAYPAL_PAYERCOUNTRY': u'NL', + 'BRQ_SERVICE_PAYPAL_PAYEREMAIL': u'fhe@openerp.com', + 'BRQ_SERVICE_PAYPAL_PAYERFIRSTNAME': u'Jan', + 'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester', + 'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de', + 'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified', + 'BRQ_SIGNATURE': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91', + 'BRQ_STATUSCODE': u'190', + 'BRQ_STATUSCODE_DETAIL': u'S001', + 'BRQ_STATUSMESSAGE': u'Transaction successfully processed', + 'BRQ_TEST': u'true', + 'BRQ_TIMESTAMP': u'2014-05-08 12:41:21', + 'BRQ_TRANSACTIONS': u'D6106678E1D54EEB8093F5B3AC42EA7B', + 'BRQ_WEBSITEKEY': u'5xTGyGyPyl', + } + + # should raise error about unknown tx + with self.assertRaises(ValidationError): + self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context) + + tx_id = self.payment_transaction.create( + cr, uid, { + 'amount': 2240.0, + 'acquirer_id': self.buckaroo_id, + 'currency_id': self.currency_euro_id, + 'reference': 'SO004', + 'partner_name': 'Norbert Buyer', + 'partner_country_id': self.country_france_id, + }, context=context + ) + # validate it + self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context) + # check state + tx = self.payment_transaction.browse(cr, uid, tx_id, context=context) + self.assertEqual(tx.state, 'done', 'Buckaroo: validation did not put tx into done state') + self.assertEqual(tx.buckaroo_txnid, buckaroo_post_data.get('BRQ_TRANSACTIONS'), 'Buckaroo: validation did not update tx payid') + + # reset tx + tx.write({'state': 'draft', 'date_validate': False, 'buckaroo_txnid': False}) + + # now buckaroo post is ok: try to modify the SHASIGN + buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a' + with self.assertRaises(ValidationError): + self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context) + + # simulate an error + buckaroo_post_data['BRQ_STATUSCODE'] = 2 + buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b' + self.payment_transaction.form_feedback(cr, uid, buckaroo_post_data, 'buckaroo', context=context) + # check state + tx = self.payment_transaction.browse(cr, uid, tx_id, context=context) + self.assertEqual(tx.state, 'error', 'Buckaroo: erroneous validation did not put tx into error state') diff --git a/addons/payment_buckaroo/views/buckaroo.xml b/addons/payment_buckaroo/views/buckaroo.xml new file mode 100644 index 00000000000..567a2691b00 --- /dev/null +++ b/addons/payment_buckaroo/views/buckaroo.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/addons/payment_buckaroo/views/payment_acquirer.xml b/addons/payment_buckaroo/views/payment_acquirer.xml new file mode 100644 index 00000000000..4eb294e49c3 --- /dev/null +++ b/addons/payment_buckaroo/views/payment_acquirer.xml @@ -0,0 +1,35 @@ + + + + + + acquirer.form.buckaroo + payment.acquirer + + + + + + + + + + + + + acquirer.transaction.form.buckaroo + payment.transaction + + + + + + + + + + + + + +