From 056e6492576b2f2ddcd6166fa1f84dd6db2b62ec Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 9 Apr 2025 19:20:20 +0530 Subject: [PATCH 01/22] fix (style) : add pre-commit configuration and install hooks. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9f5b27e..2fb83911 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'node_modules|.git' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: false From a1bf60c9abaee6644e6167664a296f775878e327 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 9 Apr 2025 19:21:42 +0530 Subject: [PATCH 02/22] feat: Integrate Bank Muscat payment gateway --- .../doctype/bankmuscat_settings/__init__.py | 0 .../bankmuscat_settings.js | 8 + .../bankmuscat_settings.json | 56 +++ .../bankmuscat_settings.py | 131 +++++++ .../test_bankmuscat_settings.py | 9 + .../templates/includes/bankmuscat_checkout.js | 40 +++ .../templates/pages/bankmuscat_checkout.html | 16 + .../templates/pages/bankmuscat_checkout.py | 321 ++++++++++++++++++ 8 files changed, 581 insertions(+) create mode 100644 payments/payment_gateways/doctype/bankmuscat_settings/__init__.py create mode 100644 payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.js create mode 100644 payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json create mode 100644 payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py create mode 100644 payments/payment_gateways/doctype/bankmuscat_settings/test_bankmuscat_settings.py create mode 100644 payments/templates/includes/bankmuscat_checkout.js create mode 100644 payments/templates/pages/bankmuscat_checkout.html create mode 100644 payments/templates/pages/bankmuscat_checkout.py diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/__init__.py b/payments/payment_gateways/doctype/bankmuscat_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.js b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.js new file mode 100644 index 00000000..bccff2a1 --- /dev/null +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("BankMuscat Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json new file mode 100644 index 00000000..fa23f54d --- /dev/null +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json @@ -0,0 +1,56 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:merchant_id", + "creation": "2025-01-16 14:46:37.045855", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "merchant_id", + "access_code", + "working_key" + ], + "fields": [ + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "label": "Merchant ID", + "unique": 1 + }, + { + "fieldname": "access_code", + "fieldtype": "Password", + "label": "Access Code" + }, + { + "fieldname": "working_key", + "fieldtype": "Password", + "label": "Working Key" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-01-17 15:39:44.858467", + "modified_by": "Administrator", + "module": "Payment Gateways", + "name": "BankMuscat Settings", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] + } \ No newline at end of file diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py new file mode 100644 index 00000000..0546c7d6 --- /dev/null +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -0,0 +1,131 @@ +from string import Template + +import frappe +from Crypto.Cipher import AES +from frappe.integrations.utils import create_request_log +from frappe.model.document import Document +from frappe.utils import call_hook_method, get_url + +from payments.utils import create_payment_gateway + + +class BankMuscatSettings(Document): + supported_currencies = ("INR", "OMR", "AED", "USD", "GBP", "EUR") + + def on_update(self): + create_payment_gateway( + f"BankMuscat-{self.merchant_id}", + settings="BankMuscat Settings", + controller=self.merchant_id, + ) + call_hook_method("payment_gateway_enabled", gateway=f"BankMuscat-{self.merchant_id}") + + def get_payment_url(self, **kwargs): + frappe.log_error("data: ", kwargs) + self.order_id = create_request_log( + kwargs, service_name="BankMuscat", name=kwargs.get("order_id", "") + ).name + return get_url(f"bankmuscat_checkout?order_id={self.order_id}") + + def decrypt(self, cipher_text, working_key): + cipher_text = bytes.fromhex(cipher_text) + nonce, ciphertext, tag = cipher_text[: AES.block_size], cipher_text[16:-16], cipher_text[-16:] + cipher = AES.new(working_key.encode(), AES.MODE_GCM, nonce=nonce) + return cipher.decrypt_and_verify(ciphertext, tag) + + def encrypt(self, plain_text, working_key): + cipher = AES.new(working_key.encode(), AES.MODE_GCM) + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode()) + return (cipher.nonce + ciphertext + tag).hex() + + def get_merchant_data(self, **kwargs): + base_url = "https://fb54-223-185-26-209.ngrok-free.app/api/method/payments.templates.pages.bankmuscat_checkout" + # base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") + + merchant_data = { + "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), + "order_id": (kwargs.get("order_id") or str(self.order_id)).replace("-", ""), + # "currency": kwargs.get("currency") or "OMR", + "currency": "OMR", + "amount": str(kwargs.get("amount", "")), + "redirect_url": f"{base_url}.verify_payment_status", + "cancel_url": f"{base_url}.cancel_payment", + "integration_type": kwargs.get("integration_type", "iframe_normal"), + } + optional_fields = [ + "language", + "billing_name", + "billing_address", + "billing_city", + "billing_state", + "billing_zip", + "billing_country", + "billing_tel", + "billing_email", + "delivery_name", + "delivery_address", + "delivery_city", + "delivery_state", + "delivery_zip", + "delivery_country", + "delivery_tel", + "merchant_param1", + "merchant_param2", + "merchant_param3", + "merchant_param4", + "merchant_param5", + "promo_code", + "customer_identifier", + ] + merchant_data.update({field: kwargs.get(field, "") for field in optional_fields}) + + return "&".join(f"{key}={value}" for key, value in merchant_data.items()) + + def get_encrypted_request(self, **kwargs): + return self.encrypt(self.get_merchant_data(**kwargs), self.get_password("working_key")) + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw( + f"Please select another payment method. BankMuscat does not support transactions in currency '{currency}'" + ) + + def validate_mandatory_values(self, **kwargs): + self.validate_transaction_currency(kwargs.get("currency")) + if not kwargs.get("amount"): + frappe.throw("Amount is missing") + if not kwargs.get("order_id") and not getattr(self, "order_id", None): + frappe.throw("Param order_id is missing") + + def get_gateway_url(self, **kwargs): + self.validate_mandatory_values(**kwargs) + encrypted_req = self.get_encrypted_request(**kwargs) + xscode = self.get_password("access_code") + custom_uat = self.custom_uat + + action_url = ( + "https://spayuattrns.bmtest.om/transaction.do?command=initiateTransaction" + if custom_uat == "Staging" + else "https://smartpaytrns.bankmuscat.com/transaction.do?command=initiateTransaction" + ) + + html = Template( + """
+ + + +
""" + ).safe_substitute(encReq=encrypted_req, xscode=xscode, action_url=action_url) + print("HTML: ", html) + + return html + + def get_payment_page_url(self, **kwargs): + return self.get_gateway_url(**kwargs) + + +def get_gateway_controller(doctype, docname, payment_gateway=None): + if not payment_gateway: + reference_doc = frappe.get_doc(doctype, docname) + payment_gateway = reference_doc.payment_gateway + return frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/test_bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/test_bankmuscat_settings.py new file mode 100644 index 00000000..62672b34 --- /dev/null +++ b/payments/payment_gateways/doctype/bankmuscat_settings/test_bankmuscat_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBankMuscatSettings(FrappeTestCase): + pass diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js new file mode 100644 index 00000000..6bdff1cd --- /dev/null +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -0,0 +1,40 @@ +$(document).ready(function() { + + + var data = {{ frappe.form_dict | json }}; // Get data from backend + + if (!data || !data.order_id) { + console.error("Error: Missing order_id"); + return; + } + + frappe.call({ + method: "payments.templates.pages.bankmuscat_checkout.get_payment_url", + freeze: true, + headers: { + "X-Requested-With": "XMLHttpRequest" + }, + args: { + data: { + "order_id": data.order_id + } + }, + callback: function(r) { + if (r && r.message && r.message.payment_url) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = r.message.payment_url; + + const form = tempDiv.querySelector('form'); + if (form) { + document.body.appendChild(form); + form.submit(); + } else { + console.error("Error: No form found in payment_url"); + } + } + }, + error: function(err) { + console.error("API call failed:", err); + } + }); +}); diff --git a/payments/templates/pages/bankmuscat_checkout.html b/payments/templates/pages/bankmuscat_checkout.html new file mode 100644 index 00000000..8dc66187 --- /dev/null +++ b/payments/templates/pages/bankmuscat_checkout.html @@ -0,0 +1,16 @@ +{% extends "templates/web.html" %} + +{% block title %} Payment {% endblock %} + +{%- block header -%}{% endblock %} + +{% block script %} + +{% endblock %} + +{%- block page_content -%} +

+ {{ _("Loading Payment System...") }} +

+ +{% endblock %} diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py new file mode 100644 index 00000000..d7ccc18a --- /dev/null +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -0,0 +1,321 @@ +import json +from urllib.parse import urlencode + +import frappe +from frappe import _ +from frappe.integrations.utils import create_request_log +from frappe.utils import flt, get_url, getdate + +from payments.payment_gateways.doctype.bankmuscat_settings.bankmuscat_settings import ( + BankMuscatSettings as object, +) +from payments.payment_gateways.doctype.bankmuscat_settings.bankmuscat_settings import get_gateway_controller + + +def check_already_payment_processed(request, reference_doctype, reference_docname): + if frappe.db.get_value("Integration Request", request, "status") == "Completed": + redirect_url = "payment-success" + redirect_url += "?" + urlencode({"doctype": reference_doctype}) + redirect_url += "&" + urlencode({"docname": reference_docname}) + return {"payment_url": get_url(redirect_url)} + + return None + + +@frappe.whitelist(allow_guest=True) +def get_payment_url(data=None): + if isinstance(data, str): + data = frappe.parse_json(data) + + data = frappe._dict(data) + + if not data.order_id: + frappe.respond_as_web_page(_("Invalid Request"), _("Order ID not found"), http_status_code=404) + return + + if request := frappe.db.exists("Integration Request", data.order_id): + order_details = frappe.parse_json(frappe.db.get_value("Integration Request", request, "data")) + order_details = frappe._dict(order_details) + reference_doctype, reference_docname = ( + order_details.reference_doctype, + order_details.reference_docname, + ) + + if redirect_url := check_already_payment_processed(request, reference_doctype, reference_docname): + return redirect_url + + if reference_doctype and reference_docname: + payment_gateway = frappe.db.get_value(reference_doctype, reference_docname, "payment_gateway") + if payment_gateway: + gateway_controller = get_gateway_controller( + reference_doctype, reference_docname, payment_gateway + ) + if gateway_controller: + try: + gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) + return {"payment_url": gateway_doc.get_payment_page_url(**order_details)} + except Exception: + frappe.log_error( + "Error while getting payment url..", frappe.get_traceback(with_context=1) + ) + frappe.respond_as_web_page( + _("Payment Failed"), _("Error while getting payment url"), http_status_code=500 + ) + else: + frappe.respond_as_web_page(_("Payment Failed"), _("Order ID not found"), http_status_code=404) + return + + +def handle_payment_response(data_dict, reference_doctype, reference_docname): + data_dict = frappe._dict(data_dict) + + if data_dict.order_status == "Success": + try: + doc_name = frappe.get_value("Payment Request", {"custom_name": data_dict.get("order_id")}) + payment_request = frappe.get_doc("Payment Request", doc_name) + payment_entry = payment_request.set_as_paid() + payment_request.db_set("transaction_status", "The payment has been completed") + + # update reference no and date in payment entry + frappe.db.set_value( + "Payment Entry", + payment_entry.name, + {"reference_no": data_dict.bank_ref_no, "reference_date": getdate(data_dict.trans_date)}, + ) + + frappe.log_error("Payment Entry", payment_entry) + except Exception: + frappe.log_error("Error while mark as paid..", frappe.get_traceback()) + else: + frappe.db.set_value( + "Integration Request", + data_dict.order_id, + {"status": "Completed", "output": json.dumps(data_dict, indent=4)}, + ) + redirect_url = "payment-success" + redirect_url += "?" + urlencode({"doctype": reference_doctype}) + redirect_url += "&" + urlencode({"docname": reference_docname}) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) + else: + try: + doc_name = frappe.get_value("Payment Request", {"custom_name": data_dict.get("order_id")}) + payment_request = frappe.get_doc("Payment Request", doc_name) + + payment_request.set_as_failed() + except Exception: + frappe.log_error("Error while mark as failed..", frappe.get_traceback()) + else: + frappe.db.set_value( + "Integration Request", + doc_name, + {"status": "Failed", "error": json.dumps(data_dict, indent=4)}, + ) + + redirect_url = "payment-failed" + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) + + +def handle_payment_page_response( + payment_request, gateway_controller, data, payment_gateway_account, kwargs=None, integration_request=False +): + if gateway_controller: + gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) + decrypted_data = gateway_doc.decrypt(data.get("encResp"), gateway_doc.get_password("working_key")) + + data_dict = dict(pair.split("=") for pair in decrypted_data.split("&")) + + frappe.log_error("Payment Success Response: ", data_dict) + + if data_dict.get("order_status") == "Success": + try: + if not integration_request: + create_request_log(kwargs, service_name="BankMuscat", name=kwargs.get("order_id", "")) + frappe.db.set_value( + "Integration Request", + data_dict.get("order_id"), + {"status": "Completed", "output": json.dumps(data_dict, indent=4)}, + ) + except Exception: + frappe.log_error("Integration request failed", frappe.get_traceback()) + try: + gateway_account = frappe.get_doc("Payment Gateway Account", payment_gateway_account) + frappe.db.set_value( + "Payment Request", + data_dict.get("order_id"), + { + "payment_gateway_account": gateway_account.name, + "payment_gateway": gateway_account.payment_gateway, + "payment_account": gateway_account.payment_account, + "payment_channel": gateway_account.payment_channel, + "transaction_status": "The payment has been completed", + }, + ) + payment_request.reload() + payment_entry = payment_request.set_as_paid() + + if payment_entry: + frappe.db.set_value( + "Payment Entry", + payment_entry.name, + { + "reference_no": data_dict.get("bank_ref_no"), + "reference_date": getdate(data_dict.get("trans_date")), + }, + ) + except Exception: + frappe.log_error("Error while mark as paid..", frappe.get_traceback()) + + redirect_url = "payment-success" + redirect_url += "?" + urlencode({"doctype": "Payment Request"}) + redirect_url += "&" + urlencode({"docname": payment_request.name}) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) + + else: + try: + frappe.db.set_value( + "Payment Request", + data_dict.get("order_id"), + {"payment_gateway_account": payment_gateway_account}, + ) + payment_request = frappe.get_doc("Payment Request", data_dict.get("order_id")) + payment_request.set_as_failed() + except Exception: + frappe.log_error("Error while mark as failed..", frappe.get_traceback()) + + try: + if not integration_request: + create_request_log( + kwargs, + service_name="BankMuscat", + name=kwargs.get("order_id", ""), + error=json.dumps(data_dict, indent=4), + ) + frappe.db.set_value("Integration Request", data_dict.get("order_id"), {"status": "Failed"}) + except Exception: + frappe.log_error("Integration request failed", frappe.get_traceback()) + + redirect_url = "payment-failed" + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) + + +@frappe.whitelist(allow_guest=True) +def verify_payment_status(): + data = frappe.form_dict + print("data: ", data) + order_id = frappe.db.get_value("Payment Request", {"custom_name": data.get("orderNo")}) + print("order_id: ", order_id) + if order_id and frappe.db.exists("Integration Request", order_id): + order_details = frappe.parse_json(frappe.db.get_value("Integration Request", order_id, "data")) + reference_doctype = order_details.get("reference_doctype") + reference_docname = order_details.get("reference_docname") + if ("reference_doctype" in order_details) and ("reference_docname" in order_details): + payment_gateway = frappe.db.get_value(reference_doctype, reference_docname, "payment_gateway") + if payment_gateway: + gateway_controller = get_gateway_controller( + reference_doctype, reference_docname, payment_gateway + ) + if gateway_controller: + gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) + decrypted_data = gateway_doc.decrypt( + data.get("encResp"), gateway_doc.get_password("working_key") + ) + decrypted_str = decrypted_data.decode("utf-8") + data_dict = dict(pair.split("=") for pair in decrypted_str.split("&")) + return handle_payment_response(data_dict, reference_doctype, reference_docname) + else: + payment_request = frappe.get_doc("Payment Request", order_id) + payment_gateway, payment_gateway_account = frappe.db.get_value( + "Default Gateway Account", + { + "parenttype": "Payment Request", + "parent": payment_request.name, + "company": frappe.db.get_value(reference_doctype, reference_docname, "company"), + "gateway_settings": ["like", "%BankMuscat%"], + }, + ["payment_gateway", "payment_gateway_account"], + ) + + gateway_controller = get_gateway_controller( + reference_doctype, reference_docname, payment_gateway + ) + + return handle_payment_page_response( + payment_request, + gateway_controller, + data, + payment_gateway_account, + kwargs=order_details, + integration_request=True, + ) + + elif order_id: + payment_request = frappe.get_doc("Payment Request", order_id) + request_data = frappe.db.get_value( + payment_request.reference_doctype, + payment_request.reference_name, + ["company", "customer_name"], + as_dict=1, + ) + request_data.update({"company": frappe.defaults.get_defaults().company}) + + kwargs = { + "amount": flt(payment_request.grand_total, payment_request.precision("grand_total")), + "title": request_data.company, + "description": payment_request.subject, + "reference_doctype": "Payment Request", + "reference_docname": payment_request.name, + "payer_email": payment_request.email_to or frappe.session.user, + "payer_name": request_data.customer_name, + "order_id": payment_request.name, + "currency": payment_request.currency, + } + + payment_gateway, payment_gateway_account = frappe.db.get_value( + "Default Gateway Account", + { + "parenttype": "Payment Request", + "parent": payment_request.name, + "company": frappe.db.get_value(payment_request.doctype, payment_request.name, "company"), + "gateway_settings": ["like", "%BankMuscat%"], + }, + ["payment_gateway", "payment_gateway_account"], + ) + + gateway_controller = get_gateway_controller( + payment_request.doctype, payment_request.name, payment_gateway + ) + + return handle_payment_page_response( + payment_request, + gateway_controller, + data, + payment_gateway_account, + kwargs=kwargs, + integration_request=False, + ) + + else: + frappe.respond_as_web_page("Payment Failed", "Order ID not found", http_status_code=404) + + +@frappe.whitelist(allow_guest=True) +def cancel_payment(): + data = frappe.form_dict + + if frappe.db.exists("Integration Request", data.get("orderNo")): + frappe.db.set_value("Integration Request", data.get("orderNo"), "status", "Cancelled") + try: + payment_request = frappe.get_doc("Payment Request", data.get("order_id")) + payment_request.set_as_cancelled() + except Exception: + frappe.log_error("Error while mark as Cancelled..", frappe.get_traceback()) + + redirect_url = "payment-cancel" + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) From 24292618c0663d34bdb9fddaabfab2096dfd250a Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 15 Apr 2025 18:54:41 +0530 Subject: [PATCH 03/22] feat: add custom button to trigger payment request status check --- payments/public/js/payment_request.js | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 payments/public/js/payment_request.js diff --git a/payments/public/js/payment_request.js b/payments/public/js/payment_request.js new file mode 100644 index 00000000..f8b2f2cf --- /dev/null +++ b/payments/public/js/payment_request.js @@ -0,0 +1,34 @@ +frappe.ui.form.on("Payment Request", { + refresh(frm) { + const isInward = frm.doc.payment_request_type === "Inward"; + const isInitiated = frm.doc.status === "Initiated"; + const gatewayAccount = frm.doc.payment_gateway_account || ""; + const firstWord = gatewayAccount.split("-")[0].trim().toLowerCase(); + + const isBankMuscat = firstWord === "bankmuscat"; + + if ( + isInward && + isInitiated && + isBankMuscat && + frm.doc.custom_payment_reference_no + ) { + frm.add_custom_button(__("Check Payment Status"), function () { + frappe.call({ + method: + "payments.templates.pages.bankmuscat_checkout.get_payment_status", + freeze: true, + freeze_message: __("Fetching payment status..."), + args: { + payment_request: frm.doc.name, + }, + callback: function (r) { + if (r.message) { + window.location.reload(); + } + }, + }); + }); + } + }, +}); From 05bcad44af18e03fa3013e76c062ce9c8d2fb7f9 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 15 Apr 2025 18:58:00 +0530 Subject: [PATCH 04/22] feat: add new HTML page to display payment processing notification --- .../templates/pages/payment-processing.html | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 payments/templates/pages/payment-processing.html diff --git a/payments/templates/pages/payment-processing.html b/payments/templates/pages/payment-processing.html new file mode 100644 index 00000000..d294b443 --- /dev/null +++ b/payments/templates/pages/payment-processing.html @@ -0,0 +1,29 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Payment Initiated") }}{% endblock %} + +{% block page_content %} +
+
+ + {{ _("Hold Tight! Payment is in Progress") }} + +
+

{{ _("Your payment has been successfully initiated and is currently being processed.") }}

+

{{ _("Sometimes it takes a little while for the bank to confirm the transaction.") }}

+

{{ _("No worries — the status will update automatically once we receive a response.") }}

+

{{ _("You can safely return now, and we’ll keep tracking the update in the background.") }}

+ +
+ + +{% endblock %} From 791075d67b6326086de394a2bb2b6daee5e22e55 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 15 Apr 2025 19:14:04 +0530 Subject: [PATCH 05/22] refactor: restructure payment settings file for better maintainability --- .../doctype/bankmuscat_settings/bankmuscat_settings.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index 0546c7d6..c131b500 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -39,8 +39,8 @@ def encrypt(self, plain_text, working_key): return (cipher.nonce + ciphertext + tag).hex() def get_merchant_data(self, **kwargs): - base_url = "https://fb54-223-185-26-209.ngrok-free.app/api/method/payments.templates.pages.bankmuscat_checkout" - # base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") + # base_url = "https://fb54-223-185-26-209.ngrok-free.app/api/method/payments.templates.pages.bankmuscat_checkout" + base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") merchant_data = { "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), @@ -116,7 +116,6 @@ def get_gateway_url(self, **kwargs): """ ).safe_substitute(encReq=encrypted_req, xscode=xscode, action_url=action_url) - print("HTML: ", html) return html @@ -129,3 +128,7 @@ def get_gateway_controller(doctype, docname, payment_gateway=None): reference_doc = frappe.get_doc(doctype, docname) payment_gateway = reference_doc.payment_gateway return frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") + + +def store_custom_name(doc, method=None): + doc.db_set("custom_name", doc.name.replace("-", "")) From 1df5f4d1b10bf06841381c0a6c72d9c9450468fc Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 16 Apr 2025 17:55:11 +0530 Subject: [PATCH 06/22] chore: Remove Sandbox test URL. --- .../doctype/bankmuscat_settings/bankmuscat_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index c131b500..b2c14946 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -39,7 +39,6 @@ def encrypt(self, plain_text, working_key): return (cipher.nonce + ciphertext + tag).hex() def get_merchant_data(self, **kwargs): - # base_url = "https://fb54-223-185-26-209.ngrok-free.app/api/method/payments.templates.pages.bankmuscat_checkout" base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") merchant_data = { From 0e759258b1f97780d1c73d303b0f63402542f416 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 16 Apr 2025 18:07:56 +0530 Subject: [PATCH 07/22] feat: add patch to create custom fields in Payments module --- payments/patches.txt | 4 ++ payments/patches/custom_fields.py | 7 +++ payments/utils/utils.py | 72 ++++++++++++++++++++++++------- 3 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 payments/patches/custom_fields.py diff --git a/payments/patches.txt b/payments/patches.txt index e69de29b..646cbf28 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -0,0 +1,4 @@ +[pre_model_sync] + +[post_model_sync] +payments.patches.custom_fields \ No newline at end of file diff --git a/payments/patches/custom_fields.py b/payments/patches/custom_fields.py new file mode 100644 index 00000000..6e69ae55 --- /dev/null +++ b/payments/patches/custom_fields.py @@ -0,0 +1,7 @@ +import frappe + +from payments.utils.utils import make_custom_fields + + +def execute(): + make_custom_fields() diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 5284b5e5..bebbb37a 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -137,7 +137,21 @@ def make_custom_fields(): "options": "Currency", "insert_after": "amount", }, - ] + ], + "Payment Request": [ + { + "fieldname": "custom_name", + "fieldtype": "Data", + "label": "Name", + "insert_after": "naming_series", + }, + { + "fieldname": "custom_payment_reference_no", + "fieldtype": "Data", + "label": "Payment Reference No", + "insert_after": "transaction_date", + }, + ], } ) @@ -165,25 +179,53 @@ def delete_custom_fields(): if frappe.get_meta("Web Form").has_field("payments_tab"): click.secho("* Uninstalling Payment Custom Fields from Web Form") - fieldnames = ( - "payments_tab", - "accept_payment", - "payment_gateway", - "payment_button_label", - "payment_button_help", - "payments_cb", - "amount_field", - "amount_based_on_field", - "amount", - "currency", - ) + custom_fields = { + "Web Form": [ + "payments_tab", + "accept_payment", + "payment_gateway", + "payment_button_label", + "payment_button_help", + "payments_cb", + "amount_field", + "amount_based_on_field", + "amount", + "currency", + ], + "Payment Request": [ + "custom_name", + "custom_payment_reference_no", + ], + } - for fieldname in fieldnames: - frappe.db.delete("Custom Field", {"name": "Web Form-" + fieldname}) + for doctype, fieldnames in custom_fields.items(): + frappe.db.delete( + "Custom Field", {"name": ["in", [f"{doctype}-" + fieldname for fieldname in fieldnames]]} + ) frappe.clear_cache(doctype="Web Form") +custom_fields = { + "Web Form": [ + "payments_tab", + "accept_payment", + "payment_gateway", + "payment_button_label", + "payment_button_help", + "payments_cb", + "amount_field", + "amount_based_on_field", + "amount", + "currency", + ], + "Payment Request": [ + "custom_name", + "custom_payment_reference_no", + ], +} + + def before_install(): # TODO: remove this # This is done for erpnext CI patch test From 9c0b71f987a7518eae1a3a92c852d207468adc9d Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 16 Apr 2025 18:26:48 +0530 Subject: [PATCH 08/22] feat: add cron job and manual button to fetch status, Add custom name --- payments/hooks.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/payments/hooks.py b/payments/hooks.py index caa07d27..55f5beba 100644 --- a/payments/hooks.py +++ b/payments/hooks.py @@ -30,6 +30,9 @@ # include js in doctype views # doctype_js = {"doctype" : "public/js/doctype.js"} +doctype_js = { + "Payment Request": "public/js/payment_request.js", +} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -100,13 +103,16 @@ # --------------- # Hook on document methods and events -# doc_events = { -# "*": { -# "on_update": "method", -# "on_cancel": "method", -# "on_trash": "method" -# } -# } +doc_events = { + # "*": { + # "on_update": "method", + # "on_cancel": "method", + # "on_trash": "method" + # } + "Payment Request": { + "on_submit": "payments.payment_gateways.doctype.bankmuscat_settings.bankmuscat_settings.store_custom_name", + } +} # Scheduled Tasks # --------------- @@ -115,6 +121,9 @@ "all": [ "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.capture_payment", ], + "daily": [ + "payments.templates.pages.bankmuscat_checkout.check_payment_status", + ], } # Testing From dce6d0e8b4826ab58cd977736778e7edf2b59446 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 16 Apr 2025 18:29:53 +0530 Subject: [PATCH 09/22] feat: handle status for bank gateway response. --- .../templates/pages/bankmuscat_checkout.py | 209 ++++++++++++++---- 1 file changed, 168 insertions(+), 41 deletions(-) diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index d7ccc18a..d88e6e5c 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -1,7 +1,8 @@ import json -from urllib.parse import urlencode +from urllib.parse import parse_qsl, urlencode import frappe +import requests from frappe import _ from frappe.integrations.utils import create_request_log from frappe.utils import flt, get_url, getdate @@ -67,55 +68,72 @@ def get_payment_url(data=None): def handle_payment_response(data_dict, reference_doctype, reference_docname): - data_dict = frappe._dict(data_dict) + data = frappe._dict(data_dict) - if data_dict.order_status == "Success": - try: - doc_name = frappe.get_value("Payment Request", {"custom_name": data_dict.get("order_id")}) - payment_request = frappe.get_doc("Payment Request", doc_name) + frappe.logger().info(f"BankMuscat Response: {json.dumps(data, indent=2)}") + + order_no = data.get("order_id") or data.get("order_no") + doc_name = frappe.get_value("Payment Request", {"custom_name": order_no}) + + if not doc_name: + frappe.throw(f"No Payment Request found for order_no: {order_no}") + + payment_request = frappe.get_doc("Payment Request", doc_name) + + # Save tracking ID if not already saved + if payment_request.status == "Initiated" and not payment_request.custom_payment_reference_no: + payment_request.db_set("custom_payment_reference_no", data.get("tracking_id")) + + order_status = data.get("order_status", "").lower() + + try: + if order_status == "success": payment_entry = payment_request.set_as_paid() payment_request.db_set("transaction_status", "The payment has been completed") - # update reference no and date in payment entry frappe.db.set_value( "Payment Entry", payment_entry.name, - {"reference_no": data_dict.bank_ref_no, "reference_date": getdate(data_dict.trans_date)}, + {"reference_no": data.get("bank_ref_no"), "reference_date": getdate(data.get("trans_date"))}, ) - frappe.log_error("Payment Entry", payment_entry) - except Exception: - frappe.log_error("Error while mark as paid..", frappe.get_traceback()) - else: frappe.db.set_value( - "Integration Request", - data_dict.order_id, - {"status": "Completed", "output": json.dumps(data_dict, indent=4)}, + "Integration Request", doc_name, {"status": "Completed", "output": json.dumps(data, indent=4)} ) - redirect_url = "payment-success" - redirect_url += "?" + urlencode({"doctype": reference_doctype}) - redirect_url += "&" + urlencode({"docname": reference_docname}) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url(redirect_url) - else: - try: - doc_name = frappe.get_value("Payment Request", {"custom_name": data_dict.get("order_id")}) - payment_request = frappe.get_doc("Payment Request", doc_name) + return redirect_response("payment-success") + + elif order_status in ("failure", "invalid", "timeout"): + payment_request.db_set("status", "Failed") + payment_request.db_set("transaction_status", "Payment Not Completed") - payment_request.set_as_failed() - except Exception: - frappe.log_error("Error while mark as failed..", frappe.get_traceback()) - else: frappe.db.set_value( - "Integration Request", - doc_name, - {"status": "Failed", "error": json.dumps(data_dict, indent=4)}, + "Integration Request", doc_name, {"status": "Failed", "error": json.dumps(data, indent=4)} ) - redirect_url = "payment-failed" - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url(redirect_url) + return redirect_response("payment-failed") + + elif order_status == "aborted": + try: + payment_request.set_as_cancelled() + payment_request.db_set("transaction_status", "Payment Cancelled") + + frappe.db.set_value( + "Integration Request", + doc_name, + {"status": "Cancelled", "error": json.dumps(data, indent=4)}, + ) + + return redirect_response("payment-cancel") + + except Exception: + frappe.log_error("Error during cancel_payment()", frappe.get_traceback()) + else: + payment_request.db_set("transaction_status", "Waiting for Payment Response") + return redirect_response("payment-processing", reference_doctype, reference_docname) + + except Exception: + frappe.log_error("Error while processing payment response", frappe.get_traceback()) def handle_payment_page_response( @@ -207,9 +225,7 @@ def handle_payment_page_response( @frappe.whitelist(allow_guest=True) def verify_payment_status(): data = frappe.form_dict - print("data: ", data) order_id = frappe.db.get_value("Payment Request", {"custom_name": data.get("orderNo")}) - print("order_id: ", order_id) if order_id and frappe.db.exists("Integration Request", order_id): order_details = frappe.parse_json(frappe.db.get_value("Integration Request", order_id, "data")) reference_doctype = order_details.get("reference_doctype") @@ -307,15 +323,126 @@ def verify_payment_status(): @frappe.whitelist(allow_guest=True) def cancel_payment(): data = frappe.form_dict + doc_name = frappe.get_value("Payment Request", {"custom_name": data.get("orderNo")}) - if frappe.db.exists("Integration Request", data.get("orderNo")): - frappe.db.set_value("Integration Request", data.get("orderNo"), "status", "Cancelled") + if frappe.db.exists("Integration Request", doc_name): + frappe.db.set_value("Integration Request", doc_name, "status", "Cancelled") try: - payment_request = frappe.get_doc("Payment Request", data.get("order_id")) + payment_request = frappe.get_doc("Payment Request", doc_name) payment_request.set_as_cancelled() except Exception: frappe.log_error("Error while mark as Cancelled..", frappe.get_traceback()) - redirect_url = "payment-cancel" + return redirect_response("payment-cancel") + + +def redirect_response(page, doctype=None, docname=None): + user = frappe.session.user + + if user != "Guest": + return True + + query_params = {} + + if doctype: + query_params["doctype"] = doctype + if docname: + query_params["docname"] = docname + + url = f"{page}" + if query_params: + url += "?" + urlencode(query_params) + frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url(redirect_url) + frappe.local.response["location"] = get_url(url) + + +def check_payment_status(): + payment_requests = frappe.get_all( + "Payment Request", + filters={"status": "Initiated"}, + fields=["name", "custom_name", "custom_payment_reference_no", "payment_gateway"], + ) + + filtered_requests = [ + pr + for pr in payment_requests + if pr.custom_payment_reference_no + and pr.payment_gateway + and pr.payment_gateway.startswith("BankMuscat-") + ] + + # Call get_payment_status for each filtered payment request + for pr in filtered_requests: + try: + get_payment_status(pr.name) + except Exception: + frappe.log_error( + title="check_payment_status", + message=f"Error calling get_payment_status for {pr.name}: {frappe.get_traceback()}", + ) + + +@frappe.whitelist() +def get_payment_status(payment_request): + try: + doc = frappe.get_doc("Payment Request", payment_request) + + reference_no = doc.custom_payment_reference_no + order_no = doc.name + + if not reference_no or not order_no: + frappe.throw("Missing order number or reference number") + + # Prepare payload + request_payload = { + "order_id": order_no, + "reference_no": reference_no, + } + json_data = json.dumps(request_payload) + + # Get Gateway name from account format e.g., "bankmuscat-1234" + gateway_name = doc.payment_gateway.split("-")[1] + bankmuscat_settings = frappe.get_doc("BankMuscat Settings", gateway_name) + + encrypted_data = bankmuscat_settings.encrypt( + json_data, bankmuscat_settings.get_password("working_key") + ) + access_code = bankmuscat_settings.get_password("access_code") + + payload = { + "enc_request": encrypted_data, + "access_code": access_code, + "request_type": "JSON", + "response_type": "JSON", + "command": "orderStatusTracker", + "version": "1.2", + } + + SMARTPAY_URL = ( + "https://spayuatapi.bmtest.om/apis/servlet/DoWebTrans?" + if bankmuscat_settings.custom_uat == "Staging" + else "https://smartpayapi.bankmuscat.com/apis/servlet/DoWebTrans?" + ) + + # Make request to SmartPay + response = requests.post(SMARTPAY_URL, data=payload) + parsed_response = dict(parse_qsl(response.text)) + + if parsed_response.get("status") != "0": + frappe.throw("API request failed: " + parsed_response.get("enc_response", "")) + + # Decrypt and parse final response + decrypted_data = bankmuscat_settings.decrypt( + parsed_response["enc_response"], bankmuscat_settings.get_password("working_key") + ) + decrypted_json = json.loads(decrypted_data) + + # Payment status handling + return handle_payment_response(decrypted_json, "Payment Request", payment_request) + + except Exception: + frappe.log_error( + title="get_payment_status", message="BankMuscat Payment Status API Error: frappe.get_traceback()" + ) + return {"status": "error", "message": frappe.get_traceback()} From c5bc4288bbec9f897faab94eae5471f301d99ffc Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 16 Apr 2025 18:51:46 +0530 Subject: [PATCH 10/22] feat: Integrate Bank Muscat payment gateway --- .../doctype/bankmuscat_settings/bankmuscat_settings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index b2c14946..f0a70ccc 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -10,7 +10,7 @@ class BankMuscatSettings(Document): - supported_currencies = ("INR", "OMR", "AED", "USD", "GBP", "EUR") + supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR") def on_update(self): create_payment_gateway( @@ -44,8 +44,7 @@ def get_merchant_data(self, **kwargs): merchant_data = { "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), "order_id": (kwargs.get("order_id") or str(self.order_id)).replace("-", ""), - # "currency": kwargs.get("currency") or "OMR", - "currency": "OMR", + "currency": kwargs.get("currency") or "OMR", "amount": str(kwargs.get("amount", "")), "redirect_url": f"{base_url}.verify_payment_status", "cancel_url": f"{base_url}.cancel_payment", From c902a1199abe874ef1888c629d81751a0b3569e0 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 10 Sep 2025 18:14:05 +0530 Subject: [PATCH 11/22] Refactor the functionality for handling the payment page UI --- payments/patches.txt | 2 +- .../bankmuscat_settings.py | 161 +++++++++++++----- .../templates/pages/bankmuscat_checkout.py | 1 - payments/utils/custom_fields.py | 7 + payments/utils/utils.py | 58 ++++--- 5 files changed, 159 insertions(+), 70 deletions(-) create mode 100644 payments/utils/custom_fields.py diff --git a/payments/patches.txt b/payments/patches.txt index 646cbf28..15bfeae5 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -1,4 +1,4 @@ [pre_model_sync] [post_model_sync] -payments.patches.custom_fields \ No newline at end of file +payments.utils.custom_fields \ No newline at end of file diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index f0a70ccc..7259dcfa 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -2,6 +2,7 @@ import frappe from Crypto.Cipher import AES +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 @@ -10,37 +11,74 @@ class BankMuscatSettings(Document): - supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR") + supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR", "INR") + # Create Payment Gateway on save def on_update(self): - create_payment_gateway( - f"BankMuscat-{self.merchant_id}", - settings="BankMuscat Settings", - controller=self.merchant_id, - ) - call_hook_method("payment_gateway_enabled", gateway=f"BankMuscat-{self.merchant_id}") + try: + gateway_name = f"BankMuscat-{self.merchant_id}" + + # Check if Payment Gateway already exists + if not frappe.db.exists("Payment Gateway", gateway_name): + create_payment_gateway( + gateway_name, + settings="BankMuscat Settings", + controller=self.merchant_id, + ) + frappe.logger().info(f"Created new Payment Gateway: {gateway_name}") + else: + frappe.logger().info(f"Payment Gateway already exists: {gateway_name}") + + # Trigger hook + call_hook_method("payment_gateway_enabled", gateway=gateway_name) + + except Exception: + frappe.log_error( + message=frappe.get_traceback(), + title=f"BankMuscat Gateway Creation Failed (Merchant: {self.merchant_id})", + ) + frappe.throw( + _( + "Unable to create or update Payment Gateway for Merchant ID {0}. Please check the Error Log." + ).format(self.merchant_id), + title=_("Payment Gateway Error"), + ) def get_payment_url(self, **kwargs): - frappe.log_error("data: ", kwargs) - self.order_id = create_request_log( - kwargs, service_name="BankMuscat", name=kwargs.get("order_id", "") - ).name - return get_url(f"bankmuscat_checkout?order_id={self.order_id}") + try: + frappe.logger().info(f"[BankMuscat] get_payment_url args: {kwargs}") + self.order_id = create_request_log( + kwargs, service_name="BankMuscat", name=kwargs.get("order_id", "") + ).name + return get_url(f"bankmuscat_checkout?order_id={self.order_id}") + except Exception: + frappe.log_error(frappe.get_traceback(), "BankMuscat get_payment_url Failed") + frappe.throw("Unable to generate payment URL. Please check the Error Log.") def decrypt(self, cipher_text, working_key): - cipher_text = bytes.fromhex(cipher_text) - nonce, ciphertext, tag = cipher_text[: AES.block_size], cipher_text[16:-16], cipher_text[-16:] - cipher = AES.new(working_key.encode(), AES.MODE_GCM, nonce=nonce) - return cipher.decrypt_and_verify(ciphertext, tag) + try: + cipher_text = bytes.fromhex(cipher_text) + nonce, ciphertext, tag = cipher_text[: AES.block_size], cipher_text[16:-16], cipher_text[-16:] + cipher = AES.new(working_key.encode(), AES.MODE_GCM, nonce=nonce) + return cipher.decrypt_and_verify(ciphertext, tag) + except Exception: + frappe.log_error(frappe.get_traceback(), "BankMuscat Decryption Failed") + frappe.throw("Unable to decrypt response from BankMuscat.") def encrypt(self, plain_text, working_key): - cipher = AES.new(working_key.encode(), AES.MODE_GCM) - ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode()) - return (cipher.nonce + ciphertext + tag).hex() + try: + cipher = AES.new(working_key.encode(), AES.MODE_GCM) + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode()) + return (cipher.nonce + ciphertext + tag).hex() + except Exception: + frappe.log_error(frappe.get_traceback(), "BankMuscat Encryption Failed") + frappe.throw("Unable to encrypt request for BankMuscat.") def get_merchant_data(self, **kwargs): + # Base URL for redirect and cancel URLs base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") + # mandatory fields merchant_data = { "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), "order_id": (kwargs.get("order_id") or str(self.order_id)).replace("-", ""), @@ -50,6 +88,8 @@ def get_merchant_data(self, **kwargs): "cancel_url": f"{base_url}.cancel_payment", "integration_type": kwargs.get("integration_type", "iframe_normal"), } + + # add optional fields optional_fields = [ "language", "billing_name", @@ -75,58 +115,87 @@ def get_merchant_data(self, **kwargs): "promo_code", "customer_identifier", ] + merchant_data.update({field: kwargs.get(field, "") for field in optional_fields}) return "&".join(f"{key}={value}" for key, value in merchant_data.items()) - def get_encrypted_request(self, **kwargs): - return self.encrypt(self.get_merchant_data(**kwargs), self.get_password("working_key")) - def validate_transaction_currency(self, currency): + if not currency: + frappe.throw(_("Currency is missing")) if currency not in self.supported_currencies: frappe.throw( - f"Please select another payment method. BankMuscat does not support transactions in currency '{currency}'" + _( + "BankMuscat does not support transactions in currency {0}. Please select another payment method." + ).format(currency) ) + # validate mandatory values def validate_mandatory_values(self, **kwargs): self.validate_transaction_currency(kwargs.get("currency")) + if not kwargs.get("amount"): frappe.throw("Amount is missing") + if not kwargs.get("order_id") and not getattr(self, "order_id", None): frappe.throw("Param order_id is missing") - def get_gateway_url(self, **kwargs): - self.validate_mandatory_values(**kwargs) - encrypted_req = self.get_encrypted_request(**kwargs) - xscode = self.get_password("access_code") - custom_uat = self.custom_uat - - action_url = ( - "https://spayuattrns.bmtest.om/transaction.do?command=initiateTransaction" - if custom_uat == "Staging" - else "https://smartpaytrns.bankmuscat.com/transaction.do?command=initiateTransaction" - ) + # Gateway url + def get_payment_page_url(self, **kwargs): + try: + self.validate_mandatory_values(**kwargs) + encrypted_req = self.encrypt(self.get_merchant_data(**kwargs), self.get_password("working_key")) + xscode = self.get_password("access_code") + + action_url = ( + "https://spayuattrns.bmtest.om/transaction.do?command=initiateTransaction" + if self.custom_uat == "Staging" + else "https://smartpaytrns.bankmuscat.com/transaction.do?command=initiateTransaction" + ) - html = Template( - """
- - - -
""" - ).safe_substitute(encReq=encrypted_req, xscode=xscode, action_url=action_url) + html = Template( + """
+ + + +
""" + ).safe_substitute(encReq=encrypted_req, xscode=xscode, action_url=action_url) - return html - - def get_payment_page_url(self, **kwargs): - return self.get_gateway_url(**kwargs) + return html + except Exception: + frappe.log_error(frappe.get_traceback(), "BankMuscat get_gateway_url Failed") + frappe.throw("Unable to generate BankMuscat payment form. Please check the Error Log.") +# Gateway controller resolver def get_gateway_controller(doctype, docname, payment_gateway=None): if not payment_gateway: reference_doc = frappe.get_doc(doctype, docname) payment_gateway = reference_doc.payment_gateway + return frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") +# store custom name without hyphen if Payment Gateway is BankMuscat def store_custom_name(doc, method=None): - doc.db_set("custom_name", doc.name.replace("-", "")) + try: + if "BankMuscat" not in (doc.payment_gateway_account or ""): + return # skip silently + + if not hasattr(doc, "name"): + frappe.throw(_("Document has no name attribute to generate custom_name.")) + + clean_name = doc.name.replace("-", "") + doc.db_set("custom_name", clean_name) + + except Exception: + frappe.log_error( + message=frappe.get_traceback(), + title=f"Failed to set custom_name for Payment Request {getattr(doc, 'name', 'Unknown')}", + ) + frappe.throw( + _("Unable to store custom name for Payment Request {0}. Please check the Error Log.").format( + getattr(doc, "name", "Unknown") + ), + title=_("Custom Name Error"), + ) diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index d88e6e5c..0d623163 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -437,7 +437,6 @@ def get_payment_status(payment_request): parsed_response["enc_response"], bankmuscat_settings.get_password("working_key") ) decrypted_json = json.loads(decrypted_data) - # Payment status handling return handle_payment_response(decrypted_json, "Payment Request", payment_request) diff --git a/payments/utils/custom_fields.py b/payments/utils/custom_fields.py new file mode 100644 index 00000000..6e69ae55 --- /dev/null +++ b/payments/utils/custom_fields.py @@ -0,0 +1,7 @@ +import frappe + +from payments.utils.utils import make_custom_fields + + +def execute(): + make_custom_fields() diff --git a/payments/utils/utils.py b/payments/utils/utils.py index bebbb37a..a4396246 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -157,6 +157,30 @@ def make_custom_fields(): frappe.clear_cache(doctype="Web Form") + if not frappe.get_meta("Payment Request").has_field("custom_name"): + print("yess") + click.secho("* Installing Payment Custom Fields in Payment Request") + create_custom_fields( + { + "Payment Request": [ + { + "fieldname": "custom_name", + "fieldtype": "Data", + "label": "Name", + "insert_after": "naming_series", + }, + { + "fieldname": "custom_payment_reference_no", + "fieldtype": "Data", + "label": "Payment Reference No", + "insert_after": "transaction_date", + }, + ] + } + ) + + frappe.clear_cache(doctype="Payment Request") + if "erpnext" in frappe.get_installed_apps(): custom_fields = { "GoCardless Mandate": [ @@ -192,10 +216,6 @@ def delete_custom_fields(): "amount", "currency", ], - "Payment Request": [ - "custom_name", - "custom_payment_reference_no", - ], } for doctype, fieldnames in custom_fields.items(): @@ -205,25 +225,19 @@ def delete_custom_fields(): frappe.clear_cache(doctype="Web Form") + if frappe.get_meta("Payment Request").has_field("custom_name"): + click.secho("* Uninstalling Payment Custom Fields from Payment Request") + + payment_request_fields = [ + "custom_name", + "custom_payment_reference_no", + ] + + frappe.db.delete( + "Custom Field", {"name": ["in", [f"Payment Request-{field}" for field in payment_request_fields]]} + ) -custom_fields = { - "Web Form": [ - "payments_tab", - "accept_payment", - "payment_gateway", - "payment_button_label", - "payment_button_help", - "payments_cb", - "amount_field", - "amount_based_on_field", - "amount", - "currency", - ], - "Payment Request": [ - "custom_name", - "custom_payment_reference_no", - ], -} + frappe.clear_cache(doctype="Payment Request") def before_install(): From e3579e5b442e58e1c7b649c0455137cc112700b3 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Wed, 1 Oct 2025 13:54:27 +0530 Subject: [PATCH 12/22] feat: add a batch for create custom fields --- payments/patches.txt | 2 +- .../doctype/bankmuscat_settings/bankmuscat_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/patches.txt b/payments/patches.txt index 15bfeae5..e7be96a3 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -1,4 +1,4 @@ [pre_model_sync] [post_model_sync] -payments.utils.custom_fields \ No newline at end of file +payments.utils.custom_fields #8 \ No newline at end of file diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index 7259dcfa..7c0f0514 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -11,7 +11,7 @@ class BankMuscatSettings(Document): - supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR", "INR") + supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR") # Create Payment Gateway on save def on_update(self): From 916729c0732449e9de0425bf1ccc2807587864fe Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Mon, 17 Nov 2025 12:53:23 +0530 Subject: [PATCH 13/22] feat: add a payment url activity log --- .../bankmuscat_settings.json | 118 ++++----- .../bankmuscat_settings.py | 233 +++++++++++------- payments/payments/doctype/__init__.py | 0 .../payment_url_activity_log/__init__.py | 0 .../payment_url_activity_log.js | 8 + .../payment_url_activity_log.json | 144 +++++++++++ .../payment_url_activity_log.py | 94 +++++++ .../test_payment_url_activity_log.py | 22 ++ .../templates/includes/bankmuscat_checkout.js | 5 + .../templates/pages/bankmuscat_checkout.py | 166 ++++++++----- payments/templates/pages/payment-failed.html | 2 +- payments/utils/utils.py | 1 - 12 files changed, 585 insertions(+), 208 deletions(-) create mode 100644 payments/payments/doctype/__init__.py create mode 100644 payments/payments/doctype/payment_url_activity_log/__init__.py create mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js create mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json create mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py create mode 100644 payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json index fa23f54d..12f86752 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.json @@ -1,56 +1,64 @@ { - "actions": [], - "allow_rename": 1, - "autoname": "field:merchant_id", - "creation": "2025-01-16 14:46:37.045855", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "merchant_id", - "access_code", - "working_key" - ], - "fields": [ - { - "fieldname": "merchant_id", - "fieldtype": "Data", - "label": "Merchant ID", - "unique": 1 - }, - { - "fieldname": "access_code", - "fieldtype": "Password", - "label": "Access Code" - }, - { - "fieldname": "working_key", - "fieldtype": "Password", - "label": "Working Key" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-01-17 15:39:44.858467", - "modified_by": "Administrator", - "module": "Payment Gateways", - "name": "BankMuscat Settings", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] - } \ No newline at end of file + "actions": [], + "allow_rename": 1, + "autoname": "field:merchant_id", + "creation": "2025-01-16 14:46:37.045855", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "merchant_id", + "access_code", + "working_key", + "base_url" + ], + "fields": [ + { + "fieldname": "merchant_id", + "fieldtype": "Data", + "label": "Merchant ID", + "unique": 1 + }, + { + "fieldname": "access_code", + "fieldtype": "Password", + "label": "Access Code" + }, + { + "fieldname": "working_key", + "fieldtype": "Password", + "label": "Working Key" + }, + { + "description": "https://{sandbox or prod domain}", + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-11-04 14:32:40.445494", + "modified_by": "Administrator", + "module": "Payment Gateways", + "name": "BankMuscat Settings", + "naming_rule": "By fieldname", + "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": [] +} diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index 7c0f0514..de664110 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -1,58 +1,79 @@ -from string import Template - import frappe -from Crypto.Cipher import AES +import json from frappe import _ -from frappe.integrations.utils import create_request_log +from string import Template +from Crypto.Cipher import AES from frappe.model.document import Document -from frappe.utils import call_hook_method, get_url - from payments.utils import create_payment_gateway - +from frappe.utils import call_hook_method, get_url +from frappe.integrations.utils import create_request_log +from urllib.parse import parse_qsl class BankMuscatSettings(Document): - supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR") - # Create Payment Gateway on save + supported_currencies = ("OMR", "AED", "USD", "GBP", "EUR", "INR") + + # Validate and create a Payment Gateway entry automatically when BankMuscat Settings is saved def on_update(self): try: - gateway_name = f"BankMuscat-{self.merchant_id}" + if not self.merchant_id: + frappe.throw(_("Merchant ID is required to create a Payment Gateway.")) - # Check if Payment Gateway already exists + gateway_name = f"BankMuscat-{self.merchant_id}" if not frappe.db.exists("Payment Gateway", gateway_name): create_payment_gateway( gateway_name, settings="BankMuscat Settings", controller=self.merchant_id, ) - frappe.logger().info(f"Created new Payment Gateway: {gateway_name}") + frappe.logger().info(f"[BankMuscat] Created new Payment Gateway: {gateway_name}") else: - frappe.logger().info(f"Payment Gateway already exists: {gateway_name}") + frappe.logger().info(f"[BankMuscat] Payment Gateway already exists: {gateway_name}") - # Trigger hook call_hook_method("payment_gateway_enabled", gateway=gateway_name) - except Exception: + error_trace = frappe.get_traceback() frappe.log_error( - message=frappe.get_traceback(), - title=f"BankMuscat Gateway Creation Failed (Merchant: {self.merchant_id})", + message=error_trace, + title=f"BankMuscat Payment Gateway Creation Failed (Merchant: {self.merchant_id or 'Unknown'})" ) + frappe.throw( _( - "Unable to create or update Payment Gateway for Merchant ID {0}. Please check the Error Log." - ).format(self.merchant_id), + "Unable to create or update Payment Gateway for Merchant ID {0}. Please check the Error Log for details." + ).format(self.merchant_id or _("Unknown")), title=_("Payment Gateway Error"), ) + # Validate input and create an Integration Request entry to generate and return the payment URL via email def get_payment_url(self, **kwargs): try: - frappe.logger().info(f"[BankMuscat] get_payment_url args: {kwargs}") + if not kwargs or not isinstance(kwargs, dict): + frappe.throw("Missing or invalid parameters for BankMuscat payment request.") + + required_fields = ["amount", "reference_doctype", "reference_docname", "currency", "payment_gateway"] + missing_fields = [field for field in required_fields if field not in kwargs or not kwargs[field]] + if missing_fields: + frappe.throw(f"Missing required fields: {', '.join(missing_fields)}") + self.order_id = create_request_log( - kwargs, service_name="BankMuscat", name=kwargs.get("order_id", "") + data=kwargs, + service_name="BankMuscat", + name=kwargs.get("order_id", "") ).name + return get_url(f"bankmuscat_checkout?order_id={self.order_id}") except Exception: - frappe.log_error(frappe.get_traceback(), "BankMuscat get_payment_url Failed") + error_message = frappe.get_traceback() + frappe.log_error(error_message, "BankMuscat get_payment_url Failed") + if getattr(self, "order_id", None): + order_name = getattr(self.order_id, "name", self.order_id) + frappe.db.set_value( + "Integration Request", + order_name, + "error", + error_message[:1000] + ) frappe.throw("Unable to generate payment URL. Please check the Error Log.") def decrypt(self, cipher_text, working_key): @@ -69,57 +90,62 @@ def encrypt(self, plain_text, working_key): try: cipher = AES.new(working_key.encode(), AES.MODE_GCM) ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode()) - return (cipher.nonce + ciphertext + tag).hex() + encrypted_hex = (cipher.nonce + ciphertext + tag).hex() + return encrypted_hex except Exception: frappe.log_error(frappe.get_traceback(), "BankMuscat Encryption Failed") - frappe.throw("Unable to encrypt request for BankMuscat.") + frappe.throw(_("Unable to encrypt request for BankMuscat. Please check the Error Log.")) + # Prepare and return the merchant data string required by BankMuscat gateway. def get_merchant_data(self, **kwargs): - # Base URL for redirect and cancel URLs - base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") - - # mandatory fields - merchant_data = { - "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), - "order_id": (kwargs.get("order_id") or str(self.order_id)).replace("-", ""), - "currency": kwargs.get("currency") or "OMR", - "amount": str(kwargs.get("amount", "")), - "redirect_url": f"{base_url}.verify_payment_status", - "cancel_url": f"{base_url}.cancel_payment", - "integration_type": kwargs.get("integration_type", "iframe_normal"), - } - - # add optional fields - optional_fields = [ - "language", - "billing_name", - "billing_address", - "billing_city", - "billing_state", - "billing_zip", - "billing_country", - "billing_tel", - "billing_email", - "delivery_name", - "delivery_address", - "delivery_city", - "delivery_state", - "delivery_zip", - "delivery_country", - "delivery_tel", - "merchant_param1", - "merchant_param2", - "merchant_param3", - "merchant_param4", - "merchant_param5", - "promo_code", - "customer_identifier", - ] - - merchant_data.update({field: kwargs.get(field, "") for field in optional_fields}) - - return "&".join(f"{key}={value}" for key, value in merchant_data.items()) - + try: + # Base URL for redirect and cancel URLs + base_url = get_url("api/method/payments.templates.pages.bankmuscat_checkout") + + if not base_url: + frappe.throw(_("Unable to generate redirect base URL for BankMuscat checkout.")) + + order_id = (kwargs.get("order_id") or str(getattr(self, "order_id", ""))).replace("-", "") + if not order_id: + frappe.throw(_("Missing 'order_id' in payment data.")) + + amount = kwargs.get("amount") + if not amount: + frappe.throw(_("Missing 'amount' for BankMuscat transaction.")) + + currency = kwargs.get("currency") or "OMR" + if not currency: + frappe.throw(_("Missing 'currency' in payment data.")) + + merchant_data = { + "merchant_id": kwargs.get("merchant_id", str(self.merchant_id)), + "order_id": order_id, + "currency": currency, + "amount": str(amount), + "redirect_url": f"{base_url}.verify_payment_status", + "cancel_url": f"{base_url}.cancel_payment", + "integration_type": kwargs.get("integration_type", "iframe_normal"), + } + + # add optional fields + optional_fields = [ + "language", + "billing_name","billing_address","billing_city","billing_state", + "billing_zip","billing_country","billing_tel","billing_email", + "delivery_name","delivery_address","delivery_city","delivery_state", + "delivery_zip","delivery_country","delivery_tel", + "merchant_param1","merchant_param2","merchant_param3","merchant_param4","merchant_param5", + "promo_code","customer_identifier", + ] + + merchant_data.update({field: kwargs.get(field, "") for field in optional_fields}) + + return "&".join(f"{key}={value}" for key, value in merchant_data.items()) + except Exception as e: + frappe.log_error(frappe.get_traceback(), "BankMuscat: get_merchant_data Failed") + frappe.throw(_("Unable to prepare merchant data for BankMuscat. Please check the Error Log.")) + + # Validate that the provided currency is supported by BankMuscat. def validate_transaction_currency(self, currency): if not currency: frappe.throw(_("Currency is missing")) @@ -130,42 +156,73 @@ def validate_transaction_currency(self, currency): ).format(currency) ) - # validate mandatory values + # Validate all mandatory fields required for processing a BankMuscat transaction def validate_mandatory_values(self, **kwargs): self.validate_transaction_currency(kwargs.get("currency")) - if not kwargs.get("amount"): - frappe.throw("Amount is missing") + amount = kwargs.get("amount") + if not amount: + frappe.throw(_("Amount is missing")) + elif float(amount) <= 0: + frappe.throw(_("Amount must be greater than zero")) - if not kwargs.get("order_id") and not getattr(self, "order_id", None): - frappe.throw("Param order_id is missing") + order_id = kwargs.get("order_id") or getattr(self, "order_id", None) + if not order_id: + frappe.throw(_("Parameter 'order_id' is missing")) - # Gateway url - def get_payment_page_url(self, **kwargs): + # Generate the BankMuscat payment page URL and return an auto-submitting HTML form. + def get_payment_page_url(self, log, **kwargs): try: self.validate_mandatory_values(**kwargs) - encrypted_req = self.encrypt(self.get_merchant_data(**kwargs), self.get_password("working_key")) + + merchant_data = self.get_merchant_data(**kwargs) + + working_key = self.get_password("working_key") + + if not working_key: + frappe.throw(_("Missing 'working_key' in BankMuscat Settings.")) + + encrypted_req = self.encrypt(merchant_data, working_key) + + frappe.logger().info(f"[BankMuscat] Encrypted request generated for Order ID: {kwargs.get('order_id')}") + xscode = self.get_password("access_code") + + base_url = self.base_url - action_url = ( - "https://spayuattrns.bmtest.om/transaction.do?command=initiateTransaction" - if self.custom_uat == "Staging" - else "https://smartpaytrns.bankmuscat.com/transaction.do?command=initiateTransaction" - ) + if not base_url: + frappe.throw(_("Base URL is not configured in BankMuscat Settings.")) + + if not xscode: + frappe.throw(_("Access code is missing in BankMuscat Settings.")) + + action_url = f"{base_url}/transaction.do?command=initiateTransaction" html = Template( - """
+ """ + -
""" - ).safe_substitute(encReq=encrypted_req, xscode=xscode, action_url=action_url) + + """ + ).safe_substitute( + encReq=encrypted_req, + xscode=xscode, + action_url=action_url + ) + log.payload_before_encryption = encrypted_req + log.payment_url = action_url + log.request_data = json.dumps(merchant_data, indent=2) + log.save(ignore_permissions=True) + return html except Exception: frappe.log_error(frappe.get_traceback(), "BankMuscat get_gateway_url Failed") - frappe.throw("Unable to generate BankMuscat payment form. Please check the Error Log.") - + frappe.throw( + _("Unable to generate BankMuscat payment form. Please check the Error Log for details.") + ) # Gateway controller resolver def get_gateway_controller(doctype, docname, payment_gateway=None): @@ -180,7 +237,7 @@ def get_gateway_controller(doctype, docname, payment_gateway=None): def store_custom_name(doc, method=None): try: if "BankMuscat" not in (doc.payment_gateway_account or ""): - return # skip silently + return if not hasattr(doc, "name"): frappe.throw(_("Document has no name attribute to generate custom_name.")) diff --git a/payments/payments/doctype/__init__.py b/payments/payments/doctype/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/payments/doctype/payment_url_activity_log/__init__.py b/payments/payments/doctype/payment_url_activity_log/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js new file mode 100644 index 00000000..7127133d --- /dev/null +++ b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Payment URL Activity Log", { +// refresh(frm) { + +// }, +// }); diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json new file mode 100644 index 00000000..176c0142 --- /dev/null +++ b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json @@ -0,0 +1,144 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2025-11-16 20:02:20.827105", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "order_id", + "payment_request", + "request_url", + "resource_payload", + "access_count", + "access_type", + "status_before", + "request_data", + "column_break_lati", + "access_timestamp", + "integration_request", + "payment_url", + "payload_before_encryption", + "ip_address", + "session_id", + "user_agent" + ], + "fields": [ + { + "fieldname": "order_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Order ID" + }, + { + "fieldname": "payment_request", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Request", + "options": "Payment Request" + }, + { + "fieldname": "integration_request", + "fieldtype": "Link", + "label": "Integration Request", + "options": "Integration Request" + }, + { + "fieldname": "payment_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Payment URL" + }, + { + "fieldname": "access_timestamp", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Access Timestamp" + }, + { + "default": "Clicked", + "fieldname": "access_type", + "fieldtype": "Select", + "label": "Access Type", + "options": "Clicked\nGateway Redirect\nSystem Check" + }, + { + "default": "1", + "fieldname": "access_count", + "fieldtype": "Int", + "label": "Access Count" + }, + { + "fieldname": "ip_address", + "fieldtype": "Data", + "label": "IP Address" + }, + { + "fieldname": "user_agent", + "fieldtype": "Small Text", + "label": "User Agent" + }, + { + "fieldname": "session_id", + "fieldtype": "Data", + "label": "Session ID" + }, + { + "fieldname": "status_before", + "fieldtype": "Data", + "label": "Status Before Access" + }, + { + "fieldname": "payload_before_encryption", + "fieldtype": "Small Text", + "label": "Encrypted Payload" + }, + { + "fieldname": "resource_payload", + "fieldtype": "Small Text", + "label": "Resource payload" + }, + { + "fieldname": "column_break_lati", + "fieldtype": "Column Break" + }, + { + "fieldname": "request_url", + "fieldtype": "Data", + "label": "Request URL" + }, + { + "fieldname": "request_data", + "fieldtype": "Long Text", + "label": "Request Data" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-11-16 21:49:04.047698", + "modified_by": "Administrator", + "module": "Payments", + "name": "Payment URL Activity Log", + "naming_rule": "Autoincrement", + "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", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py new file mode 100644 index 00000000..20baf9b8 --- /dev/null +++ b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py @@ -0,0 +1,94 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import get_request_header +from frappe.utils import now_datetime, time_diff_in_seconds + + + + + +class PaymentURLActivityLog(Document): + pass + + +def create_payment_url_activity_log( + order_id, payment_request, integration_request=None, access_type="Clicked", resource_payload=None +): + try: + request = frappe.local.request + + ip_address = request.remote_addr if request else None + + user_agent = get_request_header("User-Agent") + + referrer_url = get_request_header("Referer") + + session_id = frappe.session.sid if hasattr(frappe.session, "sid") else None + + status_before = frappe.db.get_value("Payment Request", payment_request, "status") + + existing_log_name = frappe.db.get_value( + "Payment URL Activity Log", + { + "order_id": order_id, + "payment_request": payment_request, + "access_type": access_type, + }, + ) + + if existing_log_name: + log = frappe.get_doc("Payment URL Activity Log", existing_log_name) + + last_time = log.access_timestamp + if isinstance(last_time, str): + from datetime import datetime + last_time = datetime.fromisoformat(last_time) + + current_time = now_datetime() + difference = current_time - last_time + minutes_passed = difference.total_seconds() / 60 + + if minutes_passed < 45: + message = ( + f"As per Bank Muscat regulations, this payment link can only be used after the mandatory waiting period. " + f"Please try again after {45 - int(minutes_passed)} minutes." + ) + return None, message + + log.access_count = (log.access_count or 0) + 1 + log.access_timestamp = now_datetime() + log.ip_address = ip_address + log.user_agent = user_agent + log.referrer_url = referrer_url + log.session_id = session_id + log.status_before = status_before + + log.save(ignore_permissions=True) + + return log, None + + log = frappe.new_doc("Payment URL Activity Log") + log.order_id = order_id + log.access_timestamp = now_datetime() + log.payment_request = payment_request + log.integration_request = integration_request + log.request_url = referrer_url + log.payment_url = "" + log.resource_payload = resource_payload + log.payload_before_encryption = "" + log.access_count = 1 + log.access_type = access_type + log.status_before = status_before + log.ip_address = ip_address + log.session_id = session_id + log.user_agent = user_agent + log.insert(ignore_permissions=True) + + return log, None + + except Exception as e: + frappe.log_error(e.message) + return None diff --git a/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py b/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py new file mode 100644 index 00000000..0350c6fa --- /dev/null +++ b/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py @@ -0,0 +1,22 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestPaymentURLActivityLog(IntegrationTestCase): + """ + Integration tests for PaymentURLActivityLog. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js index 6bdff1cd..2a55a542 100644 --- a/payments/templates/includes/bankmuscat_checkout.js +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -32,6 +32,11 @@ $(document).ready(function() { console.error("Error: No form found in payment_url"); } } + if (r && r.message && r.message.msg && r.message.url) { + const urlWithMsg = `${r.message.url}?msg=${encodeURIComponent(r.message.msg)}`; + window.location.href = urlWithMsg; + } + }, error: function(err) { console.error("API call failed:", err); diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 0d623163..40619a10 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -1,77 +1,122 @@ import json -from urllib.parse import parse_qsl, urlencode - import frappe import requests from frappe import _ -from frappe.integrations.utils import create_request_log +from urllib.parse import parse_qsl, urlencode from frappe.utils import flt, get_url, getdate - +from frappe.integrations.utils import create_request_log from payments.payment_gateways.doctype.bankmuscat_settings.bankmuscat_settings import ( - BankMuscatSettings as object, + BankMuscatSettings as BankMuscat, + get_gateway_controller ) -from payments.payment_gateways.doctype.bankmuscat_settings.bankmuscat_settings import get_gateway_controller - +from payments.payments.doctype.payment_url_activity_log.payment_url_activity_log import create_payment_url_activity_log +# Check if any previously created Integration Request has status = "Completed"; +# if yes, return the payment success page URL def check_already_payment_processed(request, reference_doctype, reference_docname): - if frappe.db.get_value("Integration Request", request, "status") == "Completed": - redirect_url = "payment-success" - redirect_url += "?" + urlencode({"doctype": reference_doctype}) - redirect_url += "&" + urlencode({"docname": reference_docname}) - return {"payment_url": get_url(redirect_url)} + status = frappe.db.get_value("Integration Request", request, "status") + if status != "Completed": + return None - return None + params = urlencode({ + "doctype": reference_doctype, + "docname": reference_docname + }) + redirect_url = f"payment-success?{params}" + return {"payment_url": get_url(redirect_url)} +# Fetch and return the Bank Muscat payment URL for a valid Integration Request. @frappe.whitelist(allow_guest=True) def get_payment_url(data=None): - if isinstance(data, str): - data = frappe.parse_json(data) + try: + if isinstance(data, str): + data = frappe.parse_json(data or "{}") + + data = frappe._dict(data or {}) + + if not data.get("order_id"): + frappe.throw(_("Order ID not found in request."), title="Invalid Request") + + integration_request = frappe.db.exists("Integration Request", data.order_id) + if not integration_request: + if not integration_request: + frappe.throw(_("Invalid Order ID. No Integration Request found.")) + + order_data = frappe.db.get_value("Integration Request", integration_request, "data") + if not order_data: + frappe.throw( + _("Integration Request data is missing."), + title=_("Payment Failed") + ) + + order_details = frappe._dict(frappe.parse_json(order_data)) + + log, msg = create_payment_url_activity_log( + order_id=data.order_id, + payment_request=data.order_id, + integration_request=data.order_id, + resource_payload=order_details, + access_type="System Check" + ) - data = frappe._dict(data) + if not log and msg: + return {"msg": msg, "url": get_url("/payment-failed")} - if not data.order_id: - frappe.respond_as_web_page(_("Invalid Request"), _("Order ID not found"), http_status_code=404) - return + + reference_doctype = order_details.get("reference_doctype") + + reference_docname = order_details.get("reference_docname") - if request := frappe.db.exists("Integration Request", data.order_id): - order_details = frappe.parse_json(frappe.db.get_value("Integration Request", request, "data")) - order_details = frappe._dict(order_details) - reference_doctype, reference_docname = ( - order_details.reference_doctype, - order_details.reference_docname, + redirect_url = check_already_payment_processed( + integration_request, reference_doctype, reference_docname ) - if redirect_url := check_already_payment_processed(request, reference_doctype, reference_docname): + if redirect_url: return redirect_url - if reference_doctype and reference_docname: - payment_gateway = frappe.db.get_value(reference_doctype, reference_docname, "payment_gateway") - if payment_gateway: - gateway_controller = get_gateway_controller( - reference_doctype, reference_docname, payment_gateway - ) - if gateway_controller: - try: - gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) - return {"payment_url": gateway_doc.get_payment_page_url(**order_details)} - except Exception: - frappe.log_error( - "Error while getting payment url..", frappe.get_traceback(with_context=1) - ) - frappe.respond_as_web_page( - _("Payment Failed"), _("Error while getting payment url"), http_status_code=500 - ) - else: - frappe.respond_as_web_page(_("Payment Failed"), _("Order ID not found"), http_status_code=404) - return + if not (reference_doctype and reference_docname): + frappe.throw( + _("Reference document details are missing."), + title=_("Invalid Request") + ) + + payment_gateway = frappe.db.get_value(reference_doctype, reference_docname, "payment_gateway") + if not payment_gateway: + frappe.throw( + _("Payment Gateway not linked to this transaction."), + title=_("Payment Failed") + ) + + gateway_controller = get_gateway_controller(reference_doctype, reference_docname, payment_gateway) + if not gateway_controller: + frappe.throw( + _("Unable to identify payment gateway controller."), + title=_("Payment Failed") + ) + gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) + + payment_url = gateway_doc.get_payment_page_url(**order_details, log=log) + if not payment_url: + frappe.throw( + _("Unable to generate payment URL. Please try again later."), + title=_("Payment Failed") + ) + return {"payment_url": payment_url} + + except Exception as e: + frappe.log_error( + title="BankMuscat: Payment URL Generation Failed", + message=frappe.get_traceback(with_context=True), + ) + frappe.throw(e.message) + +# Route the UI page based on the response def handle_payment_response(data_dict, reference_doctype, reference_docname): data = frappe._dict(data_dict) - frappe.logger().info(f"BankMuscat Response: {json.dumps(data, indent=2)}") - order_no = data.get("order_id") or data.get("order_no") doc_name = frappe.get_value("Payment Request", {"custom_name": order_no}) @@ -135,7 +180,7 @@ def handle_payment_response(data_dict, reference_doctype, reference_docname): except Exception: frappe.log_error("Error while processing payment response", frappe.get_traceback()) - +# Update the Payment Request and Payment Entry based on the response def handle_payment_page_response( payment_request, gateway_controller, data, payment_gateway_account, kwargs=None, integration_request=False ): @@ -221,7 +266,7 @@ def handle_payment_page_response( frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url(redirect_url) - +# Validate response data @frappe.whitelist(allow_guest=True) def verify_payment_status(): data = frappe.form_dict @@ -356,7 +401,7 @@ def redirect_response(page, doctype=None, docname=None): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url(url) - +# Check payment status daily and update the status def check_payment_status(): payment_requests = frappe.get_all( "Payment Request", @@ -382,21 +427,19 @@ def check_payment_status(): message=f"Error calling get_payment_status for {pr.name}: {frappe.get_traceback()}", ) - +# Check payment status in Payment Request @frappe.whitelist() def get_payment_status(payment_request): try: doc = frappe.get_doc("Payment Request", payment_request) - reference_no = doc.custom_payment_reference_no - order_no = doc.name - if not reference_no or not order_no: + if not reference_no or not doc.name: frappe.throw("Missing order number or reference number") # Prepare payload request_payload = { - "order_id": order_no, + "order_id": doc.name, "reference_no": reference_no, } json_data = json.dumps(request_payload) @@ -408,8 +451,9 @@ def get_payment_status(payment_request): encrypted_data = bankmuscat_settings.encrypt( json_data, bankmuscat_settings.get_password("working_key") ) - access_code = bankmuscat_settings.get_password("access_code") + access_code = bankmuscat_settings.get_password("access_code") + payload = { "enc_request": encrypted_data, "access_code": access_code, @@ -419,12 +463,8 @@ def get_payment_status(payment_request): "version": "1.2", } - SMARTPAY_URL = ( - "https://spayuatapi.bmtest.om/apis/servlet/DoWebTrans?" - if bankmuscat_settings.custom_uat == "Staging" - else "https://smartpayapi.bankmuscat.com/apis/servlet/DoWebTrans?" - ) - + SMARTPAY_URL = f"{bankmuscat_settings.base_url}/apis/servlet/DoWebTrans?" + # Make request to SmartPay response = requests.post(SMARTPAY_URL, data=payload) parsed_response = dict(parse_qsl(response.text)) diff --git a/payments/templates/pages/payment-failed.html b/payments/templates/pages/payment-failed.html index 8f416b56..0f517605 100644 --- a/payments/templates/pages/payment-failed.html +++ b/payments/templates/pages/payment-failed.html @@ -8,7 +8,7 @@ {{ _("Payment Failed") }} -

{{ _("Your payment has failed.") }}

+

{{ frappe.form_dict.msg or "Your payment has failed." }}

diff --git a/payments/utils/utils.py b/payments/utils/utils.py index a4396246..e6175f7f 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -158,7 +158,6 @@ def make_custom_fields(): frappe.clear_cache(doctype="Web Form") if not frappe.get_meta("Payment Request").has_field("custom_name"): - print("yess") click.secho("* Installing Payment Custom Fields in Payment Request") create_custom_fields( { From 2055a4be7a0c42429ed68f126c20d9e8e108ac10 Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Mon, 17 Nov 2025 19:32:16 +0530 Subject: [PATCH 14/22] fix: checking order id on payment processing time --- payments/templates/includes/bankmuscat_checkout.js | 3 +++ payments/templates/pages/bankmuscat_checkout.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js index 2a55a542..d5f13fe2 100644 --- a/payments/templates/includes/bankmuscat_checkout.js +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -8,6 +8,9 @@ $(document).ready(function() { return; } + console.log("data:",data) + console.log("id:",data.order_id) + frappe.call({ method: "payments.templates.pages.bankmuscat_checkout.get_payment_url", freeze: true, diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 40619a10..19f173e0 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -29,6 +29,7 @@ def check_already_payment_processed(request, reference_doctype, reference_docnam # Fetch and return the Bank Muscat payment URL for a valid Integration Request. @frappe.whitelist(allow_guest=True) def get_payment_url(data=None): + frappe.log_error(title="get data",message=data) try: if isinstance(data, str): data = frappe.parse_json(data or "{}") @@ -52,6 +53,8 @@ def get_payment_url(data=None): order_details = frappe._dict(frappe.parse_json(order_data)) + frappe.log_error(title="log",message=f"order id ->{data.order_id},payment request->{data.order_id}") + log, msg = create_payment_url_activity_log( order_id=data.order_id, payment_request=data.order_id, From 925e28db376659314a64241781d8bf4a35aed11b Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Tue, 18 Nov 2025 00:05:34 +0530 Subject: [PATCH 15/22] fix: fixed the cache issue --- .../templates/includes/bankmuscat_checkout.js | 17 ++++++++++++----- payments/templates/pages/bankmuscat_checkout.py | 3 --- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js index d5f13fe2..7a52b153 100644 --- a/payments/templates/includes/bankmuscat_checkout.js +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -1,15 +1,22 @@ $(document).ready(function() { + const order_id = new URLSearchParams(window.location.search).get("order_id"); - var data = {{ frappe.form_dict | json }}; // Get data from backend + console.log("ORDER ID (from client URL):", order_id); - if (!data || !data.order_id) { + if (!order_id) { console.error("Error: Missing order_id"); return; } - console.log("data:",data) - console.log("id:",data.order_id) + // var data = {{ frappe.form_dict | json }}; // Get data from backend + + // if (!data || !data.order_id) { + // console.error("Error: Missing order_id"); + // return; + // } + + console.log("id:",order_id) frappe.call({ method: "payments.templates.pages.bankmuscat_checkout.get_payment_url", @@ -19,7 +26,7 @@ $(document).ready(function() { }, args: { data: { - "order_id": data.order_id + "order_id": order_id } }, callback: function(r) { diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 19f173e0..40619a10 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -29,7 +29,6 @@ def check_already_payment_processed(request, reference_doctype, reference_docnam # Fetch and return the Bank Muscat payment URL for a valid Integration Request. @frappe.whitelist(allow_guest=True) def get_payment_url(data=None): - frappe.log_error(title="get data",message=data) try: if isinstance(data, str): data = frappe.parse_json(data or "{}") @@ -53,8 +52,6 @@ def get_payment_url(data=None): order_details = frappe._dict(frappe.parse_json(order_data)) - frappe.log_error(title="log",message=f"order id ->{data.order_id},payment request->{data.order_id}") - log, msg = create_payment_url_activity_log( order_id=data.order_id, payment_request=data.order_id, From c9489cfa9442a887e8448ce2993a80810e889196 Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Tue, 18 Nov 2025 00:17:16 +0530 Subject: [PATCH 16/22] refactor: remove the commanded code --- payments/templates/includes/bankmuscat_checkout.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js index 7a52b153..2ce27e05 100644 --- a/payments/templates/includes/bankmuscat_checkout.js +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -9,13 +9,6 @@ $(document).ready(function() { return; } - // var data = {{ frappe.form_dict | json }}; // Get data from backend - - // if (!data || !data.order_id) { - // console.error("Error: Missing order_id"); - // return; - // } - console.log("id:",order_id) frappe.call({ From 301cb115e160ee0e278d3e21d268fbc01f8f8c80 Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Tue, 18 Nov 2025 02:09:32 +0530 Subject: [PATCH 17/22] fix: resolve duplicate entry issue for Bank Muscat integration --- payments/patches.txt | 2 +- .../bankmuscat_settings.py | 7 +- .../payment_url_activity_log/__init__.py | 0 .../payment_url_activity_log.js | 8 - .../payment_url_activity_log.json | 144 ------------------ .../payment_url_activity_log.py | 94 ------------ .../test_payment_url_activity_log.py | 22 --- .../templates/includes/bankmuscat_checkout.js | 5 - .../templates/pages/bankmuscat_checkout.py | 73 +++++++-- payments/utils/utils.py | 48 ++++-- 10 files changed, 97 insertions(+), 306 deletions(-) delete mode 100644 payments/payments/doctype/payment_url_activity_log/__init__.py delete mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js delete mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json delete mode 100644 payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py delete mode 100644 payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py diff --git a/payments/patches.txt b/payments/patches.txt index e7be96a3..c0df302d 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -1,4 +1,4 @@ [pre_model_sync] [post_model_sync] -payments.utils.custom_fields #8 \ No newline at end of file +payments.utils.custom_fields #9 \ No newline at end of file diff --git a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py index de664110..dabde81f 100644 --- a/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py +++ b/payments/payment_gateways/doctype/bankmuscat_settings/bankmuscat_settings.py @@ -171,7 +171,7 @@ def validate_mandatory_values(self, **kwargs): frappe.throw(_("Parameter 'order_id' is missing")) # Generate the BankMuscat payment page URL and return an auto-submitting HTML form. - def get_payment_page_url(self, log, **kwargs): + def get_payment_page_url(self, **kwargs): try: self.validate_mandatory_values(**kwargs) @@ -211,11 +211,6 @@ def get_payment_page_url(self, log, **kwargs): xscode=xscode, action_url=action_url ) - - log.payload_before_encryption = encrypted_req - log.payment_url = action_url - log.request_data = json.dumps(merchant_data, indent=2) - log.save(ignore_permissions=True) return html except Exception: diff --git a/payments/payments/doctype/payment_url_activity_log/__init__.py b/payments/payments/doctype/payment_url_activity_log/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js deleted file mode 100644 index 7127133d..00000000 --- a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Payment URL Activity Log", { -// refresh(frm) { - -// }, -// }); diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json deleted file mode 100644 index 176c0142..00000000 --- a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "actions": [], - "autoname": "autoincrement", - "creation": "2025-11-16 20:02:20.827105", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "order_id", - "payment_request", - "request_url", - "resource_payload", - "access_count", - "access_type", - "status_before", - "request_data", - "column_break_lati", - "access_timestamp", - "integration_request", - "payment_url", - "payload_before_encryption", - "ip_address", - "session_id", - "user_agent" - ], - "fields": [ - { - "fieldname": "order_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Order ID" - }, - { - "fieldname": "payment_request", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Payment Request", - "options": "Payment Request" - }, - { - "fieldname": "integration_request", - "fieldtype": "Link", - "label": "Integration Request", - "options": "Integration Request" - }, - { - "fieldname": "payment_url", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Payment URL" - }, - { - "fieldname": "access_timestamp", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Access Timestamp" - }, - { - "default": "Clicked", - "fieldname": "access_type", - "fieldtype": "Select", - "label": "Access Type", - "options": "Clicked\nGateway Redirect\nSystem Check" - }, - { - "default": "1", - "fieldname": "access_count", - "fieldtype": "Int", - "label": "Access Count" - }, - { - "fieldname": "ip_address", - "fieldtype": "Data", - "label": "IP Address" - }, - { - "fieldname": "user_agent", - "fieldtype": "Small Text", - "label": "User Agent" - }, - { - "fieldname": "session_id", - "fieldtype": "Data", - "label": "Session ID" - }, - { - "fieldname": "status_before", - "fieldtype": "Data", - "label": "Status Before Access" - }, - { - "fieldname": "payload_before_encryption", - "fieldtype": "Small Text", - "label": "Encrypted Payload" - }, - { - "fieldname": "resource_payload", - "fieldtype": "Small Text", - "label": "Resource payload" - }, - { - "fieldname": "column_break_lati", - "fieldtype": "Column Break" - }, - { - "fieldname": "request_url", - "fieldtype": "Data", - "label": "Request URL" - }, - { - "fieldname": "request_data", - "fieldtype": "Long Text", - "label": "Request Data" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-11-16 21:49:04.047698", - "modified_by": "Administrator", - "module": "Payments", - "name": "Payment URL Activity Log", - "naming_rule": "Autoincrement", - "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", - "rows_threshold_for_grid_search": 20, - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py b/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py deleted file mode 100644 index 20baf9b8..00000000 --- a/payments/payments/doctype/payment_url_activity_log/payment_url_activity_log.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document -from frappe import get_request_header -from frappe.utils import now_datetime, time_diff_in_seconds - - - - - -class PaymentURLActivityLog(Document): - pass - - -def create_payment_url_activity_log( - order_id, payment_request, integration_request=None, access_type="Clicked", resource_payload=None -): - try: - request = frappe.local.request - - ip_address = request.remote_addr if request else None - - user_agent = get_request_header("User-Agent") - - referrer_url = get_request_header("Referer") - - session_id = frappe.session.sid if hasattr(frappe.session, "sid") else None - - status_before = frappe.db.get_value("Payment Request", payment_request, "status") - - existing_log_name = frappe.db.get_value( - "Payment URL Activity Log", - { - "order_id": order_id, - "payment_request": payment_request, - "access_type": access_type, - }, - ) - - if existing_log_name: - log = frappe.get_doc("Payment URL Activity Log", existing_log_name) - - last_time = log.access_timestamp - if isinstance(last_time, str): - from datetime import datetime - last_time = datetime.fromisoformat(last_time) - - current_time = now_datetime() - difference = current_time - last_time - minutes_passed = difference.total_seconds() / 60 - - if minutes_passed < 45: - message = ( - f"As per Bank Muscat regulations, this payment link can only be used after the mandatory waiting period. " - f"Please try again after {45 - int(minutes_passed)} minutes." - ) - return None, message - - log.access_count = (log.access_count or 0) + 1 - log.access_timestamp = now_datetime() - log.ip_address = ip_address - log.user_agent = user_agent - log.referrer_url = referrer_url - log.session_id = session_id - log.status_before = status_before - - log.save(ignore_permissions=True) - - return log, None - - log = frappe.new_doc("Payment URL Activity Log") - log.order_id = order_id - log.access_timestamp = now_datetime() - log.payment_request = payment_request - log.integration_request = integration_request - log.request_url = referrer_url - log.payment_url = "" - log.resource_payload = resource_payload - log.payload_before_encryption = "" - log.access_count = 1 - log.access_type = access_type - log.status_before = status_before - log.ip_address = ip_address - log.session_id = session_id - log.user_agent = user_agent - log.insert(ignore_permissions=True) - - return log, None - - except Exception as e: - frappe.log_error(e.message) - return None diff --git a/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py b/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py deleted file mode 100644 index 0350c6fa..00000000 --- a/payments/payments/doctype/payment_url_activity_log/test_payment_url_activity_log.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies and Contributors -# See license.txt - -# import frappe -from frappe.tests import IntegrationTestCase - - -# On IntegrationTestCase, the doctype test records and all -# link-field test record dependencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - - - -class IntegrationTestPaymentURLActivityLog(IntegrationTestCase): - """ - Integration tests for PaymentURLActivityLog. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/payments/templates/includes/bankmuscat_checkout.js b/payments/templates/includes/bankmuscat_checkout.js index 2ce27e05..fceb2956 100644 --- a/payments/templates/includes/bankmuscat_checkout.js +++ b/payments/templates/includes/bankmuscat_checkout.js @@ -1,16 +1,11 @@ $(document).ready(function() { - const order_id = new URLSearchParams(window.location.search).get("order_id"); - console.log("ORDER ID (from client URL):", order_id); - if (!order_id) { console.error("Error: Missing order_id"); return; } - console.log("id:",order_id) - frappe.call({ method: "payments.templates.pages.bankmuscat_checkout.get_payment_url", freeze: true, diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 40619a10..1970bbd3 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -52,17 +52,13 @@ def get_payment_url(data=None): order_details = frappe._dict(frappe.parse_json(order_data)) - log, msg = create_payment_url_activity_log( - order_id=data.order_id, - payment_request=data.order_id, - integration_request=data.order_id, - resource_payload=order_details, - access_type="System Check" - ) - - if not log and msg: - return {"msg": msg, "url": get_url("/payment-failed")} + condition, msg, status = check_url_usage_status(data.order_id) + if condition: + if status == "pending": + return {"msg": msg, "url": get_url("/payment-failed")} + if status == "completed": + return {"msg": msg, "url": get_url("/payment-success")} reference_doctype = order_details.get("reference_doctype") @@ -97,7 +93,7 @@ def get_payment_url(data=None): gateway_doc = frappe.get_doc("BankMuscat Settings", gateway_controller) - payment_url = gateway_doc.get_payment_page_url(**order_details, log=log) + payment_url = gateway_doc.get_payment_page_url(**order_details) if not payment_url: frappe.throw( _("Unable to generate payment URL. Please try again later."), @@ -485,3 +481,58 @@ def get_payment_status(payment_request): title="get_payment_status", message="BankMuscat Payment Status API Error: frappe.get_traceback()" ) return {"status": "error", "message": frappe.get_traceback()} + +def check_url_usage_status(id): + try: + from frappe.utils import now_datetime + integration_request = frappe.db.get_value( + "Integration Request", + id, + ["status", "url_access_time"], + as_dict=True + ) + + status = integration_request.status + last_access_time = integration_request.url_access_time + + if status != "Completed" and not last_access_time: + frappe.db.set_value( + "Integration Request", + id, + "url_access_time", + now_datetime() + ) + return False, None, None + + elif status != "Completed" and last_access_time: + + from datetime import datetime + if isinstance(last_access_time, str): + last_access_time = datetime.fromisoformat(last_access_time) + + current_time = now_datetime() + diff_minutes = (current_time - last_access_time).total_seconds() / 60 + + if diff_minutes < 45: + remaining = 45 - int(diff_minutes) + + message = _( + "This payment link will be available again after {0} minutes. Please try again later." + ).format(remaining) + + return True, message, "pending" + + return False, None, None + + elif status == "Completed": + message = ( + "This payment has already been completed successfully. No further action is required." + ) + return True, None, "completed" + + except Exception as e: + frappe.log_error( + title="BankMuscat URL Access Check Failed", + message=frappe.get_traceback() + ) + return True, "Oops! Something didn’t work as expected. Please contact our Al Farsi service team for help.", "pending" diff --git a/payments/utils/utils.py b/payments/utils/utils.py index e6175f7f..7cc16415 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -137,26 +137,31 @@ def make_custom_fields(): "options": "Currency", "insert_after": "amount", }, - ], - "Payment Request": [ - { - "fieldname": "custom_name", - "fieldtype": "Data", - "label": "Name", - "insert_after": "naming_series", - }, - { - "fieldname": "custom_payment_reference_no", - "fieldtype": "Data", - "label": "Payment Reference No", - "insert_after": "transaction_date", - }, - ], + ] } ) frappe.clear_cache(doctype="Web Form") + if not frappe.get_meta("Integration Request").has_field("url_access_time"): + click.secho("* Installing URL Access Time Custom Field in Integration Request") + + create_custom_fields( + { + "Integration Request": [ + { + "fieldname": "url_access_time", + "fieldtype": "Datetime", + "label": "Payment URL Access Time", + "insert_after": "status", + "read_only": 1, + } + ] + } + ) + + frappe.clear_cache(doctype="Integration Request") + if not frappe.get_meta("Payment Request").has_field("custom_name"): click.secho("* Installing Payment Custom Fields in Payment Request") create_custom_fields( @@ -238,6 +243,19 @@ def delete_custom_fields(): frappe.clear_cache(doctype="Payment Request") + if frappe.get_meta("Integration Request").has_field("url_access_time"): + click.secho("* Uninstalling Integration Request Custom Fields") + + integration_request_fields = [ + "url_access_time", + ] + + frappe.db.delete( + "Custom Field", + {"name": ["in", [f"Integration Request-{field}" for field in integration_request_fields]]} + ) + + frappe.clear_cache(doctype="Integration Request") def before_install(): # TODO: remove this From e715d1a91da495f48b5b20673bc230185b039caf Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Tue, 18 Nov 2025 02:17:06 +0530 Subject: [PATCH 18/22] fix: remove unwanted import funtion --- payments/templates/pages/bankmuscat_checkout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 1970bbd3..d4506899 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -9,7 +9,6 @@ BankMuscatSettings as BankMuscat, get_gateway_controller ) -from payments.payments.doctype.payment_url_activity_log.payment_url_activity_log import create_payment_url_activity_log # Check if any previously created Integration Request has status = "Completed"; # if yes, return the payment success page URL From 37ba34051a1b559c1fe6314b3d97ea0e115c05ae Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Wed, 19 Nov 2025 12:36:19 +0530 Subject: [PATCH 19/22] fix: resolved the payment entry issue --- payments/patches.txt | 2 +- payments/public/js/payment_request.js | 34 +++++++++ .../templates/pages/bankmuscat_checkout.py | 76 +++++++++++++++++-- payments/utils/utils.py | 39 +++++++++- 4 files changed, 141 insertions(+), 10 deletions(-) diff --git a/payments/patches.txt b/payments/patches.txt index c0df302d..89e67281 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -1,4 +1,4 @@ [pre_model_sync] [post_model_sync] -payments.utils.custom_fields #9 \ No newline at end of file +payments.utils.custom_fields #10 \ No newline at end of file diff --git a/payments/public/js/payment_request.js b/payments/public/js/payment_request.js index f8b2f2cf..c6ddd1c5 100644 --- a/payments/public/js/payment_request.js +++ b/payments/public/js/payment_request.js @@ -7,6 +7,16 @@ frappe.ui.form.on("Payment Request", { const isBankMuscat = firstWord === "bankmuscat"; + frappe.call({ + method: "payments.templates.pages.bankmuscat_checkout.set_payment_entry", + args:{ + "doc_name":frm.doc.name + }, + callback: function(r){ + frm.refresh_field("payment_entry"); + } + }) + if ( isInward && isInitiated && @@ -30,5 +40,29 @@ frappe.ui.form.on("Payment Request", { }); }); } + + if(frm.doc.status === "Paid" && !frm.doc.payment_entry && frm.doc.response_command){ + frappe.call({ + method:"payments.templates.pages.bankmuscat_checkout.check_roles", + args:{}, + callback:function(r){ + if(r.message){ + frm.add_custom_button(__("Create Payment Entry"), function () { + frappe.call({ + method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", + args: { docname: frm.doc.name }, + freeze: true, + callback: function (r) { + if (!r.exc) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + }).addClass("btn-primary"); + } + } + }) + } }, }); diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index d4506899..1062609b 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -122,20 +122,46 @@ def handle_payment_response(data_dict, reference_doctype, reference_docname): # Save tracking ID if not already saved if payment_request.status == "Initiated" and not payment_request.custom_payment_reference_no: - payment_request.db_set("custom_payment_reference_no", data.get("tracking_id")) + payment_request.db_set({ + "transaction_date": getdate(data.get("trans_date")), + "custom_payment_reference_no": data.get("tracking_id"), + "bank_reference_no": data.get("bank_ref_no") + }) order_status = data.get("order_status", "").lower() try: if order_status == "success": - payment_entry = payment_request.set_as_paid() - payment_request.db_set("transaction_status", "The payment has been completed") + msg = _( + "The customer has successfully completed the payment for the requested order. " + "Details: Order ID: {order_id}, Amount: {amount}, Payment Reference: {payment_ref_no}, " + "Bank Reference: {bank_ref_no}, Date: {payment_date}. " + "Please update your records accordingly." + ).format( + order_id=data.get("order_id"), + amount=data.get("amount"), + payment_ref_no=data.get("tracking_id"), + bank_ref_no=data.get("bank_ref_no"), + payment_date=data.get("trans_date") + ) frappe.db.set_value( - "Payment Entry", - payment_entry.name, - {"reference_no": data.get("bank_ref_no"), "reference_date": getdate(data.get("trans_date"))}, + reference_doctype, + reference_docname, + { + "status": "Paid", + "transaction_status": "The payment has been completed", + "response_command": msg, + } ) + # payment_entry = payment_request.set_as_paid() + # payment_request.db_set("transaction_status", "The payment has been completed") + + # frappe.db.set_value( + # "Payment Entry", + # payment_entry.name, + # {"reference_no": data.get("bank_ref_no"), "reference_date": getdate(data.get("trans_date"))}, + # ) frappe.db.set_value( "Integration Request", doc_name, {"status": "Completed", "output": json.dumps(data, indent=4)} @@ -535,3 +561,41 @@ def check_url_usage_status(id): message=frappe.get_traceback() ) return True, "Oops! Something didn’t work as expected. Please contact our Al Farsi service team for help.", "pending" + +@frappe.whitelist() +def set_payment_entry(doc_name): + + exist_doc = frappe.db.get_value( + "Payment Entry", + { "reference_no": doc_name }, + "name" + ) + + if exist_doc: + frappe.db.set_value( + "Payment Request", + doc_name, + "payment_entry", + exist_doc + ) + + return True + +@frappe.whitelist() +def check_roles(): + roles = frappe.get_all( + "DocPerm", + filters={ + "parent": "Payment Entry", + "permlevel": 0, + "create": 1 + }, + fields=["role"] + ) + + user_roles = frappe.get_roles(frappe.session.user) + + has_permission = any(r["role"] in user_roles for r in roles) + + return has_permission + diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 7cc16415..baeb479e 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -171,18 +171,47 @@ def make_custom_fields(): "fieldname": "custom_name", "fieldtype": "Data", "label": "Name", - "insert_after": "naming_series", + "insert_after": "mode_of_payment", + "read_only": 1, + }, + { + "fieldname": "response_command", + "fieldtype": "Small Text", + "label": "Response Command", + "insert_after": "custom_name", + "read_only": 1, }, { "fieldname": "custom_payment_reference_no", "fieldtype": "Data", "label": "Payment Reference No", - "insert_after": "transaction_date", + "insert_after": "failed_reason", + "read_only": 1, + }, + { + "fieldname": "bank_reference_no", + "fieldtype": "Data", + "label": "Bank Reference No", + "insert_after": "custom_payment_reference_no", + "read_only": 1, + }, + { + "fieldname": "transaction_status", + "fieldtype": "Data", + "label": "Transaction status", + "insert_after": "party_account_currency", + "read_only": 1 }, + { + "fieldname": "payment_entry", + "fieldtype": "Data", + "label": "Payment Entry", + "insert_after": "bank_reference_no", + "read_only": 1 + } ] } ) - frappe.clear_cache(doctype="Payment Request") if "erpnext" in frappe.get_installed_apps(): @@ -234,7 +263,11 @@ def delete_custom_fields(): payment_request_fields = [ "custom_name", + "response_command", "custom_payment_reference_no", + "bank_reference_no", + "transaction_status", + "payment_entry" ] frappe.db.delete( From ce5ae1d0586ec41ee7b05a667eb7f16b93778e6c Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Wed, 19 Nov 2025 19:50:21 +0530 Subject: [PATCH 20/22] fix: resolved the page routing issue --- payments/patches.txt | 2 +- payments/templates/pages/bankmuscat_checkout.py | 5 +++++ payments/templates/pages/payment_success.py | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/payments/patches.txt b/payments/patches.txt index 89e67281..e92fac3f 100644 --- a/payments/patches.txt +++ b/payments/patches.txt @@ -1,4 +1,4 @@ [pre_model_sync] [post_model_sync] -payments.utils.custom_fields #10 \ No newline at end of file +payments.utils.custom_fields #11 \ No newline at end of file diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index 1062609b..b20da0bf 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -167,6 +167,11 @@ def handle_payment_response(data_dict, reference_doctype, reference_docname): "Integration Request", doc_name, {"status": "Completed", "output": json.dumps(data, indent=4)} ) + frappe.local.session["last_payment_doc"] = { + "doctype": reference_doctype, + "docname": reference_docname + } + return redirect_response("payment-success") elif order_status in ("failure", "invalid", "timeout"): diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index e2d1115b..b1b8161c 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -7,7 +7,11 @@ def get_context(context): - doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) + payment_doc = frappe.local.session.pop("last_payment_doc", None) + if payment_doc: + doc = frappe.get_doc(payment_doc["doctype"], payment_doc["docname"]) + else: + doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) context.payment_message = "" if hasattr(doc, "get_payment_success_message"): From c1dcbb60c5303bc2ec1181795b82349e61be31e0 Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Sun, 23 Nov 2025 06:02:57 +0530 Subject: [PATCH 21/22] fix: Update the payment success response page function --- .../templates/pages/bankmuscat_checkout.py | 15 ++++-- payments/templates/pages/payment_success.py | 50 ++++++++++++++++--- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/payments/templates/pages/bankmuscat_checkout.py b/payments/templates/pages/bankmuscat_checkout.py index b20da0bf..bb43da1f 100644 --- a/payments/templates/pages/bankmuscat_checkout.py +++ b/payments/templates/pages/bankmuscat_checkout.py @@ -167,12 +167,17 @@ def handle_payment_response(data_dict, reference_doctype, reference_docname): "Integration Request", doc_name, {"status": "Completed", "output": json.dumps(data, indent=4)} ) - frappe.local.session["last_payment_doc"] = { - "doctype": reference_doctype, - "docname": reference_docname - } + token = frappe.generate_hash(length=32) - return redirect_response("payment-success") + frappe.cache().set_value( + f"payment_success:{token}", + {"doctype": reference_doctype, "docname": reference_docname}, + expires_in_sec=300 + ) + + return redirect_response(f"payment-success?token={token}") + + # return redirect_response("payment-success") elif order_status in ("failure", "invalid", "timeout"): payment_request.db_set("status", "Failed") diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index b1b8161c..25e37c50 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -7,12 +7,46 @@ def get_context(context): - payment_doc = frappe.local.session.pop("last_payment_doc", None) - if payment_doc: - doc = frappe.get_doc(payment_doc["doctype"], payment_doc["docname"]) - else: - doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) - context.payment_message = "" - if hasattr(doc, "get_payment_success_message"): - context.payment_message = doc.get_payment_success_message() + friendly_message = ( + "Your payment has been successfully completed. However, we were unable to load the confirmation page due to a technical issue. Please contact our support team for verification." + ) + + try: + token = frappe.local.form_dict.get("token") + + ref = None + doc = None + + if token: + ref = frappe.cache().get_value(f"payment_success:{token}") + + if ref: + doctype = ref.get("doctype") + docname = ref.get("docname") + + doc = frappe.get_doc(doctype, docname) + + frappe.cache().delete_key(f"payment_success:{token}") + else: + doctype = frappe.local.form_dict.doctype + docname = frappe.local.form_dict.docname + + if doctype and docname: + doc = frappe.get_doc(doctype, docname) + + else: + context.payment_message = friendly_message + return context + + if hasattr(doc, "get_payment_success_message"): + context.payment_message = doc.get_payment_success_message() + else: + context.payment_message = friendly_message + + except Exception as e: + frappe.log_error(frappe.get_traceback(), "Payment Success Page Error") + + context.payment_message = friendly_message + + return context From 752f194bda8e34086c61a70448f82467bbc3b38d Mon Sep 17 00:00:00 2001 From: Karuppasamy B Date: Sat, 29 Nov 2025 19:52:38 +0530 Subject: [PATCH 22/22] fix: notification content --- payments/templates/pages/payment_success.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index 25e37c50..72a73e6c 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -9,7 +9,7 @@ def get_context(context): context.payment_message = "" friendly_message = ( - "Your payment has been successfully completed. However, we were unable to load the confirmation page due to a technical issue. Please contact our support team for verification." + "Your payment has been successfully completed." ) try: @@ -47,6 +47,6 @@ def get_context(context): except Exception as e: frappe.log_error(frappe.get_traceback(), "Payment Success Page Error") - context.payment_message = friendly_message + context.payment_message = "Your payment has been successfully completed. However, we were unable to load the confirmation page due to a technical issue. Please contact our support team for verification." return context