From 08a7c779cb56f9c0803b49ef6edf753f77c5893c Mon Sep 17 00:00:00 2001 From: Barad Mahendra Date: Tue, 22 Mar 2022 13:31:52 +0000 Subject: [PATCH] [ADD] payment_razorpay: integrate with Razorpay's "Payment" gateway See README for technical details. task-2800823 Part-of: odoo/odoo#92848 Co-authored-by: Antoine Vandevenne (anv) Co-authored-by: Prakash Prajapati --- addons/payment/data/payment_acquirer_data.xml | 17 + addons/payment/data/payment_icon_data.xml | 6 + addons/payment/static/img/rupay.png | Bin 0 -> 72838 bytes addons/payment_razorpay/README.md | 36 +++ addons/payment_razorpay/__init__.py | 10 + addons/payment_razorpay/__manifest__.py | 19 ++ addons/payment_razorpay/const.py | 32 ++ .../payment_razorpay/controllers/__init__.py | 3 + addons/payment_razorpay/controllers/main.py | 107 +++++++ .../data/payment_acquirer_data.xml | 15 + addons/payment_razorpay/models/__init__.py | 5 + .../models/account_payment_method.py | 13 + .../models/payment_acquirer.py | 137 ++++++++ .../models/payment_transaction.py | 301 ++++++++++++++++++ .../static/description/icon.png | Bin 0 -> 16572 bytes .../static/description/icon.svg | 27 ++ addons/payment_razorpay/tests/__init__.py | 6 + addons/payment_razorpay/tests/common.py | 43 +++ .../tests/test_payment_acquirer.py | 33 ++ .../tests/test_payment_transaction.py | 70 ++++ .../tests/test_processing_flows.py | 122 +++++++ .../views/payment_razorpay_templates.xml | 21 ++ .../payment_razorpay/views/payment_views.xml | 28 ++ 23 files changed, 1051 insertions(+) create mode 100644 addons/payment/static/img/rupay.png create mode 100644 addons/payment_razorpay/README.md create mode 100644 addons/payment_razorpay/__init__.py create mode 100644 addons/payment_razorpay/__manifest__.py create mode 100644 addons/payment_razorpay/const.py create mode 100644 addons/payment_razorpay/controllers/__init__.py create mode 100644 addons/payment_razorpay/controllers/main.py create mode 100644 addons/payment_razorpay/data/payment_acquirer_data.xml create mode 100644 addons/payment_razorpay/models/__init__.py create mode 100644 addons/payment_razorpay/models/account_payment_method.py create mode 100644 addons/payment_razorpay/models/payment_acquirer.py create mode 100644 addons/payment_razorpay/models/payment_transaction.py create mode 100644 addons/payment_razorpay/static/description/icon.png create mode 100644 addons/payment_razorpay/static/description/icon.svg create mode 100644 addons/payment_razorpay/tests/__init__.py create mode 100644 addons/payment_razorpay/tests/common.py create mode 100644 addons/payment_razorpay/tests/test_payment_acquirer.py create mode 100644 addons/payment_razorpay/tests/test_payment_transaction.py create mode 100644 addons/payment_razorpay/tests/test_processing_flows.py create mode 100644 addons/payment_razorpay/views/payment_razorpay_templates.xml create mode 100644 addons/payment_razorpay/views/payment_views.xml diff --git a/addons/payment/data/payment_acquirer_data.xml b/addons/payment/data/payment_acquirer_data.xml index 69a926ea093..5c25b146290 100644 --- a/addons/payment/data/payment_acquirer_data.xml +++ b/addons/payment/data/payment_acquirer_data.xml @@ -349,6 +349,23 @@ ])]"/> + + Razorpay + Credit & Debit Card, UPI (Powered by Razorpay) + + + + + + SEPA Direct Debit 20 diff --git a/addons/payment/data/payment_icon_data.xml b/addons/payment/data/payment_icon_data.xml index 9f777e7b424..b4cd2aed0c5 100644 --- a/addons/payment/data/payment_icon_data.xml +++ b/addons/payment/data/payment_icon_data.xml @@ -37,6 +37,12 @@ + + 65 + Rupay + + + 70 Apple Pay diff --git a/addons/payment/static/img/rupay.png b/addons/payment/static/img/rupay.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd3771a4930ea11e3db42abdc73a445ad689783 GIT binary patch literal 72838 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%0D~IUf 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 literal 0 HcmV?d00001 diff --git a/addons/payment_razorpay/README.md b/addons/payment_razorpay/README.md new file mode 100644 index 00000000000..ba0dab1bcaf --- /dev/null +++ b/addons/payment_razorpay/README.md @@ -0,0 +1,36 @@ +# Razorpay + +## Implementation details + +### Supported features + +- Payment with redirection flow +- Manual capture +- Partial refunds +- Several payment methods such as debit/credit cards, netbanking, UPI, and + [others](https://razorpay.com/docs/payments/payment-methods/). +- [Webhook](https://razorpay.com/docs/webhooks). + +In addition, Razorpay also allows to implement tokenization but requires passing the card secret for +each transaction. + +### API and gateway + +We choose to integrate with +[Razorpay Hosted Checkout](https://razorpay.com/docs/payments/payment-gateway/web-integration/hosted). +The other gateways were ruled out. See the task's dev notes for the details on the other gateways. + +The version of the API implemented by this module is v1. + +## Merge details + +The first version of the module was specified in task +[2800823](https://www.odoo.com/web#id=2800823&model=project.task) and merged with PR +odoo/odoo#92848 in `saas-15.5`. + +## Testing instructions + +The partner's phone number must be a valid Indian phone number. Example: +91123456789 + +See https://razorpay.com/docs/payments/payments/test-card-upi-details/ for the list of test +payment details. diff --git a/addons/payment_razorpay/__init__.py b/addons/payment_razorpay/__init__.py new file mode 100644 index 00000000000..141de1d6b73 --- /dev/null +++ b/addons/payment_razorpay/__init__.py @@ -0,0 +1,10 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models + +from odoo.addons.payment import reset_payment_acquirer + + +def uninstall_hook(cr, registry): + reset_payment_acquirer(cr, registry, 'razorpay') diff --git a/addons/payment_razorpay/__manifest__.py b/addons/payment_razorpay/__manifest__.py new file mode 100644 index 00000000000..00c9abcd51b --- /dev/null +++ b/addons/payment_razorpay/__manifest__.py @@ -0,0 +1,19 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Payment Acquirer: Razorpay", + 'version': '1.0', + 'category': 'Accounting/Payment Acquirers', + 'sequence': 385, + 'summary': "A Indian online payment provider covering 100+ payment methods.", + 'depends': ['payment'], + 'data': [ + 'views/payment_views.xml', + 'views/payment_razorpay_templates.xml', + + 'data/payment_acquirer_data.xml', # Depends on views/payment_razorpay_templates.xml + ], + 'application': True, + 'uninstall_hook': 'uninstall_hook', + 'license': 'LGPL-3', +} diff --git a/addons/payment_razorpay/const.py b/addons/payment_razorpay/const.py new file mode 100644 index 00000000000..2aaf70bb644 --- /dev/null +++ b/addons/payment_razorpay/const.py @@ -0,0 +1,32 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# The currencies supported by Razorpay, in ISO 4217 format. Last updated on May 26, 2021. +# See https://razorpay.com/docs/payments/payments/international-payments/#supported-currencies. +SUPPORTED_CURRENCIES = [ + 'AED', 'ALL', 'AMD', 'ARS', 'AUD', 'AWG', 'BBD', 'BDT', 'BMD', 'BND', 'BOB', 'BSD', 'BWP', + 'BZD', 'CAD', 'CHF', 'CNY', 'COP', 'CRC', 'CUP', 'CZK', 'DKK', 'DOP', 'DZD', 'EGP', 'ETB', + 'EUR', 'FJD', 'GBP', 'GHS', 'GIP', 'GMD', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', + 'IDR', 'ILS', 'INR', 'JMD', 'KES', 'KGS', 'KHR', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', + 'LSL', 'MAD', 'MDL', 'MKD', 'MMK', 'MNT', 'MOP', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'NAD', + 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'PEN', 'PGK', 'PHP', 'PKR', 'QAR', 'RUB', 'SAR', 'SCR', + 'SEK', 'SGD', 'SLL', 'SOS', 'SSP', 'SVC', 'SZL', 'THB', 'TTD', 'TZS', 'USD', 'UYU', 'UZS', + 'YER', 'ZAR', +] + +# Mapping of transaction states to Razorpay's payment statuses. +# See https://razorpay.com/docs/payments/payments#payment-life-cycle. +PAYMENT_STATUS_MAPPING = { + 'pending': ('created', 'pending'), + 'authorized': ('authorized',), + 'done': ('captured', 'refunded', 'processed'), # refunded is included to discard refunded txs. + 'error': ('failed',), +} + +# Events that are handled by the webhook. +HANDLED_WEBHOOK_EVENTS = [ + 'payment.authorized', + 'payment.captured', + 'payment.failed', + 'refund.failed', + 'refund.processed', +] diff --git a/addons/payment_razorpay/controllers/__init__.py b/addons/payment_razorpay/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/addons/payment_razorpay/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/payment_razorpay/controllers/main.py b/addons/payment_razorpay/controllers/main.py new file mode 100644 index 00000000000..15c14c43b2c --- /dev/null +++ b/addons/payment_razorpay/controllers/main.py @@ -0,0 +1,107 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hmac +import logging +import pprint + +from werkzeug.exceptions import Forbidden + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + +from odoo.addons.payment_razorpay.const import HANDLED_WEBHOOK_EVENTS + + +_logger = logging.getLogger(__name__) + + +class RazorpayController(http.Controller): + _return_url = '/payment/razorpay/return' + _webhook_url = '/payment/razorpay/webhook' + + @http.route( + _return_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False, + save_session=False + ) + def razorpay_return_from_checkout(self, reference, **data): + """ Process the notification data sent by Razorpay after redirection from checkout. + + :param str reference: The transaction reference embedded in the return URL. + :param dict data: The notification data. + """ + _logger.info("Handling redirection from Razorpay with data:\n%s", pprint.pformat(data)) + if all(f'razorpay_{key}' in data for key in ('order_id', 'payment_id', 'signature')): + # Check the integrity of the notification. + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( + 'razorpay', {'description': reference} + ) # Use the same key as for webhook notifications' data. + self._verify_notification_signature(data, data.get('razorpay_signature'), tx_sudo) + + # Handle the notification data. + tx_sudo._handle_notification_data('razorpay', data) + else: # The customer cancelled the payment or the payment failed. + pass # Don't try to process this case because the payment id was not provided. + + # Redirect the user to the status page. + return request.redirect('/payment/status') + + @http.route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False) + def razorpay_webhook(self): + """ Process the notification data sent by Razorpay to the webhook. + + :return: An empty string to acknowledge the notification. + :rtype: str + """ + data = request.get_json_data() + _logger.info("Notification received from Razorpay with data:\n%s", pprint.pformat(data)) + + event_type = data['event'] + if event_type in HANDLED_WEBHOOK_EVENTS: + entity_type = 'payment' if 'payment' in event_type else 'refund' + try: + entity_data = data['payload'].get(entity_type, {}).get('entity', {}) + entity_data.update(entity_type=entity_type) + + # Check the integrity of the event. + received_signature = request.httprequest.headers.get('X-Razorpay-Signature') + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data( + 'razorpay', entity_data + ) + self._verify_notification_signature( + request.httprequest.data, received_signature, tx_sudo, is_redirect=False + ) + + # Handle the notification data. + tx_sudo._handle_notification_data('razorpay', entity_data) + except ValidationError: # Acknowledge the notification to avoid getting spammed. + _logger.exception("Unable to handle the notification data; skipping to acknowledge") + return request.make_json_response('') + + @staticmethod + def _verify_notification_signature( + notification_data, received_signature, tx_sudo, is_redirect=True + ): + """ Check that the received signature matches the expected one. + + :param dict|bytes notification_data: The notification data. + :param str received_signature: The signature to compare with the expected signature. + :param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a + `payment.transaction` record + :param bool is_redirect: Whether the notification data should be treated as redirect data + or as coming from a webhook notification. + :return: None + :raise :class:`werkzeug.exceptions.Forbidden`: If the signatures don't match. + """ + # Check for the received signature. + if not received_signature: + _logger.warning("Received notification with missing signature.") + raise Forbidden() + + # Compare the received signature with the expected signature. + expected_signature = tx_sudo.acquirer_id._razorpay_calculate_signature( + notification_data, is_redirect=is_redirect + ) + if not hmac.compare_digest(received_signature, expected_signature): + _logger.warning("Received notification with invalid signature.") + raise Forbidden() diff --git a/addons/payment_razorpay/data/payment_acquirer_data.xml b/addons/payment_razorpay/data/payment_acquirer_data.xml new file mode 100644 index 00000000000..65397ae27a6 --- /dev/null +++ b/addons/payment_razorpay/data/payment_acquirer_data.xml @@ -0,0 +1,15 @@ + + + + + razorpay + + + + + Razorpay + razorpay + inbound + + + diff --git a/addons/payment_razorpay/models/__init__.py b/addons/payment_razorpay/models/__init__.py new file mode 100644 index 00000000000..28c8703fcc1 --- /dev/null +++ b/addons/payment_razorpay/models/__init__.py @@ -0,0 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_payment_method +from . import payment_acquirer +from . import payment_transaction diff --git a/addons/payment_razorpay/models/account_payment_method.py b/addons/payment_razorpay/models/account_payment_method.py new file mode 100644 index 00000000000..7e364a300f6 --- /dev/null +++ b/addons/payment_razorpay/models/account_payment_method.py @@ -0,0 +1,13 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class AccountPaymentMethod(models.Model): + _inherit = 'account.payment.method' + + @api.model + def _get_payment_method_information(self): + res = super()._get_payment_method_information() + res['razorpay'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]} + return res diff --git a/addons/payment_razorpay/models/payment_acquirer.py b/addons/payment_razorpay/models/payment_acquirer.py new file mode 100644 index 00000000000..3b90134a11b --- /dev/null +++ b/addons/payment_razorpay/models/payment_acquirer.py @@ -0,0 +1,137 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import hashlib +import hmac +import logging +import pprint + +import requests +from werkzeug.urls import url_join + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from odoo.addons.payment_razorpay.const import SUPPORTED_CURRENCIES + + +_logger = logging.getLogger(__name__) + + +class PaymentAcquirer(models.Model): + _inherit = 'payment.acquirer' + + provider = fields.Selection( + selection_add=[('razorpay', "Razorpay")], ondelete={'razorpay': 'set default'} + ) + razorpay_key_id = fields.Char( + string="Razorpay Key Id", + help="The key solely used to identify the account with Razorpay.", + required_if_provider='razorpay', + ) + razorpay_key_secret = fields.Char( + string="Razorpay Key Secret", + required_if_provider='razorpay', + groups='base.group_system', + ) + razorpay_webhook_secret = fields.Char( + string="Razorpay Webhook Secret", + required_if_provider='razorpay', + groups='base.group_system', + ) + + #=== COMPUTE METHODS ===# + + def _compute_feature_support_fields(self): + """ Override of `payment` to enable additional features. """ + super()._compute_feature_support_fields() + self.filtered(lambda acq: acq.provider == 'razorpay').update({ + 'support_manual_capture': True, + 'support_refund': 'partial', + }) + + # === BUSINESS METHODS ===# + + @api.model + def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs): + """ Override of `payment` to filter out Razorpay acquirers for unsupported currencies. """ + acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs) + + currency = self.env['res.currency'].browse(currency_id).exists() + if currency and currency.name not in SUPPORTED_CURRENCIES: + acquirers = acquirers.filtered(lambda a: a.provider != 'razorpay') + + return acquirers + + def _razorpay_make_request(self, endpoint, payload=None, method='POST'): + """ Make a request to Razorpay API at the specified endpoint. + + Note: self.ensure_one() + + :param str endpoint: The endpoint to be reached by the request. + :param dict payload: The payload of the request. + :param str method: The HTTP method of the request. + :return The JSON-formatted content of the response. + :rtype: dict + :raise ValidationError: If an HTTP error occurs. + """ + self.ensure_one() + + url = url_join('https://api.razorpay.com/v1/', endpoint) + auth = (self.razorpay_key_id, self.razorpay_key_secret) + try: + if method == 'GET': + response = requests.get(url, params=payload, auth=auth, timeout=10) + else: + response = requests.post(url, json=payload, auth=auth, timeout=10) + try: + response.raise_for_status() + except requests.exceptions.HTTPError: + _logger.exception( + "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') + )) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + _logger.exception("Unable to reach endpoint at %s", url) + raise ValidationError( + "Razorpay: " + _("Could not establish the connection to the API.") + ) + return response.json() + + def _razorpay_calculate_signature(self, data, is_redirect=True): + """ Compute the signature for the request's data according to the Razorpay documentation. + + See https://razorpay.com/docs/webhooks/validate-test#validate-webhooks and + https://razorpay.com/docs/payments/payment-gateway/web-integration/hosted/build-integration. + + :param dict|bytes data: The data to sign. + :param bool is_redirect: Whether the data should be treated as redirect data or as coming + from a webhook notification. + :return: The calculated signature. + :rtype: str + """ + if is_redirect: + secret = self.razorpay_key_secret + signing_string = f'{data["razorpay_order_id"]}|{data["razorpay_payment_id"]}' + return hmac.new( + secret.encode(), msg=signing_string.encode(), digestmod=hashlib.sha256 + ).hexdigest() + else: # Notification data. + secret = self.razorpay_webhook_secret + return hmac.new(secret.encode(), msg=data, digestmod=hashlib.sha256).hexdigest() + + def _get_default_payment_method_id(self): + self.ensure_one() + if self.provider != 'razorpay': + return super()._get_default_payment_method_id() + return self.env.ref('payment_razorpay.payment_method_razorpay').id + + def _neutralize(self): + super()._neutralize() + self._neutralize_fields('razorpay', [ + 'razorpay_key_id', + 'razorpay_key_secret', + 'razorpay_webhook_secret', + ]) diff --git a/addons/payment_razorpay/models/payment_transaction.py b/addons/payment_razorpay/models/payment_transaction.py new file mode 100644 index 00000000000..b2feb5b1399 --- /dev/null +++ b/addons/payment_razorpay/models/payment_transaction.py @@ -0,0 +1,301 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import pprint + +from werkzeug.urls import url_encode, url_join + +from odoo import _, models +from odoo.exceptions import UserError, ValidationError + +from odoo.addons.payment import utils as payment_utils +from odoo.addons.payment_razorpay.const import PAYMENT_STATUS_MAPPING +from odoo.addons.payment_razorpay.controllers.main import RazorpayController + + +_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. + + 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 acquirer-specific rendering values. + :rtype: dict + """ + res = super()._get_specific_rendering_values(processing_values) + if self.provider != 'razorpay': + return res + + # Initiate the payment and retrieve the related order id. + payload = self._razorpay_prepare_order_request_payload() + _logger.info( + "Payload of '/orders' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(payload) + ) + order_data = self.acquirer_id._razorpay_make_request(endpoint='orders', payload=payload) + _logger.info( + "Response of '/orders' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(order_data) + ) + + # Initiate the payment + converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) + base_url = self.acquirer_id.get_base_url() + return_url_params = {'reference': self.reference} + rendering_values = { + 'key_id': self.acquirer_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': self.partner_phone, + '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. + + :return: The request payload. + :rtype: dict + """ + converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) + payload = { + 'amount': converted_amount, + 'currency': self.currency_id.name, + } + if self.acquirer_id.capture_manually: # The related payment must be only authorized. + payload.update({ + 'payment': { + 'capture': 'manual', + 'capture_options': { + 'manual_expiry_period': 7200, # The default value for this required option. + 'refund_speed': 'normal', # The default value for this required option. + } + }, + }) + return payload + + def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True): + """ Override of `payment` to send a refund request to Razorpay. + + Note: self.ensure_one() + + :param float amount_to_refund: The amount to refund. + :param bool create_refund_transaction: Whether a refund transaction should be created + :return: The refund transaction if any + :rtype: recordset of `payment.transaction` + """ + if self.provider != 'razorpay': + return super()._send_refund_request( + amount_to_refund=amount_to_refund, + create_refund_transaction=create_refund_transaction, + ) + refund_tx = super()._send_refund_request( + amount_to_refund=amount_to_refund, create_refund_transaction=True + ) + + # Make the refund request to Razorpay. + converted_amount = payment_utils.to_minor_currency_units( + -refund_tx.amount, refund_tx.currency_id + ) # The amount is negative for refund transactions. + payload = { + 'amount': converted_amount, + 'notes': { + 'reference': refund_tx.reference, # Allow retrieving the ref. from webhook data. + }, + } + _logger.info( + "Payload of '/payments//refund' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(payload) + ) + response_content = refund_tx.acquirer_id._razorpay_make_request( + f'payments/{self.acquirer_reference}/refund', payload=payload + ) + _logger.info( + "Response of '/payments//refund' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(response_content) + ) + response_content.update(entity_type='refund') + refund_tx._handle_notification_data('razorpay', response_content) + + return refund_tx + + def _send_capture_request(self): + """ Override of `payment` to send a capture request to Razorpay. + + Note: self.ensure_one() + + :return: None + """ + super()._send_capture_request() + if self.provider != 'razorpay': + return + + converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) + payload = {'amount': converted_amount, 'currency': self.currency_id.name} + _logger.info( + "Payload of '/payments//capture' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(payload) + ) + response_content = self.acquirer_id._razorpay_make_request( + f'payments/{self.acquirer_reference}/capture', payload=payload + ) + _logger.info( + "Response of '/payments//capture' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(response_content) + ) + + # Handle the capture request response. + self._handle_notification_data('razorpay', response_content) + + def _send_void_request(self): + """ Override of `payment` to explain that it is impossible to void a Razorpay transaction. + + Note: self.ensure_one() + + :return: None + """ + super()._send_void_request() + if self.provider != 'razorpay': + return + + raise UserError(_("Transactions processed by Razorpay can't be manually voided from Odoo.")) + + def _get_tx_from_notification_data(self, provider, notification_data): + """ Override of `payment` to find the transaction based on razorpay data. + + :param str provider: The provider of the acquirer that handled the transaction + :param dict notification_data: The normalized notification data sent by the provider + :return: The transaction if found + :rtype: recordset of `payment.transaction` + :raise: ValidationError if the data match no transaction + """ + tx = super()._get_tx_from_notification_data(provider, notification_data) + if provider != 'razorpay' or len(tx) == 1: + return tx + + entity_type = notification_data.get('entity_type', 'payment') + if entity_type == 'payment': + reference = notification_data.get('description') + if not reference: + raise ValidationError("Razorpay: " + _("Received data with missing reference.")) + tx = self.search([('reference', '=', reference), ('provider', '=', 'razorpay')]) + else: # 'refund' + reference = notification_data.get('notes', {}).get('reference') + if reference: # The refund was initiated from Odoo. + tx = self.search([('reference', '=', reference), ('provider', '=', 'razorpay')]) + else: # The refund was initiated from Razorpay. + # Find the source transaction based on its acquirer reference. + source_tx = self.search([ + ('acquirer_reference', '=', notification_data['payment_id']), + ('provider', '=', 'razorpay'), + ]) + if source_tx: + # Manually create a refund transaction with a new reference. + tx = self._razorpay_create_refund_tx_from_notification_data( + source_tx, notification_data + ) + else: # The refund was initiated for an unknown source transaction. + pass # Don't do anything with the refund notification. + if not tx: + raise ValidationError( + "Razorpay: " + _("No transaction found matching reference %s.", reference) + ) + + return tx + + def _razorpay_create_refund_tx_from_notification_data(self, source_tx, notification_data): + """ Create a refund transaction based on Razorpay data. + + :param recordset source_tx: The source transaction for which a refund is initiated, as a + `payment.transaction` recordset. + :param dict notification_data: The notification data sent by the provider. + :return: The newly created refund transaction. + :rtype: recordset of `payment.transaction` + :raise ValidationError: If inconsistent data were received. + """ + refund_acquirer_reference = notification_data.get('id') + amount_to_refund = notification_data.get('amount') + if not refund_acquirer_reference or not amount_to_refund: + raise ValidationError("Razorpay: " + _("Received incomplete refund data.")) + + converted_amount = payment_utils.to_major_currency_units( + amount_to_refund, source_tx.currency_id + ) + return source_tx._create_refund_transaction( + amount_to_refund=converted_amount, acquirer_reference=refund_acquirer_reference + ) + + def _process_notification_data(self, notification_data): + """ Override of `payment` to process the transaction based on Razorpay data. + + Note: self.ensure_one() + + :param dict notification_data: The notification data sent by the provider + :return: None + """ + super()._process_notification_data(notification_data) + if self.provider != 'razorpay': + return + + if 'id' in notification_data: # We have the full entity data (S2S request or webhook). + entity_data = notification_data + else: # The payment data are not complete (redirect from checkout). + # Fetch the full payment data. + entity_data = self.acquirer_id._razorpay_make_request( + f'payments/{notification_data["razorpay_payment_id"]}', method='GET' + ) + _logger.info( + "Response of '/payments' request for transaction with reference %s:\n%s", + self.reference, pprint.pformat(entity_data) + ) + entity_id = entity_data.get('id') + if not entity_id: + raise ValidationError("Razorpay: " + _("Received data with missing entity id.")) + self.acquirer_reference = entity_id + + entity_status = entity_data.get('status') + if not entity_status: + raise ValidationError("Razorpay: " + _("Received data with missing status.")) + + if entity_status in PAYMENT_STATUS_MAPPING['pending']: + self._set_pending() + elif entity_status in PAYMENT_STATUS_MAPPING['authorized']: + self._set_authorized() + elif entity_status in PAYMENT_STATUS_MAPPING['done']: + self._set_done() + + # Immediately post-process the transaction if it is a refund, as the post-processing + # will not be triggered by a customer browsing the transaction from the portal. + if self.operation == 'refund': + self.env.ref('payment.cron_post_process_payment_tx')._trigger() + elif entity_status in PAYMENT_STATUS_MAPPING['error']: + _logger.warning( + "The transaction with reference %s underwent an error. Reason: %s", + self.reference, entity_data.get('error_description') + ) + self._set_error( + _("An error occurred during the processing of your payment. Please try again.") + ) + else: # Classify unsupported payment status as the `error` tx state. + _logger.warning( + "Received data for transaction with reference %s with invalid payment status: %s", + self.reference, entity_status + ) + self._set_error( + "Razorpay: " + _("Received data with invalid status: %s", entity_status) + ) diff --git a/addons/payment_razorpay/static/description/icon.png b/addons/payment_razorpay/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..eea0cae79c8d1451b6086cf3398d7e90df6c518c GIT binary patch literal 16572 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

D}LU2YGlII;5YZHmXL3gFGjFr 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 literal 0 HcmV?d00001 diff --git a/addons/payment_razorpay/static/description/icon.svg b/addons/payment_razorpay/static/description/icon.svg new file mode 100644 index 00000000000..5f9dea6e511 --- /dev/null +++ b/addons/payment_razorpay/static/description/icon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/payment_razorpay/tests/__init__.py b/addons/payment_razorpay/tests/__init__.py new file mode 100644 index 00000000000..bad6afe5061 --- /dev/null +++ b/addons/payment_razorpay/tests/__init__.py @@ -0,0 +1,6 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common +from . import test_payment_acquirer +from . import test_payment_transaction +from . import test_processing_flows diff --git a/addons/payment_razorpay/tests/common.py b/addons/payment_razorpay/tests/common.py new file mode 100644 index 00000000000..fb0a6d9c323 --- /dev/null +++ b/addons/payment_razorpay/tests/common.py @@ -0,0 +1,43 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.payment.tests.common import PaymentCommon + + +class RazorpayCommon(PaymentCommon): + + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.acquirer = cls._prepare_acquirer('razorpay', update_values={ + 'razorpay_key_id': 'rzp_123', + 'razorpay_key_secret': 'Y63AyP9eL91', + 'razorpay_webhook_secret': 'coincoin_motherducker', + }) + + cls.payment_id = 'pay_123' + cls.refund_id = 'rfd_456' + cls.order_id = 'order_789' + cls.redirect_notification_data = { + 'razorpay_payment_id': cls.payment_id, + 'razorpay_order_id': cls.order_id, + 'razorpay_signature': 'dummy', + } + cls.payment_data = { + 'id': cls.payment_id, + 'description': cls.reference, + 'status': 'captured', + } + cls.refund_data = { + 'id': cls.refund_id, + 'payment_id': cls.payment_id, + 'amount': cls.amount, + } + cls.webhook_notification_data = { + 'event': 'payment.captured', + 'payload': { + 'payment': { + 'entity': cls.payment_data, + }, + }, + } diff --git a/addons/payment_razorpay/tests/test_payment_acquirer.py b/addons/payment_razorpay/tests/test_payment_acquirer.py new file mode 100644 index 00000000000..404c1ed92ae --- /dev/null +++ b/addons/payment_razorpay/tests/test_payment_acquirer.py @@ -0,0 +1,33 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged + +from odoo.addons.payment_razorpay.tests.common import RazorpayCommon + + +@tagged('post_install', '-at_install') +class TestPaymentAcquirer(RazorpayCommon): + + def test_incompatible_with_unsupported_currencies(self): + """ Test that Razorpay acquirers are filtered out from compatible acquirers when the + currency is not supported. """ + compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers( + self.company_id, self.partner.id, self.amount, currency_id=self.env.ref('base.AFN').id + ) + self.assertNotIn(self.acquirer, compatible_acquirers) + + def test_signature_calculation_for_redirect_data(self): + """ Test that the calculated signature matches the expected signature for redirect data. """ + calculated_signature = self.acquirer._razorpay_calculate_signature( + self.redirect_notification_data, is_redirect=True + ) + self.assertEqual( + calculated_signature, '437b72e4e87362a39951b44487cf698410b074afdbed19ec44fffd32d2f863f3' + ) + + def test_neutralize(self): + """ Test that the sensitive fields of the acquirer are correctly neutralized. """ + self.env['payment.acquirer']._neutralize() + self.assertFalse(self.acquirer.razorpay_key_id) + self.assertFalse(self.acquirer.razorpay_key_secret) + self.assertFalse(self.acquirer.razorpay_webhook_secret) diff --git a/addons/payment_razorpay/tests/test_payment_transaction.py b/addons/payment_razorpay/tests/test_payment_transaction.py new file mode 100644 index 00000000000..4162eeb1b27 --- /dev/null +++ b/addons/payment_razorpay/tests/test_payment_transaction.py @@ -0,0 +1,70 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from odoo.exceptions import UserError +from odoo.tests import tagged + +from odoo.addons.payment import utils as payment_utils +from odoo.addons.payment_razorpay.tests.common import RazorpayCommon + + +@tagged('post_install', '-at_install') +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() + 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, + }) + + def test_void_is_not_supported(self): + """ Test that trying to void an authorized transaction raises an error. """ + tx = self._create_transaction('redirect', state='authorized') + self.assertRaises(UserError, func=tx._send_void_request) + + def test_get_tx_from_notification_data_returns_refund_tx(self): + """ Test that the refund transaction is returned if it exists when processing refund + notification data. """ + refund_tx = self._create_transaction('redirect') + returned_tx = self.env['payment.transaction']._get_tx_from_notification_data( + 'razorpay', dict(self.refund_data, **{ + 'entity_type': 'refund', + 'notes': { + 'reference': refund_tx.reference, + }, + }) + ) + self.assertEqual(returned_tx, refund_tx) + + def test_get_tx_from_notification_data_creates_refund_tx_when_missing(self): + """ Test that a refund transaction is created when processing refund notification data + without reference. """ + source_tx = self._create_transaction( + 'redirect', state='done', acquirer_reference=self.payment_id + ) + refund_tx = self.env['payment.transaction']._get_tx_from_notification_data( + 'razorpay', dict(self.refund_data, entity_type='refund') + ) + self.assertTrue( + refund_tx, + msg="If no refund tx is found with the refund data, a refund tx should be created.", + ) + self.assertNotEqual(refund_tx, source_tx) + self.assertEqual(refund_tx.source_transaction_id, source_tx) + + def test_processing_notification_data_confirms_transaction(self): + """ Test that the transaction state is set to 'done' when the notification data indicate a + successful payment. """ + tx = self._create_transaction('redirect') + with patch( + 'odoo.addons.payment_razorpay.models.payment_acquirer.PaymentAcquirer' + '._razorpay_make_request', return_value=self.payment_data + ): + tx._process_notification_data(self.payment_data) + self.assertEqual(tx.state, 'done') diff --git a/addons/payment_razorpay/tests/test_processing_flows.py b/addons/payment_razorpay/tests/test_processing_flows.py new file mode 100644 index 00000000000..b01ca01facd --- /dev/null +++ b/addons/payment_razorpay/tests/test_processing_flows.py @@ -0,0 +1,122 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + +from werkzeug.exceptions import Forbidden + +from odoo.tests import tagged +from odoo.tools import mute_logger + +from odoo.addons.payment.tests.http_common import PaymentHttpCommon +from odoo.addons.payment_razorpay.controllers.main import RazorpayController +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 + notification data. """ + self._create_transaction('direct') + url = self._build_url(RazorpayController._webhook_url) + 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_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_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}') + with patch( + 'odoo.addons.payment_razorpay.controllers.main.RazorpayController' + '._verify_notification_signature' + ) as signature_check_mock, 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) + + @mute_logger('odoo.addons.payment_razorpay.controllers.main') + def test_webhook_notification_triggers_signature_check(self): + """ Test that receiving a webhook notification triggers a signature check. """ + self._create_transaction('redirect') + 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.models.payment_transaction.PaymentTransaction' + '._handle_notification_data' + ): + self._make_json_request(url, data=self.webhook_notification_data) + self.assertEqual(signature_check_mock.call_count, 1) + + def test_accept_webhook_notification_with_valid_signature(self): + """ Test the verification of a webhook notification with a valid signature. """ + tx = self._create_transaction('redirect') + with patch( + 'odoo.addons.payment_razorpay.models.payment_acquirer.PaymentAcquirer' + '._razorpay_calculate_signature', return_value='valid_signature' + ): + self._assert_does_not_raise( + Forbidden, + RazorpayController._verify_notification_signature, + self.webhook_notification_data, + 'valid_signature', + tx, + is_redirect=False, + ) + + @mute_logger('odoo.addons.payment_razorpay.controllers.main') + def test_reject_notification_with_missing_signature(self): + """ Test the verification of a notification with a missing signature. """ + tx = self._create_transaction('redirect') + self.assertRaises( + Forbidden, + RazorpayController._verify_notification_signature, + self.webhook_notification_data, + None, + tx, + ) + + @mute_logger('odoo.addons.payment_razorpay.controllers.main') + def test_reject_notification_with_invalid_signature(self): + """ Test the verification of a notification with an invalid signature. """ + tx = self._create_transaction('redirect') + with patch( + 'odoo.addons.payment_razorpay.models.payment_acquirer.PaymentAcquirer' + '._razorpay_calculate_signature', return_value='valid_signature' + ): + self.assertRaises( + Forbidden, + RazorpayController._verify_notification_signature, + self.webhook_notification_data, + 'bad_signature', + tx, + ) diff --git a/addons/payment_razorpay/views/payment_razorpay_templates.xml b/addons/payment_razorpay/views/payment_razorpay_templates.xml new file mode 100644 index 00000000000..10b868401ed --- /dev/null +++ b/addons/payment_razorpay/views/payment_razorpay_templates.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/addons/payment_razorpay/views/payment_views.xml b/addons/payment_razorpay/views/payment_views.xml new file mode 100644 index 00000000000..dab885ee257 --- /dev/null +++ b/addons/payment_razorpay/views/payment_views.xml @@ -0,0 +1,28 @@ + + + + + Razorpay Acquirer Form + payment.acquirer + + + + + + + + + + + + +