241 lines
10 KiB
Python
241 lines
10 KiB
Python
# coding: utf-8
|
|
|
|
import base64
|
|
import json
|
|
import binascii
|
|
from collections import OrderedDict
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
from itertools import chain
|
|
|
|
from werkzeug import urls
|
|
|
|
from flectra import api, fields, models, tools, _
|
|
from flectra.addons.payment.models.payment_acquirer import ValidationError
|
|
from flectra.addons.payment_adyen.controllers.main import AdyenController
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AcquirerAdyen(models.Model):
|
|
_inherit = 'payment.acquirer'
|
|
|
|
provider = fields.Selection(selection_add=[('adyen', 'Adyen')])
|
|
adyen_merchant_account = fields.Char('Merchant Account', required_if_provider='adyen', groups='base.group_user')
|
|
adyen_skin_code = fields.Char('Skin Code', required_if_provider='adyen', groups='base.group_user')
|
|
adyen_skin_hmac_key = fields.Char('Skin HMAC Key', required_if_provider='adyen', groups='base.group_user')
|
|
|
|
@api.model
|
|
def _get_adyen_urls(self, environment):
|
|
""" Adyen URLs: yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple """
|
|
return {
|
|
'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % ('live' if environment == 'prod' else environment),
|
|
}
|
|
|
|
@api.multi
|
|
def _adyen_generate_merchant_sig_sha256(self, inout, values):
|
|
""" Generate the shasign for incoming or outgoing communications., when using the SHA-256
|
|
signature.
|
|
|
|
:param string inout: 'in' (flectra contacting ogone) or 'out' (adyen
|
|
contacting flectra). In this last case only some
|
|
fields should be contained (see e-Commerce basic)
|
|
:param dict values: transaction values
|
|
:return string: shasign
|
|
"""
|
|
def escapeVal(val):
|
|
return val.replace('\\', '\\\\').replace(':', '\\:')
|
|
|
|
def signParams(parms):
|
|
signing_string = ':'.join(
|
|
escapeVal(v)
|
|
for v in chain(parms.keys(), parms.values())
|
|
)
|
|
hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256)
|
|
return base64.b64encode(hm.digest())
|
|
|
|
assert inout in ('in', 'out')
|
|
assert self.provider == 'adyen'
|
|
|
|
if inout == 'in':
|
|
# All the fields sent to Adyen must be included in the signature. ALL the fucking
|
|
# fields, despite what is claimed in the documentation. For example, in
|
|
# https://docs.adyen.com/developers/hpp-manual, it is stated: "The resURL parameter does
|
|
# not need to be included in the signature." It's a trap, it must be included as well!
|
|
keys = [
|
|
'merchantReference', 'paymentAmount', 'currencyCode', 'shipBeforeDate', 'skinCode',
|
|
'merchantAccount', 'sessionValidity', 'merchantReturnData', 'shopperEmail',
|
|
'shopperReference', 'allowedMethods', 'blockedMethods', 'offset',
|
|
'shopperStatement', 'recurringContract', 'billingAddressType',
|
|
'deliveryAddressType', 'brandCode', 'countryCode', 'shopperLocale', 'orderData',
|
|
'offerEmail', 'resURL',
|
|
]
|
|
else:
|
|
keys = [
|
|
'authResult', 'merchantReference', 'merchantReturnData', 'paymentMethod',
|
|
'pspReference', 'shopperLocale', 'skinCode',
|
|
]
|
|
|
|
hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii'))
|
|
raw_values = {k: values.get(k.encode('ascii'), '') for k in keys if k in values}
|
|
raw_values_ordered = OrderedDict(sorted(raw_values.items(), key=lambda t: t[0]))
|
|
|
|
return signParams(raw_values_ordered)
|
|
|
|
@api.multi
|
|
def _adyen_generate_merchant_sig(self, inout, values):
|
|
""" Generate the shasign for incoming or outgoing communications, when using the SHA-1
|
|
signature (deprecated by Adyen).
|
|
|
|
:param string inout: 'in' (flectra contacting ogone) or 'out' (adyen
|
|
contacting flectra). 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 self.provider == '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 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 = self.adyen_skin_hmac_key.encode('ascii')
|
|
return base64.b64encode(hmac.new(key, sign, hashlib.sha1).digest())
|
|
|
|
@api.multi
|
|
def adyen_form_generate_values(self, values):
|
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
# tmp
|
|
import datetime
|
|
from dateutil import relativedelta
|
|
|
|
if self.provider == 'adyen' and len(self.adyen_skin_hmac_key) == 64:
|
|
tmp_date = datetime.datetime.today() + relativedelta.relativedelta(days=1)
|
|
|
|
values.update({
|
|
'merchantReference': values['reference'],
|
|
'paymentAmount': '%d' % int(tools.float_round(values['amount'], 2) * 100),
|
|
'currencyCode': values['currency'] and values['currency'].name or '',
|
|
'shipBeforeDate': tmp_date.strftime('%Y-%m-%d'),
|
|
'skinCode': self.adyen_skin_code,
|
|
'merchantAccount': self.adyen_merchant_account,
|
|
'shopperLocale': values.get('partner_lang', ''),
|
|
'sessionValidity': tmp_date.isoformat('T')[:19] + "Z",
|
|
'resURL': urls.url_join(base_url, AdyenController._return_url),
|
|
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False,
|
|
'shopperEmail': values.get('partner_email', ''),
|
|
})
|
|
values['merchantSig'] = self._adyen_generate_merchant_sig_sha256('in', values)
|
|
|
|
else:
|
|
tmp_date = datetime.date.today() + relativedelta.relativedelta(days=1)
|
|
|
|
values.update({
|
|
'merchantReference': values['reference'],
|
|
'paymentAmount': '%d' % int(tools.float_round(values['amount'], 2) * 100),
|
|
'currencyCode': values['currency'] and values['currency'].name or '',
|
|
'shipBeforeDate': tmp_date,
|
|
'skinCode': self.adyen_skin_code,
|
|
'merchantAccount': self.adyen_merchant_account,
|
|
'shopperLocale': values.get('partner_lang'),
|
|
'sessionValidity': tmp_date,
|
|
'resURL': urls.url_join(base_url, AdyenController._return_url),
|
|
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False,
|
|
'merchantSig': self._adyen_generate_merchant_sig('in', values),
|
|
})
|
|
|
|
return values
|
|
|
|
@api.multi
|
|
def adyen_get_form_action_url(self):
|
|
return self._get_adyen_urls(self.environment)['adyen_form_url']
|
|
|
|
|
|
class TxAdyen(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
# --------------------------------------------------
|
|
# FORM RELATED METHODS
|
|
# --------------------------------------------------
|
|
|
|
@api.model
|
|
def _adyen_form_get_tx_from_data(self, data):
|
|
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.info(error_msg)
|
|
raise ValidationError(error_msg)
|
|
|
|
# find tx -> @TDENOTE use pspReference ?
|
|
tx = self.env['payment.transaction'].search([('reference', '=', reference)])
|
|
if not tx or len(tx) > 1:
|
|
error_msg = _('Adyen: received data for reference %s') % (reference)
|
|
if not tx:
|
|
error_msg += _('; no order found')
|
|
else:
|
|
error_msg += _('; multiple order found')
|
|
_logger.info(error_msg)
|
|
raise ValidationError(error_msg)
|
|
|
|
# verify shasign
|
|
if len(tx.acquirer_id.adyen_skin_hmac_key) == 64:
|
|
shasign_check = tx.acquirer_id._adyen_generate_merchant_sig_sha256('out', data)
|
|
else:
|
|
shasign_check = tx.acquirer_id._adyen_generate_merchant_sig('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, data):
|
|
invalid_parameters = []
|
|
|
|
# reference at acquirer: pspReference
|
|
if self.acquirer_reference and data.get('pspReference') != self.acquirer_reference:
|
|
invalid_parameters.append(('pspReference', data.get('pspReference'), self.acquirer_reference))
|
|
# seller
|
|
if data.get('skinCode') != self.acquirer_id.adyen_skin_code:
|
|
invalid_parameters.append(('skinCode', data.get('skinCode'), self.acquirer_id.adyen_skin_code))
|
|
# result
|
|
if not data.get('authResult'):
|
|
invalid_parameters.append(('authResult', data.get('authResult'), 'something'))
|
|
|
|
return invalid_parameters
|
|
|
|
def _adyen_form_validate(self, data):
|
|
status = data.get('authResult', 'PENDING')
|
|
if status == 'AUTHORISED':
|
|
self.write({
|
|
'state': 'done',
|
|
'acquirer_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':
|
|
self.write({
|
|
'state': 'pending',
|
|
'acquirer_reference': data.get('pspReference'),
|
|
})
|
|
return True
|
|
else:
|
|
error = _('Adyen: feedback error')
|
|
_logger.info(error)
|
|
self.write({
|
|
'state': 'error',
|
|
'state_message': error
|
|
})
|
|
return False
|