[IMP] payment(_*): allow dynamic token name
Before this commit, tokens were prefixed with usually 12 X’s. To improve readability, modernity and to shorten the length, this commit changes token prefixes to a standard of •••• 1111. task-2832669 closes odoo/odoo#94978 Related: odoo/upgrade#3738 Related: odoo/enterprise#29190 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
committed by
Victor Feyens
parent
4c185d4a3f
commit
c1b41bcd08
@@ -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.
|
||||
|
||||
|
||||
@@ -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 = _(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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=""):
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
t-att-data-provider="token.provider"
|
||||
data-payment-option-type="token"/>
|
||||
<!-- === Token name === -->
|
||||
<span class="payment_option_name" t-esc="token.name"/>
|
||||
<span class="payment_option_name" t-esc="token.display_name"/>
|
||||
<!-- === "V" check mark === -->
|
||||
<t t-call="payment.verified_token_checkmark"/>
|
||||
<!-- === "Unpublished" badge === -->
|
||||
@@ -254,7 +254,7 @@
|
||||
t-att-data-provider="token.provider"
|
||||
data-payment-option-type="token"/>
|
||||
<!-- === Token name === -->
|
||||
<span class="payment_option_name" t-esc="token.name"/>
|
||||
<span class="payment_option_name" t-esc="token.display_name"/>
|
||||
<!-- === "V" check mark === -->
|
||||
<t t-call="payment.verified_token_checkmark"/>
|
||||
<!-- === "Unpublished" badge === -->
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<group name="general_information">
|
||||
<field name="name"/>
|
||||
<field name="payment_details"/>
|
||||
<field name="partner_id" />
|
||||
</group>
|
||||
<group name="technical_information">
|
||||
@@ -34,7 +34,7 @@
|
||||
<field name="model">payment.token</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Payment Tokens">
|
||||
<field name="name"/>
|
||||
<field name="payment_details"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="acquirer_id" readonly="1"/>
|
||||
<field name="acquirer_ref" readonly="1"/>
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -13,6 +13,6 @@ class PaymentTestCommon(PaymentCommon):
|
||||
|
||||
cls.notification_data = {
|
||||
'reference': cls.reference,
|
||||
'cc_summary': '1234',
|
||||
'payment_details': '1234',
|
||||
'simulated_state': 'done',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user