Files
odoo_source/addons/payment/tests/http_common.py
T
Antoine Vandevenne (anv) dbefe34af5 [IMP] payment(_authorize): rework validation transactions refund flow
Before this commit, the validation flow with verification (payment of a
small amount with immediate refund) was performed with the use of
validation routes: after payment, the customer was redirected to the
validation route stored on the transaction to trigger the refund. This
implementation had an issue: if the customer never reached the
validation route, they were not refunded their validation amount. This
could happen if the customer closed the tab after paying with an
acquirer offering payments with redirection, or if the validation
payment was asynchronously confirmed through a webhook notification.

This commit gets rid of validation routes and requires acquirers to
immediately refund the validation amount when the payment is confirmed.
This way, a payment confirmation coming from a webhook can trigger the
refund too.

As the only acquirer that implements the validation with verification
flow, Authorize.net now voids validation transactions as soon as they
are authorized.

While we're at it, the logging of processing values is adapted to only
log specific rendering values if a redirect form is rendered.

task-2612977

closes odoo/odoo#74707

Related: odoo/enterprise#20060
Related: odoo/upgrade#2710
Signed-off-by: Antoine Vandevenne (anv) <AntoineVDV@users.noreply.github.com>
2021-08-04 13:03:27 +00:00

181 lines
6.0 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from uuid import uuid4
from lxml import etree, objectify
from odoo import http
from odoo.tests import HttpCase
from odoo.addons.payment.tests.utils import PaymentTestUtils
class PaymentHttpCommon(PaymentTestUtils, HttpCase):
""" HttpCase common to build and simulate requests going through payment controllers.
Only use if you effectively want to test controllers.
If you only want to test 'models' code, the PaymentCommon should be sufficient.
Note: This Common is expected to be used in parallel with the main PaymentCommon.
"""
# Helpers #
###########
def _build_jsonrpc_payload(self, params):
"""Helper to properly build jsonrpc payload"""
if not getattr(self, 'session', None):
# We need to create a session (public if no login & passwd)
# before generating a csrf token
self.authenticate('', '')
params['csrf_token'] = http.WebRequest.csrf_token(self)
return {
"jsonrpc": "2.0",
"method": "call",
"id": str(uuid4()),
"params": params,
}
def _make_http_get_request(self, url, params):
formatted_data = dict()
for k, v in params.items():
if isinstance(v, float):
formatted_data[k] = str(v)
else:
formatted_data[k] = v
return self.opener.get(url, params=formatted_data)
def _make_json_request(self, url, params):
data = self._build_jsonrpc_payload(params)
return self.opener.post(url, json=data)
def _get_tx_context(self, response, form_name):
"""Extracts txContext & other form info (acquirer & token ids)
from a payment response (with manage/checkout html form)
:param response: http Response, with a payment form as text
:param str form_name: o_payment_manage / o_payment_checkout
:return: Transaction context (+ acquirer_ids & token_ids)
:rtype: dict
"""
# Need to specify an HTML parser as parser
# Otherwise void elements (<img>, <link> without a closing / tag)
# are considered wrong and trigger a lxml.etree.XMLSyntaxError
html_tree = objectify.fromstring(
response.text,
parser=etree.HTMLParser(),
)
checkout_form = html_tree.xpath(f"//form[@name='{form_name}']")[0]
values = {}
for key, val in checkout_form.items():
if key.startswith("data-"):
formatted_key = key[5:].replace('-', '_')
if formatted_key.endswith('_id'):
formatted_val = int(val)
elif formatted_key == 'amount':
formatted_val = float(val)
else:
formatted_val = val
values[formatted_key] = formatted_val
payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']")
acquirer_ids = []
token_ids = []
for p_o_input in payment_options_inputs:
data = dict()
for key, val in p_o_input.items():
if key.startswith('data-'):
data[key[5:]] = val
if data['payment-option-type'] == 'acquirer':
acquirer_ids.append(int(data['payment-option-id']))
else:
token_ids.append(int(data['payment-option-id']))
values.update({
'acquirer_ids': acquirer_ids,
'token_ids': token_ids,
})
return values
# payment/pay #
###############
def _prepare_pay_values(self, amount=0.0, currency=None, reference='', partner=None):
"""Prepare basic payment/pay route values
NOTE: needs PaymentCommon to enable fallback values.
:rtype: dict
"""
amount = amount or self.amount
currency = currency or self.currency
reference = reference or self.reference
partner = partner or self.partner
return {
'amount': amount,
'currency_id': currency.id,
'reference': reference,
'partner_id': partner.id,
'access_token': self._generate_test_access_token(partner.id, amount, currency.id),
}
def portal_pay(self, **route_kwargs):
"""/payment/pay txContext feedback
NOTE: must be authenticated before calling method.
Or an access_token should be specified in route_kwargs
"""
uri = '/payment/pay'
url = self._build_url(uri)
return self._make_http_get_request(url, route_kwargs)
def get_tx_checkout_context(self, **route_kwargs):
response = self.portal_pay(**route_kwargs)
self.assertEqual(response.status_code, 200)
return self._get_tx_context(response, 'o_payment_checkout')
# /my/payment_method #
######################
def portal_payment_method(self):
"""/my/payment_method txContext feedback
NOTE: must be authenticated before calling method
validation flow is restricted to logged users
"""
uri = '/my/payment_method'
url = self._build_url(uri)
return self._make_http_get_request(url, {})
def get_tx_manage_context(self, **route_kwargs):
response = self.portal_payment_method(**route_kwargs)
self.assertEqual(response.status_code, 200)
return self._get_tx_context(response, 'o_payment_manage')
# payment/transaction #
#######################
def portal_transaction(self, **route_kwargs):
"""/payment/transaction feedback
:returns: processing values for given route_kwargs
:rtype: dict
"""
uri = '/payment/transaction'
url = self._build_url(uri)
return self._make_json_request(url, route_kwargs)
def get_processing_values(self, **route_kwargs):
response = self.portal_transaction(**route_kwargs)
self.assertEqual(response.status_code, 200)
resp_content = json.loads(response.content)
return resp_content['result']