@@ -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 | |||
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. | |||
@@ -9,7 +9,7 @@ | |||
{{ _("Success") }}</span> | |||
</div> | |||
<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> | |||
</div> | |||
<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 |