[IMP] payment(_adyen, _authorize): disable tokens of disabled acquirers

Before this commit, zombie payment tokens (= tokens linked to disabled
acquirers) could still
1) be used by internal users and
2) reactivated when the acquirer’s state changed to ‘test’ or ‘enabled’.
This is not desirable because zombie tokens should neither be used,
nor reactivated.

After this commit, all tokens related to an acquirer are unassigned
from linked documents and archived as soon as the acquirer’s state is
changed to ‘disabled’. Creating a payment with an archived token is
prohibited. In addition, archived tokens cannot be un-archived anymore.

task-2649806

closes odoo/odoo#93774

Related: odoo/enterprise#28661
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Laura Schauer
2022-07-12 03:09:44 +02:00
committed by Antoine Vandevenne (anv)
parent 2432b4a490
commit 2ee4251d07
10 changed files with 124 additions and 161 deletions
+44
View File
@@ -233,6 +233,35 @@ class PaymentAcquirer(models.Model):
self.ensure_one()
return self.env.ref('account.account_payment_method_manual_in').id
#=== ONCHANGE METHODS ===#
@api.onchange('state')
def _onchange_state(self):
""" Display a warning about the consequences of disabling an acquirer.
Let the user know that tokens related to an acquirer get archived if it is disabled or if
its state is changed from 'test' to 'enabled' and vice versa.
:return: The warning message in a client action.
:rtype: dict
"""
self.ensure_one()
if self._origin.state in ('test', 'enabled') and self._origin.state != self.state:
related_tokens = self.env['payment.token'].search(
[('acquirer_id', '=', self._origin.id)]
)
if related_tokens:
return {
'warning': {
'title': _("Warning"),
'message': _(
"This action will also archive %s tokens that are registered with this "
"acquirer. Archiving tokens is irreversible.", len(related_tokens)
)
}
}
#=== CONSTRAINT METHODS ===#
@api.constrains('fees_dom_var', 'fees_int_var')
@@ -257,8 +286,16 @@ class PaymentAcquirer(models.Model):
return acquirers
def write(self, values):
# Handle acquirer disabling.
if 'state' in values:
state_changed_acquirers = self.filtered(
lambda acq: acq.state not in ('disabled', values['state'])
) # Don't handle acquirers being enabled or whose state is not updated.
state_changed_acquirers._handle_state_change()
result = super().write(values)
self._check_required_if_provider()
return result
def _check_required_if_provider(self):
@@ -287,6 +324,13 @@ class PaymentAcquirer(models.Model):
_("The following fields must be filled: %s", ", ".join(field_names))
)
def _handle_state_change(self):
""" Archive all the payment tokens linked to these acquirers.
:return: None
"""
self.env['payment.token'].search([('acquirer_id', 'in', self.ids)]).write({'active': False})
#=== ACTION METHODS ===#
def button_immediate_install(self):
+16 -36
View File
@@ -2,7 +2,8 @@
import logging
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -60,54 +61,33 @@ class PaymentToken(models.Model):
return dict()
def write(self, values):
""" Delegate the handling of active state switch to dedicated methods.
""" Prevent unarchiving tokens and handle their archiving.
Unless an exception is raised in the handling methods, the toggling proceeds no matter what.
This is because allowing users to hide their saved payment methods comes before making sure
that the recorded payment details effectively get deleted.
:return: The result of the write
:return: The result of the call to the parent method.
:rtype: bool
:raise UserError: If at least one token is being unarchived.
"""
# Let acquirers handle activation/deactivation requests
if 'active' in values:
for token in self:
# Call handlers in sudo mode because this method might have been called by RPC
if values['active'] and not token.active:
token.sudo()._handle_reactivation_request()
elif not values['active'] and token.active:
token.sudo()._handle_deactivation_request()
if values['active']:
if any(not token.active for token in self):
raise UserError(_("A token cannot be unarchived once it has been archived."))
else:
# Call the handlers in sudo mode because this method might have been called by RPC.
self.filtered('active').sudo()._handle_archiving()
# Proceed with the toggling of the active state
return super().write(values)
#=== BUSINESS METHODS ===#
def _handle_deactivation_request(self):
""" Handle the request for deactivation of the token.
def _handle_archiving(self):
""" Handle the archiving of the current tokens.
For an acquirer to support deactivation of tokens, or perform additional operations when a
token is deactivated, it must overwrite this method and raise an UserError if the token
cannot be deactivated.
Note: self.ensure_one()
For a module to perform additional operations when a token is archived, it must override
this method.
:return: None
"""
self.ensure_one()
def _handle_reactivation_request(self):
""" Handle the request for reactivation of the token.
For an acquirer to support reactivation of tokens, or perform additional operations when a
token is reactivated, it must overwrite this method and raise an UserError if the token
cannot be reactivated.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
return None
def get_linked_records_info(self):
""" Return a list of information about records linked to the current token.
+47 -25
View File
@@ -10,7 +10,7 @@ import psycopg2
from dateutil import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.exceptions import UserError, ValidationError
from odoo.tools import consteq, format_amount, ustr
from odoo.tools.misc import hmac as hmac_tool
@@ -174,6 +174,12 @@ class PaymentTransaction(models.Model):
', '.join(set(illegal_authorize_state_txs.mapped('acquirer_id.name')))
))
@api.constrains('token_id')
def _check_token_is_active(self):
""" Check that the token used to create the transaction is active. """
if self.token_id and not self.token_id.active:
raise ValidationError(_("Creating a transaction from an archived token is forbidden."))
#=== CRUD METHODS ===#
@api.model_create_multi
@@ -547,6 +553,7 @@ class PaymentTransaction(models.Model):
:return: None
"""
self.ensure_one()
self._ensure_acquirer_is_not_disabled()
self._log_sent_message()
def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True):
@@ -563,6 +570,7 @@ class PaymentTransaction(models.Model):
:rtype: recordset of `payment.transaction`
"""
self.ensure_one()
self._ensure_acquirer_is_not_disabled()
if create_refund_transaction:
refund_tx = self._create_refund_transaction(amount_to_refund=amount_to_refund)
@@ -571,30 +579,6 @@ class PaymentTransaction(models.Model):
else:
return self.env['payment.transaction']
def _send_capture_request(self):
""" Request the provider of the acquirer handling the transaction to capture it.
For an acquirer to support authorization, it must override this method and request a capture
to its provider.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
def _send_void_request(self):
""" Request the provider of the acquirer handling the transaction to void it.
For an acquirer to support authorization, it must override this method and request the
transaction to be voided to its provider.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
def _create_refund_transaction(self, amount_to_refund=None, **custom_create_values):
""" Create a new transaction with operation 'refund' and link it to the current transaction.
@@ -617,6 +601,44 @@ class PaymentTransaction(models.Model):
**custom_create_values,
})
def _send_capture_request(self):
""" Request the provider of the acquirer handling the transaction to capture it.
For an acquirer to support authorization, it must override this method and request a capture
to its provider.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
self._ensure_acquirer_is_not_disabled()
def _send_void_request(self):
""" Request the provider of the acquirer handling the transaction to void it.
For an acquirer to support authorization, it must override this method and request the
transaction to be voided to its provider.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
self._ensure_acquirer_is_not_disabled()
def _ensure_acquirer_is_not_disabled(self):
""" Ensure that the acquirer's state is not 'disabled' before sending a request to its
provider.
:return: None
:raise UserError: If the acquirer's state is 'disabled'.
"""
if self.acquirer_id.state == 'disabled':
raise UserError(_(
"Making a request to the provider is not possible because the acquirer is disabled."
))
def _handle_notification_data(self, provider, notification_data):
""" Match the transaction with the notification data, update its state and return it.
+1
View File
@@ -192,6 +192,7 @@ class PaymentCommon(AccountTestInvoicingCommon):
'acquirer_id': self.acquirer.id,
'partner_id': self.partner.id,
'acquirer_ref': "Acquirer Ref (TEST)",
'active': True,
}
return self.env['payment.token'].sudo(sudo).create(dict(default_values, **values))
@@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentToken(PaymentCommon):
def test_token_cannot_be_unarchived(self):
""" Test that unarchiving disabled tokens is forbidden. """
token = self._create_token(active=False)
with self.assertRaises(UserError):
token.active = True
@@ -1,7 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, fields, models
from odoo.exceptions import UserError, ValidationError
class PaymentToken(models.Model):
@@ -10,45 +9,3 @@ class PaymentToken(models.Model):
adyen_shopper_reference = fields.Char(
string="Shopper Reference", help="The unique reference of the partner owning this token",
readonly=True)
#=== BUSINESS METHODS ===#
def _handle_deactivation_request(self):
""" Override of payment to request request Adyen to delete the token.
Note: self.ensure_one()
:return: None
"""
super()._handle_deactivation_request()
if self.provider != 'adyen':
return
data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account,
'shopperReference': self.adyen_shopper_reference,
'recurringDetailReference': self.acquirer_ref,
}
try:
self.acquirer_id._adyen_make_request(
url_field_name='adyen_recurring_api_url',
endpoint='/disable',
payload=data,
method='POST'
)
except ValidationError:
pass # Deactivating the token in Odoo is more important than in Adyen
def _handle_reactivation_request(self):
""" Override of payment to raise an error informing that Adyen tokens cannot be restored.
Note: self.ensure_one()
:return: None
:raise: UserError if the token is managed by Adyen
"""
super()._handle_reactivation_request()
if self.provider != 'adyen':
return
raise UserError(_("Saved payment methods cannot be restored once they have been deleted."))
-6
View File
@@ -39,12 +39,6 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
processing_values['access_token'], self.reference, converted_amount, self.partner.id
))
def test_token_activation(self):
"""Activation of disabled adyen tokens is forbidden"""
token = self._create_token(active=False)
with self.assertRaises(UserError):
token._handle_reactivation_request()
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
def test_send_refund_request(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False
@@ -23,30 +23,3 @@ class PaymentToken(models.Model):
selection=[("credit_card", "Credit Card"), ("bank_account", "Bank Account (USA Only)")],
)
def _handle_deactivation_request(self):
""" Override of payment to request Authorize.Net to delete the token.
Note: self.ensure_one()
:return: None
"""
super()._handle_deactivation_request()
if self.provider != 'authorize':
return
authorize_API = AuthorizeAPI(self.acquirer_id)
res_content = authorize_API.delete_customer_profile(self.authorize_profile)
_logger.info("delete_customer_profile request response:\n%s", pprint.pformat(res_content))
def _handle_reactivation_request(self):
""" Override of payment to raise an error informing that Auth.net tokens cannot be restored.
Note: self.ensure_one()
:return: None
"""
super()._handle_reactivation_request()
if self.provider != 'authorize':
return
raise UserError(_("Saved payment methods cannot be restored once they have been deleted."))
@@ -49,12 +49,6 @@ class AuthorizeTest(AuthorizeCommon):
self.assertEqual(self.authorize._get_validation_amount(), 0.01)
self.assertEqual(self.authorize._get_validation_currency(), self.currency_usd)
def test_token_activation(self):
"""Activation of disabled authorize tokens is forbidden"""
token = self._create_token(active=False)
with self.assertRaises(UserError):
token._handle_reactivation_request()
def test_authorize_neutralize(self):
self.env['payment.acquirer']._neutralize()
@@ -6,21 +6,3 @@ from odoo.exceptions import UserError
class PaymentToken(models.Model):
_inherit = 'payment.token'
def _handle_reactivation_request(self):
""" Override of payment to raise an error informing that Ogone tokens cannot be restored.
More specifically, permanents tokens are never deleted in Ogone's backend but we don't
distinguish them from temporary tokens which are archived at creation time. So we simply
block the reactivation of every token.
Note: self.ensure_one()
:return: None
:raise: UserError if the token is managed by Ogone
"""
super()._handle_reactivation_request()
if self.provider != 'ogone':
return
raise UserError(_("Saved payment methods cannot be restored once they have been archived."))