summaryrefslogtreecommitdiff
path: root/addons/payment_adyen/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/payment_adyen/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/payment_adyen/models')
-rw-r--r--addons/payment_adyen/models/__init__.py3
-rw-r--r--addons/payment_adyen/models/payment.py269
2 files changed, 272 insertions, 0 deletions
diff --git a/addons/payment_adyen/models/__init__.py b/addons/payment_adyen/models/__init__.py
new file mode 100644
index 00000000..ef125336
--- /dev/null
+++ b/addons/payment_adyen/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import payment
diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py
new file mode 100644
index 00000000..7a5388fa
--- /dev/null
+++ b/addons/payment_adyen/models/payment.py
@@ -0,0 +1,269 @@
+# 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 odoo import api, fields, models, tools, _
+from odoo.addons.payment.models.payment_acquirer import ValidationError
+from odoo.addons.payment_adyen.controllers.main import AdyenController
+from odoo.tools.pycompat import to_text
+
+_logger = logging.getLogger(__name__)
+
+# https://docs.adyen.com/developers/development-resources/currency-codes
+CURRENCY_CODE_MAPS = {
+ "BHD": 3,
+ "CVE": 0,
+ "DJF": 0,
+ "GNF": 0,
+ "IDR": 0,
+ "JOD": 3,
+ "JPY": 0,
+ "KMF": 0,
+ "KRW": 0,
+ "KWD": 3,
+ "LYD": 3,
+ "OMR": 3,
+ "PYG": 0,
+ "RWF": 0,
+ "TND": 3,
+ "UGX": 0,
+ "VND": 0,
+ "VUV": 0,
+ "XAF": 0,
+ "XOF": 0,
+ "XPF": 0,
+}
+
+
+class AcquirerAdyen(models.Model):
+ _inherit = 'payment.acquirer'
+
+ provider = fields.Selection(selection_add=[
+ ('adyen', 'Adyen')
+ ], ondelete={'adyen': 'set default'})
+ 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 _adyen_convert_amount(self, amount, currency):
+ """
+ Adyen requires the amount to be multiplied by 10^k,
+ where k depends on the currency code.
+ """
+ k = CURRENCY_CODE_MAPS.get(currency.name, 2)
+ paymentAmount = int(tools.float_round(amount, k) * (10**k))
+ return paymentAmount
+
+ @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),
+ }
+
+ 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' (odoo contacting adyen) or 'out' (adyen
+ contacting odoo). 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, '') 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)
+
+ 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' (odoo contacting adyen) or 'out' (adyen
+ contacting odoo). 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())
+
+ def adyen_form_generate_values(self, values):
+ base_url = self.get_base_url()
+ # tmp
+ import datetime
+ from dateutil import relativedelta
+
+ paymentAmount = self._adyen_convert_amount(values['amount'], values['currency'])
+ 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' % paymentAmount,
+ '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') or values.get('billing_partner_email') or '',
+ })
+ 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' % paymentAmount,
+ '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,
+ })
+ values['merchantSig'] = self._adyen_generate_merchant_sig('in', values)
+
+ return values
+
+ def adyen_get_form_action_url(self):
+ self.ensure_one()
+ environment = 'prod' if self.state == 'enabled' else 'test'
+ return self._get_adyen_urls(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 to_text(shasign_check) != to_text(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({'acquirer_reference': data.get('pspReference')})
+ self._set_transaction_done()
+ return True
+ elif status == 'PENDING':
+ self.write({'acquirer_reference': data.get('pspReference')})
+ self._set_transaction_pending()
+ return True
+ else:
+ error = _('Adyen: feedback error')
+ _logger.info(error)
+ self.write({'state_message': error})
+ self._set_transaction_cancel()
+ return False