[REF] payment_alipay: migrate Alipay to the new payment API

See the merge commit for more details.

task-2333027
This commit is contained in:
Arnaud Joset
2021-03-30 09:25:51 +02:00
committed by Antoine Vandevenne (anv)
parent a30aa2aeed
commit 6b42234b8b
14 changed files with 445 additions and 419 deletions
+7 -2
View File
@@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
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_acquirer(cr, registry, 'alipay')
+4 -3
View File
@@ -1,18 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Alipay Payment Acquirer',
'category': 'Accounting/Payment Acquirers',
'version': '2.0',
'sequence': 345,
'summary': 'Payment Acquirer: Alipay Implementation',
'description': """Alipay Payment Acquirer""",
'depends': ['payment'],
'data': [
'views/alipay_views.xml',
'views/payment_alipay_templates.xml',
'views/payment_views.xml',
'data/payment_acquirer_data.xml',
],
'application': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
'post_init_hook': 'create_missing_journals',
'uninstall_hook': 'uninstall_hook',
}
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+43 -44
View File
@@ -1,63 +1,62 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
import requests
import werkzeug
from odoo import http
from odoo import _, http
from odoo.exceptions import ValidationError
from odoo.http import request
_logger = logging.getLogger(__name__)
class AlipayController(http.Controller):
_notify_url = '/payment/alipay/notify'
_return_url = '/payment/alipay/return'
_notify_url = '/payment/alipay/notify'
def _alipay_validate_data(self, **post):
resp = post.get('trade_status')
if resp:
if resp in ['TRADE_FINISHED', 'TRADE_SUCCESS']:
_logger.info('Alipay: validated data')
elif resp == 'TRADE_CLOSED':
_logger.warning('Alipay: payment refunded to user and closed the transaction')
else:
_logger.warning('Alipay: unrecognized alipay answer, received %s instead of TRADE_FINISHED/TRADE_SUCCESS and TRADE_CLOSED' % (post['trade_status']))
if post.get('out_trade_no') and post.get('trade_no'):
post['reference'] = request.env['payment.transaction'].sudo().search([('reference', '=', post['out_trade_no'])]).reference
return request.env['payment.transaction'].sudo().form_feedback(post, 'alipay')
return False
def _alipay_validate_notification(self, **post):
if post.get('out_trade_no'):
alipay = request.env['payment.transaction'].sudo().search([('reference', '=', post.get('out_trade_no'))]).acquirer_id
else:
alipay = request.env['payment.acquirer'].sudo().search([('provider', '=', 'alipay')])
val = {
'service': 'notify_verify',
'partner': alipay.alipay_merchant_partner_id,
'notify_id': post['notify_id']
}
response = requests.post(alipay.alipay_get_form_action_url(), val)
response.raise_for_status()
_logger.info('Validate alipay Notification %s' % response.text)
# After program is executed, the page must print “success” (without quote). If not, Alipay server would keep re-sending notification, until over 24 hour 22 minutes Generally, there are 8 notifications within 25 hours (Frequency: 2m,10m,15m,1h,2h,6h,15h)
if response.text == 'true':
self._alipay_validate_data(**post)
return 'success'
return ""
@http.route('/payment/alipay/return', type='http', auth="public", methods=['GET', 'POST'])
def alipay_return(self, **post):
@http.route(_return_url, type='http', auth="public", methods=['GET'])
def alipay_return_from_redirect(self, **data):
""" Alipay return """
_logger.info('Beginning Alipay form_feedback with post data %s', pprint.pformat(post))
self._alipay_validate_data(**post)
return werkzeug.utils.redirect('/payment/process')
_logger.info("received Alipay return data:\n%s", pprint.pformat(data))
request.env['payment.transaction'].sudo()._handle_feedback_data('alipay', data)
return werkzeug.utils.redirect('/payment/status')
@http.route('/payment/alipay/notify', type='http', auth='public', methods=['POST'], csrf=False)
@http.route(_notify_url, type='http', auth='public', methods=['POST'], csrf=False)
def alipay_notify(self, **post):
""" Alipay Notify """
_logger.info('Beginning Alipay notification form_feedback with post data %s', pprint.pformat(post))
return self._alipay_validate_notification(**post)
_logger.info("received Alipay notification data:\n%s", pprint.pformat(post))
self._alipay_validate_notification(**post)
request.env['payment.transaction'].sudo()._handle_feedback_data('alipay', post)
return 'success' # Return 'success' to stop receiving notifications for this tx
def _alipay_validate_notification(self, **post):
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
'alipay', post
)
if not tx_sudo:
raise ValidationError(
"Alipay: " + _(
"Received notification data with unknown reference:\n%s", pprint.pformat(post)
)
)
# Ensure that the notification was sent by Alipay
# See https://global.alipay.com/docs/ac/wap/async
acquirer_sudo = tx_sudo.acquirer_id
val = {
'service': 'notify_verify',
'partner': acquirer_sudo.alipay_merchant_partner_id,
'notify_id': post['notify_id']
}
response = requests.post(acquirer_sudo._alipay_get_api_url(), val, timeout=60)
response.raise_for_status()
if response.text != 'true':
raise ValidationError(
"Alipay: " + _(
"Received notification data not acknowledged by Alipay:\n%s",
pprint.pformat(post)
)
)
@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="payment.payment_acquirer_alipay" model="payment.acquirer">
<field name="name">Alipay</field>
<field name="provider">alipay</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="alipay_form"/>
<field name="redirect_form_view_id" ref="redirect_form"/>
<field name="support_authorization">False</field>
<field name="support_fees_computation">True</field>
<field name="support_tokenization">False</field>
</record>
</odoo>
+2 -2
View File
@@ -1,4 +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
-226
View File
@@ -1,226 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from hashlib import md5
from werkzeug import urls
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_compare
from odoo.addons.payment_alipay.controllers.main import AlipayController
from odoo.addons.payment.models.payment_acquirer import ValidationError
_logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[
('alipay', 'Alipay')
], ondelete={'alipay': 'set default'})
alipay_payment_method = fields.Selection([
('express_checkout', 'Express Checkout (only for Chinese Merchant)'),
('standard_checkout', 'Cross-border'),
], string='Account', default='express_checkout',
help=" * Cross-border: For the Overseas seller \n * Express Checkout: For the Chinese Seller")
alipay_merchant_partner_id = fields.Char(
string='Merchant Partner ID', required_if_provider='alipay', groups='base.group_user',
help='The Merchant Partner ID is used to ensure communications coming from Alipay are valid and secured.')
alipay_md5_signature_key = fields.Char(
string='MD5 Signature Key', required_if_provider='alipay', groups='base.group_user',
help="The MD5 private key is the 32-byte string which is composed of English letters and numbers.")
alipay_seller_email = fields.Char(string='Alipay Seller Email', groups='base.group_user')
def _get_feature_support(self):
res = super(PaymentAcquirer, self)._get_feature_support()
res['fees'].append('alipay')
return res
@api.model
def _get_alipay_urls(self, environment):
""" Alipay URLS """
if environment == 'prod':
return 'https://mapi.alipay.com/gateway.do'
return 'https://openapi.alipaydev.com/gateway.do'
def alipay_compute_fees(self, amount, currency_id, country_id):
""" Compute alipay fees.
:param float amount: the amount to pay
:param integer country_id: an ID of a res.country, or None. This is
the customer's country, to be compared to
the acquirer company country.
:return float fees: computed fees
"""
fees = 0.0
if self.fees_active:
country = self.env['res.country'].browse(country_id)
if country and self.company_id.sudo().country_id.id == country.id:
percentage = self.fees_dom_var
fixed = self.fees_dom_fixed
else:
percentage = self.fees_int_var
fixed = self.fees_int_fixed
fees = (percentage / 100.0 * amount + fixed) / (1 - percentage / 100.0)
return fees
def _build_sign(self, val):
# Rearrange parameters in the data set alphabetically
data_to_sign = sorted(val.items())
# Exclude parameters that should not be signed
data_to_sign = ["{}={}".format(k, v) for k, v in data_to_sign if k not in ['sign', 'sign_type', 'reference']]
# And connect rearranged parameters with &
data_string = '&'.join(data_to_sign)
data_string += self.alipay_md5_signature_key
return md5(data_string.encode('utf-8')).hexdigest()
def _get_alipay_tx_values(self, values):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
alipay_tx_values = ({
'_input_charset': 'utf-8',
'notify_url': urls.url_join(base_url, AlipayController._notify_url),
'out_trade_no': values.get('reference'),
'partner': self.alipay_merchant_partner_id,
'return_url': urls.url_join(base_url, AlipayController._return_url),
'subject': values.get('reference'),
'total_fee': values.get('amount') + values.get('fees'),
})
if self.alipay_payment_method == 'standard_checkout':
alipay_tx_values.update({
'service': 'create_forex_trade',
'product_code': 'NEW_OVERSEAS_SELLER',
'currency': values.get('currency').name,
})
else:
alipay_tx_values.update({
'service': 'create_direct_pay_by_user',
'payment_type': 1,
'seller_email': self.alipay_seller_email,
})
sign = self._build_sign(alipay_tx_values)
alipay_tx_values.update({
'sign_type': 'MD5',
'sign': sign,
})
return alipay_tx_values
def alipay_form_generate_values(self, values):
values.update(self._get_alipay_tx_values(values))
return values
def alipay_get_form_action_url(self):
self.ensure_one()
environment = 'prod' if self.state == 'enabled' else 'test'
return self._get_alipay_urls(environment)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _check_alipay_configuration(self, vals):
acquirer_id = int(vals.get('acquirer_id'))
acquirer = self.env['payment.acquirer'].sudo().browse(acquirer_id)
if acquirer and acquirer.provider == 'alipay' and acquirer.alipay_payment_method == 'express_checkout':
currency_id = int(vals.get('currency_id'))
if currency_id:
currency = self.env['res.currency'].sudo().browse(currency_id)
if currency and currency.name != 'CNY':
_logger.info("Only CNY currency is allowed for Alipay Express Checkout")
raise ValidationError(_("""
Only transactions in Chinese Yuan (CNY) are allowed for Alipay Express Checkout.\n
If you wish to use another currency than CNY for your transactions, switch your
configuration to a Cross-border account on the Alipay payment acquirer in Odoo.
"""))
return True
def write(self, vals):
if vals.get('currency_id') or vals.get('acquirer_id'):
for payment in self:
check_vals = {
'acquirer_id': vals.get('acquirer_id', payment.acquirer_id.id),
'currency_id': vals.get('currency_id', payment.currency_id.id)
}
payment._check_alipay_configuration(check_vals)
return super(PaymentTransaction, self).write(vals)
@api.model
def create(self, vals):
self._check_alipay_configuration(vals)
return super(PaymentTransaction, self).create(vals)
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
@api.model
def _alipay_form_get_tx_from_data(self, data):
reference, txn_id, sign = data.get('reference'), data.get('trade_no'), data.get('sign')
if not reference or not txn_id:
_logger.info('Alipay: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id))
raise ValidationError(_('Alipay: received data with missing reference (%s) or txn_id (%s)') % (reference, txn_id))
txs = self.env['payment.transaction'].search([('reference', '=', reference)])
if not txs or len(txs) > 1:
error_msg = _('Alipay: received data for reference %s') % (reference)
logger_msg = 'Alipay: received data for reference %s' % (reference)
if not txs:
error_msg += _('; no order found')
logger_msg += '; no order found'
else:
error_msg += _('; multiple order found')
logger_msg += '; multiple order found'
_logger.info(logger_msg)
raise ValidationError(error_msg)
# verify sign
sign_check = txs.acquirer_id._build_sign(data)
if sign != sign_check:
_logger.info('Alipay: invalid sign, received %s, computed %s, for data %s' % (sign, sign_check, data))
raise ValidationError(_('Alipay: invalid sign, received %s, computed %s, for data %s') % (sign, sign_check, data))
return txs
def _alipay_form_get_invalid_parameters(self, data):
invalid_parameters = []
if float_compare(float(data.get('total_fee', '0.0')), (self.amount + self.fees), 2) != 0:
invalid_parameters.append(('total_fee', data.get('total_fee'), '%.2f' % (self.amount + self.fees))) # mc_gross is amount + fees
if self.acquirer_id.alipay_payment_method == 'standard_checkout':
if data.get('currency') != self.currency_id.name:
invalid_parameters.append(('currency', data.get('currency'), self.currency_id.name))
else:
if data.get('seller_email') != self.acquirer_id.alipay_seller_email:
invalid_parameters.append(('seller_email', data.get('seller_email'), self.acquirer_id.alipay_seller_email))
return invalid_parameters
def _alipay_form_validate(self, data):
if self.state in ['done']:
_logger.info('Alipay: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = data.get('trade_status')
res = {
'acquirer_reference': data.get('trade_no'),
}
if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']:
_logger.info('Validated Alipay payment for tx %s: set as done' % (self.reference))
date_validate = fields.Datetime.now()
res.update(date=date_validate)
self._set_transaction_done()
self.write(res)
self.execute_callback()
return True
elif status == 'TRADE_CLOSED':
_logger.info('Received notification for Alipay payment %s: set as Canceled' % (self.reference))
res.update(state_message=data.get('close_reason', ''))
self._set_transaction_cancel()
return self.write(res)
else:
error = 'Received unrecognized status for Alipay payment %s: %s, set as error' % (self.reference, status)
_logger.info(error)
res.update(state_message=error)
self._set_transaction_error()
return self.write(res)
@@ -0,0 +1,60 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from hashlib import md5
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(
selection_add=[('alipay', "Alipay")], ondelete={'alipay': 'set default'})
alipay_payment_method = fields.Selection(
string="Account",
help="* Cross-border: For the overseas seller \n* Express Checkout: For the Chinese Seller",
selection=[
('express_checkout', 'Express Checkout (only for Chinese merchants)'),
('standard_checkout', 'Cross-border')
], default='express_checkout', required_if_provider='alipay')
alipay_merchant_partner_id = fields.Char(
string="Merchant Partner ID",
help="The public partner ID solely used to identify the account with Alipay",
required_if_provider='alipay')
alipay_md5_signature_key = fields.Char(
string="MD5 Signature Key", required_if_provider='alipay', groups='base.group_system')
alipay_seller_email = fields.Char(
string="Alipay Seller Email", help="The public Alipay partner email")
@api.model
def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Alipay acquirers for unsupported currencies. """
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 != 'CNY':
acquirers = acquirers.filtered(
lambda a: a.provider != 'alipay' or a.alipay_payment_method != 'express_checkout'
)
return acquirers
def _alipay_build_sign(self, val):
# Rearrange parameters in the data set alphabetically
data_to_sign = sorted(val.items())
# Format key-value pairs of parameters that should be signed
data_to_sign = [f"{k}={v}" for k, v in data_to_sign
if k not in ['sign', 'sign_type', 'reference']]
# Build the data string of &-separated key-value pairs
data_string = '&'.join(data_to_sign)
data_string += self.alipay_md5_signature_key
return md5(data_string.encode('utf-8')).hexdigest()
def _alipay_get_api_url(self):
if self.state == 'enabled':
return 'https://mapi.alipay.com/gateway.do'
else: # test environment
return 'https://openapi.alipaydev.com/gateway.do'
@@ -0,0 +1,168 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from werkzeug import urls
from odoo import _, api, models
from odoo.exceptions import ValidationError
from odoo.tools.float_utils import float_compare
from odoo.addons.payment_alipay.controllers.main import AlipayController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return Alipay-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 != 'alipay':
return res
base_url = self.acquirer_id._get_base_url()
if self.fees:
# Similarly to what is done in `payment::payment.transaction.create`, we need to round
# the sum of the amount and of the fees to avoid inconsistent string representations.
# E.g., str(1111.11 + 7.09) == '1118.1999999999998'
total_fee = self.currency_id.round(self.amount + self.fees)
else:
total_fee = self.amount
rendering_values = {
'_input_charset': 'utf-8',
'notify_url': urls.url_join(base_url, AlipayController._notify_url),
'out_trade_no': self.reference,
'partner': self.acquirer_id.alipay_merchant_partner_id,
'return_url': urls.url_join(base_url, AlipayController._return_url),
'subject': self.reference,
'total_fee': total_fee,
}
if self.acquirer_id.alipay_payment_method == 'standard_checkout':
# https://global.alipay.com/docs/ac/global/create_forex_trade
rendering_values.update({
'service': 'create_forex_trade',
'product_code': 'NEW_OVERSEAS_SELLER',
'currency': self.currency_id.name,
})
else:
rendering_values.update({
'service': 'create_direct_pay_by_user',
'payment_type': 1,
'seller_email': self.acquirer_id.alipay_seller_email,
})
sign = self.acquirer_id._alipay_build_sign(rendering_values)
rendering_values.update({
'sign_type': 'MD5',
'sign': sign,
'api_url': self.acquirer_id._alipay_get_api_url(),
})
return rendering_values
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Override of payment to find the transaction based on Alipay 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 inconsistent data were received
:raise: ValidationError if the data match no transaction
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'alipay':
return tx
reference = data.get('reference') or data.get('out_trade_no')
txn_id = data.get('trade_no')
if not reference or not txn_id:
raise ValidationError(
"Alipay: " + _(
"Received data with missing reference %(r)s or txn_id %(t)s.",
r=reference, t=txn_id
)
)
tx = self.search([('reference', '=', reference), ('provider', '=', 'alipay')])
if not tx:
raise ValidationError(
"Alipay: " + _("No transaction found matching reference %s.", reference)
)
# Verify signature (done here because we need the reference to get the acquirer)
sign_check = tx.acquirer_id._alipay_build_sign(data)
sign = data.get('sign')
if sign != sign_check:
raise ValidationError(
"Alipay: " + _(
"Expected signature %(sc) but received %(sign)s.", sc=sign_check, sign=sign
)
)
return tx
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Alipay data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
:raise: ValidationError if inconsistent data were received
"""
super()._process_feedback_data(data)
if self.provider != 'alipay':
return
if float_compare(float(data.get('total_fee', '0.0')), (self.amount + self.fees), 2) != 0:
# mc_gross is amount + fees
logging_values = {
'amount': data.get('total_fee', '0.0'),
'total': self.amount,
'fees': self.fees,
'reference': self.reference,
}
_logger.error(
"the paid amount (%(amount)s) does not match the total + fees (%(total)s + "
"%(fees)s) for the transaction with reference %(reference)s", logging_values
)
raise ValidationError("Alipay: " + _("The amount does not match the total + fees."))
if self.acquirer_id.alipay_payment_method == 'standard_checkout':
if data.get('currency') != self.currency_id.name:
raise ValidationError(
"Alipay: " + _(
"The currency returned by Alipay %(rc)s does not match the transaction "
"currency %(tc)s.", rc=data.get('currency'), tc=self.currency_id.name
)
)
elif data.get('seller_email') != self.acquirer_id.alipay_seller_email:
_logger.error(
"the seller email (%s) does not match the configured Alipay account (%s).",
data.get('seller_email'), self.acquirer_id.alipay_seller_email
)
raise ValidationError(
"Alipay: " + _("The seller email does not match the configured Alipay account.")
)
self.acquirer_reference = data.get('trade_no')
status = data.get('trade_status')
if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']:
self._set_done()
elif status == 'TRADE_CLOSED':
self._set_canceled()
else:
_logger.info(
"received invalid transaction status for transaction with reference %s: %s",
self.reference, status
)
self._set_error("Alipay: " + _("received invalid transaction status: %s", status))
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_alipay
+22
View File
@@ -0,0 +1,22 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.payment.tests.common import PaymentCommon
class AlipayCommon(PaymentCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.currency_yuan = cls._prepare_currency('CNY')
cls.alipay = cls._prepare_acquirer('alipay', update_values={
'alipay_merchant_partner_id': 'dummy',
'alipay_md5_signature_key': 'dummy',
'alipay_seller_email': 'dummy',
'fees_active': False, # Only activate fees in dedicated tests
})
# override defaults for helpers
cls.acquirer = cls.alipay
cls.currency = cls.currency_yuan
+120 -129
View File
@@ -1,87 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug import urls
from lxml import objectify
import odoo
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.addons.payment_alipay.controllers.main import AlipayController
@odoo.tests.tagged('post_install', '-at_install', 'external', '-standard')
class AlipayTest(PaymentAcquirerCommon):
from .common import AlipayCommon
from ..controllers.main import AlipayController
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.currency_yuan = cls.env['res.currency'].search([('name', '=', 'CNY'),
'|',
('active', '=', True),
('active', '=', False)], limit=1)
cls.alipay = cls.env.ref('payment.payment_acquirer_alipay')
cls.alipay.write({
'alipay_merchant_partner_id': 'dummy',
'alipay_md5_signature_key': 'dummy',
'alipay_seller_email': 'dummy',
'state': 'test',
})
@tagged('post_install', '-at_install')
class AlipayTest(AlipayCommon):
def test_10_alipay_form_render(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
self.assertEqual(self.alipay.state, 'test', 'test without test environment')
def test_compatible_acquirers(self):
self.alipay.alipay_payment_method = 'express_checkout'
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=self.currency_yuan.id, # 'CNY'
company_id=self.company.id,
)
self.assertIn(self.alipay, acquirers)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=self.currency_euro.id,
company_id=self.company.id,
)
self.assertNotIn(self.alipay, acquirers)
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
self.alipay.alipay_payment_method = 'standard_checkout'
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=self.currency_yuan.id, # 'CNY'
company_id=self.company.id,
)
self.assertIn(self.alipay, acquirers)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=self.currency_euro.id,
company_id=self.company.id,
)
self.assertIn(self.alipay, acquirers)
# render the button
res = self.alipay.render(
'test_ref0', 0.01, self.currency_euro.id,
values=self.buyer_values)
def test_01_redirect_form_standard_checkout(self):
self.alipay.alipay_payment_method = 'standard_checkout'
self._test_alipay_redirect_form()
form_values = {
def test_02_redirect_form_express_checkout(self):
self.alipay.alipay_payment_method = 'express_checkout'
self._test_alipay_redirect_form()
def _test_alipay_redirect_form(self):
tx = self.create_transaction(flow='redirect') # Only flow implemented
expected_values = {
'_input_charset': 'utf-8',
'notify_url': urls.url_join(base_url, AlipayController._notify_url),
'out_trade_no': 'SO12345-1',
'notify_url': self._build_url(AlipayController._notify_url),
'out_trade_no': self.reference,
'partner': self.alipay.alipay_merchant_partner_id,
'return_url': urls.url_join(base_url, AlipayController._return_url),
'subject': 'test_ref0',
'total_fee': '0.01',
'return_url': self._build_url(AlipayController._return_url),
'subject': self.reference,
'total_fee': str(self.amount), # Fees disabled by default
}
if self.alipay.alipay_payment_method == 'standard_checkout':
form_values.update({
expected_values.update({
'service': 'create_forex_trade',
'currency': 'EUR',
'product_code': 'NEW_OVERSEAS_SELLER',
'currency': self.currency_yuan.name,
})
else:
form_values.update({
'payment_type': '1',
expected_values.update({
'service': 'create_direct_pay_by_user',
'payment_type': str(1),
'seller_email': self.alipay.alipay_seller_email,
'service': 'create_direct_pay_by_user'
})
sign = self.alipay._build_sign(form_values)
sign = self.alipay._alipay_build_sign(expected_values)
form_values.update({'sign': sign, 'sign_type': 'MD5'})
# check form result
tree = objectify.fromstring(res)
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = tx._get_processing_values()
redirect_form_data = self._extract_values_from_html_form(processing_values['redirect_form_html'])
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'Alipay: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://openapi.alipaydev.com/gateway.do', 'alipay: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit', 'data_set', 'sign', 'out_trade_no']:
continue
self.assertEqual(form_input.get('value'), form_values[form_input.get('name')], 'alipay: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')]))
expected_values.update({
'sign': sign,
'sign_type': 'MD5',
})
def test_11_alipay_form_with_fees(self):
self.assertEqual(self.alipay.state, 'test', 'test without test environment')
self.assertEqual(
redirect_form_data['action'],
'https://openapi.alipaydev.com/gateway.do',
)
self.assertDictEqual(
expected_values,
redirect_form_data['inputs'],
"Alipay: invalid inputs specified in the redirect form.",
)
def test_03_redirect_form_with_fees(self):
# update acquirer: compute fees
self.alipay.write({
'fees_active': True,
@@ -91,34 +104,41 @@ class AlipayTest(PaymentAcquirerCommon):
'fees_int_var': 0.50,
})
# render the button
res = self.alipay.render(
'test_ref0', 12.50, self.currency_euro.id,
values=self.buyer_values)
transaction_fees = self.currency.round(
self.alipay._compute_fees(
self.amount,
self.currency,
self.partner.country_id,
)
)
self.assertEqual(transaction_fees, 7.09)
total_fee = self.currency.round(self.amount + transaction_fees)
self.assertEqual(total_fee, 1118.2)
tree = objectify.fromstring(res)
tx = self.create_transaction(flow='redirect')
self.assertEqual(tx.fees, 7.09)
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = tx._get_processing_values()
redirect_form_data = self._extract_values_from_html_form(processing_values['redirect_form_html'])
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'alipay: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://openapi.alipaydev.com/gateway.do', 'alipay: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['total_fee']:
self.assertEqual(form_input.get('value'), '14.07', 'alipay: wrong computed fees') # total amount = amount + fees
self.assertEqual(redirect_form_data['inputs']['total_fee'], str(total_fee))
@mute_logger('odoo.addons.payment_alipay.models.payment', 'ValidationError')
def test_20_alipay_form_management(self):
def test_21_standard_checkout_feedback(self):
self.alipay.alipay_payment_method = 'standard_checkout'
self._test_20_alipay_form_management()
self.currency = self.currency_euro
self._test_alipay_feedback_processing()
def test_22_express_checkout_feedback(self):
self.alipay.alipay_payment_method = 'express_checkout'
self._test_20_alipay_form_management()
def _test_20_alipay_form_management(self):
self.assertEqual(self.alipay.state, 'test', 'test without test environment')
self.currency = self.currency_yuan
self._test_alipay_feedback_processing()
def _test_alipay_feedback_processing(self):
# typical data posted by alipay after client has successfully paid
custom_reference = 'test_ref_' + self.alipay.alipay_payment_method
alipay_post_data = {
'trade_no': '2017112321001003690200384552',
'reference': 'test_ref_' + self.alipay.alipay_payment_method,
'reference': custom_reference,
'total_fee': 1.95,
'trade_status': 'TRADE_CLOSED',
}
@@ -132,33 +152,25 @@ class AlipayTest(PaymentAcquirerCommon):
'currency': 'EUR',
})
alipay_post_data['sign'] = self.alipay._build_sign(alipay_post_data)
# should raise error about unknown tx
with self.assertRaises(ValidationError):
self.env['payment.transaction'].form_feedback(alipay_post_data, 'alipay')
alipay_post_data['sign'] = self.alipay._alipay_build_sign(alipay_post_data)
with self.assertRaises(ValidationError): # unknown transactiion
self.env['payment.transaction']._handle_feedback_data('alipay', alipay_post_data)
if self.alipay.alipay_payment_method == 'express_checkout':
currency = self.currency_yuan
else:
currency = self.currency_euro
# create tx
tx = self.env['payment.transaction'].create({
'amount': 1.95,
'acquirer_id': self.alipay.id,
'currency_id': currency.id,
'reference': 'test_ref_' + self.alipay.alipay_payment_method,
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id
'currency_id': self.currency.id,
'reference': custom_reference,
'partner_id': self.partner.id
})
# validate tx
tx.form_feedback(alipay_post_data, 'alipay')
# check tx
self.assertEqual(tx.state, 'cancel', 'alipay: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552', 'alipay: wrong txn_id after receiving a valid pending notification')
self.env['payment.transaction']._handle_feedback_data('alipay', alipay_post_data)
self.assertEqual(tx.state, 'cancel',
'Alipay: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552',
'Alipay: wrong txn_id after receiving a valid pending notification')
# update tx
# reset the transaction
tx.write({'state': 'draft', 'acquirer_reference': False})
# update notification from alipay should not go through since it has already been set as 'done'
@@ -166,37 +178,16 @@ class AlipayTest(PaymentAcquirerCommon):
alipay_post_data['trade_status'] = 'TRADE_FINISHED'
else:
alipay_post_data['trade_status'] = 'TRADE_SUCCESS'
alipay_post_data['sign'] = self.alipay._build_sign(alipay_post_data)
# validate tx
tx.form_feedback(alipay_post_data, 'alipay')
# check tx
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552', 'alipay: notification should not go throught since it has already been validated')
alipay_post_data['sign'] = self.alipay._alipay_build_sign(alipay_post_data)
self.env['payment.transaction']._handle_feedback_data('alipay', alipay_post_data)
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552',
'Alipay: notification should not go throught since it has already been validated')
# this time it should go through since the transaction is not validated yet
tx.write({'state': 'draft', 'acquirer_reference': False})
tx.form_feedback(alipay_post_data, 'alipay')
self.assertEqual(tx.state, 'done', 'alipay: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552', 'alipay: wrong txn_id after receiving a valid pending notification')
@mute_logger('odoo.addons.payment_alipay.models.payment', 'ValidationError')
def test_30_alipay_bad_configuration(self):
self.alipay.alipay_payment_method = 'express_checkout'
# should raise error since `express_checkout` must only be used with CNY currency
with self.assertRaises(ValidationError):
# create tx
tx = self.env['payment.transaction'].create({
'acquirer_id': self.alipay.id,
'amount': 4,
'currency_id': self.currency_euro.id,
'reference': 'test_ref_2',
'partner_country_id': self.country_france.id
})
tx = self.env['payment.transaction'].create({
'acquirer_id': self.alipay.id,
'amount': 4,
'currency_id': self.currency_yuan.id,
'reference': 'test_ref_2',
'partner_country_id': self.country_france.id
})
self.env['payment.transaction']._handle_feedback_data('alipay', alipay_post_data)
self.assertEqual(tx.state, 'done',
'Alipay: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '2017112321001003690200384552',
'Alipay: wrong txn_id after receiving a valid pending notification')
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="alipay_form">
<div>
<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="_input_charset" t-att-value="_input_charset"/>
<input t-if="service == 'create_forex_trade'" type="hidden" name="currency" t-att-value="currency"/>
<input type="hidden" name="notify_url" t-att-value="notify_url"/>
@@ -17,6 +17,7 @@
<input type="hidden" name="sign_type" t-att-value="sign_type"/>
<input t-if="payment_type" type="hidden" name="payment_type" t-att-value="payment_type"/>
<input t-if="seller_email" type="hidden" name="seller_email" t-att-value="seller_email"/>
</div>
</form>
</template>
</odoo>
@@ -1,18 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_acquirer_view_form_inherit_payment_alipay" model="ir.ui.view">
<field name="name">payment.acquirer.view.form.inherit.payment.alipay</field>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">Alipay Acquirer Form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<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', '!=', 'alipay')]}">
<field name="alipay_payment_method" widget="radio"/>
<field name="alipay_seller_email" attrs="{'invisible': [('alipay_payment_method', '=', 'standard_checkout')], 'required': [('provider', '=', 'alipay'), ('alipay_payment_method', '=', 'express_checkout')]}"/>
<field name="alipay_seller_email"
attrs="{'invisible': [('alipay_payment_method', '=', 'standard_checkout')], 'required': [('provider', '=', 'alipay'), ('alipay_payment_method', '=', 'express_checkout')]}"/>
<field name="alipay_merchant_partner_id" password="True"/>
<field name="alipay_md5_signature_key" password="True"/>
</group>
</xpath>
</field>
</record>
</odoo>