@@ -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}.", [`<a href='https://erpnext.com/docs/user/manual/en/erpnext_integration/paytm-integration'>${__('Click here')}</a>`])); | |||||
} | |||||
}); |
@@ -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 | |||||
} |
@@ -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 |
@@ -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 |
@@ -64,6 +64,9 @@ from __future__ import unicode_literals | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
import json | import json | ||||
import hmac | |||||
import razorpay | |||||
import hashlib | |||||
from six.moves.urllib.parse import urlencode | from six.moves.urllib.parse import urlencode | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import get_url, call_hook_method, cint, get_timestamp | 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): | class RazorpaySettings(Document): | ||||
supported_currencies = ["INR"] | 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): | def validate(self): | ||||
create_payment_gateway('Razorpay') | create_payment_gateway('Razorpay') | ||||
call_hook_method('payment_gateway_enabled', gateway='Razorpay') | call_hook_method('payment_gateway_enabled', gateway='Razorpay') | ||||
@@ -317,6 +325,20 @@ class RazorpaySettings(Document): | |||||
except Exception: | except Exception: | ||||
frappe.log_error(frappe.get_traceback()) | 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): | def capture_payment(is_sandbox=False, sanbox_response=None): | ||||
""" | """ | ||||
Verifies the purchase as complete by the merchant. | Verifies the purchase as complete by the merchant. | ||||
@@ -9,7 +9,7 @@ | |||||
{{ _("Success") }}</span> | {{ _("Success") }}</span> | ||||
</div> | </div> | ||||
<p>{{ _("Your connection request to Google Calendar was successfully accepted") }}</p> | <p>{{ _("Your connection request to Google Calendar was successfully accepted") }}</p> | ||||
<div><a href='{{ "/desk" }}' class='btn btn-primary btn-sm'> | |||||
<div><a href='{{ "/app" }}' class='btn btn-primary btn-sm'> | |||||
{{ _("Back to Desk") }}</a></div> | {{ _("Back to Desk") }}</a></div> | ||||
</div> | </div> | ||||
<style> | <style> | ||||
@@ -0,0 +1,43 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%} | |||||
<head> | |||||
<title>Merchant Checkout Page</title> | |||||
</head> | |||||
{% endblock %} | |||||
{% block script %} | |||||
<script defer type="text/javascript"> | |||||
document.paytm_form.submit(); | |||||
</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<body> | |||||
<div class="centered"> | |||||
<h2>Please do not refresh this page...</h2> | |||||
<form method="post" action="{{ url }}" name="paytm_form"> | |||||
{% for name, value in payment_details.items() %} | |||||
<input type="hidden" name="{{ name }}" value="{{ value }}"> | |||||
{% endfor %} | |||||
</form> | |||||
</div> | |||||
</body> | |||||
{% endblock %} | |||||
{% block style %} | |||||
<style> | |||||
.centered { | |||||
position: absolute; | |||||
top: 50%; | |||||
left: 50%; | |||||
transform: translate(-50%, -50%); | |||||
} | |||||
.web-footer { | |||||
display: none; | |||||
} | |||||
</style> | |||||
{% endblock %} |
@@ -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 |