[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:
Laura Schauer
2022-08-19 14:28:08 +02:00
committed by Victor Feyens
parent 4c185d4a3f
commit c1b41bcd08
18 changed files with 103 additions and 38 deletions
+42 -5
View File
@@ -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.
+4 -2
View File
@@ -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 = _(
+1
View File
@@ -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
+1 -1
View File
@@ -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')
-14
View File
@@ -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=""):
+2 -2
View File
@@ -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 === -->
+2 -2
View File
@@ -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(() => {
+1 -1
View File
@@ -13,6 +13,6 @@ class PaymentTestCommon(PaymentCommon):
cls.notification_data = {
'reference': cls.reference,
'cc_summary': '1234',
'payment_details': '1234',
'simulated_state': 'done',
}