Skip to content

Commit 8d0c0a7

Browse files
committed
[IMP] payment, _*: add fpx for razorpay
_* = payment_razorpay This commit introduces support for FPX as a payment method in the Razorpay. Why this solution? When using the Razorpay Orders API, specifying fpx as the payment method in the payload results in an error: 'currency should be INR when method is fpx'. This limitation prevents FPX from being used directly in the API call. What is the solution? To address this, FPX has been added to the FALLBACK_PAYMENT_METHOD_CODES. This adjustment ensures that no specific payment method is included in the API payload, allowing Razorpay to present the user with a payment form listing all available methods. From there, the user can select FPX as their preferred payment method. task-4364895 closes odoo#192837 Signed-off-by: Shrey Mehta (shrm) <[email protected]>
1 parent 78519af commit 8d0c0a7

File tree

5 files changed

+82
-14
lines changed

5 files changed

+82
-14
lines changed

addons/payment/data/payment_provider_data.xml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,12 +330,13 @@
330330
<field name="payment_method_ids"
331331
eval="[Command.set([
332332
ref('payment.payment_method_card'),
333+
ref('payment.payment_method_emi_india'),
334+
ref('payment.payment_method_fpx'),
333335
ref('payment.payment_method_netbanking'),
334-
ref('payment.payment_method_upi'),
335-
ref('payment.payment_method_wallets_india'),
336336
ref('payment.payment_method_paylater_india'),
337-
ref('payment.payment_method_emi_india'),
338337
ref('payment.payment_method_paynow'),
338+
ref('payment.payment_method_upi'),
339+
ref('payment.payment_method_wallets_india'),
339340
])]"
340341
/>
341342
</record>

addons/payment_razorpay/const.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,15 @@
115115

116116
# The codes of payment methods that are not recognized by the orders API.
117117
FALLBACK_PAYMENT_METHOD_CODES = {
118-
'wallets_india',
119-
'paylater_india',
120118
'emi_india',
119+
'fpx',
120+
'paylater_india',
121+
'wallets_india',
122+
}
123+
124+
# The codes of payment methods that require redirection back to the website
125+
REDIRECT_PAYMENT_METHOD_CODES = {
126+
'fpx',
121127
}
122128

123129
# Mapping of payment method codes to Razorpay codes.

addons/payment_razorpay/controllers/main.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,47 @@
1717

1818

1919
class RazorpayController(http.Controller):
20+
_return_url = '/payment/razorpay/return'
2021
_webhook_url = '/payment/razorpay/webhook'
2122

23+
@http.route(
24+
_return_url,
25+
type='http',
26+
auth='public',
27+
methods=['POST'],
28+
csrf=False,
29+
save_session=False,
30+
)
31+
def razorpay_return_from_checkout(self, reference, **data):
32+
""" Process the notification data sent by Razorpay after redirection from checkout.
33+
34+
The route is configured with save_session=False to prevent Odoo from creating a new session
35+
when the user is redirected here via a POST request. Indeed, as the session cookie is
36+
created without a `SameSite` attribute, some browsers that don't implement the recommended
37+
default `SameSite=Lax` behavior will not include the cookie in the redirection request from
38+
the payment provider to Odoo. However, the redirection to the /payment/status page will
39+
satisfy any specification of the `SameSite` attribute, the session of the user will be
40+
retrieved and with it the transaction which will be immediately post-processed.
41+
42+
:param str reference: The transaction reference embedded in the return URL.
43+
:param dict data: The notification data.
44+
"""
45+
_logger.info("Handling redirection from Razorpay with data:\n%s", pprint.pformat(data))
46+
if all(f'razorpay_{key}' in data for key in ('order_id', 'payment_id', 'signature')):
47+
# Check the integrity of the notification.
48+
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data(
49+
'razorpay', {'description': reference}
50+
) # Use the same key as for webhook notifications' data.
51+
self._verify_notification_signature(data, data.get('razorpay_signature'), tx_sudo)
52+
53+
# Handle the notification data.
54+
tx_sudo._handle_notification_data('razorpay', data)
55+
else: # The customer cancelled the payment or the payment failed.
56+
pass # Don't try to process this case because the payment id was not provided.
57+
58+
# Redirect the user to the status page.
59+
return request.redirect('/payment/status')
60+
2261
@http.route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False)
2362
def razorpay_webhook(self):
2463
""" Process the notification data sent by Razorpay to the webhook.
@@ -42,7 +81,7 @@ def razorpay_webhook(self):
4281
'razorpay', entity_data
4382
)
4483
self._verify_notification_signature(
45-
request.httprequest.data, received_signature, tx_sudo
84+
request.httprequest.data, received_signature, tx_sudo, is_redirect=False
4685
)
4786

4887
# Handle the notification data.
@@ -52,13 +91,17 @@ def razorpay_webhook(self):
5291
return request.make_json_response('')
5392

5493
@staticmethod
55-
def _verify_notification_signature(notification_data, received_signature, tx_sudo):
94+
def _verify_notification_signature(
95+
notification_data, received_signature, tx_sudo, is_redirect=True
96+
):
5697
""" Check that the received signature matches the expected one.
5798
5899
:param dict|bytes notification_data: The notification data.
59100
:param str received_signature: The signature to compare with the expected signature.
60101
:param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a
61102
`payment.transaction` record
103+
:param bool is_redirect: Whether the notification data should be treated as redirect data
104+
or as coming from a webhook notification.
62105
:return: None
63106
:raise :class:`werkzeug.exceptions.Forbidden`: If the signatures don't match.
64107
"""
@@ -68,7 +111,9 @@ def _verify_notification_signature(notification_data, received_signature, tx_sud
68111
raise Forbidden()
69112

70113
# Compare the received signature with the expected signature.
71-
expected_signature = tx_sudo.provider_id._razorpay_calculate_signature(notification_data)
114+
expected_signature = tx_sudo.provider_id._razorpay_calculate_signature(
115+
notification_data, is_redirect=is_redirect
116+
)
72117
if (
73118
expected_signature is None
74119
or not hmac.compare_digest(received_signature, expected_signature)

addons/payment_razorpay/models/payment_provider.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,29 @@ def _razorpay_make_request(self, endpoint, payload=None, method='POST', api_vers
214214
)
215215
return response.json()
216216

217-
def _razorpay_calculate_signature(self, data):
217+
def _razorpay_calculate_signature(self, data, is_redirect=True):
218218
""" Compute the signature for the request's data according to the Razorpay documentation.
219219
220220
See https://razorpay.com/docs/webhooks/validate-test#validate-webhooks.
221221
222222
:param bytes data: The data to sign.
223+
:param bool is_redirect: Whether the data should be treated as redirect data or as coming
224+
from a webhook notification.
223225
:return: The calculated signature.
224226
:rtype: str
225227
"""
226-
secret = self.razorpay_webhook_secret
227-
if not secret:
228-
_logger.warning("Missing webhook secret; aborting signature calculation.")
229-
return None
230-
return hmac.new(secret.encode(), msg=data, digestmod=hashlib.sha256).hexdigest()
228+
if is_redirect:
229+
secret = self.razorpay_key_secret
230+
signing_string = f'{data["razorpay_order_id"]}|{data["razorpay_payment_id"]}'
231+
return hmac.new(
232+
secret.encode(), msg=signing_string.encode(), digestmod=hashlib.sha256
233+
).hexdigest()
234+
else: # Notification data.
235+
secret = self.razorpay_webhook_secret
236+
if not secret:
237+
_logger.warning("Missing webhook secret; aborting signature calculation.")
238+
return None
239+
return hmac.new(secret.encode(), msg=data, digestmod=hashlib.sha256).hexdigest()
231240

232241
def _get_default_payment_method_codes(self):
233242
""" Override of `payment` to return the default payment method codes. """

addons/payment_razorpay/models/payment_transaction.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from datetime import datetime
77

88
from dateutil.relativedelta import relativedelta
9+
from werkzeug.urls import url_encode, url_join
910

1011
from odoo import _, api, models
1112
from odoo.exceptions import UserError, ValidationError
1213

1314
from odoo.addons.payment import utils as payment_utils
1415
from odoo.addons.payment_razorpay import const
16+
from odoo.addons.payment_razorpay.controllers.main import RazorpayController
1517

1618

1719
_logger = logging.getLogger(__name__)
@@ -45,6 +47,11 @@ def _get_specific_processing_values(self, processing_values):
4547
'razorpay_customer_id': customer_id,
4648
'is_tokenize_request': self.tokenize,
4749
'razorpay_order_id': order_id,
50+
'callback_url': url_join(
51+
self.provider_id.get_base_url(),
52+
f'{RazorpayController._return_url}?{url_encode({"reference": self.reference})}'
53+
),
54+
'redirect': self.payment_method_id.code in const.REDIRECT_PAYMENT_METHOD_CODES,
4855
}
4956

5057
def _razorpay_create_customer(self):

0 commit comments

Comments
 (0)