[ADD] payment_acquirer_adyen: Adyen payment acquirer

Added support of adyen, only form-based. Please refer to adyen.com for more details
about the acquirer. This is a classic implementation of a form-based acquirer with
its controller, and alread integrated in the ecommerce.

Tests will be improved.

bzr revid: tde@openerp.com-20131202150342-191ipeoeru0jssah
This commit is contained in:
Thibault Delavallée 2013-12-02 16:03:42 +01:00
parent 37c3ed60f6
commit 19ea5b9674
15 changed files with 605 additions and 0 deletions

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import controllers

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
{
'name': 'Adyen Payment Acquirer',
'category': 'Hidden',
'summary': 'Payment Acquirer: Adyen Implementation',
'version': '1.0',
'description': """Adyen Payment Acquirer""",
'author': 'OpenERP SA',
'depends': ['payment_acquirer'],
'data': [
'views/adyen.xml',
'views/payment_acquirer.xml',
'data/adyen.xml',
],
'installable': True,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.addons.website.models import website
try:
import simplejson as json
except ImportError:
import json
import logging
import pprint
_logger = logging.getLogger(__name__)
class AdyenController(http.Controller):
_return_url = '/payment/adyen/return/'
@website.route([
'/payment/adyen/return/',
], type='http', auth='public')
def adyen_return(self, **post):
""" Paypal IPN. """
_logger.info('Beginning Adyen form_feedback with post data %s', pprint.pformat(post)) # debug
request.registry['payment.transaction'].form_feedback(request.cr, request.uid, post, 'adyen', context=request.context)
return_url = post.pop('return_url', '')
if not return_url:
custom = json.loads(post.pop('merchantReturnData', '{}'))
return_url = custom.pop('return_url', '/')
return request.redirect(return_url)

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="payment_acquirer_adyen" model="payment.acquirer">
<field name="name">adyen</field>
<field name="view_template_id" ref="adyen_acquirer_button"/>
<field name="env">test</field>
<field name="adyen_merchant_account">OpenERPCOM</field>
<field name="adyen_skin_code">cbqYWvVL</field>
<field name="adyen_skin_hmac_key">cbqYWvVL</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- Module creation

View File

@ -0,0 +1,14 @@
Payment module documentation
============================
Payment documentation topics
''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import adyen

View File

@ -0,0 +1,198 @@
# -*- coding: utf-'8' "-*-"
import base64
try:
import simplejson as json
except ImportError:
import json
from hashlib import sha1
import hmac
import logging
import urlparse
from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
from openerp.addons.payment_acquirer_adyen.controllers.main import AdyenController
from openerp.osv import osv, fields
from openerp.tools import float_round
_logger = logging.getLogger(__name__)
class AcquirerAdyen(osv.Model):
_inherit = 'payment.acquirer'
def _get_adyen_urls(self, cr, uid, ids, name, args, context=None):
""" Adyen URLs
- yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple
"""
res = {}
for acquirer in self.browse(cr, uid, ids, context=context):
qualif = acquirer.env
res[acquirer.id] = {
'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % qualif,
}
return res
_columns = {
'adyen_merchant_account': fields.char('Merchant Account', required_if_provider='adyen'),
'adyen_skin_code': fields.char('Skin Code', required_if_provider='adyen'),
'adyen_skin_hmac_key': fields.char('Skin HMAC Key', required_if_provider='adyen'),
'adyen_form_url': fields.function(
_get_adyen_urls, multi='_get_adyen_urls',
type='char', string='Transaction URL', required_if_provider='adyen'),
}
def _adyen_generate_merchant_sig(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 ogone) or 'out' (adyen
contacting openerp). In this last case only some
fields should be contained (see e-Commerce basic)
:param dict values: transaction values
:return string: shasign
"""
assert inout in ('in', 'out')
assert acquirer.name == 'adyen'
if inout == 'in':
keys = "paymentAmount currencyCode shipBeforeDate merchantReference skinCode merchantAccount sessionValidity shopperEmail shopperReference recurringContract allowedMethods blockedMethods shopperStatement merchantReturnData billingAddressType deliveryAddressType offset".split()
else:
keys = "authResult pspReference merchantReference skinCode paymentMethod shopperLocale merchantReturnData".split()
def get_value(key):
if values.get(key):
return values[key]
return ''
sign = ''.join('%s' % get_value(k) for k in keys).encode('ascii')
key = acquirer.adyen_skin_hmac_key.encode('ascii')
return base64.b64encode(hmac.new(key, sign, sha1).digest())
def adyen_form_generate_values(self, cr, uid, id, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None, context=None):
if partner_values is None:
partner_values = {}
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
acquirer = self.browse(cr, uid, id, context=context)
# tmp
import datetime
from dateutil import relativedelta
tmp_date = datetime.date.today() + relativedelta.relativedelta(days=1)
partner = None
if partner_id:
partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
tx_values = {
'merchantReference': reference,
'paymentAmount': '%d' % int(float_round(amount, 2) * 100),
'currencyCode': currency and currency.name or 'EUR',
'shipBeforeDate': tmp_date,
'skinCode': acquirer.adyen_skin_code,
'merchantAccount': acquirer.adyen_merchant_account,
'shopperLocale': partner and partner.lang or partner_values.get('lang', 'en_US'),
'sessionValidity': tmp_date,
'merchantSig': 'oij',
'resURL': '%s' % urlparse.urljoin(base_url, AdyenController._return_url),
}
if tx_custom_values and tx_custom_values.get('return_url'):
tx_values['merchantReturnData'] = json.dumps({'return_url': '%s' % tx_custom_values.pop('return_url')})
if tx_custom_values:
tx_values.update(tx_custom_values)
tx_values['merchantSig'] = self._adyen_generate_merchant_sig(acquirer, 'in', tx_values)
return tx_values
def adyen_get_form_action_url(self, cr, uid, id, context=None):
acquirer = self.browse(cr, uid, id, context=context)
return acquirer.adyen_form_url
class TxAdyen(osv.Model):
_inherit = 'payment.transaction'
_columns = {
'adyen_psp_reference': fields.char('Adyen PSP Reference'),
}
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
def adyen_form_generate_values(self, cr, uid, id, tx_custom_values=None, context=None):
tx = self.browse(cr, uid, id, context=context)
tx_data = {
'shopperLocale': tx.partner_lang,
}
if tx_custom_values:
tx_data.update(tx_custom_values)
return self.pool['payment.acquirer'].paypal_form_generate_values(
cr, uid, tx.acquirer_id.id,
tx.reference, tx.amount, tx.currency_id,
tx_custom_values=tx_data,
context=context
)
def _adyen_form_get_tx_from_data(self, cr, uid, data, context=None):
reference, pspReference = data.get('merchantReference'), data.get('pspReference')
if not reference or not pspReference:
error_msg = 'Adyen: received data with missing reference (%s) or missing pspReference (%s)' % (reference, pspReference)
_logger.error(error_msg)
raise ValidationError(error_msg)
# find tx -> @TDENOTE use pspReference ?
tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
if not tx_ids or len(tx_ids) > 1:
error_msg = 'Adyen: 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']._adyen_generate_merchant_sig(tx.acquirer_id, 'out', data)
if shasign_check != data.get('merchantSig'):
error_msg = 'Adyen: invalid merchantSig, received %s, computed %s' % (data.get('merchantSig'), shasign_check)
_logger.warning(error_msg)
# raise ValidationError(error_msg)
return tx
def _adyen_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
# TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
invalid_parameters = []
if data.get('skinCode') != tx.acquirer_id.adyen_skin_code:
invalid_parameters.append(('skinCode', data.get('skinCode'), tx.acquirer_id.adyen_skin_code))
if not data.get('authResult'):
invalid_parameters.append(('authResult', data.get('authResult'), 'something'))
return invalid_parameters
def _adyen_form_validate(self, cr, uid, tx, data, context=None):
status = data.get('authResult', 'PENDING')
if status == 'AUTHORISED':
tx.write({
'state': 'done',
'adyen_psp_reference': data.get('pspReference'),
# 'date_validate': data.get('payment_date', fields.datetime.now()),
# 'paypal_txn_type': data.get('express_checkout')
})
return True
elif status == 'PENDING':
tx.write({
'state': 'pending',
'adyen_psp_reference': data.get('pspReference'),
})
return True
else:
error = 'Paypal: feedback error'
_logger.info(error)
tx.write({
'state': 'error',
'state_message': error
})
return False

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import test_adyen
checks = [
test_adyen,
]

View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
from openerp.addons.payment_acquirer.tests.common import PaymentAcquirerCommon
from openerp.addons.payment_acquirer_adyen.controllers.main import AdyenController
from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
from lxml import objectify
import urlparse
class AdyenCommon(PaymentAcquirerCommon):
def setUp(self):
super(AdyenCommon, self).setUp()
cr, uid = self.cr, self.uid
self.base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url')
model, self.paypal_view_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_acquirer_adyen', 'adyen_acquirer_button')
# create a new ogone account
self.adyen_id = self.payment_acquirer.create(
cr, uid, {
'name': 'adyen',
'env': 'test',
'view_template_id': self.paypal_view_id,
'adyen_merchant_account': 'OpenERP',
'adyen_skin_code': 'cbqYWvVL',
'adyen_skin_hmac_key': 'cbqYWvVL',
})
# some CC (always use expiration date 06 / 2016, cvc 737, cid 7373 (amex))
self.amex = (('370000000000002', '7373'))
self.dinersclub = (('36006666333344', '737'))
self.discover = (('6011601160116611', '737'), ('644564456445644', '737'))
self.jcb = (('3530111333300000', '737'))
self.mastercard = (('5555444433331111', '737'), ('5555555555554444', '737'))
self.visa = (('4111 1111 1111 1111', '737'), ('4444333322221111', '737'))
self.mcdebit = (('5500000000000004', '737'))
self.visadebit = (('4400000000000008', '737'))
self.maestro = (('6731012345678906', '737'))
self.laser = (('630495060000000000', '737'))
self.hipercard = (('6062828888666688', '737'))
self.dsmastercard = (('521234567890 1234', '737', 'user', 'password'))
self.dsvisa = (('4212345678901237', '737', 'user', 'password'))
self.mistercash = (('6703444444444449', None, 'user', 'password'))
class AdyenServer2Server(AdyenCommon):
def test_00_tx_management(self):
cr, uid, context = self.cr, self.uid, {}
# res = self.payment_acquirer._paypal_s2s_get_access_token(cr, uid, [self.paypal_id], context=context)
# self.assertTrue(res[self.paypal_id] is not False, 'paypal: did not generate access token')
# tx_id = self.payment_transaction.s2s_create(
# cr, uid, {
# 'amount': 0.01,
# 'acquirer_id': self.paypal_id,
# 'currency_id': self.currency_euro_id,
# 'reference': 'test_reference',
# 'partner_id': self.buyer_id,
# }, {
# 'number': self.visa[0][0],
# 'cvc': self.visa[0][1],
# 'brand': 'visa',
# 'expiry_mm': 9,
# 'expiry_yy': 2015,
# }, context=context
# )
# tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
# self.assertTrue(tx.paypal_txn_id is not False, 'paypal: txn_id should have been set after s2s request')
# self.payment_transaction.write(cr, uid, tx_id, {'paypal_txn_id': False}, context=context)
class AdyenForm(AdyenCommon):
def test_10_adyen_form_render(self):
cr, uid, context = self.cr, self.uid, {}
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
form_values = {
'cmd': '_xclick',
'business': 'tde+paypal-facilitator@openerp.com',
'item_name': 'test_ref0',
'item_number': 'test_ref0',
'first_name': 'Buyer',
'last_name': 'Norbert',
'amount': '0.01',
'currency_code': 'EUR',
'address1': 'Huge Street 2/543',
'city': 'Sin City',
'zip': '1000',
'country': 'Belgium',
'email': 'norbert.buyer@example.com',
'return': '%s' % urlparse.urljoin(self.base_url, AdyenController._return_url),
}
# render the button
res = self.payment_acquirer.render(
cr, uid, self.adyen_id,
'test_ref0', 0.01, self.currency_euro,
partner_id=None,
partner_values=self.buyer_values,
context=context)
print res
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'paypal: 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')],
'paypal: wrong value for form: received %s instead of %s' % (form_input.get('value'), form_values[form_input.get('name')])
)
@mute_logger('openerp.addons.payment_acquirer_paypal.models.paypal', 'ValidationError')
def test_20_paypal_form_management(self):
cr, uid, context = self.cr, self.uid, {}
# typical data posted by paypal after client has successfully paid
paypal_post_data = {
'protection_eligibility': u'Ineligible',
'last_name': u'Poilu',
'txn_id': u'08D73520KX778924N',
'receiver_email': u'tde+paypal-facilitator@openerp.com',
'payment_status': u'Pending',
'payment_gross': u'',
'tax': u'0.00',
'residence_country': u'FR',
'address_state': u'Alsace',
'payer_status': u'verified',
'txn_type': u'web_accept',
'address_street': u'Av. de la Pelouse, 87648672 Mayet',
'handling_amount': u'0.00',
'payment_date': u'03:21:19 Nov 18, 2013 PST',
'first_name': u'Norbert',
'item_name': u'test_ref_2',
'address_country': u'France',
'charset': u'windows-1252',
'custom': u'',
'notify_version': u'3.7',
'address_name': u'Norbert Poilu',
'pending_reason': u'multi_currency',
'item_number': u'test_ref_2',
'receiver_id': u'DEG7Z7MYGT6QA',
'transaction_subject': u'',
'business': u'tde+paypal-facilitator@openerp.com',
'test_ipn': u'1',
'payer_id': u'VTDKRZQSAHYPS',
'verify_sign': u'An5ns1Kso7MWUdW4ErQKJJJ4qi4-AVoiUf-3478q3vrSmqh08IouiYpM',
'address_zip': u'75002',
'address_country_code': u'FR',
'address_city': u'Paris',
'address_status': u'unconfirmed',
'mc_currency': u'EUR',
'shipping': u'0.00',
'payer_email': u'tde+buyer@openerp.com',
'payment_type': u'instant',
'mc_gross': u'1.95',
'ipn_track_id': u'866df2ccd444b',
'quantity': u'1'
}
# should raise error about unknown tx
with self.assertRaises(ValidationError):
self.payment_transaction.form_feedback(cr, uid, paypal_post_data, 'paypal', context=context)
# create tx
tx_id = self.payment_transaction.create(
cr, uid, {
'amount': 1.95,
'acquirer_id': self.paypal_id,
'currency_id': self.currency_euro_id,
'reference': 'test_ref_2',
'partner_name': 'Norbert Buyer',
}, context=context
)
# validate it
self.payment_transaction.form_feedback(cr, uid, paypal_post_data, 'paypal', context=context)
# check
tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification')
self.assertEqual(tx.paypal_txn_id, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertFalse(tx.date_validate, 'paypal: validation date should not be updated whenr receiving pending notification')
# update tx
self.payment_transaction.write(cr, uid, [tx_id], {
'state': 'draft',
'paypal_txn_id': False,
}, context=context)
# update notification from paypal
paypal_post_data['payment_status'] = 'Completed'
# validate it
self.payment_transaction.form_feedback(cr, uid, paypal_post_data, 'paypal', context=context)
# check
tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.paypal_txn_id, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertEqual(tx.date_validate, '2013-11-18 03:21:19', 'paypal: wrong validation date')
# {'authResult': u'AUTHORISED',
# 'merchantReference': u'SO014',
# 'merchantReturnData': u'return_url=/shop/payment/validate',
# 'merchantSig': u'GaLRO8aMHFaQX3gQ5BVP/YETzeA=',
# 'paymentMethod': u'visa',
# 'pspReference': u'8813859935907337',
# 'shopperLocale': u'en_US',
# 'skinCode': u'cbqYWvVL'}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<template id="adyen_acquirer_button">
<form t-if="acquirer.adyen_merchant_account" t-att-action="acquirer.adyen_form_url" method="post" target="_self">
<input type="hidden" name="merchantReference" t-att-value="tx_values['merchantReference']"/>
<input type="hidden" name="paymentAmount" t-att-value="tx_values['paymentAmount']"/>
<input type="hidden" name="currencyCode" t-att-value="tx_values['currencyCode']"/>
<input type="hidden" name="shipBeforeDate" t-att-value="tx_values['shipBeforeDate']"/>
<input type="hidden" name="skinCode" t-att-value="tx_values['skinCode']"/>
<input type="hidden" name="merchantAccount" t-att-value="tx_values['merchantAccount']"/>
<input type="hidden" name="shopperLocale" t-att-value="tx_values['shopperLocale']"/>
<input type="hidden" name="sessionValidity" t-att-value="tx_values['sessionValidity']"/>
<input type="hidden" name="merchantSig" t-att-value="tx_values['merchantSig']"/>
<!-- URLs -->
<input t-if="tx_values.get('resURL')" type='hidden' name='resURL'
t-att-value="tx_values.get('resURL')"/>
<!-- custom -->
<input t-if="tx_values.get('merchantReturnData')" type='hidden' name='merchantReturnData'
t-att-value="tx_values.get('merchantReturnData')"/>
<!-- button -->
<input type="image" name="submit" id="payment_submit"
width="100px"
src="/payment_acquirer_adyen/static/src/img/logo.jpg"/>
</form>
</template>
</data>
</openerp>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form_adyen" model="ir.ui.view">
<field name="name">acquirer.form.adyen</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment_acquirer.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer_base"]' position='after'>
<group string="Adyen Details"
attrs="{'invisible': [('name', '!=', 'adyen')]}">
<field name="adyen_merchant_account"/>
</group>
</xpath>
</field>
</record>
<record id="transaction_form_adyen" model="ir.ui.view">
<field name="name">acquirer.transaction.form.adyen</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment_acquirer.transaction_form"/>
<field name="arch" type="xml">
<xpath expr='//notebook' position='inside'>
<page string="Adyen TX Details">
<group>
<field name="adyen_psp_reference"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>