[REF] payment_sips: migrate Sips to the new payment API

See the merge commit for more details.

task-2333041
This commit is contained in:
Kevin Baptiste
2021-03-30 09:25:51 +02:00
committed by Antoine Vandevenne (anv)
parent b14ce986ce
commit a9ac72d821
15 changed files with 459 additions and 436 deletions
+7 -3
View File
@@ -1,7 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
from odoo.addons.payment import reset_payment_provider
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook
def uninstall_hook(cr, registry):
reset_payment_provider(cr, registry, 'sips')
reset_payment_acquirer(cr, registry, 'sips')
+3 -7
View File
@@ -1,12 +1,9 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Copyright 2015 Eezee-It
# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
{
'name': 'Worldline SIPS',
'version': '1.1',
'author': 'Eezee-It',
'version': '2.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 385,
'description': """
@@ -21,8 +18,7 @@ not guaranteed.""",
'views/payment_sips_templates.xml',
'data/payment_acquirer_data.xml',
],
'installable': True,
'application': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
'post_init_hook': 'create_missing_journals',
'uninstall_hook': 'uninstall_hook',
}
+1 -1
View File
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+39 -32
View File
@@ -1,50 +1,57 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
# Copyright 2015 Eezee-It
import json
import logging
import pprint
import werkzeug
from odoo import http
from odoo.exceptions import ValidationError
from odoo.http import request
_logger = logging.getLogger(__name__)
class SipsController(http.Controller):
_notify_url = '/payment/sips/ipn/'
_return_url = '/payment/sips/dpn/'
_notify_url = '/payment/sips/ipn/'
def sips_validate_data(self, **post):
sips = request.env['payment.acquirer'].search([('provider', '=', 'sips')], limit=1)
security = sips.sudo()._sips_generate_shasign(post)
if security == post['Seal']:
_logger.debug('Sips: validated data')
return request.env['payment.transaction'].sudo().form_feedback(post, 'sips')
_logger.warning('Sips: data are corrupted')
return False
@http.route('/payment/sips/ipn/', type='http', auth='public', methods=['POST'], csrf=False)
def sips_ipn(self, **post):
""" Sips IPN. """
_logger.info('Beginning Sips IPN form_feedback with post data %s', pprint.pformat(post)) # debug
if not post:
# SIPS sometimes sends empty notifications, the reason why is
# unclear but they tend to pollute logs and do not provide any
# meaningful information; log as a warning instead of a traceback
_logger.warning('Sips: received empty notification; skip.')
else:
self.sips_validate_data(**post)
return ''
@http.route('/payment/sips/dpn', type='http', auth="public", methods=['POST'], csrf=False)
@http.route(_return_url, type='http', auth='public', methods=['POST'], csrf=False)
def sips_dpn(self, **post):
""" Sips DPN """
_logger.info("beginning Sips DPN _handle_feedback_data with data %s", pprint.pformat(post))
try:
_logger.info('Beginning Sips DPN form_feedback with post data %s', pprint.pformat(post)) # debug
self.sips_validate_data(**post)
except:
if self._sips_validate_data(post):
request.env['payment.transaction'].sudo()._handle_feedback_data('sips', post)
except ValidationError:
pass
return werkzeug.utils.redirect('/payment/process')
return werkzeug.utils.redirect('/payment/status')
@http.route(_notify_url, type='http', auth='public', methods=['POST'], csrf=False)
def sips_ipn(self, **post):
""" Sips IPN. """
_logger.info("beginning Sips IPN _handle_feedback_data with data %s", pprint.pformat(post))
if not post:
# SIPS sometimes sends empty notifications, the reason why is unclear but they tend to
# pollute logs and do not provide any meaningful information; log as a warning instead
# of a traceback.
_logger.warning("received empty notification; skip.")
else:
try:
if self._sips_validate_data(post):
request.env['payment.transaction'].sudo()._handle_feedback_data('sips', post)
except ValidationError:
pass # Acknowledge the notification to avoid getting spammed
return ''
def _sips_validate_data(self, post):
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data('sips', post)
acquirer_sudo = tx_sudo.acquirer_id
security = acquirer_sudo._sips_generate_shasign(post['Data'])
if security == post['Seal']:
_logger.debug('validated data')
return True
else:
_logger.warning('data are tampered')
return False
@@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<odoo noupdate="1">
<record id="payment.payment_acquirer_sips" model="payment.acquirer">
<field name="name">Sips</field>
<field name="image_128" type="base64" file="payment_sips/static/src/img/sips_icon.png"/>
<field name="provider">sips</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="payment_sips.sips_form"/>
</record>
<record id="payment.payment_acquirer_sips" model="payment.acquirer">
<field name="provider">sips</field>
<field name="redirect_form_view_id" ref="redirect_form"/>
<field name="support_authorization">False</field>
<field name="support_fees_computation">False</field>
<field name="support_tokenization">False</field>
</record>
</data>
</odoo>
+3 -2
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
from . import payment_acquirer
from . import payment_transaction
+60 -52
View File
@@ -1,7 +1,4 @@
from collections import namedtuple
Currency = namedtuple('Currency', ['iso_id', 'decimal'])
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# ISO 4217 Data for currencies supported by sips
# NOTE: these are listed on the Atos Wordline SIPS POST documentation page
@@ -12,52 +9,63 @@ Currency = namedtuple('Currency', ['iso_id', 'decimal'])
# restrictive, I'm gonna assume they are supported when using the right flow
# and payment methods, which may not work in SIMU...
# Since SIPS advises to use 'in production', well...
SIPS_SUPPORTED_CURRENCIES = {
'ARS': Currency('032', 2),
'AUD': Currency('036', 2),
'BHD': Currency('048', 3),
'KHR': Currency('116', 2),
'CAD': Currency('124', 2),
'LKR': Currency('144', 2),
'CNY': Currency('156', 2),
'HRK': Currency('191', 2),
'CZK': Currency('203', 2),
'DKK': Currency('208', 2),
'HKD': Currency('344', 2),
'HUF': Currency('348', 2),
'ISK': Currency('352', 0),
'INR': Currency('356', 2),
'ILS': Currency('376', 2),
'JPY': Currency('392', 0),
'KRW': Currency('410', 0),
'KWD': Currency('414', 3),
'MYR': Currency('458', 2),
'MUR': Currency('480', 2),
'MXN': Currency('484', 2),
'NPR': Currency('524', 2),
'NZD': Currency('554', 2),
'NOK': Currency('578', 2),
'QAR': Currency('634', 2),
'RUB': Currency('643', 2),
'SAR': Currency('682', 2),
'SGD': Currency('702', 2),
'ZAR': Currency('710', 2),
'SEK': Currency('752', 2),
'CHF': Currency('756', 2),
'THB': Currency('764', 2),
'AED': Currency('784', 2),
'TND': Currency('788', 3),
'GBP': Currency('826', 2),
'USD': Currency('840', 2),
'TWD': Currency('901', 2),
'RSD': Currency('941', 2),
'RON': Currency('946', 2),
'TRY': Currency('949', 2),
'XOF': Currency('952', 0),
'XPF': Currency('953', 0),
'BGN': Currency('975', 2),
'EUR': Currency('978', 2),
'UAH': Currency('980', 2),
'PLN': Currency('996', 2),
'BRL': Currency('986', 2),
SUPPORTED_CURRENCIES = {
'ARS': '032',
'AUD': '036',
'BHD': '048',
'KHR': '116',
'CAD': '124',
'LKR': '144',
'CNY': '156',
'HRK': '191',
'CZK': '203',
'DKK': '208',
'HKD': '344',
'HUF': '348',
'ISK': '352',
'INR': '356',
'ILS': '376',
'JPY': '392',
'KRW': '410',
'KWD': '414',
'MYR': '458',
'MUR': '480',
'MXN': '484',
'NPR': '524',
'NZD': '554',
'NOK': '578',
'QAR': '634',
'RUB': '643',
'SAR': '682',
'SGD': '702',
'ZAR': '710',
'SEK': '752',
'CHF': '756',
'THB': '764',
'AED': '784',
'TND': '788',
'GBP': '826',
'USD': '840',
'TWD': '901',
'RSD': '941',
'RON': '946',
'TRY': '949',
'XOF': '952',
'XPF': '953',
'BGN': '975',
'EUR': '978',
'UAH': '980',
'PLN': '996',
'BRL': '986',
}
# Mapping of transaction states to Sips response codes.
# See https://documentation.sips.worldline.com/en/WLSIPS.001-GD-Data-dictionary.html#Sips.001_DD_en-Value-currencyCode
RESPONSE_CODES_MAPPING = {
'pending': ('60',),
'done': ('00',),
'cancel': (
'03', '05', '12', '14', '17', '24', '25', '30', '34', '40', '51', '54', '63', '75', '90',
'94', '97', '99'
),
}
-215
View File
@@ -1,215 +0,0 @@
# coding: utf-8
# Copyright 2015 Eezee-It
import datetime
from dateutil import parser
import json
import logging
import pytz
import re
import time
from hashlib import sha256
from werkzeug import urls
from odoo import models, fields, api
from odoo.tools.float_utils import float_compare
from odoo.tools.translate import _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_sips.controllers.main import SipsController
from .const import SIPS_SUPPORTED_CURRENCIES
_logger = logging.getLogger(__name__)
class AcquirerSips(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[('sips', 'Sips')], ondelete={'sips': 'set default'})
sips_merchant_id = fields.Char('Merchant ID', required_if_provider='sips', groups='base.group_user')
sips_secret = fields.Char('Secret Key', size=64, required_if_provider='sips', groups='base.group_user')
sips_test_url = fields.Char("Test url", required_if_provider='sips', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
sips_prod_url = fields.Char("Production url", required_if_provider='sips', default='https://payment-webinit.sips-atos.com/paymentInit')
sips_version = fields.Char("Interface Version", required_if_provider='sips', default='HP_2.31')
sips_key_version = fields.Integer("Secret Key Version", required_if_provider='sips', default=2)
def _sips_generate_shasign(self, values):
""" Generate the shasign for incoming or outgoing communications.
:param dict values: transaction values
:return string: shasign
"""
if self.provider != 'sips':
raise ValidationError(_('Incorrect payment acquirer provider'))
data = values['Data']
key = self.sips_secret
shasign = sha256((data + key).encode('utf-8'))
return shasign.hexdigest()
def sips_form_generate_values(self, values):
self.ensure_one()
base_url = self.get_base_url()
currency = self.env['res.currency'].sudo().browse(values['currency_id'])
sips_currency = SIPS_SUPPORTED_CURRENCIES.get(currency.name)
if not sips_currency:
raise ValidationError(_('Currency not supported by Wordline: %s') % currency.name)
# rounded to its smallest unit, depends on the currency
amount = round(values['amount'] * (10 ** sips_currency.decimal))
sips_tx_values = dict(values)
data = {
'amount': amount,
'currencyCode': sips_currency.iso_id,
'merchantId': self.sips_merchant_id,
'normalReturnUrl': urls.url_join(base_url, SipsController._return_url),
'automaticResponseUrl': urls.url_join(base_url, SipsController._notify_url),
'transactionReference': values['reference'],
'statementReference': values['reference'],
'keyVersion': self.sips_key_version,
}
sips_tx_values.update({
'Data': '|'.join([f'{k}={v}' for k,v in data.items()]),
'InterfaceVersion': self.sips_version,
})
return_context = {}
if sips_tx_values.get('return_url'):
return_context['return_url'] = urls.url_quote(sips_tx_values.get('return_url'))
return_context['reference'] = sips_tx_values['reference']
sips_tx_values['Data'] += '|returnContext=%s' % (json.dumps(return_context))
shasign = self._sips_generate_shasign(sips_tx_values)
sips_tx_values['Seal'] = shasign
return sips_tx_values
def sips_get_form_action_url(self):
self.ensure_one()
return self.sips_prod_url if self.state == 'enabled' else self.sips_test_url
class TxSips(models.Model):
_inherit = 'payment.transaction'
_sips_valid_tx_status = ['00']
_sips_wait_tx_status = ['90', '99']
_sips_refused_tx_status = ['05', '14', '34', '54', '75', '97']
_sips_error_tx_status = ['03', '12', '24', '25', '30', '40', '51', '63', '94']
_sips_pending_tx_status = ['60']
_sips_cancel_tx_status = ['17']
@api.model
def _compute_reference(self, values=None, prefix=None):
res = super()._compute_reference(values=values, prefix=prefix)
acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
if acquirer and acquirer.provider == 'sips':
return re.sub(r'[^0-9a-zA-Z]+', 'x', res) + 'x' + str(int(time.time()))
return res
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
def _sips_data_to_object(self, data):
res = {}
for element in data.split('|'):
(key, value) = element.split('=')
res[key] = value
return res
@api.model
def _sips_form_get_tx_from_data(self, data):
""" Given a data dict coming from sips, verify it and find the related
transaction record. """
data = self._sips_data_to_object(data.get('Data'))
reference = data.get('transactionReference')
if not reference:
return_context = json.loads(data.get('returnContext', '{}'))
reference = return_context.get('reference')
payment_tx = self.search([('reference', '=', reference)])
if not payment_tx:
error_msg = _('Sips: received data for reference %s; no order found') % reference
_logger.error(error_msg)
raise ValidationError(error_msg)
return payment_tx
def _sips_form_get_invalid_parameters(self, data):
invalid_parameters = []
data = self._sips_data_to_object(data.get('Data'))
# amounts should match
# get currency decimals from const
sips_currency = SIPS_SUPPORTED_CURRENCIES.get(self.currency_id.name)
# convert from int to float using decimals from currency
amount_converted = float(data.get('amount', '0.0')) / (10 ** sips_currency.decimal)
if float_compare(amount_converted, self.amount, sips_currency.decimal) != 0:
invalid_parameters.append(('amount', data.get('amount'), '%.2f' % self.amount))
return invalid_parameters
def _sips_form_validate(self, data):
data = self._sips_data_to_object(data.get('Data'))
status = data.get('responseCode')
date = data.get('transactionDateTime')
if date:
try:
# dateutil.parser 2.5.3 and up should handle dates formatted as
# '2020-04-08T05:54:18+02:00', which strptime does not
# (+02:00 does not work as %z expects +0200 before Python 3.7)
# See odoo/odoo#49160
date = parser.parse(date).astimezone(pytz.utc).replace(tzinfo=None)
except:
# fallback on now to avoid failing to register the payment
# because a provider formats their dates badly or because
# some library is not behaving
date = fields.Datetime.now()
data = {
'acquirer_reference': data.get('transactionReference'),
'date': date,
}
res = False
if status in self._sips_valid_tx_status:
msg = f'ref: {self.reference}, got valid response [{status}], set as done.'
_logger.info(msg)
data.update(state_message=msg)
self.write(data)
self._set_transaction_done()
res = True
elif status in self._sips_error_tx_status:
msg = f'ref: {self.reference}, got response [{status}], set as cancel.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_wait_tx_status:
msg = f'ref: {self.reference}, got wait response [{status}], set as cancel.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_refused_tx_status:
msg = f'ref: {self.reference}, got refused response [{status}], set as cancel.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
elif status in self._sips_pending_tx_status:
msg = f'ref: {self.reference}, got pending response [{status}], set as pending.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_pending()
elif status in self._sips_cancel_tx_status:
msg = f'ref: {self.reference}, got cancel response [{status}], set as cancel.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
else:
msg = f'ref: {self.reference}, got unrecognized response [{status}], set as cancel.'
data.update(state_message=msg)
self.write(data)
self._set_transaction_cancel()
_logger.info(msg)
return res
@@ -0,0 +1,56 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
from hashlib import sha256
from odoo import api, fields, models
from .const import SUPPORTED_CURRENCIES
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(
selection_add=[('sips', "Sips")], ondelete={'sips': 'set default'})
sips_merchant_id = fields.Char(
string="Merchant ID", help="The ID solely used to identify the merchant account with Sips",
required_if_provider='sips')
sips_secret = fields.Char(
string="SIPS Secret Key", size=64, required_if_provider='sips', groups='base.group_system')
sips_key_version = fields.Integer(
string="Secret Key Version", required_if_provider='sips', default=2)
sips_test_url = fields.Char(
string="Test URL", required_if_provider='sips',
default="https://payment-webinit.simu.sips-atos.com/paymentInit")
sips_prod_url = fields.Char(
string="Production URL", required_if_provider='sips',
default="https://payment-webinit.sips-atos.com/paymentInit")
sips_version = fields.Char(
string="Interface Version", required_if_provider='sips', default="HP_2.31")
@api.model
def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Sips acquirers when the currency is not supported. """
acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists()
if currency and currency.name not in SUPPORTED_CURRENCIES:
acquirers = acquirers.filtered(lambda a: a.provider != 'sips')
return acquirers
def _sips_generate_shasign(self, data):
""" Generate the shasign for incoming or outgoing communications.
Note: self.ensure_one()
:param str data: The data to use to generate the shasign
:return: shasign
:rtype: str
"""
self.ensure_one()
key = self.sips_secret
shasign = sha256((data + key).encode('utf-8'))
return shasign.hexdigest()
@@ -0,0 +1,168 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Original Copyright 2015 Eezee-It, modified and maintained by Odoo.
import json
import logging
from werkzeug import urls
from odoo import _, api, models
from odoo.exceptions import ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_sips.controllers.main import SipsController
from .const import RESPONSE_CODES_MAPPING, SUPPORTED_CURRENCIES
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
@api.model
def _compute_reference(self, provider, prefix=None, separator='-', **kwargs):
""" Override of payment to ensure that Sips requirements for references are satisfied.
Sips requirements for transaction are as follows:
- References can only be made of alphanumeric characters.
This is satisfied by forcing the custom separator to 'x' to ensure that no '-' character
will be used to append a suffix. Additionally, the prefix is sanitized if it was provided,
and generated with 'tx' as default otherwise. This prevents the prefix to be generated
based on document names that may contain non-alphanum characters (eg: INV/2020/...).
- References must be unique at provider level for a given merchant account.
This is satisfied by singularizing the prefix with the current datetime. If two
transactions are created simultaneously, `_compute_reference` ensures the uniqueness of
references by suffixing a sequence number.
:param str provider: The provider of the acquirer handling the transaction
:param str prefix: The custom prefix used to compute the full reference
:param str separator: The custom separator used to separate the prefix from the suffix
:return: The unique reference for the transaction
:rtype: str
"""
if provider == 'sips':
# We use an empty separator for cosmetic reasons: As the default prefix is 'tx', we want
# the singularized prefix to look like 'tx2020...' and not 'txx2020...'.
prefix = payment_utils.singularize_reference_prefix(separator='')
separator = 'x' # Still, we need a dedicated separator between the prefix and the seq.
return super()._compute_reference(provider, prefix=prefix, separator=separator, **kwargs)
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Sips-specific rendering values.
Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic and specific processing values of the transaction
:return: The dict of acquirer-specific processing values
:rtype: dict
"""
res = super()._get_specific_rendering_values(processing_values)
if self.provider != 'sips':
return res
base_url = self.get_base_url()
data = {
'amount': payment_utils.to_minor_currency_units(self.amount, self.currency_id),
'currencyCode': SUPPORTED_CURRENCIES[self.currency_id.name], # The ISO 4217 code
'merchantId': self.acquirer_id.sips_merchant_id,
'normalReturnUrl': urls.url_join(base_url, SipsController._return_url),
'automaticResponseUrl': urls.url_join(base_url, SipsController._notify_url),
'transactionReference': self.reference,
'statementReference': self.reference,
'keyVersion': self.acquirer_id.sips_key_version,
'returnContext': json.dumps(dict(reference=self.reference)),
}
api_url = self.acquirer_id.sips_prod_url if self.acquirer_id.state == 'enabled' \
else self.acquirer_id.sips_test_url
data = '|'.join([f'{k}={v}' for k, v in data.items()])
return {
'api_url': api_url,
'Data': data,
'InterfaceVersion': self.acquirer_id.sips_version,
'Seal': self.acquirer_id._sips_generate_shasign(data),
}
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Override of payment to find the transaction based on Sips data.
:param str provider: The provider of the acquirer that handled the transaction
:param dict data: The feedback data sent by the provider
:return: The transaction if found
:rtype: recordset of `payment.transaction`
:raise: ValidationError if the data match no transaction
:raise: ValidationError if the currency is not supported
:raise: ValidationError if the amount mismatch
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'sips':
return tx
data = self._sips_data_to_object(data['Data'])
reference = data.get('transactionReference')
if not reference:
return_context = json.loads(data.get('returnContext', '{}'))
reference = return_context.get('reference')
tx = self.search([('reference', '=', reference), ('provider', '=', 'sips')])
if not tx:
raise ValidationError(
"Sips: " + _("No transaction found matching reference %s.", reference)
)
sips_currency = SUPPORTED_CURRENCIES.get(tx.currency_id.name)
if not sips_currency:
raise ValidationError(
"Sips: " + _("This currency is not supported: %s.", tx.currency_id.name)
)
amount_converted = payment_utils.to_major_currency_units(
float(data.get('amount', '0.0')), tx.currency_id
)
if tx.currency_id.compare_amounts(amount_converted, tx.amount) != 0:
raise ValidationError(
"Sips: " + _(
"Incorrect amount: received %(received).2f, expected %(expected).2f",
received=amount_converted, expected=tx.amount
)
)
return tx
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Sips data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
"""
super()._process_feedback_data(data)
if self.provider != 'sips':
return
data = self._sips_data_to_object(data.get('Data'))
self.acquirer_reference = data.get('transactionReference')
response_code = data.get('responseCode')
if response_code in RESPONSE_CODES_MAPPING['pending']:
status = "pending"
self._set_pending()
elif response_code in RESPONSE_CODES_MAPPING['done']:
status = "done"
self._set_done()
elif response_code in RESPONSE_CODES_MAPPING['cancel']:
status = "cancel"
self._set_canceled()
else:
status = "error"
self._set_error(_("Unrecognized response received from the payment provider."))
_logger.info(
"ref: %s, got response [%s], set as '%s'.", self.reference, response_code, status
)
def _sips_data_to_object(self, data):
res = {}
for element in data.split('|'):
key, value = element.split('=')
res[key] = value
return res
+2 -1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_sips
+18
View File
@@ -0,0 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.payment.tests.common import PaymentCommon
class SipsCommon(PaymentCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.sips = cls._prepare_acquirer('sips', update_values={
'sips_merchant_id': 'dummy_mid',
'sips_secret': 'dummy_secret',
})
# Override default values
cls.acquirer = cls.sips
cls.currency = cls.currency_euro
+71 -89
View File
@@ -1,40 +1,67 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from freezegun import freeze_time
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.tools import mute_logger
from .common import SipsCommon
from ..controllers.main import SipsController
from ..models.payment_acquirer import SUPPORTED_CURRENCIES
@tagged('post_install', '-at_install', '-standard', 'external')
class SipsTest(PaymentAcquirerCommon):
@tagged('post_install', '-at_install')
class SipsTest(SipsCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.sips = cls.env.ref('payment.payment_acquirer_sips')
cls.sips.write({
'state': 'test',
'sips_merchant_id': 'dummy_mid',
'sips_secret': 'dummy_secret',
})
def test_compatible_acquirers(self):
for curr in SUPPORTED_CURRENCIES:
currency = self._prepare_currency(curr)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
company_id=self.company.id,
currency_id=currency.id,
)
self.assertIn(self.sips, acquirers)
def test_10_sips_form_render(self):
self.assertEqual(self.sips.state, 'test', 'test without test environment')
unsupported_currency = self._prepare_currency('VEF')
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
company_id=self.company.id,
currency_id=unsupported_currency.id,
)
self.assertNotIn(self.sips, acquirers)
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
# freeze time for consistent singularize_prefix behavior during the test
@freeze_time("2011-11-02 12:00:21")
def test_reference(self):
tx = self.create_transaction(flow="redirect", reference="")
self.assertEqual(tx.reference, "tx20111102120021",
"Payulatam: transaction reference wasn't correctly singularized.")
# render the button
tx = self.env['payment.transaction'].create({
'acquirer_id': self.sips.id,
'amount': 100.0,
'reference': 'SO404',
'currency_id': self.currency_euro.id,
})
self.sips.render('SO404', 100.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8')
def test_redirect_form_values(self):
tx = self.create_transaction(flow="redirect")
def test_20_sips_form_management(self):
self.assertEqual(self.sips.state, 'test', 'test without test environment')
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = tx._get_processing_values()
form_info = self._extract_values_from_html_form(processing_values['redirect_form_html'])
form_inputs = form_info['inputs']
self.assertEqual(form_info['action'], self.sips.sips_test_url)
self.assertEqual(form_inputs['InterfaceVersion'], self.sips.sips_version)
return_url = self._build_url(SipsController._return_url)
notify_url = self._build_url(SipsController._notify_url)
self.assertEqual(form_inputs['Data'],
f'amount=111111|currencyCode=978|merchantId=dummy_mid|normalReturnUrl={return_url}|' \
f'automaticResponseUrl={notify_url}|transactionReference={self.reference}|' \
f'statementReference={self.reference}|keyVersion={self.sips.sips_key_version}|' \
f'returnContext={json.dumps(dict(reference=self.reference))}'
)
self.assertEqual(form_inputs['Seal'],
'4d7cc67c0168e8ce11c25fbe1937231c644861e320702ab544022b032b9eb4a2')
def test_feedback_processing(self):
# typical data posted by Sips after client has successfully paid
sips_post_data = {
'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
@@ -56,22 +83,22 @@ class SipsTest(PaymentAcquirerCommon):
'Encode': '',
'InterfaceVersion': 'HP_2.4',
'Seal': 'f03f64da6f57c171904d12bf709b1d6d3385131ac914e97a7e1db075ed438f3e',
'locale': 'en'
'locale': 'en',
}
tx = self.env['payment.transaction'].create({
'amount': 314.0,
'acquirer_id': self.sips.id,
'currency_id': self.currency_euro.id,
'reference': 'SO100x1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
with self.assertRaises(ValidationError): # unknown transaction
self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
# validate it
tx.form_feedback(sips_post_data, 'sips')
self.amount = 314.0
self.reference = 'SO100x1'
tx = self.create_transaction(flow="redirect")
# Validate the transaction
self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
self.assertEqual(tx.state, 'done', 'Sips: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, 'SO100x1', 'Sips: validation did not update tx id')
self.assertEqual(tx.acquirer_reference, self.reference, 'Sips: validation did not update tx id')
# same process for an payment in error on sips's end
sips_post_data = {
'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
@@ -87,53 +114,8 @@ class SipsTest(PaymentAcquirerCommon):
'InterfaceVersion': 'HP_2.4',
'Seal': '6e1995ea5432580860a04d8515b6eb1507996f97b3c5fa04fb6d9568121a16a2'
}
tx = self.env['payment.transaction'].create({
'amount': 314.0,
'acquirer_id': self.sips.id,
'currency_id': self.currency_euro.id,
'reference': 'SO100x2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
tx.form_feedback(sips_post_data, 'sips')
# check state
self.assertEqual(tx.state, 'cancel', 'Sips: erroneous validation did not put tx into error state')
self.reference = 'SO100x2'
tx2 = self.create_transaction(flow="redirect")
def test_30_sips_badly_formatted_date(self):
self.assertEqual(self.sips.state, 'test', 'test without test environment')
# typical data posted by Sips after client has successfully paid
bad_date = '2020-04-08T06:15:59+56:00'
sips_post_data = {
'Data': 'captureDay=0|captureMode=AUTHOR_CAPTURE|currencyCode=840|'
'merchantId=002001000000001|orderChannel=INTERNET|'
'responseCode=00|transactionDateTime=%s|'
'transactionReference=SO100x1|keyVersion=1|'
'acquirerResponseCode=00|amount=31400|authorisationId=0020000006791167|'
'paymentMeanBrand=IDEAL|paymentMeanType=CREDIT_TRANSFER|'
'customerIpAddress=127.0.0.1|returnContext={"return_url": '
'"/payment/process", "reference": '
'"SO100x1"}|holderAuthentRelegation=N|holderAuthentStatus=|'
'transactionOrigin=INTERNET|paymentPattern=ONE_SHOT|customerMobilePhone=null|'
'mandateAuthentMethod=null|mandateUsage=null|transactionActors=null|'
'mandateId=null|captureLimitDate=20200408|dccStatus=null|dccResponseCode=null|'
'dccAmount=null|dccCurrencyCode=null|dccExchangeRate=null|'
'dccExchangeRateValidity=null|dccProvider=null|'
'statementReference=SO100x1|panEntryMode=MANUAL|walletType=null|'
'holderAuthentMethod=NO_AUTHENT_METHOD' % (bad_date,),
'Encode': '',
'InterfaceVersion': 'HP_2.4',
'Seal': 'f03f64da6f57c171904d12bf709b1d6d3385131ac914e97a7e1db075ed438f3e',
'locale': 'en'
}
tx = self.env['payment.transaction'].create({
'amount': 314.0,
'acquirer_id': self.sips.id,
'currency_id': self.currency_euro.id,
'reference': 'SO100x1',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# validate it
tx.form_feedback(sips_post_data, 'sips')
self.assertEqual(tx.state, 'done', 'Sips: validation did not put tx into done state when date format was weird')
self.env['payment.transaction']._handle_feedback_data('sips', sips_post_data)
self.assertEqual(tx2.state, 'cancel', 'Sips: erroneous validation did not put tx into error state')
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<template id="sips_form">
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<template id="redirect_form">
<form t-att-action="api_url" method="post">
<input type="hidden" name="Data" t-att-value="Data"/>
<input type="hidden" name="InterfaceVersion" t-att-value="InterfaceVersion"/>
<input type="hidden" name="Seal" t-att-value="Seal"/>
</template>
</data>
</form>
</template>
</odoo>
+17 -19
View File
@@ -1,24 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="acquirer_form_sips" model="ir.ui.view">
<field name="name">acquirer.form.sips</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
<field name="sips_merchant_id" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
<field name="sips_secret" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
<field name="sips_test_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
<field name="sips_prod_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
<field name="sips_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" />
<field name="sips_key_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" />
</group>
</xpath>
</field>
</record>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">Sips Acquirer Form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
<field name="sips_merchant_id" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
<field name="sips_secret" string="Secret Key" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}"/>
<field name="sips_key_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" />
<field name="sips_test_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
<field name="sips_prod_url" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" groups='base.group_no_one'/>
<field name="sips_version" attrs="{'required':[ ('provider', '=', 'sips'), ('state', '!=', 'disabled')]}" />
</group>
</xpath>
</field>
</record>
</data>
</odoo>