diff --git a/odex25_donation/ensan_donation_request/models/donation_recurring.py b/odex25_donation/ensan_donation_request/models/donation_recurring.py
index f917ccfc5..b1fffb712 100644
--- a/odex25_donation/ensan_donation_request/models/donation_recurring.py
+++ b/odex25_donation/ensan_donation_request/models/donation_recurring.py
@@ -22,6 +22,7 @@ class DonationRecurring(models.Model):
string="Next Donation Date", tracking=True,
help="The date when the next donation will be processed.", default=lambda self: fields.Date.context_today(self)
)
+ send_recurring_sms = fields.Boolean()
recurring_interval = fields.Integer(
string="Repeat Every",
default=1,
@@ -39,10 +40,11 @@ class DonationRecurring(models.Model):
)
active = fields.Boolean(default=True)
state = fields.Selection([
+ ('new', 'New'),
('active', 'Active'),
('paused', 'Paused'),
('cancel', 'Cancelled'),
- ], default='active', string='Status', tracking=True)
+ ], default='new', string='Status', tracking=True)
recurring_line_ids = fields.One2many(
'donation.recurring.line',
'recurring_id',
@@ -150,6 +152,7 @@ class DonationRecurring(models.Model):
sale_final_vals = sale_draft._convert_to_write(sale_draft._cache)
sale_order = self.env['sale.order'].sudo().create(sale_final_vals)
+ line.sale_order_id = sale_order.id
# 4. Prepare Sale Order Line vals
line_vals = {
'order_id': sale_order.id,
@@ -168,17 +171,18 @@ class DonationRecurring(models.Model):
line_final_vals = line_draft._convert_to_write(line_draft._cache)
self.env['sale.order.line'].sudo().create(line_final_vals)
- # 7. Confirm the order
- sale_order.action_confirm()
-
- # 8. Link the recurring line
- line.sale_order_id = sale_order.id
-
+ confirmed = self._recurring_confirm_sale_order(sale_order)
+ if confirmed:
+ return sale_order
+ return False
+
+ def _recurring_confirm_sale_order(self, order):
+ order.with_context(skip_donation_sms=not self.send_recurring_sms).action_confirm()
self.message_post(
- body=_("✅ Sale Order %s created for donation dated %s.") % (sale_order.name, line.date)
+ body=_("✅ Sale Order %s created for donation dated %s.") % (order.name, order.date_order)
)
+ return True
- return sale_order
# def process_delivery_from_order(self, order, quantity_done=None):
# picking = order.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel'))
@@ -201,6 +205,26 @@ class DonationRecurring(models.Model):
invoice.action_post()
self.message_post(body=_("🧾 Invoice %s posted for Sale Order %s.") % (invoice.name, order.name))
return invoice
+
+ def action_activate(self):
+ for rec in self:
+ rec.name = self.env['ir.sequence'].next_by_code('donation.recurring') or _('New')
+ for line in rec.recurring_line_ids:
+ order = rec._create_sale_order(line)
+ if not order:
+ continue
+ rec.sudo().create_invoice_from_order(order)
+ rec._advance_next_date()
+ rec.state = 'active'
+ template = self.env.company.donation_recurring_created_sms_template_id
+ if not template:
+ raise ValidationError(_("⚠️ SMS template for 'Send When Created' is not configured in Company settings."))
+
+ rec._message_sms_with_template(
+ template=template,
+ partner_ids=rec.partner_id.ids,
+ put_in_queue=True
+ )
def action_pause(self):
for record in self:
@@ -307,28 +331,6 @@ class DonationRecurring(models.Model):
elif rec.frequency == 'monthly':
rec.recurring_next_date += relativedelta(months=interval)
- @api.model
- def create(self, vals):
- if vals.get('name', _('New')) == _('New'):
- vals['name'] = self.env['ir.sequence'].next_by_code('donation.recurring') or _('New')
- res = super(DonationRecurring, self).create(vals)
- for line in res.recurring_line_ids:
- order = res._create_sale_order(line)
- if not order:
- continue
- res.sudo().create_invoice_from_order(order)
- res._advance_next_date()
- template = self.env.company.donation_recurring_created_sms_template_id
- if not template:
- raise ValidationError(_("⚠️ SMS template for 'Send When Created' is not configured in Company settings."))
-
- res._message_sms_with_template(
- template=template,
- partner_ids=res.partner_id.ids,
- put_in_queue=True
- )
- return res
-
def unlink(self):
for rec in self:
if rec.state == 'active':
diff --git a/odex25_donation/ensan_sale_management/models/sale_order.py b/odex25_donation/ensan_sale_management/models/sale_order.py
index bd15bef07..5170179e3 100644
--- a/odex25_donation/ensan_sale_management/models/sale_order.py
+++ b/odex25_donation/ensan_sale_management/models/sale_order.py
@@ -100,7 +100,7 @@ class SaleOrder(models.Model):
sms_template_id = self.env.ref('ensan_sale_management.sms_template_data_donation')
donar_sms_template_id = self.env.ref('ensan_sale_management.sms_template_donors_data_donation')
for rec in self:
- if rec.state == 'sale':
+ if rec.state == 'sale' and not self._context.get('skip_donation_sms'):
if rec.order_mobile_number:
rec._message_sms_with_template(
template=sms_template_id,
diff --git a/odex25_donation/payment_hyperpay_tokenization/__init__.py b/odex25_donation/payment_hyperpay_tokenization/__init__.py
new file mode 100644
index 000000000..19240f4ea
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/__init__.py
@@ -0,0 +1,2 @@
+from . import controllers
+from . import models
\ No newline at end of file
diff --git a/odex25_donation/payment_hyperpay_tokenization/__manifest__.py b/odex25_donation/payment_hyperpay_tokenization/__manifest__.py
new file mode 100644
index 000000000..75b7f917c
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/__manifest__.py
@@ -0,0 +1,17 @@
+{
+ 'name': 'payment_hyperpay_tokenization',
+ 'version': '1.0',
+ 'description': '',
+ 'summary': '',
+ 'author': '',
+ 'website': '',
+ 'license': 'LGPL-3',
+ 'category': '',
+ 'depends': [
+ 'base', 'payment_hyperpay'
+ ],
+ 'data': [
+ 'views/payment_acquirer_views.xml'
+ ]
+
+}
\ No newline at end of file
diff --git a/odex25_donation/payment_hyperpay_tokenization/controllers/__init__.py b/odex25_donation/payment_hyperpay_tokenization/controllers/__init__.py
new file mode 100644
index 000000000..deec4a8b8
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
\ No newline at end of file
diff --git a/odex25_donation/payment_hyperpay_tokenization/controllers/main.py b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py
new file mode 100644
index 000000000..c8c67fbe0
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/controllers/main.py
@@ -0,0 +1,147 @@
+import requests
+import re
+
+from odoo.http import route, request, Controller
+from odoo.addons.payment_hyperpay.data.payment_icon import payment_icon
+from odoo.addons.payment.controllers.portal import PaymentProcessing
+
+
+
+TEST_URL = "https://eu-test.oppwa.com"
+LIVE_URL = "https://eu-prod.oppwa.com"
+
+class HyperPayTokenization(Controller):
+
+ @route('/hyperpay/tokens/checkout', type='json', auth='public', website=True, methods=['POST'])
+ def token_checkout(self, **kwargs):
+ if not kwargs.get('acquirer_id'):
+ return {'state': False, 'message': 'Couldn\'t identify acquirer'}
+ try:
+ self._save_session_data(kwargs)
+ checkout_data = self._register_payment_card(kwargs)
+ return checkout_data
+ except Exception as er:
+ return {
+ 'state': False,
+ 'message': er
+ }
+
+ def _save_session_data(self, data):
+ # save data in session as a form of naive saving
+ return True
+
+ def _register_payment_card(self, data):
+ acquirer_id = int(data.get('acquirer_id'))
+ acquirer = request.env['payment.acquirer'].sudo().search([('id', '=', acquirer_id)])
+ payment_icons = acquirer.payment_icon_ids.mapped('name')
+ data_brands = "VISA"
+ if (len(payment_icon) > 1):
+ brands = [payment_icon[i.upper()] for i in payment_icons if i.upper() in payment_icon.keys()]
+ data_brands = brands and " ".join(brands) or data_brands
+ base_url = request.httprequest.host_url
+ payload = {
+ "entityId": acquirer.hyperpay_merchant_id,
+ 'createRegistration': True,
+ }
+
+ payload.update(self._get_hyperpay_token_custom_parameters(data))
+
+ if acquirer.state == 'test':
+ domain = TEST_URL
+ else:
+ domain = LIVE_URL
+
+ url = f"{domain}/v1/checkouts"
+
+ headers = {
+ "Authorization": f"Bearer {acquirer.hyperpay_authorization}"
+ }
+
+ response = requests.post(url=url, data=payload, headers=headers).json()
+
+ result = response.get('result', {})
+ result_code = result.get('code')
+
+ if result_code and not re.match(r"^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.1[12]0|000\.400\.0[^3]|000\.400\.100|000\.200)", result_code):
+ return {'state': False, 'message': result.get('description', '')}
+
+ return_url = f'{base_url}hyperpay/tokens/result?acquirer_id={acquirer_id}'
+
+ return {
+ 'state': True,
+ 'checkout_id': response.get('id'),
+ 'service_domain': domain,
+ 'base_url': base_url,
+ 'data_brands': data_brands,
+ 'return_url': return_url,
+ }
+
+ def _get_hyperpay_token_custom_parameters(self, data):
+ reference_id = request.session.get('hyperpay_token_reference_id')
+ reference_model = request.session.get('hyperpay_token_reference_model')
+ return {
+ "customParameters[SHOPPER_acquirer_id]": data.get('acquirer_id', 0),
+ "customParameters[SHOPPER_hyperpay_token_reference_id]": reference_id,
+ "customParameters[SHOPPER_hyperpay_token_reference_model]": reference_model,
+ }
+
+ @route('/hyperpay/tokens/result', type='http', auth='public', website=True)
+ def token_return(self, **post):
+ try:
+ if post.get('transaction_id'):
+ transaction = request.env['payment.transaction'].sudo().search([('id', '=', int(post.get('transaction_id')))])
+ if transaction:
+ transaction.s2s_do_refund()
+ else:
+ acquirer_id = request.env['payment.acquirer'].sudo().search([('id', '=', int(post.get('acquirer_id', 0)))])
+
+ if acquirer_id.state == 'test':
+ domain = TEST_URL
+ else:
+ domain = LIVE_URL
+
+ url = f"{domain}{post.get('resourcePath')}?entityId={acquirer_id.hyperpay_merchant_id}"
+ headers = {
+ "Authorization": f"Bearer {acquirer_id.hyperpay_authorization}"
+ }
+ resp = requests.get(url=url, headers=headers).json()
+ result = resp.get('result', {})
+ result_code = result.get('code')
+
+ if result_code and not re.match(r"^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.1[12]0|000\.400\.0[^3]|000\.400\.100|000\.200)", result_code):
+ # Handle failed cards logic here
+ return {'state': False, 'message': result.get('description', ''), 'errors': resp.get('parameterErrors', [])}
+
+ # create card record and activate recurring donation
+ card = resp.get('card', {})
+ if not card:
+ return {'state': False, 'message': 'Card data not found'}
+ self._post_process_token_return(resp)
+ except Exception as er:
+ request.env.cr.rollback()
+ raise er
+
+ return request.redirect('/my/recurring_donation')
+
+ def _post_process_token_return(self, data):
+ acquirer_id = int(data.get('customParameters', {}).get('SHOPPER_acquirer_id', 0))
+ card = data.get('card', {})
+ card_vals = {
+ 'name': f"{card.get('bin', '')}XXXXXXXXXXXX{card.get('last4Digits', '')}",
+ 'partner_id': request.env.user.partner_id.id,
+ 'acquirer_id': acquirer_id,
+ 'acquirer_ref': data.get('id', ''),
+ 'hyperpay_payment_brand': data.get('paymentBrand'),
+ }
+
+ token_id = request.env['payment.token'].create(card_vals)
+ tx = token_id.validate()
+ PaymentProcessing.add_payment_transaction(tx)
+ reference_id = int(data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_id', 0))
+ reference_model = data.get('customParameters', {}).get('SHOPPER_hyperpay_token_reference_model', '')
+ if reference_id and reference_model and reference_model in request.env:
+ record_id = request.env[reference_model].search([('id', '=', reference_id)])
+ if record_id and hasattr(record_id, '_post_process_card_tokenization'):
+ record_id._post_process_card_tokenization(token_id)
+ return True
+
diff --git a/odex25_donation/payment_hyperpay_tokenization/models/__init__.py b/odex25_donation/payment_hyperpay_tokenization/models/__init__.py
new file mode 100644
index 000000000..f65284264
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/models/__init__.py
@@ -0,0 +1 @@
+from . import payment
diff --git a/odex25_donation/payment_hyperpay_tokenization/models/payment.py b/odex25_donation/payment_hyperpay_tokenization/models/payment.py
new file mode 100644
index 000000000..9e0fbe837
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/models/payment.py
@@ -0,0 +1,142 @@
+from odoo import models, fields
+from odoo.http import request
+from odoo.addons.payment.models.payment_acquirer import PaymentToken
+import requests
+import re
+import dateutil
+import pytz
+
+TEST_URL = "https://eu-test.oppwa.com"
+LIVE_URL = "https://eu-prod.oppwa.com"
+
+
+
+class AcquirerHyperPay(models.Model):
+ _inherit = 'payment.acquirer'
+
+ hyperpay_s2s_entity_id = fields.Char('Server2Server Entity Id', groups='base.group_user')
+
+class HyperPayTransaction(models.Model):
+ _inherit = 'payment.transaction'
+
+ def hyperpay_s2s_do_transaction(self, **kwargs):
+ self.ensure_one()
+
+ if self.acquirer_id.state == 'test':
+ domain = TEST_URL
+ else:
+ domain = LIVE_URL
+
+ url = f"{domain}/v1/registrations/{self.payment_token_id.acquirer_ref}/payments"
+
+ payload = self._hyperpay_get_s2s_transaction_payload(kwargs)
+
+ headers = {
+ "Authorization": f"Bearer {self.acquirer_id.hyperpay_authorization}"
+ }
+ response = requests.post(url, data=payload, headers=headers)
+ data = response.json()
+ return self._hyperpay_s2s_validate_transaction(data)
+
+ def _hyperpay_get_s2s_transaction_payload(self, data):
+ partner_id = self.env.user.partner_id
+ base_url = request.httprequest.host_url
+ lang_code = str(self.env['res.lang'].search([('code', '=', self.env.user.lang)]).iso_code or '').upper()
+ payload = {
+ "entityId": self.acquirer_id.hyperpay_s2s_entity_id,
+ "amount": '%.2f' % self.amount,
+ "currency": self.currency_id.name,
+ 'paymentBrand': self.payment_token_id.hyperpay_payment_brand,
+ "paymentType": "DB",
+ 'standingInstruction.mode': 'INITIAL',
+ 'standingInstruction.source': 'CIT',
+ 'standingInstruction.type': 'UNSCHEDULED',
+ 'shopperResultUrl': f'{base_url}hyperpay/tokens/result?transaction_id={self.id}',
+ 'notificationUrl': f'{base_url}hyperpay/tokens/result?transaction_id={self.id}',
+ "billing.street1": partner_id.street or 'Riyadh',
+ "billing.street2": partner_id.street2 or '',
+ "billing.city": partner_id.city or 'Riyadh',
+ "billing.state": partner_id.state_id.name or 'Riyadh',
+ "billing.postcode": partner_id.zip or '',
+ "billing.country": partner_id.country_id.code or 'SA',
+ "customer.givenName": partner_id.name,
+ "customer.surname": '',
+ "customer.email": partner_id.email,
+ "customer.mobile": partner_id.mobile or partner_id.phone or '',
+ "customer.phone": partner_id.phone or partner_id.mobile or '',
+ 'customer.ip': request.httprequest.environ["REMOTE_ADDR"],
+ 'customer.language': lang_code,
+ }
+ if self.type != 'validation':
+ payload.update({
+ 'standingInstruction.mode': 'REPEATED',
+ 'standingInstruction.source': 'MIT',
+ 'standingInstruction.initialTransactionId': self.payment_token_id.hyperpay_initial_transaction_id
+ })
+ return payload
+
+ def hyperpay_s2s_do_refund(self, **kwargs):
+ self.ensure_one()
+
+ if self.acquirer_id.state == 'test':
+ domain = TEST_URL
+ else:
+ domain = LIVE_URL
+
+ url = f"{domain}/v1/payments/{self.acquirer_reference}"
+ base_url = request.httprequest.host_url
+
+
+ payload = {
+ 'entityId': self.acquirer_id.hyperpay_s2s_entity_id,
+ 'paymentBrand': self.payment_token_id.hyperpay_payment_brand,
+ 'paymentType': 'RF',
+ 'amount': '%.2f' % self.amount,
+ 'currency': self.currency_id.name,
+ 'shopperResultUrl': f'{base_url}hyperpay/tokens/result',
+ }
+ headers = {
+ "Authorization": f"Bearer {self.acquirer_id.hyperpay_authorization}"
+ }
+ response = requests.post(url, data=payload, headers=headers)
+ data = response.json()
+ return self._hyperpay_s2s_validate_transaction(data)
+
+ def _hyperpay_s2s_validate_transaction(self, data):
+ success_re = r"^(000\.000\.|000\.100\.1|000\.[36]|000\.400\.1[12]0|000\.400\.0[^3]|000\.400\.100)"
+ pending_re = r"^(000\.200|800\.400\.5|100\.400\.500)"
+
+ result = data.get('result')
+ result_code = result.get('code')
+ res = {
+ 'acquirer_reference': data.get('id'),
+ 'state_message': result.get('description', '')
+ }
+
+ if re.match(success_re, result_code):
+ date_validate = dateutil.parser.parse(data.get('timestamp')).astimezone(pytz.utc).replace(tzinfo=None)
+ res.update(date=date_validate)
+ if self.type == 'validation' and not self.payment_token_id.verified:
+ self.payment_token_id.write({
+ 'verified': True,
+ 'hyperpay_initial_transaction_id': data.get('id', '')
+ })
+ self.payment_token_id.verified = True
+ self._set_transaction_done()
+ elif re.match(pending_re, result_code):
+ self._set_transaction_pending()
+ else:
+ self._set_transaction_error(result.get('description', ''))
+ return self.write(res)
+
+
+class HyperPayToken(models.Model):
+ _inherit = 'payment.token'
+
+ hyperpay_payment_brand = fields.Char('Payment Brand')
+ hyperpay_initial_transaction_id = fields.Char()
+
+
+PaymentToken.VALIDATION_AMOUNTS.update({
+ 'SAR': 5.00
+})
diff --git a/odex25_donation/payment_hyperpay_tokenization/views/payment_acquirer_views.xml b/odex25_donation/payment_hyperpay_tokenization/views/payment_acquirer_views.xml
new file mode 100644
index 000000000..0350c7d43
--- /dev/null
+++ b/odex25_donation/payment_hyperpay_tokenization/views/payment_acquirer_views.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ payment.acquirer.view.form
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
diff --git a/odex25_donation/recurring_donation_payment/__init__.py b/odex25_donation/recurring_donation_payment/__init__.py
new file mode 100644
index 000000000..19240f4ea
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/__init__.py
@@ -0,0 +1,2 @@
+from . import controllers
+from . import models
\ No newline at end of file
diff --git a/odex25_donation/recurring_donation_payment/__manifest__.py b/odex25_donation/recurring_donation_payment/__manifest__.py
new file mode 100644
index 000000000..4c8cc53f4
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ 'name': 'Recurring Donation Payment',
+ 'version': '1.0',
+ 'description': '',
+ 'summary': '',
+ 'author': 'Abdul Rahman Saber',
+ 'website': '',
+ 'license': 'LGPL-3',
+ 'category': '',
+ 'depends': [
+ 'base', 'ensan_donation_request', 'payment_hyperpay_tokenization'
+ ],
+ 'auto_install': True,
+}
\ No newline at end of file
diff --git a/odex25_donation/recurring_donation_payment/controllers/__init__.py b/odex25_donation/recurring_donation_payment/controllers/__init__.py
new file mode 100644
index 000000000..12a7e529b
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/odex25_donation/recurring_donation_payment/controllers/main.py b/odex25_donation/recurring_donation_payment/controllers/main.py
new file mode 100644
index 000000000..67b77b95c
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/controllers/main.py
@@ -0,0 +1,32 @@
+from odoo.http import request
+
+from odoo.addons.payment_hyperpay_tokenization.controllers.main import HyperPayTokenization
+
+class RecurringDonationTokenization(HyperPayTokenization):
+
+ def _save_session_data(self, data):
+ try:
+ product_id = data.get('product_id')
+ amount = data.get('amount')
+ sms_status = data.get('sms_status')
+ frequency = data.get('frequency')
+
+ if not all([product_id, amount, frequency]):
+ return {'state': False, 'message': 'invalid payload'}
+
+ recurring_donation_id = request.env['donation.recurring'].sudo().create({
+ 'recurring_line_ids': [(0, 0, {
+ 'product_id': product_id,
+ 'quantity': 1,
+ 'price_unit': amount,
+ })],
+ 'partner_id': request.env.user.partner_id.id,
+ 'frequency': frequency,
+ 'send_recurring_sms': sms_status
+ })
+ request.session['hyperpay_token_reference_id'] = recurring_donation_id.id
+ request.session['hyperpay_token_reference_model'] = recurring_donation_id._name
+ return True
+ except Exception as e:
+ request.env.cr.rollback()
+ raise e
\ No newline at end of file
diff --git a/odex25_donation/recurring_donation_payment/models/__init__.py b/odex25_donation/recurring_donation_payment/models/__init__.py
new file mode 100644
index 000000000..a2935e633
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/models/__init__.py
@@ -0,0 +1 @@
+from . import recurring_donation
\ No newline at end of file
diff --git a/odex25_donation/recurring_donation_payment/models/recurring_donation.py b/odex25_donation/recurring_donation_payment/models/recurring_donation.py
new file mode 100644
index 000000000..ec8e63352
--- /dev/null
+++ b/odex25_donation/recurring_donation_payment/models/recurring_donation.py
@@ -0,0 +1,37 @@
+from odoo import models, fields, _
+from odoo.exceptions import ValidationError
+
+class RecurringDonation(models.Model):
+ _inherit = 'donation.recurring'
+
+ preferred_payment_token_id = fields.Many2one('payment.token', domain="[('partner_id', '=', partner_id)]")
+
+ def _post_process_card_tokenization(self, token_id):
+ self.write({
+ 'preferred_payment_token_id': token_id.id
+ })
+ if token_id.verified:
+ self.action_activate()
+
+ def _recurring_confirm_sale_order(self, order):
+ transaction_id = self._create_payment_transaction(order)
+ if transaction_id.state == 'done':
+ return super()._recurring_confirm_sale_order(order)
+ return False
+
+ def _create_payment_transaction(self, order):
+ payment_token_id = self._get_recurring_payment_token_id()
+ if not payment_token_id:
+ raise ValidationError(_('This partner doesn\'t have registered payment tokens'))
+ vals = {
+ 'type': 'form_save',
+ 'payment_token_id': payment_token_id
+ }
+ return order._create_payment_transaction(vals)
+
+ def _get_recurring_payment_token_id(self):
+ self.ensure_one()
+ if self.preferred_payment_token_id:
+ return self.preferred_payment_token_id.id
+
+ return self.env['payment.token'].sudo().search([('partner_id', '=', self.partner_id.id), ('verified', '=', True)], limit=1).id