From 513be97b2f6e6e341111220ea96d68ce65033baf Mon Sep 17 00:00:00 2001 From: kcv-odoo Date: Mon, 25 Sep 2023 12:43:30 +0530 Subject: [PATCH] [IMP] payment_razorpay: support tokenization in Razorpay's "Payment" gateway See README for technical details. task-3495914 closes odoo/odoo#143525 Related: odoo/documentation#6732 Signed-off-by: Kartik Chavda (kcv) --- addons/payment/data/payment_method_data.xml | 2 +- addons/payment_razorpay/README.md | 31 ++- addons/payment_razorpay/__manifest__.py | 5 + addons/payment_razorpay/const.py | 6 + addons/payment_razorpay/controllers/main.py | 22 +- .../i18n/payment_razorpay.pot | 49 +++- .../models/payment_provider.py | 17 +- .../models/payment_transaction.py | 242 ++++++++++++++---- .../static/src/js/payment_form.js | 84 ++++++ addons/payment_razorpay/tests/common.py | 11 + .../tests/test_payment_transaction.py | 52 +++- .../tests/test_processing_flows.py | 37 +-- .../views/payment_provider_views.xml | 12 + .../views/payment_razorpay_templates.xml | 16 +- 14 files changed, 452 insertions(+), 134 deletions(-) create mode 100644 addons/payment_razorpay/static/src/js/payment_form.js diff --git a/addons/payment/data/payment_method_data.xml b/addons/payment/data/payment_method_data.xml index b1d209c75c4..7610bec608b 100644 --- a/addons/payment/data/payment_method_data.xml +++ b/addons/payment/data/payment_method_data.xml @@ -3150,7 +3150,7 @@ 1000 False - False + True False partial \n" +" Enable recurring payments on Razorpay" +msgstr "" + #. module: payment_razorpay #. odoo-python #: code:addons/payment_razorpay/models/payment_transaction.py:0 @@ -62,6 +69,13 @@ msgstr "" msgid "Payment Transaction" msgstr "" +#. module: payment_razorpay +#. odoo-javascript +#: code:addons/payment_razorpay/static/src/js/payment_form.js:0 +#, python-format +msgid "Payment processing failed" +msgstr "" + #. module: payment_razorpay #: model:ir.model.fields.selection,name:payment_razorpay.selection__payment_provider__code__razorpay msgid "Razorpay" @@ -82,6 +96,13 @@ msgstr "" msgid "Razorpay Webhook Secret" msgstr "" +#. module: payment_razorpay +#. odoo-python +#: code:addons/payment_razorpay/models/payment_provider.py:0 +#, python-format +msgid "Razorpay gave us the following information: '%s'" +msgstr "" + #. module: payment_razorpay #. odoo-python #: code:addons/payment_razorpay/models/payment_transaction.py:0 @@ -118,12 +139,8 @@ msgid "Received incomplete refund data." msgstr "" #. module: payment_razorpay -#. odoo-python -#: code:addons/payment_razorpay/models/payment_provider.py:0 -#, python-format -msgid "" -"The communication with the API failed. Razorpay gave us the following " -"information: '%s'" +#: model_terms:ir.ui.view,arch_db:payment_razorpay.redirect_form +msgid "TODO: remove in master" msgstr "" #. module: payment_razorpay @@ -131,6 +148,13 @@ msgstr "" msgid "The key solely used to identify the account with Razorpay." msgstr "" +#. module: payment_razorpay +#. odoo-python +#: code:addons/payment_razorpay/models/payment_transaction.py:0 +#, python-format +msgid "The phone number is invalid." +msgstr "" + #. module: payment_razorpay #. odoo-python #: code:addons/payment_razorpay/models/payment_transaction.py:0 @@ -143,6 +167,13 @@ msgstr "" msgid "The technical code of this payment provider." msgstr "" +#. module: payment_razorpay +#. odoo-python +#: code:addons/payment_razorpay/models/payment_transaction.py:0 +#, python-format +msgid "The transaction is not linked to a token." +msgstr "" + #. module: payment_razorpay #. odoo-python #: code:addons/payment_razorpay/models/payment_transaction.py:0 diff --git a/addons/payment_razorpay/models/payment_provider.py b/addons/payment_razorpay/models/payment_provider.py index 2283ebb4fa5..120a2cdc0a5 100644 --- a/addons/payment_razorpay/models/payment_provider.py +++ b/addons/payment_razorpay/models/payment_provider.py @@ -47,6 +47,7 @@ class PaymentProvider(models.Model): self.filtered(lambda p: p.code == 'razorpay').update({ 'support_manual_capture': 'full_only', 'support_refund': 'partial', + 'support_tokenization': True, }) # === BUSINESS METHODS ===# @@ -88,8 +89,8 @@ class PaymentProvider(models.Model): "Invalid API request at %s with data:\n%s", url, pprint.pformat(payload), ) raise ValidationError("Razorpay: " + _( - "The communication with the API failed. Razorpay gave us the following " - "information: '%s'", response.json().get('error', {}).get('description') + "Razorpay gave us the following information: '%s'", + response.json().get('error', {}).get('description') )) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): _logger.exception("Unable to reach endpoint at %s", url) @@ -126,3 +127,15 @@ class PaymentProvider(models.Model): if self.code != 'razorpay': return default_codes return const.DEFAULT_PAYMENT_METHODS_CODES + + def _get_validation_amount(self): + """ Override of `payment` to return the amount for Razorpay validation operations. + + :return: The validation amount. + :rtype: float + """ + res = super()._get_validation_amount() + if self.code != 'razorpay': + return res + + return 1.0 diff --git a/addons/payment_razorpay/models/payment_transaction.py b/addons/payment_razorpay/models/payment_transaction.py index d77dd659d2e..07c9da19a9c 100644 --- a/addons/payment_razorpay/models/payment_transaction.py +++ b/addons/payment_razorpay/models/payment_transaction.py @@ -2,15 +2,16 @@ import logging import pprint +import time +from datetime import datetime -from werkzeug.urls import url_encode, url_join +from dateutil.relativedelta import relativedelta -from odoo import _, models +from odoo import _, api, models from odoo.exceptions import UserError, ValidationError from odoo.addons.payment import utils as payment_utils from odoo.addons.payment_razorpay import const -from odoo.addons.payment_razorpay.controllers.main import RazorpayController _logger = logging.getLogger(__name__) @@ -19,76 +20,121 @@ _logger = logging.getLogger(__name__) class PaymentTransaction(models.Model): _inherit = 'payment.transaction' - def _get_specific_rendering_values(self, processing_values): - """ Override of `payment` to return razorpay-specific rendering values. + def _get_specific_processing_values(self, processing_values): + """ Override of `payment` to return razorpay-specific processing values. Note: self.ensure_one() from `_get_processing_values` :param dict processing_values: The generic and specific processing values of the transaction. - :return: The dict of provider-specific rendering values. + :return: The provider-specific processing values. :rtype: dict """ - res = super()._get_specific_rendering_values(processing_values) + res = super()._get_specific_processing_values(processing_values) if self.provider_code != 'razorpay': return res - # Initiate the payment and retrieve the related order id. - payload = self._razorpay_prepare_order_request_payload() + customer_id = self._razorpay_create_customer()['id'] + order_id = self._razorpay_create_order(customer_id)['id'] + return { + 'razorpay_key_id': self.provider_id.razorpay_key_id, + 'razorpay_customer_id': customer_id, + 'is_tokenize_request': self.tokenize, + 'razorpay_order_id': order_id, + } + + def _razorpay_create_customer(self): + """ Create and return a Customer object. + + :return: The created Customer. + :rtype: dict + """ + payload = { + 'name': self.partner_name, + 'email': self.partner_email, + 'contact': self._validate_phone_number(self.partner_phone), + 'fail_existing': '0', # Don't throw an error if the customer already exists. + } _logger.info( - "Payload of '/orders' request for transaction with reference %s:\n%s", + "Sending '/customers' request for transaction with reference %s:\n%s", self.reference, pprint.pformat(payload) ) - order_data = self.provider_id._razorpay_make_request(endpoint='orders', payload=payload) + customer_data = self.provider_id._razorpay_make_request('customers', payload=payload) + _logger.info( + "Response of '/customers' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(customer_data) + ) + return customer_data + + @api.model + def _validate_phone_number(self, phone): + """ Validate and format the phone number. + + :param str phone: The phone number to validate. + :return str: The formatted phone number. + :raise ValidationError: If the phone number is missing or incorrect. + """ + if not phone: + raise ValidationError("Razorpay: " + _("The phone number is missing.")) + + try: + phone = self._phone_format( + number=phone, country=self.partner_country_id, raise_exception=True + ) + except Exception: + raise ValidationError("Razorpay: " + _("The phone number is invalid.")) + return phone + + def _razorpay_create_order(self, customer_id=None): + """ Create and return an Order object to initiate the payment. + + :param str customer_id: The ID of the Customer object to assign to the Order for + non-subsequent payments. + :return: The created Order. + :rtype: dict + """ + payload = self._razorpay_prepare_order_payload(customer_id=customer_id) + _logger.info( + "Sending '/orders' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(payload) + ) + order_data = self.provider_id._razorpay_make_request('orders', payload=payload) _logger.info( "Response of '/orders' request for transaction with reference %s:\n%s", self.reference, pprint.pformat(order_data) ) + return order_data - # Initiate the payment - converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) - base_url = self.provider_id.get_base_url() - return_url_params = {'reference': self.reference} - - phone = self.partner_phone - if phone: - # sanitize partner phone - try: - phone = self._phone_format(number=phone, country=self.partner_country_id, raise_exception=True) - except Exception as err: - raise ValidationError("Razorpay: " + str(err)) from err - else: - raise ValidationError("Razorpay: " + _("The phone number is missing.")) - - rendering_values = { - 'key_id': self.provider_id.razorpay_key_id, - 'name': self.company_id.name, - 'description': self.reference, - 'company_logo': url_join(base_url, f'web/image/res.company/{self.company_id.id}/logo'), - 'order_id': order_data['id'], - 'amount': converted_amount, - 'currency': self.currency_id.name, - 'partner_name': self.partner_name, - 'partner_email': self.partner_email, - 'partner_phone': phone, - 'method': self.payment_method_code, - 'return_url': url_join( - base_url, f'{RazorpayController._return_url}?{url_encode(return_url_params)}' - ), - } - return rendering_values - - def _razorpay_prepare_order_request_payload(self): - """ Create the payload for the order request based on the transaction values. + def _razorpay_prepare_order_payload(self, customer_id=None): + """ Prepare the payload for the order request based on the transaction values. + :param str customer_id: The ID of the Customer object to assign to the Order for + non-subsequent payments. :return: The request payload. :rtype: dict """ converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) + pm_code = (self.payment_method_id.primary_payment_method_id or self.payment_method_id).code payload = { 'amount': converted_amount, 'currency': self.currency_id.name, + 'method': pm_code, } + if self.operation in ['online_direct', 'validation']: + payload['customer_id'] = customer_id # Required for only non-subsequent payments. + if self.tokenize: + payload['token'] = { + 'max_amount': payment_utils.to_minor_currency_units( + self._get_mandate_max_amount(), self.currency_id + ), + 'expire_at': time.mktime( + (datetime.today() + relativedelta(years=10)).timetuple() + ), # Don't expire the token before at least 10 years. + 'frequency': 'as_presented', + } + else: # 'online_token', 'offline' + # Required for only subsequent payments. + payload['payment_capture'] = not self.provider_id.capture_manually if self.provider_id.capture_manually: # The related payment must be only authorized. payload.update({ 'payment': { @@ -101,6 +147,70 @@ class PaymentTransaction(models.Model): }) return payload + def _get_mandate_max_amount(self): + """ Return the eMandate's maximum amount to define. + + :return: The eMandate's maximum amount. + :rtype: int + """ + mandate_values = self._get_mandate_values() + if 'amount' in mandate_values: + max_amount = mandate_values['amount'] * 5 # FP's rule of thumb for a good max amount. + else: + pm_code = ( + self.payment_method_id.primary_payment_method_id or self.payment_method_id + ).code + max_amount = const.MANDATE_MAX_AMOUNT.get(pm_code, 100000) + return max_amount + + def _send_payment_request(self): + """ Override of `payment` to send a payment request to Razorpay. + + Note: self.ensure_one() + + :return: None + :raise UserError: If the transaction is not linked to a token. + """ + super()._send_payment_request() + if self.provider_code != 'razorpay': + return + + if not self.token_id: + raise UserError("Razorpay: " + _("The transaction is not linked to a token.")) + + try: + order_data = self._razorpay_create_order() + phone = self._validate_phone_number(self.partner_phone) + customer_id, token_id = self.token_id.provider_ref.split(',') + payload = { + 'email': self.partner_email, + 'contact': phone, + 'amount': order_data['amount'], + 'currency': self.currency_id.name, + 'order_id': order_data['id'], + 'customer_id': customer_id, + 'token': token_id, + 'description': self.reference, + 'recurring': '1', + } + _logger.info( + "Sending '/payments/create/recurring' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(payload) + ) + recurring_payment_data = self.provider_id._razorpay_make_request( + 'payments/create/recurring', payload=payload + ) + _logger.info( + "Response of '/payments/create/recurring' request for transaction with reference " + "%s:\n%s", self.reference, pprint.pformat(recurring_payment_data) + ) + self._handle_notification_data('razorpay', recurring_payment_data) + except ValidationError as e: + if self.operation == 'offline': + self._set_error(str(e)) + else: + raise + def _send_refund_request(self, amount_to_refund=None): """ Override of `payment` to send a refund request to Razorpay. @@ -286,6 +396,8 @@ class PaymentTransaction(models.Model): elif entity_status in const.PAYMENT_STATUS_MAPPING['authorized']: self._set_authorized() elif entity_status in const.PAYMENT_STATUS_MAPPING['done']: + if self.tokenize: + self._razorpay_tokenize_from_notification_data(notification_data) self._set_done() # Immediately post-process the transaction if it is a refund, as the post-processing @@ -308,3 +420,41 @@ class PaymentTransaction(models.Model): self._set_error( "Razorpay: " + _("Received data with invalid status: %s", entity_status) ) + + def _razorpay_tokenize_from_notification_data(self, notification_data): + """ Create a new token based on the notification data. + + :param dict notification_data: The notification data built with Razorpay objects. + See `_process_notification_data`. + :return: None + """ + pm_code = (self.payment_method_id.primary_payment_method_id or self.payment_method_id).code + if pm_code == 'card': + details = notification_data.get('card', {}).get('last4') + elif pm_code == 'upi': + temp_vpa = notification_data.get('vpa') + details = temp_vpa[temp_vpa.find('@') - 1:] + else: + details = pm_code + + token = self.env['payment.token'].create({ + 'provider_id': self.provider_id.id, + 'payment_method_id': self.payment_method_id.id, + 'payment_details': details, + 'partner_id': self.partner_id.id, + # Razorpay requires both the customer ID and the token ID which are extracted from here. + 'provider_ref': f'{notification_data["customer_id"]},{notification_data["token_id"]}', + }) + self.write({ + 'token_id': token, + 'tokenize': False, + }) + _logger.info( + "Created token with id %(token_id)s for partner with id %(partner_id)s from " + "transaction with reference %(ref)s", + { + 'token_id': token.id, + 'partner_id': self.partner_id.id, + 'ref': self.reference, + }, + ) diff --git a/addons/payment_razorpay/static/src/js/payment_form.js b/addons/payment_razorpay/static/src/js/payment_form.js new file mode 100644 index 00000000000..c09d2adef64 --- /dev/null +++ b/addons/payment_razorpay/static/src/js/payment_form.js @@ -0,0 +1,84 @@ +/** @odoo-module **/ +/* global Razorpay */ + +import { _t } from "@web/core/l10n/translation"; +import { loadJS } from "@web/core/assets"; +import paymentForm from '@payment/js/payment_form'; + +paymentForm.include({ + + // #=== DOM MANIPULATION ===# + + /** + * Update the payment context to set the flow to 'direct'. + * + * @override method from @payment/js/payment_form + * @private + * @param {number} providerId - The id of the selected payment option's provider. + * @param {string} providerCode - The code of the selected payment option's provider. + * @param {number} paymentOptionId - The id of the selected payment option + * @param {string} paymentMethodCode - The code of the selected payment method, if any. + * @param {string} flow - The online payment flow of the selected payment option. + * @return {void} + */ + async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) { + if (providerCode !== 'razorpay') { + this._super(...arguments); + return; + } + + if (flow === 'token') { + return; // No need to update the flow for tokens. + } + + // Overwrite the flow of the select payment method. + this._setPaymentFlow('direct'); + }, + + // #=== PAYMENT FLOW ===# + + async _processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) { + if (providerCode !== 'razorpay') { + this._super(...arguments); + return; + } + const razorpayOptions = this._prepareRazorpayOptions(processingValues); + await loadJS('https://checkout.razorpay.com/v1/checkout.js'); + const RazorpayJS = Razorpay(razorpayOptions); + RazorpayJS.open(); + RazorpayJS.on('payment.failed', response => { + this._displayErrorDialog(_t("Payment processing failed"), response.error.description); + }); + }, + + /** + * Prepare the options to init the RazorPay SDK Object. + * + * @param {object} processingValues - The processing values. + * @return {object} + */ + _prepareRazorpayOptions(processingValues) { + return Object.assign({}, processingValues, { + 'key': processingValues['razorpay_key_id'], + 'order_id': processingValues['razorpay_order_id'], + 'customer_id': processingValues['razorpay_customer_id'], + 'description': processingValues['reference'], + 'recurring': processingValues['is_tokenize_request'] ? '1': '0', + 'handler': response => { + if ( + response['razorpay_payment_id'] + && response['razorpay_order_id'] + && response['razorpay_signature'] + ) { // The payment reached a final state; redirect to the status page. + window.location = '/payment/status'; + } + }, + 'modal': { + 'ondismiss': () => { + window.location.reload(); + } + }, + }); + }, + +}); diff --git a/addons/payment_razorpay/tests/common.py b/addons/payment_razorpay/tests/common.py index 28394c508a2..4be7dcb86fe 100644 --- a/addons/payment_razorpay/tests/common.py +++ b/addons/payment_razorpay/tests/common.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.payment.tests.common import PaymentCommon +from odoo.fields import Command class RazorpayCommon(PaymentCommon): @@ -13,8 +14,12 @@ class RazorpayCommon(PaymentCommon): 'razorpay_key_id': 'rzp_123', 'razorpay_key_secret': 'Y63AyP9eL91', 'razorpay_webhook_secret': 'coincoin_motherducker', + 'payment_method_ids': [Command.set([cls.env.ref('payment.payment_method_card').id])], }) + cls.razorpay_customer_id = 'cust_123' + cls.razorpay_token_id = 'token_404' + cls.payment_id = 'pay_123' cls.refund_id = 'rfd_456' cls.order_id = 'order_789' @@ -23,11 +28,17 @@ class RazorpayCommon(PaymentCommon): 'razorpay_order_id': cls.order_id, 'razorpay_signature': 'dummy', } + cls.payment_method_id = cls.provider.payment_method_ids[:1].id cls.payment_data = { 'id': cls.payment_id, 'description': cls.reference, 'status': 'captured', } + cls.tokenize_payment_data = { + **cls.payment_data, + 'customer_id': cls.razorpay_customer_id, + 'token_id': cls.razorpay_token_id, + } cls.refund_data = { 'id': cls.refund_id, 'payment_id': cls.payment_id, diff --git a/addons/payment_razorpay/tests/test_payment_transaction.py b/addons/payment_razorpay/tests/test_payment_transaction.py index 91e3834cb8a..81f56ce699c 100644 --- a/addons/payment_razorpay/tests/test_payment_transaction.py +++ b/addons/payment_razorpay/tests/test_payment_transaction.py @@ -1,5 +1,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +import time + +from datetime import datetime +from dateutil.relativedelta import relativedelta + from unittest.mock import patch from odoo.exceptions import UserError @@ -14,13 +19,15 @@ class TestPaymentTransaction(RazorpayCommon): def test_no_item_missing_from_order_request_payload(self): """ Test that the request values are conform to the transaction fields. """ - tx = self._create_transaction('redirect') - request_payload = tx._razorpay_prepare_order_request_payload() + tx = self._create_transaction('redirect', operation='online_direct', payment_method_id=self.payment_method_id) + request_payload = tx._razorpay_prepare_order_payload(customer_id=self.razorpay_customer_id) self.maxDiff = 10000 # Allow comparing large dicts. converted_amount = payment_utils.to_minor_currency_units(tx.amount, tx.currency_id) self.assertDictEqual(request_payload, { 'amount': converted_amount, 'currency': tx.currency_id.name, + 'customer_id': self.razorpay_customer_id, + 'method': 'card', }) def test_void_is_not_supported(self): @@ -68,3 +75,44 @@ class TestPaymentTransaction(RazorpayCommon): ): tx._process_notification_data(self.payment_data) self.assertEqual(tx.state, 'done') + + def test_order_request_payload_for_tokenize_tx(self): + """ Test that order payload for tokenize tx is proper. """ + tx = self._create_transaction('redirect', operation='online_direct', tokenize=True, payment_method_id=self.payment_method_id) + self.assertDictEqual(tx._get_specific_rendering_values(None), {}, "Should return empty dict of rendering values for tokenize transaction") + + request_payload = tx._razorpay_prepare_order_payload(customer_id=self.razorpay_customer_id) + converted_amount = payment_utils.to_minor_currency_units(tx.amount, tx.currency_id) + token_expiry_date = datetime.today() + relativedelta(years=10) + token_expiry_timeslamp = time.mktime(token_expiry_date.timetuple()) + self.assertDictEqual(request_payload, { + 'token': { + "expire_at": token_expiry_timeslamp, + "frequency": "as_presented", + 'max_amount': 50000000, + }, + 'amount': converted_amount, + 'currency': tx.currency_id.name, + 'customer_id': self.razorpay_customer_id, + 'method': 'card', + + }) + + def test_processing_notification_data_confirms_tokenize_transaction(self): + """ Test that the transaction state is set to 'done' when the notification data indicate a + successful payment. """ + tx = self._create_transaction('redirect', tokenize=True, payment_method_id=self.payment_method_id) + tx._process_notification_data(self.tokenize_payment_data) + self.assertEqual(tx.state, 'done') + + def test_token_creation_for_tokenize_transaction(self): + """ Test that the token is create on confirmation of tokenize transaction """ + tx = self._create_transaction('redirect', tokenize=True, payment_method_id=self.payment_method_id) + tx._process_notification_data(self.tokenize_payment_data) + token = tx.token_id + self.assertTrue(token, "Should create token for tokenize transction") + self.assertFalse(tx.tokenize, "Trasection should be non tokenize after token creation") + self.assertEqual( + token.provider_ref, f"{self.tokenize_payment_data['customer_id']},{self.tokenize_payment_data['token_id']}", + "Should set proper values for provider_ref to get customer_id and token_id from that field" + ) diff --git a/addons/payment_razorpay/tests/test_processing_flows.py b/addons/payment_razorpay/tests/test_processing_flows.py index d2263428aea..e84ea5560c3 100644 --- a/addons/payment_razorpay/tests/test_processing_flows.py +++ b/addons/payment_razorpay/tests/test_processing_flows.py @@ -15,22 +15,6 @@ from odoo.addons.payment_razorpay.tests.common import RazorpayCommon @tagged('post_install', '-at_install') class TestProcessingFlows(RazorpayCommon, PaymentHttpCommon): - @mute_logger('odoo.addons.payment_razorpay.controllers.main') - def test_redirect_notification_triggers_processing(self): - """ Test that receiving a redirect notification triggers the processing of the notification - data. """ - self._create_transaction('redirect') - url = self._build_url(f'{RazorpayController._return_url}?reference={self.reference}') - with patch( - 'odoo.addons.payment_razorpay.controllers.main.RazorpayController' - '._verify_notification_signature' - ), patch( - 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' - '._handle_notification_data' - ) as handle_notification_data_mock: - self._make_http_post_request(url, data=self.redirect_notification_data) - self.assertEqual(handle_notification_data_mock.call_count, 1) - @mute_logger('odoo.addons.payment_razorpay.controllers.main') def test_webhook_notification_triggers_processing(self): """ Test that receiving a valid webhook notification triggers the processing of the @@ -48,19 +32,20 @@ class TestProcessingFlows(RazorpayCommon, PaymentHttpCommon): self.assertEqual(handle_notification_data_mock.call_count, 1) @mute_logger('odoo.addons.payment_razorpay.controllers.main') - def test_redirect_notification_triggers_signature_check(self): - """ Test that receiving a redirect notification triggers a signature check. """ - self._create_transaction('redirect') - url = self._build_url(f'{RazorpayController._return_url}?reference={self.reference}') + def test_webhook_notification_triggers_processing_for_tokenize_transaction(self): + """ Test that receiving a valid webhook notification triggers the processing of the + notification data for tokenize transaction. """ + self._create_transaction('redirect', tokenize=True, payment_method_id=self.payment_method_id) + url = self._build_url(RazorpayController._webhook_url) with patch( - 'odoo.addons.payment_razorpay.controllers.main.RazorpayController' - '._verify_notification_signature' - ) as signature_check_mock, patch( + 'odoo.addons.payment_razorpay.controllers.main.RazorpayController.' + '_verify_notification_signature' + ), patch( 'odoo.addons.payment.models.payment_transaction.PaymentTransaction' '._handle_notification_data' - ): - self._make_http_post_request(url, data=self.redirect_notification_data) - self.assertEqual(signature_check_mock.call_count, 1) + ) as handle_notification_data_mock: + self._make_json_request(url, data=self.webhook_notification_data) + self.assertEqual(handle_notification_data_mock.call_count, 1) @mute_logger('odoo.addons.payment_razorpay.controllers.main') def test_webhook_notification_triggers_signature_check(self): diff --git a/addons/payment_razorpay/views/payment_provider_views.xml b/addons/payment_razorpay/views/payment_provider_views.xml index 3c95a70191e..225840cd2fa 100644 --- a/addons/payment_razorpay/views/payment_provider_views.xml +++ b/addons/payment_razorpay/views/payment_provider_views.xml @@ -22,6 +22,18 @@ password="True"/> + + + diff --git a/addons/payment_razorpay/views/payment_razorpay_templates.xml b/addons/payment_razorpay/views/payment_razorpay_templates.xml index e7769b90600..bf25c9d0497 100644 --- a/addons/payment_razorpay/views/payment_razorpay_templates.xml +++ b/addons/payment_razorpay/views/payment_razorpay_templates.xml @@ -2,21 +2,7 @@