From d18526c806c15a04d076182f72a4b3d07b01e8de Mon Sep 17 00:00:00 2001 From: mehairwamlake Date: Fri, 26 Jun 2026 17:06:59 +0300 Subject: [PATCH 1/5] l --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a4cd298db..3e6f92b32 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ payments/docs/current node_modules/ __pycache__/ .aider* -.helix \ No newline at end of file +.helix +.a \ No newline at end of file From f4b94e316c571f0b9251d1617384e59c425a1ad2 Mon Sep 17 00:00:00 2001 From: mehairwamlake Date: Fri, 26 Jun 2026 17:21:15 +0300 Subject: [PATCH 2/5] l --- .gitignore | 3 +- .../doctype/chapa_settings/__init__.py | 0 .../doctype/chapa_settings/chapa_settings.js | 4 + .../chapa_settings/chapa_settings.json | 273 +++++++++++++++++ .../doctype/chapa_settings/chapa_settings.py | 290 ++++++++++++++++++ .../chapa_settings/test_braintree_settings.py | 7 + 6 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 payments/payment_gateways/doctype/chapa_settings/__init__.py create mode 100644 payments/payment_gateways/doctype/chapa_settings/chapa_settings.js create mode 100644 payments/payment_gateways/doctype/chapa_settings/chapa_settings.json create mode 100644 payments/payment_gateways/doctype/chapa_settings/chapa_settings.py create mode 100644 payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py diff --git a/.gitignore b/.gitignore index 3e6f92b32..a4cd298db 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ payments/docs/current node_modules/ __pycache__/ .aider* -.helix -.a \ No newline at end of file +.helix \ No newline at end of file diff --git a/payments/payment_gateways/doctype/chapa_settings/__init__.py b/payments/payment_gateways/doctype/chapa_settings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.js b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.js new file mode 100644 index 000000000..96612bd26 --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.js @@ -0,0 +1,4 @@ +// Copyright (c) 2018, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Chapa Settings", {}); diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json new file mode 100644 index 000000000..502c6e5a0 --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json @@ -0,0 +1,273 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:gateway_name", + "beta": 0, + "creation": "2018-02-05 13:46:12.101852", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "gateway_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Payment Gateway Name", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "merchant_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Merchant ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "public_key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Public Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "private_key", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Private Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "use_sandbox", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Use Sandbox", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header_img", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2022-07-24 14:33:06.050377", + "modified_by": "Administrator", + "module": "Payment Gateways", + "name": "Braintree Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 0, + "track_seen": 0 +} \ No newline at end of file diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py new file mode 100644 index 000000000..936139bea --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py @@ -0,0 +1,290 @@ +# Copyright (c) 2018, Frappe Technologies and contributors +# License: MIT. See LICENSE + +from urllib.parse import urlencode + +import braintree +import frappe +from frappe import _ +from frappe.integrations.utils import create_request_log +from frappe.model.document import Document +from frappe.utils import call_hook_method, get_url + +from payments.utils import create_payment_gateway + + +class BraintreeSettings(Document): + supported_currencies = ( + "AED", + "AMD", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BWP", + "BYN", + "BZD", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "ISK", + "JMD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LTL", + "MAD", + "MDL", + "MKD", + "MNT", + "MOP", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMK", + "ZWD", + ) + + def validate(self): + if not self.flags.ignore_mandatory: + self.configure_braintree() + + def on_update(self): + create_payment_gateway( + "Braintree-" + self.gateway_name, + settings="Braintree Settings", + controller=self.gateway_name, + ) + call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) + + def configure_braintree(self): + if self.use_sandbox: + environment = "sandbox" + else: + environment = "production" + + braintree.Configuration.configure( + environment=environment, + merchant_id=self.merchant_id, + public_key=self.public_key, + private_key=self.get_password(fieldname="private_key", raise_exception=False), + ) + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw( + _( + "Please select another payment method. Stripe does not support transactions in currency '{0}'" + ).format(currency) + ) + + def get_payment_url(self, **kwargs): + return get_url(f"./braintree_checkout?{urlencode(kwargs)}") + + def create_payment_request(self, data): + self.data = frappe._dict(data) + + try: + self.integration_request = create_request_log(self.data, service_name="Braintree") + return self.create_charge_on_braintree() + + except Exception: + frappe.log_error(frappe.get_traceback()) + return { + "redirect_to": frappe.redirect_to_message( + _("Server Error"), + _( + "There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account." + ), + ), + "status": 401, + } + + def create_charge_on_braintree(self): + self.configure_braintree() + + redirect_to = self.data.get("redirect_to") or None + redirect_message = self.data.get("redirect_message") or None + + result = braintree.Transaction.sale( + { + "amount": self.data.amount, + "payment_method_nonce": self.data.payload_nonce, + "options": {"submit_for_settlement": True}, + } + ) + + if result.is_success: + self.integration_request.db_set("status", "Completed", update_modified=False) + self.flags.status_changed_to = "Completed" + self.integration_request.db_set("output", result.transaction.status, update_modified=False) + + elif result.transaction: + self.integration_request.db_set("status", "Failed", update_modified=False) + error_log = frappe.log_error( + "code: " + + str(result.transaction.processor_response_code) + + " | text: " + + str(result.transaction.processor_response_text), + "Braintree Payment Error", + ) + self.integration_request.db_set("error", error_log.error, update_modified=False) + else: + self.integration_request.db_set("status", "Failed", update_modified=False) + for error in result.errors.deep_errors: + error_log = frappe.log_error( + "code: " + str(error.code) + " | message: " + str(error.message), + "Braintree Payment Error", + ) + self.integration_request.db_set("error", error_log.error, update_modified=False) + + if self.flags.status_changed_to == "Completed": + status = "Completed" + if self.data.reference_doctype and self.data.reference_docname: + custom_redirect_to = None + try: + custom_redirect_to = frappe.get_doc( + self.data.reference_doctype, self.data.reference_docname + ).run_method("on_payment_authorized", self.flags.status_changed_to) + braintree_success_page = frappe.get_hooks("braintree_success_page") + if braintree_success_page: + custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) + except Exception: + frappe.log_error(frappe.get_traceback()) + + if custom_redirect_to: + redirect_to = custom_redirect_to + + redirect_url = "payment-success" + else: + status = "Error" + redirect_url = "payment-failed" + + get_parameters = [("doctype", self.data.reference_doctype), ("docname", self.data.reference_docname)] + if redirect_to: + get_parameters.append(("redirect_to", redirect_to)) + if redirect_message: + get_parameters.append(("redirect_message", redirect_message)) + + redirect_url += "?" + urlencode(get_parameters) + return {"redirect_to": redirect_url, "status": status} + + +def get_gateway_controller(doc): + payment_request = frappe.get_doc("Payment Request", doc) + return frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") + + +def get_client_token(doc): + gateway_controller = get_gateway_controller(doc) + settings = frappe.get_doc("Braintree Settings", gateway_controller) + settings.configure_braintree() + + return braintree.ClientToken.generate() diff --git a/payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py b/payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py new file mode 100644 index 000000000..38d8909df --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py @@ -0,0 +1,7 @@ +# Copyright (c) 2018, Frappe Technologies and Contributors +# License: MIT. See LICENSE +import unittest + + +class TestBraintreeSettings(unittest.TestCase): + pass From e8643a429455ff05b745711e88993af7dba92e01 Mon Sep 17 00:00:00 2001 From: mehairwamlake Date: Fri, 26 Jun 2026 17:34:29 +0300 Subject: [PATCH 3/5] l --- .../payment_gateways/chapa_integration.py | 35 ++ .../chapa_settings/chapa_settings.json | 2 +- .../doctype/chapa_settings/chapa_settings.py | 388 +++++------------- 3 files changed, 144 insertions(+), 281 deletions(-) create mode 100644 payments/payment_gateways/chapa_integration.py diff --git a/payments/payment_gateways/chapa_integration.py b/payments/payment_gateways/chapa_integration.py new file mode 100644 index 000000000..46f152708 --- /dev/null +++ b/payments/payment_gateways/chapa_integration.py @@ -0,0 +1,35 @@ +import frappe +import requests + +class ChapaGateway: + + BASE_URL = "https://api.chapa.co/v1" + + def __init__(self): + settings = frappe.get_single("Chapa Settings") + + self.secret = settings.get_password("secret_key") + + self.headers = { + "Authorization": f"Bearer {self.secret}" + } + + def initialize(self, data): + + payload = { + "amount": str(data.amount), + "currency": "ETB", + "email": data.payer_email, + "first_name": data.payer_name, + "tx_ref": data.reference_doctype + "-" + data.reference_docname, + "callback_url": data.callback_url, + "return_url": data.return_url + } + + r = requests.post( + self.BASE_URL + "/transaction/initialize", + json=payload, + headers=self.headers + ) + + return r.json() \ No newline at end of file diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json index 502c6e5a0..35d7f6b3e 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json @@ -237,7 +237,7 @@ "modified": "2022-07-24 14:33:06.050377", "modified_by": "Administrator", "module": "Payment Gateways", - "name": "Braintree Settings", + "name": "Chapa Settings", "name_case": "", "owner": "Administrator", "permissions": [ diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py index 936139bea..51de5deaf 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py @@ -1,290 +1,118 @@ -# Copyright (c) 2018, Frappe Technologies and contributors -# License: MIT. See LICENSE +# Copyright (c) 2026, TechVision and contributors +# License: MIT from urllib.parse import urlencode -import braintree import frappe +import requests 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 +from frappe.utils import get_url from payments.utils import create_payment_gateway -class BraintreeSettings(Document): - supported_currencies = ( - "AED", - "AMD", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BWP", - "BYN", - "BZD", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "ISK", - "JMD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTL", - "MAD", - "MDL", - "MKD", - "MNT", - "MOP", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMK", - "ZWD", - ) - - def validate(self): - if not self.flags.ignore_mandatory: - self.configure_braintree() - - def on_update(self): - create_payment_gateway( - "Braintree-" + self.gateway_name, - settings="Braintree Settings", - controller=self.gateway_name, - ) - call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) - - def configure_braintree(self): - if self.use_sandbox: - environment = "sandbox" - else: - environment = "production" - - braintree.Configuration.configure( - environment=environment, - merchant_id=self.merchant_id, - public_key=self.public_key, - private_key=self.get_password(fieldname="private_key", raise_exception=False), - ) - - def validate_transaction_currency(self, currency): - if currency not in self.supported_currencies: - frappe.throw( - _( - "Please select another payment method. Stripe does not support transactions in currency '{0}'" - ).format(currency) - ) - - def get_payment_url(self, **kwargs): - return get_url(f"./braintree_checkout?{urlencode(kwargs)}") - - def create_payment_request(self, data): - self.data = frappe._dict(data) - - try: - self.integration_request = create_request_log(self.data, service_name="Braintree") - return self.create_charge_on_braintree() - - except Exception: - frappe.log_error(frappe.get_traceback()) - return { - "redirect_to": frappe.redirect_to_message( - _("Server Error"), - _( - "There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account." - ), - ), - "status": 401, - } - - def create_charge_on_braintree(self): - self.configure_braintree() - - redirect_to = self.data.get("redirect_to") or None - redirect_message = self.data.get("redirect_message") or None - - result = braintree.Transaction.sale( - { - "amount": self.data.amount, - "payment_method_nonce": self.data.payload_nonce, - "options": {"submit_for_settlement": True}, - } - ) - - if result.is_success: - self.integration_request.db_set("status", "Completed", update_modified=False) - self.flags.status_changed_to = "Completed" - self.integration_request.db_set("output", result.transaction.status, update_modified=False) - - elif result.transaction: - self.integration_request.db_set("status", "Failed", update_modified=False) - error_log = frappe.log_error( - "code: " - + str(result.transaction.processor_response_code) - + " | text: " - + str(result.transaction.processor_response_text), - "Braintree Payment Error", - ) - self.integration_request.db_set("error", error_log.error, update_modified=False) - else: - self.integration_request.db_set("status", "Failed", update_modified=False) - for error in result.errors.deep_errors: - error_log = frappe.log_error( - "code: " + str(error.code) + " | message: " + str(error.message), - "Braintree Payment Error", - ) - self.integration_request.db_set("error", error_log.error, update_modified=False) - - if self.flags.status_changed_to == "Completed": - status = "Completed" - if self.data.reference_doctype and self.data.reference_docname: - custom_redirect_to = None - try: - custom_redirect_to = frappe.get_doc( - self.data.reference_doctype, self.data.reference_docname - ).run_method("on_payment_authorized", self.flags.status_changed_to) - braintree_success_page = frappe.get_hooks("braintree_success_page") - if braintree_success_page: - custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) - except Exception: - frappe.log_error(frappe.get_traceback()) - - if custom_redirect_to: - redirect_to = custom_redirect_to - - redirect_url = "payment-success" - else: - status = "Error" - redirect_url = "payment-failed" - - get_parameters = [("doctype", self.data.reference_doctype), ("docname", self.data.reference_docname)] - if redirect_to: - get_parameters.append(("redirect_to", redirect_to)) - if redirect_message: - get_parameters.append(("redirect_message", redirect_message)) - - redirect_url += "?" + urlencode(get_parameters) - return {"redirect_to": redirect_url, "status": status} - - -def get_gateway_controller(doc): - payment_request = frappe.get_doc("Payment Request", doc) - return frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") - - -def get_client_token(doc): - gateway_controller = get_gateway_controller(doc) - settings = frappe.get_doc("Braintree Settings", gateway_controller) - settings.configure_braintree() - - return braintree.ClientToken.generate() +class ChapaSettings(Document): + supported_currencies = ("ETB",) + + def on_update(self): + create_payment_gateway( + "Chapa", + settings="Chapa Settings", + controller="Chapa", + ) + + if not self.flags.ignore_mandatory: + self.validate_credentials() + + def validate_credentials(self): + """Validate Secret Key by calling Chapa banks endpoint.""" + + if not self.secret_key: + return + + headers = { + "Authorization": f"Bearer {self.get_password('secret_key')}" + } + + try: + response = requests.get( + "https://api.chapa.co/v1/banks", + headers=headers, + timeout=15, + ) + + if response.status_code != 200: + frappe.throw(_("Invalid Chapa Secret Key")) + + except requests.RequestException: + frappe.throw(_("Unable to connect to Chapa.")) + + def validate_transaction_currency(self, currency): + if currency not in self.supported_currencies: + frappe.throw( + _("Chapa only supports transactions in ETB.") + ) + + def get_payment_url(self, **kwargs): + return get_url(f"./chapa_checkout?{urlencode(kwargs)}") + + def create_request(self, data): + """ + Called by Payment Request. + """ + + self.data = frappe._dict(data) + + return self.initialize_transaction() + + def initialize_transaction(self): + headers = { + "Authorization": f"Bearer {self.get_password('secret_key')}", + "Content-Type": "application/json", + } + + payload = { + "amount": str(self.data.amount), + "currency": self.data.currency, + "email": self.data.payer_email, + "first_name": self.data.payer_name, + "tx_ref": self.data.reference_docname, + "callback_url": get_url( + "/api/method/payments.payment_gateways.doctype.chapa_settings.chapa_settings.chapa_callback" + ), + "return_url": get_url( + "/payment-success" + ), + "customization": { + "title": frappe.db.get_single_value( + "Website Settings", + "app_name" + ), + "description": self.data.description, + }, + } + + try: + response = requests.post( + "https://api.chapa.co/v1/transaction/initialize", + json=payload, + headers=headers, + timeout=30, + ) + + response.raise_for_status() + + result = response.json() + + if result.get("status") != "success": + frappe.throw(result.get("message")) + + return { + "redirect_to": result["data"]["checkout_url"], + "status": "Initialized", + } + + except Exception: + frappe.log_error(frappe.get_traceback(), "Chapa Initialize") + frappe.throw(_("Unable to initialize Chapa payment.")) \ No newline at end of file From 902987deb1b5a1592d10366f9e7322de28e1a4b6 Mon Sep 17 00:00:00 2001 From: mehairwamlake Date: Fri, 26 Jun 2026 21:44:34 +0300 Subject: [PATCH 4/5] l --- .../doctype/chapa_settings/chapa.py | 14 ++++++++ .../chapa_settings/chapa_settings.json | 34 +++++++++++++++++-- .../doctype/chapa_settings/chapa_settings.py | 13 +++---- .../chapa_settings/gateway_controller.json | 6 ++++ ...ree_settings.py => test_chapa_settings.py} | 2 +- pyproject.toml | 4 +-- 6 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 payments/payment_gateways/doctype/chapa_settings/chapa.py create mode 100644 payments/payment_gateways/doctype/chapa_settings/gateway_controller.json rename payments/payment_gateways/doctype/chapa_settings/{test_braintree_settings.py => test_chapa_settings.py} (69%) diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa.py b/payments/payment_gateways/doctype/chapa_settings/chapa.py new file mode 100644 index 000000000..3a7336d24 --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/chapa.py @@ -0,0 +1,14 @@ +import frappe + +class Chapa: + def __init__(self): + self.settings = frappe.get_cached_doc("Chapa Settings") + + def validate_transaction_currency(self, currency): + self.settings.validate_transaction_currency(currency) + + def get_payment_url(self, **kwargs): + return self.settings.get_payment_url(**kwargs) + + def create_request(self, data): + return self.settings.create_request(data) \ No newline at end of file diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json index 35d7f6b3e..d7a384b23 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.json @@ -139,7 +139,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "private_key", + "fieldname": "secret_key", "fieldtype": "Password", "hidden": 0, "ignore_user_permissions": 0, @@ -148,7 +148,37 @@ "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, - "label": "Private Key", + "label": "Secret Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "webhook_secret", + "fieldtype": "Password", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Webhook Secret", "length": 0, "no_copy": 0, "permlevel": 0, diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py index 51de5deaf..7e0e617f4 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py @@ -14,16 +14,13 @@ class ChapaSettings(Document): supported_currencies = ("ETB",) - def on_update(self): create_payment_gateway( - "Chapa", - settings="Chapa Settings", - controller="Chapa", - ) - - if not self.flags.ignore_mandatory: - self.validate_credentials() + "Chapa", + settings="Chapa Settings", + controller="Chapa", + ) + self.validate_credentials() def validate_credentials(self): """Validate Secret Key by calling Chapa banks endpoint.""" diff --git a/payments/payment_gateways/doctype/chapa_settings/gateway_controller.json b/payments/payment_gateways/doctype/chapa_settings/gateway_controller.json new file mode 100644 index 000000000..81fea6596 --- /dev/null +++ b/payments/payment_gateways/doctype/chapa_settings/gateway_controller.json @@ -0,0 +1,6 @@ +[ + { + "doctype": "Gateway Controller", + "name": "Chapa" + } +] \ No newline at end of file diff --git a/payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py b/payments/payment_gateways/doctype/chapa_settings/test_chapa_settings.py similarity index 69% rename from payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py rename to payments/payment_gateways/doctype/chapa_settings/test_chapa_settings.py index 38d8909df..4b35ca531 100644 --- a/payments/payment_gateways/doctype/chapa_settings/test_braintree_settings.py +++ b/payments/payment_gateways/doctype/chapa_settings/test_chapa_settings.py @@ -3,5 +3,5 @@ import unittest -class TestBraintreeSettings(unittest.TestCase): +class TestChapaSettings(unittest.TestCase): pass diff --git a/pyproject.toml b/pyproject.toml index 541f64349..725abb51d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ authors = [ { name = "Frappe Technologies Pvt Ltd", email = "hello@frappe.io"} ] description = "Payments app for frappe" -requires-python = ">=3.14" +requires-python = ">=3.11" readme = "README.md" dynamic = ["version"] dependencies = [ @@ -63,4 +63,4 @@ Repository = "https://github.com/frappe/payments.git" "Bug Reports" = "https://github.com/frappe/payments/issues" [tool.bench.frappe-dependencies] -frappe = ">=17.0.0-dev,<18.0.0" +frappe = ">=15.0.0,<18.0.0" From 678b73774ba945c47dc1bc590e3069fc43b9e2d3 Mon Sep 17 00:00:00 2001 From: mehairwamlake Date: Sat, 27 Jun 2026 02:02:07 +0300 Subject: [PATCH 5/5] l --- .../payment_gateways/chapa_integration.py | 42 +++-- .../doctype/chapa_settings/chapa.py | 17 +- .../doctype/chapa_settings/chapa_settings.py | 148 ++++++++++++------ payments/templates/pages/chapa_checkout.html | 0 payments/templates/pages/chapa_checkout.py | 57 +++++++ 5 files changed, 184 insertions(+), 80 deletions(-) create mode 100644 payments/templates/pages/chapa_checkout.html create mode 100644 payments/templates/pages/chapa_checkout.py diff --git a/payments/payment_gateways/chapa_integration.py b/payments/payment_gateways/chapa_integration.py index 46f152708..3699178bb 100644 --- a/payments/payment_gateways/chapa_integration.py +++ b/payments/payment_gateways/chapa_integration.py @@ -1,35 +1,29 @@ import frappe import requests -class ChapaGateway: - BASE_URL = "https://api.chapa.co/v1" +class ChapaGateway: + BASE_URL = "https://api.chapa.co/v1" - def __init__(self): - settings = frappe.get_single("Chapa Settings") + def __init__(self): + settings = frappe.get_single("Chapa Settings") - self.secret = settings.get_password("secret_key") + self.secret = settings.get_password("secret_key") - self.headers = { - "Authorization": f"Bearer {self.secret}" - } + self.headers = {"Authorization": f"Bearer {self.secret}"} - def initialize(self, data): + def initialize(self, data): - payload = { - "amount": str(data.amount), - "currency": "ETB", - "email": data.payer_email, - "first_name": data.payer_name, - "tx_ref": data.reference_doctype + "-" + data.reference_docname, - "callback_url": data.callback_url, - "return_url": data.return_url - } + payload = { + "amount": str(data.amount), + "currency": "ETB", + "email": data.payer_email, + "first_name": data.payer_name, + "tx_ref": data.reference_doctype + "-" + data.reference_docname, + "callback_url": data.callback_url, + "return_url": data.return_url, + } - r = requests.post( - self.BASE_URL + "/transaction/initialize", - json=payload, - headers=self.headers - ) + r = requests.post(self.BASE_URL + "/transaction/initialize", json=payload, headers=self.headers) - return r.json() \ No newline at end of file + return r.json() diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa.py b/payments/payment_gateways/doctype/chapa_settings/chapa.py index 3a7336d24..c935456b5 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa.py +++ b/payments/payment_gateways/doctype/chapa_settings/chapa.py @@ -1,14 +1,15 @@ import frappe + class Chapa: - def __init__(self): - self.settings = frappe.get_cached_doc("Chapa Settings") + def __init__(self): + self.settings = frappe.get_cached_doc("Chapa Settings") - def validate_transaction_currency(self, currency): - self.settings.validate_transaction_currency(currency) + def validate_transaction_currency(self, currency): + self.settings.validate_transaction_currency(currency) - def get_payment_url(self, **kwargs): - return self.settings.get_payment_url(**kwargs) + def get_payment_url(self, **kwargs): + return self.settings.get_payment_url(**kwargs) - def create_request(self, data): - return self.settings.create_request(data) \ No newline at end of file + def create_request(self, data): + return self.settings.create_request(data) diff --git a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py index 7e0e617f4..3d2c84013 100644 --- a/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py +++ b/payments/payment_gateways/doctype/chapa_settings/chapa_settings.py @@ -1,10 +1,9 @@ # Copyright (c) 2026, TechVision and contributors # License: MIT -from urllib.parse import urlencode - -import frappe +import uuid import requests +import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import get_url @@ -13,18 +12,26 @@ class ChapaSettings(Document): + supported_currencies = ("ETB",) + + + # ----------------------------- + # REGISTER GATEWAY + # ----------------------------- def on_update(self): create_payment_gateway( - "Chapa", - settings="Chapa Settings", - controller="Chapa", - ) + "Chapa", + settings="Chapa Settings", + controller="ChapaSettings", + ) + self.validate_credentials() + # ----------------------------- + # VALIDATE SECRET KEY + # ----------------------------- def validate_credentials(self): - """Validate Secret Key by calling Chapa banks endpoint.""" - if not self.secret_key: return @@ -33,83 +40,128 @@ def validate_credentials(self): } try: - response = requests.get( + r = requests.get( "https://api.chapa.co/v1/banks", headers=headers, - timeout=15, + timeout=10, ) - if response.status_code != 200: + if r.status_code != 200: frappe.throw(_("Invalid Chapa Secret Key")) except requests.RequestException: - frappe.throw(_("Unable to connect to Chapa.")) - - def validate_transaction_currency(self, currency): - if currency not in self.supported_currencies: - frappe.throw( - _("Chapa only supports transactions in ETB.") - ) + frappe.throw(_("Unable to connect to Chapa API")) + # ----------------------------- + # CALLED BY LMS / PAYMENT FLOW + # ----------------------------- def get_payment_url(self, **kwargs): - return get_url(f"./chapa_checkout?{urlencode(kwargs)}") - - def create_request(self, data): - """ - Called by Payment Request. - """ + self.validate_transaction_currency(kwargs.get("currency")) - self.data = frappe._dict(data) + self.data = frappe._dict(kwargs) return self.initialize_transaction() + # ----------------------------- + # INIT PAYMENT (CHAPA API) + # ----------------------------- def initialize_transaction(self): + url = "https://api.chapa.co/v1/transaction/initialize" + headers = { "Authorization": f"Bearer {self.get_password('secret_key')}", "Content-Type": "application/json", } + tx_ref = f"{self.data.reference_docname}-{uuid.uuid4().hex[:10]}" + payload = { "amount": str(self.data.amount), - "currency": self.data.currency, - "email": self.data.payer_email, - "first_name": self.data.payer_name, - "tx_ref": self.data.reference_docname, + "currency": "ETB", + "email": self.data.payer_email or "mehariwamlake@gmail.com", + "first_name": self.data.payer_name or "User", + "last_name": "", + "phone_number": self.data.get("phone_number", ""), + + "tx_ref": tx_ref, + "callback_url": get_url( - "/api/method/payments.payment_gateways.doctype.chapa_settings.chapa_settings.chapa_callback" - ), - "return_url": get_url( - "/payment-success" + "/api/method/payments.payment_gateways.doctype.chapa_settings.chapa_settings.verify_payment" ), + + "return_url": get_url(self.data.redirect_to or "/"), + "customization": { - "title": frappe.db.get_single_value( - "Website Settings", - "app_name" - ), - "description": self.data.description, + "title": self.data.title or "LMS Payment", + "description": self.data.description or "", }, } try: response = requests.post( - "https://api.chapa.co/v1/transaction/initialize", + url, json=payload, headers=headers, timeout=30, ) - response.raise_for_status() + # IMPORTANT DEBUG + if response.status_code != 200: + frappe.log_error(response.text, "Chapa API Error") + frappe.throw(_("Chapa API request failed")) result = response.json() + print(result) if result.get("status") != "success": - frappe.throw(result.get("message")) + frappe.log_error(str(result), "Chapa Init Error") + frappe.throw(_("Chapa rejected the request")) - return { - "redirect_to": result["data"]["checkout_url"], - "status": "Initialized", - } + # 🔥 CRITICAL: redirect LMS directly + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = result["data"]["checkout_url"] + + return except Exception: - frappe.log_error(frappe.get_traceback(), "Chapa Initialize") - frappe.throw(_("Unable to initialize Chapa payment.")) \ No newline at end of file + frappe.log_error(frappe.get_traceback(), "Chapa Initialize Error") + frappe.throw(_("Unable to initialize Chapa payment")) + + # ----------------------------- + # VERIFY PAYMENT (WEBHOOK) + # ----------------------------- + @frappe.whitelist(allow_guest=True) + def verify_payment(self): + tx_ref = frappe.form_dict.get("tx_ref") + + if not tx_ref: + frappe.throw(_("Missing tx_ref")) + + headers = { + "Authorization": f"Bearer {self.get_password('secret_key')}" + } + + r = requests.get( + f"https://api.chapa.co/v1/transaction/verify/{tx_ref}", + headers=headers, + timeout=15, + ) + + r.raise_for_status() + data = r.json() + + if data.get("status") != "success": + frappe.throw(_("Payment verification failed")) + + return { + "status": "success", + "tx_ref": tx_ref, + "data": data.get("data"), + } + + # ----------------------------- + # VALIDATION + # ----------------------------- + def validate_transaction_currency(self, currency): + if currency and currency not in self.supported_currencies: + frappe.throw(_("Chapa only supports ETB transactions")) \ No newline at end of file diff --git a/payments/templates/pages/chapa_checkout.html b/payments/templates/pages/chapa_checkout.html new file mode 100644 index 000000000..e69de29bb diff --git a/payments/templates/pages/chapa_checkout.py b/payments/templates/pages/chapa_checkout.py new file mode 100644 index 000000000..797d2650d --- /dev/null +++ b/payments/templates/pages/chapa_checkout.py @@ -0,0 +1,57 @@ +import frappe +import requests +import uuid + + +CHAPA_URL = "https://api.chapa.co/v1/transaction/initialize" + + +@frappe.whitelist(allow_guest=True) +def chapa_checkout( + amount, + title, + description, + reference_doctype=None, + reference_docname=None, + payer_email=None, + payer_name=None, + currency="ETB", + redirect_to="/", +): + """ + Create Chapa payment session and return checkout URL + """ + + # 🔐 Get API key from settings (create doctype if needed) + settings = frappe.get_single("Chapa Settings") + secret_key = settings.secret_key + + # 🔑 unique transaction reference + tx_ref = f"{self.data.reference_docname}-{uuid.uuid4().hex[:10]}" + + payload = { + "amount": float(amount), + "currency": currency, + "email": payer_email or "mehariwamlake@gmail.com", + "first_name": payer_name or "Guest", + "last_name": "User", + "tx_ref": tx_ref, + "callback_url": frappe.utils.get_url( + f"/api/method/payments.payment_gateways.doctype.chapa_settings.chapa_settings.chapa_callback" + ), + "return_url": frappe.utils.get_url(redirect_to), + "customization": {"title": title, "description": description}, + } + + headers = {"Authorization": f"Bearer {secret_key}", "Content-Type": "application/json"} + + response = requests.post(CHAPA_URL, json=payload, headers=headers) + data = response.json() + + if response.status_code != 200 or data.get("status") != "success": + frappe.throw(f"Chapa Error: {data}") + + # 🔥 Redirect user to checkout page + checkout_url = data["data"]["checkout_url"] + + return {"checkout_url": checkout_url, "tx_ref": tx_ref}