diff --git a/payments/payment_gateways/doctype/paytm_settings/__init__.py b/payments/payment_gateways/doctype/paytm_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js new file mode 100644 index 0000000..fe2ee7c --- /dev/null +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Paytm Settings', { + refresh: function(frm) { + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); + } +}); diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.json b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.json new file mode 100644 index 0000000..93fbd0d --- /dev/null +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "creation": "2020-04-02 00:11:22.846697", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "merchant_id", + "merchant_key", + "staging", + "column_break_4", + "industry_type_id", + "website" + ], + "fields": [ + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Merchant ID", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "merchant_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Merchant Key", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "fieldname": "staging", + "fieldtype": "Check", + "label": "Staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "depends_on": "eval: !doc.staging", + "fieldname": "industry_type_id", + "fieldtype": "Data", + "label": "Industry Type ID", + "mandatory_depends_on": "eval: !doc.staging", + "show_days": 1, + "show_seconds": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-06-08 13:36:09.703143", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Paytm Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py new file mode 100644 index 0000000..616c383 --- /dev/null +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import requests +from six.moves.urllib.parse import urlencode + +import frappe +from frappe.model.document import Document +from frappe import _ +from frappe.utils import get_url, call_hook_method, cint, flt, cstr +from frappe.integrations.utils import create_request_log, create_payment_gateway +from frappe.utils import get_request_site_address +from paytmchecksum import generateSignature, verifySignature +from frappe.utils.password import get_decrypted_password + +class PaytmSettings(Document): + supported_currencies = ["INR"] + + def validate(self): + create_payment_gateway('Paytm') + call_hook_method('payment_gateway_enabled', gateway='Paytm') + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw(_("Please select another payment method. Paytm does not support transactions in currency '{0}'").format(currency)) + + def get_payment_url(self, **kwargs): + '''Return payment url with several params''' + # create unique order id by making it equal to the integration request + integration_request = create_request_log(kwargs, "Host", "Paytm") + kwargs.update(dict(order_id=integration_request.name)) + + return get_url("./integrations/paytm_checkout?{0}".format(urlencode(kwargs))) + +def get_paytm_config(): + ''' Returns paytm config ''' + + paytm_config = frappe.db.get_singles_dict('Paytm Settings') + paytm_config.update(dict(merchant_key=get_decrypted_password('Paytm Settings', 'Paytm Settings', 'merchant_key'))) + + if cint(paytm_config.staging): + paytm_config.update(dict( + website="WEBSTAGING", + url='https://securegw-stage.paytm.in/order/process', + transaction_status_url='https://securegw-stage.paytm.in/order/status', + industry_type_id='RETAIL' + )) + else: + paytm_config.update(dict( + url='https://securegw.paytm.in/order/process', + transaction_status_url='https://securegw.paytm.in/order/status', + )) + return paytm_config + +def get_paytm_params(payment_details, order_id, paytm_config): + + # initialize a dictionary + paytm_params = dict() + + redirect_uri = get_request_site_address(True) + "/api/method/frappe.integrations.doctype.paytm_settings.paytm_settings.verify_transaction" + + + paytm_params.update({ + "MID" : paytm_config.merchant_id, + "WEBSITE" : paytm_config.website, + "INDUSTRY_TYPE_ID" : paytm_config.industry_type_id, + "CHANNEL_ID" : "WEB", + "ORDER_ID" : order_id, + "CUST_ID" : payment_details['payer_email'], + "EMAIL" : payment_details['payer_email'], + "TXN_AMOUNT" : cstr(flt(payment_details['amount'], 2)), + "CALLBACK_URL" : redirect_uri, + }) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + + paytm_params.update({ + "CHECKSUMHASH" : checksum + }) + + return paytm_params + +@frappe.whitelist(allow_guest=True) +def verify_transaction(**paytm_params): + '''Verify checksum for received data in the callback and then verify the transaction''' + paytm_config = get_paytm_config() + is_valid_checksum = False + + paytm_params.pop('cmd', None) + paytm_checksum = paytm_params.pop('CHECKSUMHASH', None) + + if paytm_params and paytm_config and paytm_checksum: + # Verify checksum + is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) + + if is_valid_checksum and paytm_params.get('RESPCODE') == '01': + verify_transaction_status(paytm_config, paytm_params['ORDERID']) + else: + frappe.respond_as_web_page("Payment Failed", + "Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.", + http_status_code=401, indicator_color='red') + frappe.log_error("Order unsuccessful. Failed Response:"+cstr(paytm_params), 'Paytm Payment Failed') + +def verify_transaction_status(paytm_config, order_id): + '''Verify transaction completion after checksum has been verified''' + paytm_params=dict( + MID=paytm_config.merchant_id, + ORDERID= order_id + ) + + checksum = generateSignature(paytm_params, paytm_config.merchant_key) + paytm_params["CHECKSUMHASH"] = checksum + + post_data = json.dumps(paytm_params) + url = paytm_config.transaction_status_url + + response = requests.post(url, data = post_data, headers = {"Content-type": "application/json"}).json() + finalize_request(order_id, response) + +def finalize_request(order_id, transaction_response): + request = frappe.get_doc('Integration Request', order_id) + transaction_data = frappe._dict(json.loads(request.data)) + redirect_to = transaction_data.get('redirect_to') or None + redirect_message = transaction_data.get('redirect_message') or None + + if transaction_response['STATUS'] == "TXN_SUCCESS": + if transaction_data.reference_doctype and transaction_data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc(transaction_data.reference_doctype, + transaction_data.reference_docname).run_method("on_payment_authorized", 'Completed') + request.db_set('status', 'Completed') + except Exception: + request.db_set('status', 'Failed') + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = '/integrations/payment-success' + else: + request.db_set('status', 'Failed') + redirect_url = '/integrations/payment-failed' + + if redirect_to: + redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect_url + +def get_gateway_controller(doctype, docname): + reference_doc = frappe.get_doc(doctype, docname) + gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") + return gateway_controller \ No newline at end of file diff --git a/payments/payment_gateways/doctype/paytm_settings/test_paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/test_paytm_settings.py new file mode 100644 index 0000000..77a16c8 --- /dev/null +++ b/payments/payment_gateways/doctype/paytm_settings/test_paytm_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPaytmSettings(unittest.TestCase): + pass diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 5e464d4..af7686c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -64,6 +64,9 @@ from __future__ import unicode_literals import frappe from frappe import _ import json +import hmac +import razorpay +import hashlib from six.moves.urllib.parse import urlencode from frappe.model.document import Document from frappe.utils import get_url, call_hook_method, cint, get_timestamp @@ -73,6 +76,11 @@ from frappe.integrations.utils import (make_get_request, make_post_request, crea class RazorpaySettings(Document): supported_currencies = ["INR"] + def init_client(self): + if self.api_key: + secret = self.get_password(fieldname="api_secret", raise_exception=False) + self.client = razorpay.Client(auth=(self.api_key, secret)) + def validate(self): create_payment_gateway('Razorpay') call_hook_method('payment_gateway_enabled', gateway='Razorpay') @@ -317,6 +325,20 @@ class RazorpaySettings(Document): except Exception: frappe.log_error(frappe.get_traceback()) + def verify_signature(self, body, signature, key): + key = bytes(key, 'utf-8') + body = bytes(body, 'utf-8') + + dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) + + generated_signature = dig.hexdigest() + result = hmac.compare_digest(generated_signature, signature) + + if not result: + frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError) + + return result + def capture_payment(is_sandbox=False, sanbox_response=None): """ Verifies the purchase as complete by the merchant. diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html index 1ce9ba5..4138644 100644 --- a/payments/templates/pages/gcalendar-success.html +++ b/payments/templates/pages/gcalendar-success.html @@ -9,7 +9,7 @@ {{ _("Success") }}
{{ _("Your connection request to Google Calendar was successfully accepted") }}
- +{% endblock %} \ No newline at end of file diff --git a/payments/templates/pages/paytm_checkout.py b/payments/templates/pages/paytm_checkout.py new file mode 100644 index 0000000..bc385b5 --- /dev/null +++ b/payments/templates/pages/paytm_checkout.py @@ -0,0 +1,27 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +import frappe +from frappe import _ +import json +from frappe.integrations.doctype.paytm_settings.paytm_settings import get_paytm_params, get_paytm_config + +def get_context(context): + context.no_cache = 1 + paytm_config = get_paytm_config() + + try: + doc = frappe.get_doc("Integration Request", frappe.form_dict['order_id']) + + context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) + + context.url = paytm_config.url + + except Exception: + frappe.log_error() + frappe.redirect_to_message(_('Invalid Token'), + _('Seems token you are using is invalid!'), + http_status_code=400, indicator_color='red') + + frappe.local.flags.redirect_location = frappe.local.response.location + raise frappe.Redirect \ No newline at end of file