diff --git a/addons/payment/models/payment_token.py b/addons/payment/models/payment_token.py index f9b958d23af..82f38414370 100644 --- a/addons/payment/models/payment_token.py +++ b/addons/payment/models/payment_token.py @@ -16,9 +16,9 @@ class PaymentToken(models.Model): acquirer_id = fields.Many2one( string="Acquirer Account", comodel_name='payment.acquirer', required=True) provider = fields.Selection(related='acquirer_id.provider') - name = fields.Char( - string="Name", help="The anonymized acquirer reference of the payment method", - required=True) + payment_details = fields.Char( + string="Payment Details", help="The clear part of the payment method's payment details.", + ) partner_id = fields.Many2one(string="Partner", comodel_name='res.partner', required=True) company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others) related='acquirer_id.company_id', store=True, index=True) @@ -77,8 +77,6 @@ class PaymentToken(models.Model): return super().write(values) - #=== BUSINESS METHODS ===# - def _handle_archiving(self): """ Handle the archiving of the current tokens. @@ -89,6 +87,45 @@ class PaymentToken(models.Model): """ return None + def name_get(self): + return [(token.id, token._build_display_name()) for token in self] + + #=== BUSINESS METHODS ===# + + def _build_display_name(self, *args, max_length=34, should_pad=True, **kwargs): + """ Build a token name of the desired maximum length with the format •••• 1234. + + The payment details are padded on the left with up to four padding characters. The padding + is only added if there is enough room for it. If not, it is either reduced or not added at + all. If there is not enough room for the payment details either, they are trimmed from the + left. + + For a module to customize the display name of a token, it must override this method. + + Note: self.ensure_one() + + :param list args: The arguments passed by QWeb when calling this method. + :param int max_length: The desired maximum length of the token name. + :param bool should_pad: Whether the token should be padded or not. + :param dict kwargs: Optional data used in overrides of this method. + :return: The padded token name. + :rtype: str + """ + self.ensure_one() + + padding_length = max_length - len(self.payment_details) + if not self.payment_details: + create_date_str = self.create_date.strftime('%Y/%m/%d') + display_name = _("Payment details saved on %(date)s", date=create_date_str) + elif padding_length >= 2: # Enough room for padding. + padding = '•' * min(padding_length - 1, 4) + ' ' if should_pad else '' + display_name = ''.join([padding, self.payment_details]) + elif padding_length > 0: # Not enough room for padding. + display_name = self.payment_details + else: # Not enough room for neither padding nor the payment details. + display_name = self.payment_details[-max_length:] if max_length > 0 else '' + return display_name + def get_linked_records_info(self): """ Return a list of information about records linked to the current token. diff --git a/addons/payment/models/payment_transaction.py b/addons/payment/models/payment_transaction.py index 279f588f717..45d53510e22 100644 --- a/addons/payment/models/payment_transaction.py +++ b/addons/payment/models/payment_transaction.py @@ -1059,8 +1059,10 @@ class PaymentTransaction(models.Model): elif self.operation in ('online_token', 'offline'): message = _( "A transaction with reference %(ref)s has been initiated using the payment method " - "%(token_name)s (%(acq_name)s).", - ref=self.reference, token_name=self.token_id.name, acq_name=self.acquirer_id.name + "%(token)s (%(acq_name)s).", + ref=self.reference, + token=self.token_id._build_display_name(), + acq_name=self.acquirer_id.name ) else: # 'validation' message = _( diff --git a/addons/payment/tests/__init__.py b/addons/payment/tests/__init__.py index fb7389458f4..61d0926039d 100644 --- a/addons/payment/tests/__init__.py +++ b/addons/payment/tests/__init__.py @@ -6,4 +6,5 @@ from . import test_account_payment from . import test_flows from . import test_multicompany_flows from . import test_payment_acquirer +from . import test_payment_token from . import test_payment_transaction diff --git a/addons/payment/tests/common.py b/addons/payment/tests/common.py index 45aa009d1a2..f5911183881 100644 --- a/addons/payment/tests/common.py +++ b/addons/payment/tests/common.py @@ -188,7 +188,7 @@ class PaymentCommon(AccountTestInvoicingCommon): def _create_token(self, sudo=True, **values): default_values = { - 'name': "XXXXXXXXXXXXXXX-2565 (TEST)", + 'payment_details': "1234", 'acquirer_id': self.acquirer.id, 'partner_id': self.partner.id, 'acquirer_ref': "Acquirer Ref (TEST)", diff --git a/addons/payment/tests/test_payment_token.py b/addons/payment/tests/test_payment_token.py index c788cb9ebc6..2e3428ac4b1 100644 --- a/addons/payment/tests/test_payment_token.py +++ b/addons/payment/tests/test_payment_token.py @@ -1,5 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import date + from odoo.exceptions import UserError from odoo.tests import tagged @@ -14,3 +16,26 @@ class TestPaymentToken(PaymentCommon): token = self._create_token(active=False) with self.assertRaises(UserError): token.active = True + + def test_display_name_is_padded(self): + """ Test that the display name is built by padding the payment details. """ + token = self._create_token() + self.assertEqual(token._build_display_name(), '•••• 1234') + + def test_display_name_for_empty_payment_details(self): + """ Test that the display name is still built for token without payment details. """ + token = self._create_token(payment_details='') + self.assertEqual( + token._build_display_name(), + f"Payment details saved on {date.today().strftime('%Y/%m/%d')}", + ) + + def test_display_name_is_shortened_to_max_length(self): + """ Test that the display name is not fully padded when a `max_length` is passed. """ + token = self._create_token() + self.assertEqual(token._build_display_name(max_length=6), '• 1234') + + def test_display_name_is_not_padded(self): + """ Test that the display name is not padded when `should_pad` is `False`. """ + token = self._create_token() + self.assertEqual(token._build_display_name(should_pad=False), '1234') diff --git a/addons/payment/utils.py b/addons/payment/utils.py index 961f212c3c0..32ebf969cd0 100644 --- a/addons/payment/utils.py +++ b/addons/payment/utils.py @@ -115,20 +115,6 @@ def to_minor_currency_units(major_amount, currency, arbitrary_decimal_number=Non return int(float_round(major_amount * (10**decimal_number), precision_digits=0)) -# Token values formatting - -def build_token_name(payment_details_short=None, final_length=16): - """ Pad plain payment details with leading X's to build a token name of the desired length. - - :param str payment_details_short: The plain part of the payment details (usually last 4 digits) - :param int final_length: The desired final length of the token name (16 for a bank card) - :return: The padded token name - :rtype: str - """ - payment_details_short = payment_details_short or '????' - return f"{'X' * (final_length - len(payment_details_short))}{payment_details_short}" - - # Partner values formatting def format_partner_address(address1="", address2=""): diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml index 1cbbb9f839d..f9eb75c487e 100644 --- a/addons/payment/views/payment_templates.xml +++ b/addons/payment/views/payment_templates.xml @@ -120,7 +120,7 @@ t-att-data-provider="token.provider" data-payment-option-type="token"/> - + @@ -254,7 +254,7 @@ t-att-data-provider="token.provider" data-payment-option-type="token"/> - + diff --git a/addons/payment/views/payment_token_views.xml b/addons/payment/views/payment_token_views.xml index 3bd5eb043a7..69c6cb86d06 100644 --- a/addons/payment/views/payment_token_views.xml +++ b/addons/payment/views/payment_token_views.xml @@ -16,7 +16,7 @@ - + @@ -34,7 +34,7 @@ payment.token - + diff --git a/addons/payment_adyen/models/payment_transaction.py b/addons/payment_adyen/models/payment_transaction.py index 2fd6f765364..1cdb35f0a16 100644 --- a/addons/payment_adyen/models/payment_transaction.py +++ b/addons/payment_adyen/models/payment_transaction.py @@ -415,7 +415,7 @@ class PaymentTransaction(models.Model): additional_data = notification_data['additionalData'] token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': payment_utils.build_token_name(additional_data.get('cardSummary')), + 'payment_details': additional_data.get('cardSummary'), 'partner_id': self.partner_id.id, 'acquirer_ref': additional_data['recurring.recurringDetailReference'], 'adyen_shopper_reference': additional_data['recurring.shopperReference'], diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 36cfa8a75ba..4d0df3f70e8 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -138,9 +138,10 @@ class AuthorizeAPI: payment = response.get('paymentProfile', {}).get('payment', {}) if self.payment_method_type == 'credit_card': - res['name'] = payment.get('creditCard', {}).get('cardNumber') + # Authorize.net pads the card and account numbers with X's. + res['payment_details'] = payment.get('creditCard', {}).get('cardNumber')[-4:] else: - res['name'] = payment.get('bankAccount', {}).get('accountNumber') + res['payment_details'] = payment.get('bankAccount', {}).get('accountNumber')[-4:] return res def delete_customer_profile(self, profile_id): diff --git a/addons/payment_authorize/models/payment_transaction.py b/addons/payment_authorize/models/payment_transaction.py index cbce1681255..7a4fc1e29f4 100644 --- a/addons/payment_authorize/models/payment_transaction.py +++ b/addons/payment_authorize/models/payment_transaction.py @@ -293,7 +293,7 @@ class PaymentTransaction(models.Model): if cust_profile: token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': cust_profile.get('name'), + 'payment_details': cust_profile.get('payment_details'), 'partner_id': self.partner_id.id, 'acquirer_ref': cust_profile.get('payment_profile_id'), 'authorize_profile': cust_profile.get('profile_id'), diff --git a/addons/payment_flutterwave/models/payment_transaction.py b/addons/payment_flutterwave/models/payment_transaction.py index ec2625d4b0c..0994799d2c8 100644 --- a/addons/payment_flutterwave/models/payment_transaction.py +++ b/addons/payment_flutterwave/models/payment_transaction.py @@ -178,7 +178,7 @@ class PaymentTransaction(models.Model): token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': payment_utils.build_token_name(notification_data['card']['last_4digits']), + 'payment_details': notification_data['card']['last_4digits'], 'partner_id': self.partner_id.id, 'acquirer_ref': notification_data['card']['token'], 'flutterwave_customer_email': notification_data['customer']['email'], diff --git a/addons/payment_ogone/models/payment_transaction.py b/addons/payment_ogone/models/payment_transaction.py index 9d195ec15fe..45d0c5c7446 100644 --- a/addons/payment_ogone/models/payment_transaction.py +++ b/addons/payment_ogone/models/payment_transaction.py @@ -228,10 +228,9 @@ class PaymentTransaction(models.Model): :param dict notification_data: The notification data sent by the provider :return: None """ - token_name = notification_data.get('CARDNO') or payment_utils.build_token_name() token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': token_name, # Already padded with 'X's + 'payment_details': notification_data.get('CARDNO')[-4:], # Ogone pads details with X's. 'partner_id': self.partner_id.id, 'acquirer_ref': notification_data['ALIAS'], 'verified': True, # The payment is authorized, so the payment method is valid diff --git a/addons/payment_stripe/models/payment_transaction.py b/addons/payment_stripe/models/payment_transaction.py index 07a739af574..a219a41c5a2 100644 --- a/addons/payment_stripe/models/payment_transaction.py +++ b/addons/payment_stripe/models/payment_transaction.py @@ -463,7 +463,7 @@ class PaymentTransaction(models.Model): token = self.env['payment.token'].create({ 'acquirer_id': self.acquirer_id.id, - 'name': payment_utils.build_token_name(payment_method['card'].get('last4')), + 'payment_details': payment_method['card'].get('last4'), 'partner_id': self.partner_id.id, 'acquirer_ref': customer_id, 'verified': True, diff --git a/addons/payment_test/models/payment_token.py b/addons/payment_test/models/payment_token.py index 7bddb51285a..16cd53a142c 100644 --- a/addons/payment_test/models/payment_token.py +++ b/addons/payment_test/models/payment_token.py @@ -16,3 +16,18 @@ class PaymentToken(models.Model): ('error', "Error"), ], ) + + def _build_display_name(self, *args, should_pad=True, **kwargs): + """ Override of `payment` to build the display name without padding. + + Note: self.ensure_one() + + :param list args: The arguments passed by QWeb when calling this method. + :param bool should_pad: Whether the token should be padded or not. + :param dict kwargs: Optional data. + :return: The test token name. + :rtype: str + """ + if self.provider != 'test': + return super()._build_display_name(*args, should_pad=should_pad, **kwargs) + return super()._build_display_name(*args, should_pad=False, **kwargs) diff --git a/addons/payment_test/models/payment_transaction.py b/addons/payment_test/models/payment_transaction.py index 109f7a0cbee..c244d69bee7 100644 --- a/addons/payment_test/models/payment_transaction.py +++ b/addons/payment_test/models/payment_transaction.py @@ -202,11 +202,10 @@ class PaymentTransaction(models.Model): """ 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), + 'payment_details': notification_data['payment_details'], 'partner_id': self.partner_id.id, 'acquirer_ref': 'fake acquirer reference', 'verified': True, diff --git a/addons/payment_test/static/src/js/payment_form.js b/addons/payment_test/static/src/js/payment_form.js index 13ae6f9e6d6..d622270a6fa 100644 --- a/addons/payment_test/static/src/js/payment_form.js +++ b/addons/payment_test/static/src/js/payment_form.js @@ -31,7 +31,7 @@ odoo.define('payment_test.payment_form', require => { route: '/payment/test/simulate_payment', params: { 'reference': processingValues.reference, - 'cc_summary': customerInput.slice(-4), + 'payment_details': customerInput, 'simulated_state': simulatedPaymentState, }, }).then(() => { diff --git a/addons/payment_test/tests/common.py b/addons/payment_test/tests/common.py index 1ed165a5201..78b5582d076 100644 --- a/addons/payment_test/tests/common.py +++ b/addons/payment_test/tests/common.py @@ -13,6 +13,6 @@ class PaymentTestCommon(PaymentCommon): cls.notification_data = { 'reference': cls.reference, - 'cc_summary': '1234', + 'payment_details': '1234', 'simulated_state': 'done', }