After the payment apocalypse, `payment_test` was cleaned and stripped of all useless parts. It was a very basic testing acquirer that allowed to enter fake arbitrary payment details, choose whether they should be tokenized, and then pay. It was intended to test the payment flow of various applications (Sales, Subscriptions, Invoicing, ...) for a single scenario: successful payments with immediate capture. In order to extensively test an app's payment flow (explore other scenarios), we need `payment_test` to have additional features. Now, `payment_test` will allow profound testing of these features: - separate authorization and capture - customer fees computation - tokenization - refunds - on the /pay page, users will be able to select the status of the payment - on the payment transaction form view, users can change the status of a payment 'pending' to either succeed or fail - on the payment token form view, users can change the state of future transactions made with that token task-2512196 closes odoo/odoo#78083 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com> Co-authored-by: xlu-odoo <xlu@odoo.com>
222 lines
7.8 KiB
Python
222 lines
7.8 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
|
|
from odoo import _, fields, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
from odoo.addons.payment import utils as payment_utils
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentTransaction(models.Model):
|
|
_inherit = 'payment.transaction'
|
|
|
|
capture_manually = fields.Boolean(related='acquirer_id.capture_manually')
|
|
|
|
#=== ACTION METHODS ===#
|
|
|
|
def action_test_set_done(self):
|
|
""" Set the state of the test transaction to 'done'.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
self.ensure_one()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
notification_data = {'reference': self.reference, 'simulated_state': 'done'}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
def action_test_set_canceled(self):
|
|
""" Set the state of the test transaction to 'cancel'.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
self.ensure_one()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
notification_data = {'reference': self.reference, 'simulated_state': 'cancel'}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
def action_test_set_error(self):
|
|
""" Set the state of the test transaction to 'error'.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
self.ensure_one()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
notification_data = {'reference': self.reference, 'simulated_state': 'error'}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
#=== BUSINESS METHODS ===#
|
|
|
|
def _send_payment_request(self):
|
|
""" Override of payment to simulate a payment request.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
super()._send_payment_request()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
if not self.token_id:
|
|
raise UserError("Test: " + _("The transaction is not linked to a token."))
|
|
|
|
simulated_state = self.token_id.test_simulated_state
|
|
notification_data = {'reference': self.reference, 'simulated_state': simulated_state}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
def _send_refund_request(self, create_refund_transaction=True, **kwargs):
|
|
""" Override of payment to simulate a refund.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param bool create_refund_transaction: Whether a refund transaction should be created.
|
|
:param dict kwargs: The keyword arguments.
|
|
:return: The refund transaction if any
|
|
:rtype: recordset of `payment.transaction`
|
|
"""
|
|
if self.provider != 'test':
|
|
return super()._send_refund_request(
|
|
create_refund_transaction=create_refund_transaction, **kwargs
|
|
)
|
|
refund_tx = super()._send_refund_request(create_refund_transaction=True, **kwargs)
|
|
|
|
notification_data = {'reference': refund_tx.reference, 'simulated_state': 'done'}
|
|
refund_tx._handle_notification_data('test', notification_data)
|
|
|
|
return refund_tx
|
|
|
|
def _send_capture_request(self):
|
|
""" Override of payment to simulate a capture request.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
super()._send_capture_request()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
notification_data = {
|
|
'reference': self.reference,
|
|
'simulated_state': 'done',
|
|
'manual_capture': True, # Distinguish manual captures from regular one-step captures.
|
|
}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
def _send_void_request(self):
|
|
""" Override of payment to simulate a void request.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:return: None
|
|
"""
|
|
super()._send_void_request()
|
|
if self.provider != 'test':
|
|
return
|
|
|
|
notification_data = {'reference': self.reference, 'simulated_state': 'cancel'}
|
|
self._handle_notification_data('test', notification_data)
|
|
|
|
def _get_tx_from_notification_data(self, provider, notification_data):
|
|
""" Override of payment to find the transaction based on dummy data.
|
|
|
|
:param str provider: The provider of the acquirer that handled the transaction
|
|
:param dict notification_data: The dummy notification data
|
|
:return: The transaction if found
|
|
:rtype: recordset of `payment.transaction`
|
|
:raise: ValidationError if the data match no transaction
|
|
"""
|
|
tx = super()._get_tx_from_notification_data(provider, notification_data)
|
|
if provider != 'test' or len(tx) == 1:
|
|
return tx
|
|
|
|
reference = notification_data.get('reference')
|
|
tx = self.search([('reference', '=', reference), ('provider', '=', 'test')])
|
|
if not tx:
|
|
raise ValidationError(
|
|
"Test: " + _("No transaction found matching reference %s.", reference)
|
|
)
|
|
return tx
|
|
|
|
def _process_notification_data(self, notification_data):
|
|
""" Override of payment to process the transaction based on dummy data.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param dict notification_data: The dummy notification data
|
|
:return: None
|
|
:raise: ValidationError if inconsistent data were received
|
|
"""
|
|
super()._process_notification_data(notification_data)
|
|
if self.provider != "test":
|
|
return
|
|
|
|
if self.tokenize:
|
|
# The reasons why we immediately tokenize the transaction regardless of the state rather
|
|
# than waiting for the payment method to be validated ('authorized' or 'done') like the
|
|
# other payment acquirers do are:
|
|
# - To save the simulated state and payment details on the token while we have them.
|
|
# - To allow customers to create tokens whose transactions will always end up in the
|
|
# said simulated state.
|
|
self._test_tokenize_from_notification_data(notification_data)
|
|
|
|
state = notification_data['simulated_state']
|
|
if state == 'pending':
|
|
self._set_pending()
|
|
elif state == 'done':
|
|
if self.capture_manually and not notification_data.get('manual_capture'):
|
|
self._set_authorized()
|
|
else:
|
|
self._set_done()
|
|
# Immediately post-process the transaction if it is a refund, as the post-processing
|
|
# will not be triggered by a customer browsing the transaction from the portal.
|
|
if self.operation == 'refund':
|
|
self.env.ref('payment.cron_post_process_payment_tx')._trigger()
|
|
elif state == 'cancel':
|
|
self._set_canceled()
|
|
else: # Simulate an error state.
|
|
self._set_error(_("You selected the following test payment status: %s", state))
|
|
|
|
def _test_tokenize_from_notification_data(self, notification_data):
|
|
""" Create a new token based on the notification data.
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param dict notification_data: The fake notification data to tokenize from.
|
|
:return: None
|
|
"""
|
|
self.ensure_one()
|
|
|
|
payment_details_short = notification_data['cc_summary']
|
|
state = notification_data['simulated_state']
|
|
token = self.env['payment.token'].create({
|
|
'acquirer_id': self.acquirer_id.id,
|
|
'name': payment_utils.build_token_name(payment_details_short=payment_details_short),
|
|
'partner_id': self.partner_id.id,
|
|
'acquirer_ref': 'fake acquirer reference',
|
|
'verified': True,
|
|
'test_simulated_state': state,
|
|
})
|
|
self.write({
|
|
'token_id': token,
|
|
'tokenize': False,
|
|
})
|
|
_logger.info(
|
|
"Created token with id %s for partner with id %s.", token.id, self.partner_id.id
|
|
)
|