diff --git a/payments/hooks.py b/payments/hooks.py index caa07d27..54c80052 100644 --- a/payments/hooks.py +++ b/payments/hooks.py @@ -13,6 +13,10 @@ # include js, css files in header of desk.html # app_include_css = "/assets/pay/css/pay.css" # app_include_js = "/assets/pay/js/pay.js" +app_include_js = [ + # ...existing entries... + "payments/public/js/ccavenue_session_handler.js" +] # include js, css files in header of web template # web_include_css = "/assets/pay/css/pay.css" @@ -107,6 +111,14 @@ # "on_trash": "method" # } # } +doc_events = { + "Payment Request": { + "on_payment_authorized":"payments.utils.ivyliving_methods.handle_payment_authorization_payment_request" + }, + "Customer":{ + "on_payment_authorized":"payments.utils.ivyliving_methods.handle_payment_authorization_customer" + } +} # Scheduled Tasks # --------------- diff --git a/payments/payment_gateways/doctype/ccavenue_merchant/__init__.py b/payments/payment_gateways/doctype/ccavenue_merchant/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.js b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.js new file mode 100644 index 00000000..aa84fae8 --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("CCAvenue Merchant", { +// refresh(frm) { + +// }, +// }); diff --git a/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.json b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.json new file mode 100644 index 00000000..0af4ae0f --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.json @@ -0,0 +1,107 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:{merchant_name}", + "creation": "2025-04-14 14:53:53.832399", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "merchant_name", + "merchant_id", + "access_code", + "encryption_key", + "enviroment", + "column_break_itky", + "bank_account", + "debtors_account", + "company" + ], + "fields": [ + { + "fieldname": "merchant_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Merchant Name", + "reqd": 1 + }, + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Merchant ID", + "reqd": 1 + }, + { + "fieldname": "access_code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Access Code", + "reqd": 1 + }, + { + "fieldname": "encryption_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Encryption Key", + "reqd": 1 + }, + { + "fieldname": "enviroment", + "fieldtype": "Select", + "label": "Enviroment", + "options": "Sandbox\nProduction", + "reqd": 1 + }, + { + "description": "If empty it will select the Debters account of the company from student records. Give only prefix, the company will be fetched from the company field.", + "fieldname": "debtors_account", + "fieldtype": "Data", + "label": "Debtors Account" + }, + { + "description": "If empty it will select the CCAvenue account of the company from student records. Give only prefix, the company will be fetched from the company field.", + "fieldname": "bank_account", + "fieldtype": "Data", + "label": "Bank Account" + }, + { + "description": "If empty it will select the default students company from the student records.", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "column_break_itky", + "fieldtype": "Column Break" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-04-14 15:22:03.307321", + "modified_by": "Administrator", + "module": "Payment Gateways", + "name": "CCAvenue Merchant", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "merchant_name" +} \ No newline at end of file diff --git a/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.py b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.py new file mode 100644 index 00000000..1cf01847 --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_merchant/ccavenue_merchant.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class CCAvenueMerchant(Document): + pass diff --git a/payments/payment_gateways/doctype/ccavenue_merchant/test_ccavenue_merchant.py b/payments/payment_gateways/doctype/ccavenue_merchant/test_ccavenue_merchant.py new file mode 100644 index 00000000..b16328b7 --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_merchant/test_ccavenue_merchant.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCCAvenueMerchant(FrappeTestCase): + pass diff --git a/payments/payment_gateways/doctype/ccavenue_settings/__init__.py b/payments/payment_gateways/doctype/ccavenue_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.js b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.js new file mode 100644 index 00000000..8a01afbc --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.js @@ -0,0 +1,35 @@ +// Copyright (c) 2024, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("CCAvenue Settings", { + refresh: function (frm) { + frm.add_custom_button(__("Clear"), function () { + frm.call({ + doc: frm.doc, + method: "clear", + callback: function (r) { + frm.refresh(); + }, + }); + }); + + frm.add_custom_button(__("Test Connection"), function () { + frappe.call({ + method: "payments.payment_gateways.doctype.ccavenue_settings.ccavenue_utils.test_connection", + args: { + merchant_id: frm.doc.merchant_id, + access_code: frm.doc.access_code, + encryption_key: frm.doc.encryption_key, + environment: frm.doc.environment + }, + callback: function (r) { + if (r.message && r.message.success) { + frappe.msgprint(__('Connection Successful')); + } else { + frappe.msgprint(__('Connection Failed: ') + (r.message ? r.message.error : "Unknown error")); + } + } + }); + }); + }, + }); \ No newline at end of file diff --git a/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.json b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.json new file mode 100644 index 00000000..f6aeafa1 --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.json @@ -0,0 +1,94 @@ +{ + "actions": [], + "creation": "2024-06-04 00:00:00.000000", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "credentials_section", + "merchant_id", + "access_code", + "encryption_key", + "environment", + "integration_settings_section", + "redirect_to", + "api_header_section", + "header_img" + ], + "fields": [ + { + "fieldname": "credentials_section", + "fieldtype": "Section Break", + "label": "Credentials" + }, + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "label": "Merchant ID", + "reqd": 1 + }, + { + "fieldname": "access_code", + "fieldtype": "Data", + "label": "Access Code", + "reqd": 1 + }, + { + "fieldname": "encryption_key", + "fieldtype": "Password", + "label": "Encryption Key", + "reqd": 1 + }, + { + "default": "Sandbox", + "fieldname": "environment", + "fieldtype": "Select", + "label": "Environment", + "options": "Sandbox\nProduction", + "reqd": 1 + }, + { + "fieldname": "integration_settings_section", + "fieldtype": "Section Break", + "label": "Integration Settings" + }, + { + "description": "Mention transaction completion page URL", + "fieldname": "redirect_to", + "fieldtype": "Data", + "label": "Redirect To" + }, + { + "fieldname": "api_header_section", + "fieldtype": "Section Break", + "label": "API Header" + }, + { + "fieldname": "header_img", + "fieldtype": "Attach Image", + "label": "Header Image" + } + ], + "issingle": 1, + "links": [], + "modified": "2024-06-04 00:00:00.000000", + "modified_by": "Administrator", + "module": "Payment Gateways", + "name": "CCAvenue 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/ccavenue_settings/ccavenue_settings.py b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.py new file mode 100644 index 00000000..9e62bcce --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_settings.py @@ -0,0 +1,463 @@ +# Copyright (c) 2024, Frappe Technologies and contributors +# License: MIT. See LICENSE + +""" +# Integrating CCAvenue + +### 1. Validate Currency Support + +Example: + + from payments.utils import get_payment_gateway_controller + + controller = get_payment_gateway_controller("CCAvenue") + controller().validate_transaction_currency(currency) + +### 2. Redirect for payment + +Example: + + payment_details = { + "amount": 600, + "title": "Payment for bill : 111", + "description": "payment via cart", + "reference_doctype": "Payment Request", + "reference_docname": "PR0001", + "payer_email": "NuranVerkleij@example.com", + "payer_name": "Nuran Verkleij", + "order_id": "111", + "currency": "INR", + "payment_gateway": "CCAvenue" + } + + # Redirect the user to this url + url = controller().get_payment_url(**payment_details) + + +### 3. On Completion of Payment + +Write a method for `on_payment_authorized` in the reference doctype + +Example: + + def on_payment_authorized(payment_status): + # this method will be called when payment is complete + + +##### Notes: + +payment_status - payment gateway will put payment status on callback. +For CCAvenue payment status is Completed +""" + +import json +import urllib +from urllib.parse import urlencode, quote_plus + +import frappe +from frappe import _ +from frappe.integrations.utils import create_request_log +from frappe.model.document import Document +from frappe.utils import call_hook_method, get_url, random_string + +from payments.payment_gateways.doctype.ccavenue_settings.ccavenue_utils import decrypt, encrypt +from payments.utils import create_payment_gateway + + +class CCAvenueSettings(Document): + supported_currencies = ("INR", "USD", "SGD", "GBP", "EUR") + + def validate(self): + create_payment_gateway("CCAvenue") + call_hook_method("payment_gateway_enabled", gateway="CCAvenue") + if not self.flags.ignore_mandatory: + self.validate_ccavenue_credentials() + + def validate_ccavenue_credentials(self): + if self.merchant_id and self.access_code and self.encryption_key: + # We can't validate CCAvenue credentials without making an actual API call + pass + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw( + _( + "Please select another payment method. CCAvenue 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, service_name="CCAvenue") + kwargs.update(dict(order_id=integration_request.name)) + + return get_url(f"./ccavenue_checkout?token={integration_request.name}") + + def create_request(self, data): + """Create a CCAvenue request""" + self.data = frappe._dict(data) + + try: + self.integration_request = frappe.get_doc("Integration Request", self.data.token) + self.integration_request.update_status(self.data, "Queued") + return self.authorize_payment() + + except Exception: + frappe.log_error(frappe.get_traceback()) + return { + "redirect_to": frappe.redirect_to_message( + _("Server Error"), + _( + "Seems issue with server's CCAvenue configuration. Don't worry, in case of failure amount will get refunded to your account." + ), + ), + "status": 401, + } + + def authorize_payment(self): + """ + Authorize payment when user submits the form on CCAvenue page + """ + data = self.data + + # Get payment status from data + if data.get("order_status") == "Success": + status = "Completed" + self.integration_request.update_status(data, "Completed") + self.flags.status_changed_to = "Completed" + else: + status = "Failed" + self.integration_request.update_status(data, "Failed") + + redirect_to = data.get("redirect_to") or None + redirect_message = data.get("redirect_message") or None + + if self.flags.status_changed_to == "Completed": + if self.data.reference_doctype and self.data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc( + self.data.reference_doctype, self.data.reference_docname + ).run_method("on_payment_authorized", self.flags.status_changed_to) + except Exception: + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = ( + f"payment-success?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}" + ) + else: + redirect_url = "payment-failed" + + if redirect_to: + redirect_url += "&" + urlencode({"redirect_to": redirect_to}) + if redirect_message: + redirect_url += "&" + urlencode({"redirect_message": redirect_message}) + + return {"redirect_to": redirect_url, "status": status} + + def create_encrypted_request_data(self, integration_request_name, **kwargs): + """Create encrypted data for CCAvenue request""" + # Format the data as required by CCAvenue + token = kwargs.get('order_id') + "@" + integration_request_name + merchant_name = kwargs.get('custom_merchant_name') + if merchant_name: + merchant_doc = frappe.get_doc("CCAvenue Merchant", merchant_name) + merchant_dict = merchant_doc.as_dict() + merchant_data = { + 'merchant_id': merchant_dict.get('merchant_id'), + 'order_id': token, + 'currency': kwargs.get('currency', 'INR'), + 'amount': str(kwargs.get('amount')), + 'redirect_url': get_url( + f"/api/method/payments.payment_gateways.doctype.ccavenue_settings.ccavenue_settings.verify_transaction?merchant={merchant_name}"), + 'cancel_url': get_url( + f"/api/method/payments.payment_gateways.doctype.ccavenue_settings.ccavenue_settings.verify_transaction?merchant={merchant_name}"), + 'language': 'EN', + 'integration_type': 'iframe_normal', + "merchant_param1": json.dumps({ + "reference_doctype": kwargs.get("reference_doctype"), + "reference_docname": kwargs.get("reference_docname"), + "token": token, + "user": frappe.session.user # Add the current user + }), + 'customer_identifier': kwargs.get('payer_email', '') + } + + merchant_data_string = '&'.join([ + f"{key}={value}" for key, value in merchant_data.items() + ]) + merchant_data_string = merchant_data_string + '&' + + # Encrypt the data using CCAvenue's encryption method + encrypted_data = encrypt(merchant_data_string, + merchant_doc.get_password(fieldname="encryption_key", raise_exception=False)) + + return { + "encRequest": encrypted_data, + "access_code": merchant_dict.get('access_code'), + "merchant_id": merchant_dict.get('merchant_id'), + "non_encrypted_data": merchant_data_string + } + merchant_data = { + 'merchant_id': self.merchant_id, + 'order_id': token, + 'currency': kwargs.get('currency', 'INR'), + 'amount': str(kwargs.get('amount')), + 'redirect_url': get_url( + "/api/method/payments.payment_gateways.doctype.ccavenue_settings.ccavenue_settings.verify_transaction"), + 'cancel_url': get_url( + "/api/method/payments.payment_gateways.doctype.ccavenue_settings.ccavenue_settings.verify_transaction"), + 'language': 'EN', + 'integration_type': 'iframe_normal', + "merchant_param1": json.dumps({ + "reference_doctype": kwargs.get("reference_doctype"), + "reference_docname": kwargs.get("reference_docname"), + "token": token, + "user": frappe.session.user # Add the current user + }), + 'customer_identifier': kwargs.get('payer_email', '') + } + + # Create the merchant data string exactly as CCAvenue expects + merchant_data_string = '&'.join([ + f"{key}={value}" for key, value in merchant_data.items() + ]) + merchant_data_string = merchant_data_string + '&' + + # Encrypt the data using CCAvenue's encryption method + encrypted_data = encrypt(merchant_data_string, + self.get_password(fieldname="encryption_key", raise_exception=False)) + + return { + "encRequest": encrypted_data, + "access_code": self.access_code, + "merchant_id": self.merchant_id, + "non_encrypted_data": merchant_data_string + } + + def get_api_url(self): + """Get the CCAvenue API URL based on environment""" + if self.environment == "Production": + return "https://secure.ccavenue.com/transaction/transaction.do?command=initiateTransaction" + else: + return "https://test.ccavenue.com/transaction/transaction.do?command=initiateTransaction" + + @frappe.whitelist() + def clear(self): + """Clear all CCAvenue settings""" + self.merchant_id = self.access_code = None + self.encryption_key = None + self.header_img = None + self.flags.ignore_mandatory = True + self.save() + + +@frappe.whitelist(allow_guest=True) +def verify_transaction(): + """Handle CCAvenue's return request after payment""" + try: + # Get the encrypted response from CCAvenue + encResp = frappe.request.form.get("encResp") + merchant_name = None + if frappe.request.query_string.decode("utf-8") != '': + merchant_name_encoded = frappe.request.query_string.decode('utf-8').split('=')[1] + merchant_name = urllib.parse.unquote(merchant_name_encoded) + if not encResp: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url("payment-failed") + return + + # Get CCAvenue settings + settings = frappe.get_doc("CCAvenue Settings") + + # Decrypt the response + if merchant_name: + merchat_doc = frappe.get_doc("CCAvenue Merchant", merchant_name) + decrypted_data = decrypt(encResp, merchat_doc.get_password(fieldname="encryption_key", raise_exception=False)) + else: + decrypted_data = decrypt(encResp, settings.get_password(fieldname="encryption_key", raise_exception=False)) + + # Parse the decrypted data (URL encoded key-value pairs) + response_data = {} + for param in decrypted_data.split('&'): + if param and '=' in param: + key, value = param.split('=', 1) + response_data[key] = value + + # Extract merchant_param1 and parse the JSON + merchant_param_str = response_data.get("merchant_param1", "") + merchant_data = {} + + try: + # Properly parse the JSON data + if merchant_param_str: + parts = merchant_param_str.split(", ") + for part in parts: + if ":" in part: + k, v = part.split(":", 1) + merchant_data[k.strip()] = v.strip() + elif " " in part: + k, v = part.split(" ", 1) + merchant_data[k.strip()] = v.strip() + # CRITICAL: Set the session user from merchant_data if available + user = merchant_data.get("user") + if user and user != "Guest" and frappe.session.user == "Guest": + frappe.set_user(user) + + # Create a new session for the user + frappe.local.login_manager.login_as(user) + + # Log that we've restored the user + frappe.logger().info(f"CCAvenue: Restored user session for {user}") + + except Exception as e: + frappe.log_error(f"Error parsing merchant data: {str(e)}") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url("payment-failed") + return + + # For security, log the current user + frappe.logger().info(f"CCAvenue callback - Current user: {frappe.session.user}") + + # Get the integration request + order_id = merchant_data.get("token") + integration_request = None + + if order_id: + integration_request = frappe.get_doc("Integration Request", order_id.split('@')[1]) + + if not integration_request: + frappe.log_error(f"Integration request not found for token: {order_id}", "CCAvenue Payment Error") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url("payment-failed") + return + + # Update the data with the response from CCAvenue + data = json.loads(integration_request.data) + + # Save the user in the integration request data for future reference + data["user"] = user or frappe.session.user + + data.update({ + "order_status": response_data.get("order_status"), + "tracking_id": response_data.get("tracking_id"), + "bank_ref_no": response_data.get("bank_ref_no"), + "payment_mode": response_data.get("payment_mode"), + "failure_message": response_data.get("failure_message"), + "ccavenue_response": response_data + }) + + # Create a new controller instance + controller = frappe.get_doc("CCAvenue Settings") + controller.data = frappe._dict(data) + controller.integration_request = integration_request + + # Update the integration request with the updated data + integration_request.data = json.dumps(data) + integration_request.save() + + # Set status based on order_status + if response_data.get("order_status") == "Success": + controller.flags.status_changed_to = "Completed" + + # Call authorize_payment to complete the flow + result = controller.authorize_payment() + + # Preserve cookies in the redirect + redirect_location = get_url(result["redirect_to"]) + + # Make sure to authenticate user via session cookie + if user and user != "Guest": + # Set session cookies explicitly + frappe.local.cookie_manager.set_cookie("system_user", user) + frappe.local.cookie_manager.set_cookie("full_name", frappe.db.get_value("User", user, "full_name") or "") + frappe.local.cookie_manager.set_cookie("user_id", user) + frappe.local.cookie_manager.set_cookie("sid", frappe.session.sid) + + # Set the cookies in response + frappe.local.response["set_cookie"] = frappe.local.cookie_manager.cookies + + # Set the redirect with cookie preservation + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = redirect_location + + except Exception as e: + frappe.log_error(f"{str(e)}\n{frappe.get_traceback()}", "CCAvenue Payment Verification Error") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url("payment-failed") + + +@frappe.whitelist(allow_guest=True) +def get_api_key(): + """Get CCAvenue API key (access code)""" + return frappe.db.get_single_value("CCAvenue Settings", "access_code") + + +@frappe.whitelist(allow_guest=True) +def restore_user_session(): + """Attempt to restore user session from stored payment data""" + try: + # Check if we can get the user from URL params + reference_doctype = frappe.form_dict.get("reference_doctype") + reference_docname = frappe.form_dict.get("reference_docname") + + if reference_doctype and reference_docname: + # Find the most recent integration request for this reference + integration_requests = frappe.get_all("Integration Request", + filters={ + "reference_doctype": reference_doctype, + "reference_docname": reference_docname, + "status": ["in", ["Completed", "Authorized"]] + }, + order_by="creation desc", + limit=1) + + if integration_requests: + integration_request = frappe.get_doc("Integration Request", integration_requests[0].name) + data = json.loads(integration_request.data) + + # Try to extract user from data + user = None + + # First try to get from integration request data + if data.get("user"): + user = data.get("user") + + # Next try merchant_param1 in data + if not user: + merchant_data = {} + try: + if data.get("merchant_param1"): + merchant_data = json.loads(data["merchant_param1"]) + except: + pass + + user = merchant_data.get("user") + + if user and user != "Guest": + # Create a new session for the user + frappe.set_user(user) + + # Create a session using login_manager + frappe.local.login_manager.login_as(user) + + # Set session cookies + frappe.local.cookie_manager.set_cookie("system_user", user) + frappe.local.cookie_manager.set_cookie("full_name", + frappe.db.get_value("User", user, "full_name") or "") + frappe.local.cookie_manager.set_cookie("user_id", user) + frappe.local.cookie_manager.set_cookie("sid", frappe.session.sid) + + # Set cookies in response + frappe.local.response["set_cookie"] = frappe.local.cookie_manager.cookies + + return {"success": True, "user": user} + + return {"success": False} + except Exception as e: + frappe.log_error(f"Session restoration error: {str(e)}\n{frappe.get_traceback()}") + return {"success": False, "error": str(e)} diff --git a/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_utils.py b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_utils.py new file mode 100644 index 00000000..3a096cb8 --- /dev/null +++ b/payments/payment_gateways/doctype/ccavenue_settings/ccavenue_utils.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024, Frappe Technologies and contributors +# License: MIT. See LICENSE + +import hashlib +from Crypto.Cipher import AES + +import frappe +from frappe import _ + +def pad(data): + length = 16 - (len(data) % 16) + return data + (chr(length) * length).encode('utf-8') + +def encrypt(plainText, workingKey): + # Convert string IV to bytes + iv = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + + # Ensure plainText is bytes + if isinstance(plainText, str): + plainText = plainText.encode('utf-8') + + plainText = pad(plainText) + + # Create MD5 hash of working key + encDigest = hashlib.md5() + if isinstance(workingKey, str): + workingKey = workingKey.encode('utf-8') + encDigest.update(workingKey) + + # Create cipher and encrypt + enc_cipher = AES.new(encDigest.digest(), AES.MODE_CBC, iv) + encryptedText = enc_cipher.encrypt(plainText).hex() + + return encryptedText + +def decrypt(cipherText, workingKey): + # Convert string IV to bytes + iv = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' + + # Create MD5 hash of working key + decDigest = hashlib.md5() + if isinstance(workingKey, str): + workingKey = workingKey.encode('utf-8') + decDigest.update(workingKey) + + # Convert hex to bytes and decrypt + encryptedText = bytes.fromhex(cipherText) + dec_cipher = AES.new(decDigest.digest(), AES.MODE_CBC, iv) + decryptedText = dec_cipher.decrypt(encryptedText) + + # Remove padding + padding_length = decryptedText[-1] + return decryptedText[:-padding_length].decode('utf-8') + +@frappe.whitelist() +def test_connection(merchant_id, access_code, encryption_key, environment): + """ + Test connection to CCAvenue + + Args: + merchant_id: The merchant ID from CCAvenue + access_code: The access code from CCAvenue + encryption_key: The encryption key from CCAvenue + environment: 'Sandbox' or 'Production' + + Returns: + dict: Result of the test connection + """ + try: + # Create a simple test request to check encryption + test_data = f"merchant_id={merchant_id}¤cy=INR&amount=1.00" + encrypted_data = encrypt(test_data, encryption_key) + + # If encryption works, we assume the credentials are valid + return { + "success": True, + "message": "Connection test successful" + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), "CCAvenue Connection Test Error") + return { + "success": False, + "error": str(e) + } \ No newline at end of file diff --git a/payments/public/js/ccavenue.js b/payments/public/js/ccavenue.js new file mode 100644 index 00000000..f1e7a2d7 --- /dev/null +++ b/payments/public/js/ccavenue.js @@ -0,0 +1,76 @@ +// Copyright (c) 2024, Frappe Technologies and contributors +// For license information, please see license.txt + +/* HOW-TO + +CCAvenue Payment + +1. Include checkout script in your code + {{ include_script('/assets/payments/js/ccavenue.js') }} + +2. Create payment details in your backend + def get_ccavenue_payment(self): + from payments.utils import get_payment_gateway_controller + + controller = get_payment_gateway_controller("CCAvenue") + + payment_details = { + "amount": 300, + "title": "Payment for Order #123", + "description": "Payment for Order #123", + "reference_doctype": "Your DocType", + "reference_docname": self.name, + "payer_name": "Customer Name", + "payer_email": "customer@example.com", + "order_id": self.name, + "currency": "INR" + } + + return controller.get_payment_url(**payment_details) + +3. Initiate the payment in client + function make_payment() { + var payment_url = {{ get_ccavenue_payment() }}; + window.location.href = payment_url; + } +*/ + +frappe.provide("frappe.checkout"); + +frappe.checkout.ccavenue = class CCAvenueCheckout { + constructor(opts) { + Object.assign(this, opts); + } + + init() { + var me = this; + + return new Promise(function(resolve, reject) { + if(me.order_id) { + me.process_payment(); + resolve(); + } else { + reject("Missing order_id"); + } + }); + } + + process_payment() { + var me = this; + + frappe.call({ + method: "frappe.client.get", + args: { + doctype: me.doctype, + name: me.docname + }, + callback: function(r) { + if(r.message && r.message.ccavenue_payment_url) { + window.location.href = r.message.ccavenue_payment_url; + } else { + frappe.msgprint(__("Unable to process payment. Please try again.")); + } + } + }); + } +}; \ No newline at end of file diff --git a/payments/public/js/ccavenue_session_handler.js b/payments/public/js/ccavenue_session_handler.js new file mode 100644 index 00000000..b141cb67 --- /dev/null +++ b/payments/public/js/ccavenue_session_handler.js @@ -0,0 +1,28 @@ +frappe.provide("payments.ccavenue"); + +payments.ccavenue.checkAndRestoreSession = function() { + // Check if we're coming back from a payment + const urlParams = new URLSearchParams(window.location.search); + const isPaymentReturn = urlParams.has("doctype") && urlParams.has("docname"); + + if (isPaymentReturn && frappe.session.user === "Guest") { + // Try to restore session + frappe.call({ + method: "payments.payment_gateways.doctype.ccavenue_settings.ccavenue_settings.restore_user_session", + args: { + "reference_doctype": urlParams.get("doctype"), + "reference_docname": urlParams.get("docname") + }, + callback: function(r) { + if (r.message && r.message.success) { + // Reload to refresh the session + window.location.reload(); + } + } + }); + } +}; + +$(document).ready(function() { + payments.ccavenue.checkAndRestoreSession(); +}); \ No newline at end of file diff --git a/payments/templates/pages/ccavenue_checkout.html b/payments/templates/pages/ccavenue_checkout.html new file mode 100644 index 00000000..2859c217 --- /dev/null +++ b/payments/templates/pages/ccavenue_checkout.html @@ -0,0 +1,30 @@ +{% block script %} + +{% endblock %} {%- block page_content -%} +