├── payments ├── patches.txt ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── public │ ├── .gitkeep │ └── js │ │ └── razorpay.js ├── tests │ └── __init__.py ├── overrides │ ├── __init__.py │ └── payment_webform.py ├── payments │ ├── __init__.py │ └── doctype │ │ └── payment_gateway │ │ ├── __init__.py │ │ ├── payment_gateway.py │ │ ├── payment_gateway.js │ │ ├── test_payment_gateway.py │ │ └── payment_gateway.json ├── templates │ ├── __init__.py │ ├── pages │ │ ├── __init__.py │ │ ├── payment_cancel.py │ │ ├── payment_success.py │ │ ├── gocardless_checkout.html │ │ ├── gocardless_confirmation.html │ │ ├── payment-failed.html │ │ ├── payment-cancel.html │ │ ├── razorpay_checkout.html │ │ ├── payment-success.html │ │ ├── paytm_checkout.html │ │ ├── paytm_checkout.py │ │ ├── braintree_checkout.html │ │ ├── braintree_checkout.py │ │ ├── stripe_checkout.html │ │ ├── razorpay_checkout.py │ │ ├── stripe_checkout.css │ │ ├── gocardless_checkout.py │ │ ├── stripe_checkout.py │ │ └── gocardless_confirmation.py │ └── includes │ │ ├── gocardless_checkout.js │ │ ├── gocardless_confirmation.js │ │ ├── braintree_checkout.js │ │ ├── razorpay_checkout.js │ │ └── stripe_checkout.js ├── payment_gateways │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── mpesa_settings │ │ │ ├── __init__.py │ │ │ ├── account_balance.html │ │ │ ├── mpesa_settings.js │ │ │ ├── mpesa_custom_fields.py │ │ │ ├── mpesa_settings.json │ │ │ ├── mpesa_connector.py │ │ │ └── mpesa_settings.py │ │ ├── paymob_settings │ │ │ ├── __init__.py │ │ │ ├── test_paymob_settings.py │ │ │ ├── paymob_settings.js │ │ │ ├── paymob_settings.json │ │ │ └── paymob_settings.py │ │ ├── paypal_settings │ │ │ ├── __init__.py │ │ │ ├── paypal_settings.js │ │ │ └── paypal_settings.json │ │ ├── paytm_settings │ │ │ ├── __init__.py │ │ │ ├── test_paytm_settings.py │ │ │ ├── paytm_settings.js │ │ │ ├── paytm_settings.json │ │ │ └── paytm_settings.py │ │ ├── razorpay_settings │ │ │ ├── __init__.py │ │ │ ├── razorpay_settings.js │ │ │ └── razorpay_settings.json │ │ ├── stripe_settings │ │ │ ├── __init__.py │ │ │ ├── test_stripe_settings.py │ │ │ ├── stripe_settings.js │ │ │ ├── stripe_settings.py │ │ │ └── stripe_settings.json │ │ ├── braintree_settings │ │ │ ├── __init__.py │ │ │ ├── braintree_settings.js │ │ │ ├── test_braintree_settings.py │ │ │ ├── braintree_settings.json │ │ │ └── braintree_settings.py │ │ ├── gocardless_mandate │ │ │ ├── __init__.py │ │ │ ├── gocardless_mandate.js │ │ │ ├── test_gocardless_mandate.py │ │ │ ├── gocardless_mandate.py │ │ │ └── gocardless_mandate.json │ │ └── gocardless_settings │ │ │ ├── test_gocardless_settings.py │ │ │ ├── gocardless_settings.js │ │ │ ├── gocardless_settings.json │ │ │ ├── __init__.py │ │ │ └── gocardless_settings.py │ ├── paymob │ │ ├── constants.py │ │ ├── response_feedback_dataclass.py │ │ ├── response_codes.py │ │ ├── paymob_urls.py │ │ ├── accept_api.py │ │ ├── connection.py │ │ └── hmac_validator.py │ └── stripe_integration.py ├── __init__.py ├── modules.txt ├── utils │ ├── __init__.py │ └── utils.py └── hooks.py ├── .gitignore ├── .github ├── labeler.yml ├── workflows │ ├── labeller.yml │ ├── linter.yml │ └── ci.yml └── helper │ ├── site_config.json │ ├── flake8.conf │ └── install.sh ├── commitlint.config.js ├── .git-blame-ignore-revs ├── license.txt ├── README.md ├── pyproject.toml ├── .pre-commit-config.yaml └── .eslintrc /payments/patches.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/overrides/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/templates/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payments/doctype/payment_gateway/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/modules.txt: -------------------------------------------------------------------------------- 1 | Payments 2 | Payment Gateways 3 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/mpesa_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paymob_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paypal_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paytm_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/razorpay_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/stripe_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/braintree_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_mandate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | payments/docs/current 7 | node_modules/ 8 | __pycache__/ 9 | .aider* 10 | .helix -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Any python files modifed but no test files modified 2 | needs-tests: 3 | - any: ['payments/**/*.py'] 4 | all: ['!payments/**/test*.py'] 5 | -------------------------------------------------------------------------------- /payments/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | 4 | def get_data(): 5 | return [{"module_name": "Payments", "type": "module", "label": _("Payments")}] 6 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/constants.py: -------------------------------------------------------------------------------- 1 | class AcceptCallbackTypes: 2 | TRANSACTION = "TRANSACTION" 3 | CARD_TOKEN = "TOKEN" 4 | DELIVERY_STATUS = "DELIVERY_STATUS" 5 | -------------------------------------------------------------------------------- /payments/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from payments.utils.utils import ( 2 | before_install, 3 | create_payment_gateway, 4 | delete_custom_fields, 5 | erpnext_app_import_guard, 6 | get_payment_gateway_controller, 7 | make_custom_fields, 8 | ) 9 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/braintree_settings/braintree_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Braintree Settings", {}); 5 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("GoCardless Mandate", {}); 5 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and Contributors 2 | # License: MIT. See LICENSE 3 | import unittest 4 | 5 | 6 | class TestStripeSettings(unittest.TestCase): 7 | pass 8 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_mandate/test_gocardless_mandate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and Contributors 2 | # See license.txt 3 | 4 | import unittest 5 | 6 | 7 | class TestGoCardlessMandate(unittest.TestCase): 8 | pass 9 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and Contributors 2 | # License: MIT. See LICENSE 3 | import unittest 4 | 5 | 6 | class TestBraintreeSettings(unittest.TestCase): 7 | pass 8 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_settings/test_gocardless_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and Contributors 2 | # See license.txt 3 | 4 | import unittest 5 | 6 | 7 | class TestGoCardlessSettings(unittest.TestCase): 8 | pass 9 | -------------------------------------------------------------------------------- /payments/payments/doctype/payment_gateway/payment_gateway.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors 2 | # License: MIT. See LICENSE 3 | 4 | from frappe.model.document import Document 5 | 6 | 7 | class PaymentGateway(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paypal_settings/paypal_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("PayPal Settings", { 5 | refresh: function (frm) {}, 6 | }); 7 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paytm_settings/test_paytm_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Frappe Technologies and Contributors 2 | # License: MIT. See LICENSE 3 | # import frappe 4 | import unittest 5 | 6 | 7 | class TestPaytmSettings(unittest.TestCase): 8 | pass 9 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/stripe_settings/stripe_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Stripe Settings", { 5 | refresh: function (frm) {}, 6 | }); 7 | -------------------------------------------------------------------------------- /payments/payments/doctype/payment_gateway/payment_gateway.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Payment Gateway", { 5 | refresh: function (frm) {}, 6 | }); 7 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | // frappe.ui.form.on('GoCardless Settings', { 5 | // refresh(frm) { 6 | 7 | // }, 8 | // }); 9 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paymob_settings/test_paymob_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Frappe Technologies and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestPaymobSettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/response_feedback_dataclass.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Optional 3 | 4 | 5 | @dataclass 6 | class ResponseFeedBack: 7 | message: str | None 8 | data: Any = None 9 | status_code: int = None 10 | exception_error: str = None 11 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and contributors 2 | # For license information, please see license.txt 3 | 4 | 5 | from frappe.model.document import Document 6 | 7 | 8 | class GoCardlessMandate(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /.github/workflows/labeller.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | pull_request_target: 4 | types: [opened, reopened] 5 | 6 | jobs: 7 | triage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/labeler@v4 11 | with: 12 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 13 | -------------------------------------------------------------------------------- /payments/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/payments" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | 10 | def get_context(context): 11 | context.brand_html = "Payments" 12 | -------------------------------------------------------------------------------- /payments/payments/doctype/payment_gateway/test_payment_gateway.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | import unittest 4 | 5 | # test_records = frappe.get_test_records('Payment Gateway') 6 | 7 | 8 | class TestPaymentGateway(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /payments/templates/pages/payment_cancel.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | 4 | import frappe 5 | 6 | 7 | def get_context(context): 8 | token = frappe.local.form_dict.token 9 | 10 | if token: 11 | frappe.db.set_value("Integration Request", token, "status", "Cancelled") 12 | frappe.db.commit() 13 | -------------------------------------------------------------------------------- /payments/templates/pages/payment_success.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | 4 | import frappe 5 | 6 | no_cache = True 7 | 8 | 9 | def get_context(context): 10 | doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) 11 | 12 | context.payment_message = "" 13 | if hasattr(doc, "get_payment_success_message"): 14 | context.payment_message = doc.get_payment_success_message() 15 | -------------------------------------------------------------------------------- /payments/templates/pages/gocardless_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%}{% endblock %} 6 | 7 | {% block script %} 8 | 9 | {% endblock %} 10 | 11 | {%- block page_content -%} 12 |

13 | {{ _("Loading Payment System") }} 14 |

15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /payments/templates/pages/gocardless_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%}{% endblock %} 6 | 7 | {% block script %} 8 | 9 | {% endblock %} 10 | 11 | {%- block page_content -%} 12 |

13 | {{ _("Payment Confirmation") }} 14 |

15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Razorpay Settings", { 5 | refresh: function (frm) { 6 | frm.add_custom_button(__("Clear"), function () { 7 | frm.call({ 8 | doc: frm.doc, 9 | method: "clear", 10 | callback: function (r) { 11 | frm.refresh(); 12 | }, 13 | }); 14 | }); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paytm_settings/paytm_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Paytm Settings", { 5 | refresh: function (frm) { 6 | frm.dashboard.set_headline( 7 | __("For more information, {0}.", [ 8 | `${__( 9 | "Click here" 10 | )}`, 11 | ]) 12 | ); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /.github/helper/site_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_host": "127.0.0.1", 3 | "db_port": 3306, 4 | "db_name": "test_frappe", 5 | "db_password": "test_frappe", 6 | "auto_email_id": "test@example.com", 7 | "mail_server": "smtp.example.com", 8 | "mail_login": "test@example.com", 9 | "mail_password": "test", 10 | "admin_password": "admin", 11 | "root_login": "root", 12 | "root_password": "root", 13 | "host_name": "http://test_site:8000", 14 | "install_apps": ["payments", "erpnext"], 15 | "throttle_user_limit": 100 16 | } -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserPreset: "conventional-changelog-conventionalcommits", 3 | rules: { 4 | "subject-empty": [2, "never"], 5 | "type-case": [2, "always", "lower-case"], 6 | "type-empty": [2, "never"], 7 | "type-enum": [ 8 | 2, 9 | "always", 10 | [ 11 | "build", 12 | "chore", 13 | "ci", 14 | "docs", 15 | "feat", 16 | "fix", 17 | "perf", 18 | "refactor", 19 | "revert", 20 | "style", 21 | "test", 22 | ], 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/response_codes.py: -------------------------------------------------------------------------------- 1 | # Response Codes 2 | SUCCESS = 10 3 | 4 | # Request Related Error Codes 5 | JSON_DECODE_EXCEPTION = 20 6 | REQUEST_EXCEPTION = 21 7 | HTTP_EXCEPTION = 22 8 | UNHANDLED_EXCEPTION = 23 9 | 10 | 11 | # Error Messages Templates 12 | JSON_DECODE_EXCEPTION_MESSAGE = "An Error Occurred While Parsing the Response into JSON" 13 | REQUEST_EXCEPTION_MESSAGE = "An Error Occurred During the Request" 14 | HTTP_EXCEPTION_MESSAGE = "Non 2xx Status Code Returned." 15 | UNHANDLED_EXCEPTION_MESSAGE = "Unhandled Exception" 16 | SUCCESS_MESSAGE = "API Successfully Called." 17 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Since version 2.23 (released in August 2019), git-blame has a feature 2 | # to ignore or bypass certain commits. 3 | # 4 | # This file contains a list of commits that are not likely what you 5 | # are looking for in a blame, such as mass reformatting or renaming. 6 | # You can set this file as a default ignore file for blame by running 7 | # the following command. 8 | # 9 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs 10 | 11 | # pre-commit formatting ruff, eslint, prettier (automated) 12 | 63a4acf6c9fe9657fa6d7ad659465b0d5ef3d73f 13 | 14 | # pre-commit formatting ruff, eslint, prettier (manual fixup) 15 | cecf0bec9de2dcd176fc632e8a5348ab2f491cbe -------------------------------------------------------------------------------- /payments/templates/pages/payment-failed.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %}{{ _("Payment Failed") }}{% endblock %} 4 | 5 | {%- block page_content -%} 6 |
7 |
8 | 9 | {{ _("Payment Failed") }} 10 |
11 |

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

12 |
13 | {{ _("Continue") }}
14 |
15 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /payments/templates/includes/gocardless_checkout.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var data = {{ frappe.form_dict | json }}; 3 | var doctype = "{{ reference_doctype }}" 4 | var docname = "{{ reference_docname }}" 5 | 6 | frappe.call({ 7 | method: "payments.templates.pages.gocardless_checkout.check_mandate", 8 | freeze: true, 9 | headers: { 10 | "X-Requested-With": "XMLHttpRequest" 11 | }, 12 | args: { 13 | "data": JSON.stringify(data), 14 | "reference_doctype": doctype, 15 | "reference_docname": docname 16 | }, 17 | callback: function(r) { 18 | if (r.message) { 19 | window.location.href = r.message.redirect_to 20 | } 21 | } 22 | }) 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /payments/templates/pages/payment-cancel.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %}{{ _("Payment Cancelled") }}{% endblock %} 4 | 5 | {%- block page_content -%} 6 |
7 |
8 | 9 | {{ _("Payment Cancelled") }} 10 |
11 |

{{ _("Your payment is cancelled.") }}

12 |
13 | {{ _("Continue") }}
14 |
15 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /payments/templates/includes/gocardless_confirmation.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var redirect_flow_id = "{{ redirect_flow_id }}"; 3 | var doctype = "{{ reference_doctype }}"; 4 | var docname = "{{ reference_docname }}"; 5 | 6 | frappe.call({ 7 | method: "payments.templates.pages.gocardless_confirmation.confirm_payment", 8 | freeze: true, 9 | headers: { 10 | "X-Requested-With": "XMLHttpRequest" 11 | }, 12 | args: { 13 | "redirect_flow_id": redirect_flow_id, 14 | "reference_doctype": doctype, 15 | "reference_docname": docname 16 | }, 17 | callback: function(r) { 18 | if (r.message) { 19 | window.location.href = r.message.redirect_to; 20 | } 21 | } 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /payments/templates/pages/razorpay_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%}{% endblock %} 6 | 7 | {% block script %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {%- block page_content -%} 13 | 14 |

15 | Loading Payment System 16 | 17 |

18 | 19 | {% endblock %} 20 | 21 | {% block style %} 22 | 28 | {% endblock %} -------------------------------------------------------------------------------- /payments/templates/pages/payment-success.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %}{{ _("Payment Success") }}{% endblock %} 4 | 5 | {%- block page_content -%} 6 |
7 |
8 | 9 | {{ _("Success") }} 10 |
11 |

{{ payment_message or _("Your payment was successfully accepted") }}

12 | {% if not payment_message %} 13 |
14 | 17 | {{ _("Continue") }} 18 | 19 |
20 | {% endif %} 21 |
22 | 25 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paymob_settings/paymob_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025, Frappe Technologies and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Paymob Settings", { 5 | refresh(frm) { 6 | frm.add_custom_button(__("Get Access Token"), () => { 7 | frm.trigger("get_access_token"); 8 | }); 9 | }, 10 | get_access_token: function (frm) { 11 | try { 12 | frm 13 | .call({ 14 | method: "refresh_access_token", 15 | doc: frm.doc, 16 | freeze: true, 17 | freeze_message: __("Getting Access Token ..."), 18 | }) 19 | .then((r) => { 20 | if (!r.exc && r.message) { 21 | frm.set_value("token", r.message); 22 | frappe.show_alert({ 23 | message: __("Access Token Updated"), 24 | indicator: "green", 25 | }); 26 | } 27 | }); 28 | } catch (e) { 29 | console.log(e); 30 | } 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /payments/templates/pages/paytm_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%} 6 | 7 | Merchant Checkout Page 8 | 9 | {% endblock %} 10 | 11 | {% block script %} 12 | 15 | {% endblock %} 16 | 17 | {%- block page_content -%} 18 | 19 |
20 |

Please do not refresh this page...

21 | 22 |
23 | {% for name, value in payment_details.items() %} 24 | 25 | {% endfor %} 26 |
27 |
28 | 29 | {% endblock %} 30 | 31 | {% block style %} 32 | 43 | {% endblock %} -------------------------------------------------------------------------------- /.github/helper/flake8.conf: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | B001, 4 | B007, 5 | B009, 6 | B010, 7 | B950, 8 | E101, 9 | E111, 10 | E114, 11 | E116, 12 | E117, 13 | E121, 14 | E122, 15 | E123, 16 | E124, 17 | E125, 18 | E126, 19 | E127, 20 | E128, 21 | E131, 22 | E201, 23 | E202, 24 | E203, 25 | E211, 26 | E221, 27 | E222, 28 | E223, 29 | E224, 30 | E225, 31 | E226, 32 | E228, 33 | E231, 34 | E241, 35 | E242, 36 | E251, 37 | E261, 38 | E262, 39 | E265, 40 | E266, 41 | E271, 42 | E272, 43 | E273, 44 | E274, 45 | E301, 46 | E302, 47 | E303, 48 | E305, 49 | E306, 50 | E402, 51 | E501, 52 | E502, 53 | E701, 54 | E702, 55 | E703, 56 | E741, 57 | F401, 58 | F403, 59 | F405, 60 | W191, 61 | W291, 62 | W292, 63 | W293, 64 | W391, 65 | W503, 66 | W504, 67 | E711, 68 | E129, 69 | F841, 70 | E713, 71 | E712, 72 | 73 | 74 | max-line-length = 200 75 | -------------------------------------------------------------------------------- /payments/templates/pages/paytm_checkout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | import json 4 | 5 | import frappe 6 | from frappe import _ 7 | 8 | from payments.payment_gateways.doctype.paytm_settings.paytm_settings import ( 9 | get_paytm_config, 10 | get_paytm_params, 11 | ) 12 | from payments.utils.utils import validate_integration_request 13 | 14 | 15 | def get_context(context): 16 | context.no_cache = 1 17 | paytm_config = get_paytm_config() 18 | 19 | try: 20 | validate_integration_request(frappe.form_dict["order_id"]) 21 | 22 | doc = frappe.get_doc("Integration Request", frappe.form_dict["order_id"]) 23 | 24 | context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) 25 | 26 | context.url = paytm_config.url 27 | 28 | except Exception: 29 | frappe.log_error() 30 | frappe.redirect_to_message( 31 | _("Invalid Token"), 32 | _("Seems token you are using is invalid!"), 33 | http_status_code=400, 34 | indicator_color="red", 35 | ) 36 | 37 | frappe.local.flags.redirect_location = frappe.local.response.location 38 | raise frappe.Redirect 39 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016-2021 Frappe Technologies Pvt. Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/mpesa_settings/account_balance.html: -------------------------------------------------------------------------------- 1 | {% if not jQuery.isEmptyObject(data) %} 2 |
{{ __("Balance Details") }}
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for(const [key, value] of Object.entries(data)) { %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% } %} 23 | 24 |
{{ __("Account Type") }}{{ __("Current Balance") }}{{ __("Available Balance") }}{{ __("Reserved Balance") }}{{ __("Uncleared Balance") }}
{%= key %} {%= value["current_balance"] %} {%= value["available_balance"] %} {%= value["reserved_balance"] %} {%= value["uncleared_balance"] %}
25 | {% else %} 26 |

Account Balance Information Not Available.

27 | {% endif %} 28 | -------------------------------------------------------------------------------- /payments/templates/pages/braintree_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%}{% endblock %} 6 | 7 | {% block script %} 8 | 9 | 10 | {% endblock %} 11 | 12 | {%- block page_content -%} 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 | 32 |
33 |
34 | 35 | 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("Mpesa Settings", { 5 | onload_post_render: function (frm) { 6 | frm.events.setup_account_balance_html(frm); 7 | }, 8 | 9 | refresh: function (frm) { 10 | frappe.realtime.on("refresh_mpesa_dashboard", function () { 11 | frm.reload_doc(); 12 | frm.events.setup_account_balance_html(frm); 13 | }); 14 | }, 15 | 16 | get_account_balance: function (frm) { 17 | if (!frm.doc.initiator_name && !frm.doc.security_credential) { 18 | frappe.throw( 19 | __("Please set the initiator name and the security credential") 20 | ); 21 | } 22 | frappe.call({ 23 | method: "get_account_balance_info", 24 | doc: frm.doc, 25 | }); 26 | }, 27 | 28 | setup_account_balance_html: function (frm) { 29 | if (!frm.doc.account_balance) return; 30 | $("div").remove(".form-dashboard-section.custom"); 31 | frm.dashboard.add_section( 32 | frappe.render_template("account_balance", { 33 | data: JSON.parse(frm.doc.account_balance), 34 | }) 35 | ); 36 | frm.dashboard.show(); 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /payments/payments/doctype/payment_gateway/payment_gateway.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:gateway", 4 | "creation": "2022-01-24 21:09:47.229371", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "gateway", 10 | "gateway_settings", 11 | "gateway_controller" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "gateway", 16 | "fieldtype": "Data", 17 | "in_list_view": 1, 18 | "label": "Gateway", 19 | "reqd": 1, 20 | "unique": 1 21 | }, 22 | { 23 | "fieldname": "gateway_settings", 24 | "fieldtype": "Link", 25 | "label": "Gateway Settings", 26 | "options": "DocType" 27 | }, 28 | { 29 | "fieldname": "gateway_controller", 30 | "fieldtype": "Dynamic Link", 31 | "label": "Gateway Controller", 32 | "options": "gateway_settings" 33 | } 34 | ], 35 | "links": [], 36 | "modified": "2022-07-24 21:17:03.864719", 37 | "modified_by": "Administrator", 38 | "module": "Payments", 39 | "name": "Payment Gateway", 40 | "naming_rule": "By fieldname", 41 | "owner": "Administrator", 42 | "permissions": [ 43 | { 44 | "create": 1, 45 | "delete": 1, 46 | "read": 1, 47 | "role": "System Manager", 48 | "write": 1 49 | } 50 | ], 51 | "quick_entry": 1, 52 | "sort_field": "modified", 53 | "sort_order": "DESC", 54 | "states": [] 55 | } -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:mandate", 4 | "creation": "2018-02-08 11:33:15.721919", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "disabled", 10 | "mandate", 11 | "gocardless_customer" 12 | ], 13 | "fields": [ 14 | { 15 | "default": "0", 16 | "fieldname": "disabled", 17 | "fieldtype": "Check", 18 | "label": "Disabled" 19 | }, 20 | { 21 | "fieldname": "mandate", 22 | "fieldtype": "Data", 23 | "label": "Mandate", 24 | "read_only": 1, 25 | "reqd": 1, 26 | "unique": 1 27 | }, 28 | { 29 | "fieldname": "gocardless_customer", 30 | "fieldtype": "Data", 31 | "in_list_view": 1, 32 | "label": "GoCardless Customer", 33 | "read_only": 1, 34 | "reqd": 1 35 | } 36 | ], 37 | "links": [], 38 | "modified": "2023-09-27 10:33:44.453462", 39 | "modified_by": "Administrator", 40 | "module": "Payment Gateways", 41 | "name": "GoCardless Mandate", 42 | "naming_rule": "By fieldname", 43 | "owner": "Administrator", 44 | "permissions": [ 45 | { 46 | "create": 1, 47 | "delete": 1, 48 | "email": 1, 49 | "export": 1, 50 | "print": 1, 51 | "read": 1, 52 | "report": 1, 53 | "role": "System Manager", 54 | "share": 1, 55 | "write": 1 56 | } 57 | ], 58 | "quick_entry": 1, 59 | "sort_field": "modified", 60 | "sort_order": "DESC", 61 | "states": [], 62 | "track_changes": 1 63 | } -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: [ develop ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: commitcheck-frappe-${{ github.event.number }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | commit-lint: 18 | name: 'Semantic Commits' 19 | runs-on: ubuntu-latest 20 | if: github.event_name == 'pull_request' 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 200 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 16 29 | check-latest: true 30 | 31 | - name: Check commit titles 32 | run: | 33 | npm install @commitlint/cli @commitlint/config-conventional 34 | npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} 35 | 36 | linter: 37 | name: 'Frappe Linter' 38 | runs-on: ubuntu-latest 39 | if: github.event_name == 'pull_request' 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-python@v5 44 | with: 45 | python-version: '3.10' 46 | - uses: pre-commit/action@v3.0.0 47 | 48 | - name: Download Semgrep rules 49 | run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules 50 | 51 | - name: Run Semgrep rules 52 | run: | 53 | pip install semgrep==1.90.0 54 | semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payments 2 | 3 | A payments app for frappe. 4 | 5 | ## Installation 6 | 1. Install [bench & frappe](https://frappeframework.com/docs/v14/user/en/installation). 7 | 8 | 2. Once setup is complete, add the payments app to your bench by running 9 | ``` 10 | $ bench get-app payments 11 | ``` 12 | 3. Install the payments app on the required site by running 13 | ``` 14 | $ bench --site install-app payments 15 | ``` 16 | 17 | ## App Structure & Details 18 | App has 2 modules - Payments and Payment Gateways. 19 | 20 | Payment Module contains the Payment Gateway DocType which creates links for the payment gateways and Payment Gateways Module contain all the Payment Gateway (Razorpay, Stripe, Braintree, Paypal, PayTM) DocTypes. 21 | 22 | App adds custom fields to Web Form for facilitating payments upon installation and removes them upon uninstallation. 23 | 24 | All general utils are stored in [utils](payments/utils) directory. The utils are written in [utils.py](payments/utils/utils.py) and then imported into the [`__init__.py`](payments/utils/__init__.py) file for easier importing/namespacing. 25 | 26 | [overrides](payments/overrides) directory has all the overrides for overriding standard frappe code. Currently it overrides WebForm DocType controller as well as a WebForm whitelisted method. 27 | 28 | [templates](payments/templates) directory has all the payment gateways' custom checkout pages. 29 | 30 | ## Ongoing Work 31 | - New API design: https://github.com/frappe/payments/pull/53 32 | - Mollie Integration: https://github.com/frappe/payments/pull/68 (awaiting the former, but you may use the branc) 33 | 34 | ## License 35 | MIT ([license.txt](license.txt)) 36 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/mpesa_settings/mpesa_custom_fields.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields 3 | 4 | 5 | def create_custom_pos_fields(): 6 | """Create custom fields corresponding to POS Settings and POS Invoice.""" 7 | pos_field = { 8 | "POS Invoice": [ 9 | { 10 | "fieldname": "request_for_payment", 11 | "label": "Request for Payment", 12 | "fieldtype": "Button", 13 | "hidden": 1, 14 | "insert_after": "contact_email", 15 | }, 16 | { 17 | "fieldname": "mpesa_receipt_number", 18 | "label": "Mpesa Receipt Number", 19 | "fieldtype": "Data", 20 | "read_only": 1, 21 | "insert_after": "company", 22 | }, 23 | ] 24 | } 25 | if not frappe.get_meta("POS Invoice").has_field("request_for_payment"): 26 | create_custom_fields(pos_field) 27 | 28 | record_dict = [ 29 | { 30 | "doctype": "POS Field", 31 | "fieldname": "contact_mobile", 32 | "label": "Mobile No", 33 | "fieldtype": "Data", 34 | "options": "Phone", 35 | "parenttype": "POS Settings", 36 | "parent": "POS Settings", 37 | "parentfield": "invoice_fields", 38 | }, 39 | { 40 | "doctype": "POS Field", 41 | "fieldname": "request_for_payment", 42 | "label": "Request for Payment", 43 | "fieldtype": "Button", 44 | "parenttype": "POS Settings", 45 | "parent": "POS Settings", 46 | "parentfield": "invoice_fields", 47 | }, 48 | ] 49 | create_pos_settings(record_dict) 50 | 51 | 52 | def create_pos_settings(record_dict): 53 | for record in record_dict: 54 | if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}): 55 | continue 56 | frappe.get_doc(record).insert() 57 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/paymob_urls.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import frappe 4 | 5 | 6 | @dataclass 7 | class PaymobUrls: 8 | base_url: str = "https://accept.paymob.com/" 9 | 10 | # Auth 11 | auth: str = "api/auth/tokens" 12 | 13 | # Ecommerce 14 | order: str = "api/ecommerce/orders" 15 | inquire_transaction: str = "api/ecommerce/orders/transaction_inquiry" 16 | tracking: str = "api/ecommerce/orders/{order_id}/delivery_status?token={token}" 17 | preparing_package: str = "api/ecommerce/orders/{order_id}/airway_bill?token={token}" 18 | 19 | # Acceptance 20 | payment_key: str = "api/acceptance/payment_keys" 21 | payment: str = "api/acceptance/payments/pay" 22 | capture: str = "api/acceptance/capture" 23 | refund: str = "api/acceptance/void_refund/refund" 24 | void: str = "api/acceptance/void_refund/void?token={token}" 25 | retrieve_transaction: str = "api/acceptance/transactions/{id}" 26 | retrieve_transactions: str = ( 27 | "api/acceptance/portal-transactions?page={from_page}&page_size={page_size}&token={token}" 28 | ) 29 | loyalty_checkout: str = "api/acceptance/loyalty_checkout" 30 | iframe: str = "api/acceptance/iframes/{iframe_id}?payment_token={payment_token}" 31 | intention: str = "v1/intention/" 32 | 33 | def get_url(self, endpoint, **kwargs): 34 | # based on available attributes and passed keyword arguments 35 | return f"{self.base_url}{getattr(self, endpoint)}".format(**kwargs) 36 | 37 | 38 | # Example usage 39 | # paymob_urls = PaymobUrls() 40 | # order_registration_url = paymob_urls.get_url("order") 41 | # void_transaction_url = paymob_urls.get_url("void", token="your_token") 42 | # tracking_url = paymob_urls.get_url("tracking", order_id="123", token="your_token") 43 | -------------------------------------------------------------------------------- /payments/templates/includes/braintree_checkout.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | var button = document.querySelector('#submit-button'); 4 | var form = document.querySelector('#payment-form'); 5 | var data = {{ frappe.form_dict | json }}; 6 | var doctype = "{{ reference_doctype }}" 7 | var docname = "{{ reference_docname }}" 8 | 9 | braintree.dropin.create({ 10 | authorization: "{{ client_token }}", 11 | container: '#bt-dropin', 12 | paypal: { 13 | flow: 'vault' 14 | } 15 | }, function(createErr, instance) { 16 | form.addEventListener('submit', function(event) { 17 | event.preventDefault(); 18 | instance.requestPaymentMethod(function(err, payload) { 19 | if (err) { 20 | console.log('Error', err); 21 | return; 22 | } 23 | frappe.call({ 24 | method: "payments.templates.pages.braintree_checkout.make_payment", 25 | freeze: true, 26 | headers: { 27 | "X-Requested-With": "XMLHttpRequest" 28 | }, 29 | args: { 30 | "payload_nonce": payload.nonce, 31 | "data": JSON.stringify(data), 32 | "reference_doctype": doctype, 33 | "reference_docname": docname 34 | }, 35 | callback: function(r) { 36 | if (r.message && r.message.status == "Completed") { 37 | window.location.href = r.message.redirect_to 38 | } else if (r.message && r.message.status == "Error") { 39 | window.location.href = r.message.redirect_to 40 | } 41 | } 42 | }) 43 | }); 44 | }); 45 | 46 | instance.on('paymentMethodRequestable', function (event) { 47 | button.removeAttribute('disabled'); 48 | }); 49 | 50 | instance.on('noPaymentMethodRequestable', function () { 51 | button.setAttribute('disabled', true); 52 | }); 53 | }); 54 | 55 | }) 56 | -------------------------------------------------------------------------------- /payments/templates/includes/razorpay_checkout.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | (function(e){ 3 | var options = { 4 | "key": "{{ api_key }}", 5 | "amount": cint({{ amount }} * 100), // 2000 paise = INR 20 6 | "currency": "{{ currency }}", 7 | "name": "{{ title }}", 8 | "description": "{{ description }}", 9 | "subscription_id": "{{ subscription_id }}", 10 | "handler": function (response){ 11 | razorpay.make_payment_log(response, options, "{{ reference_doctype }}", "{{ reference_docname }}", "{{ token }}"); 12 | }, 13 | "prefill": { 14 | "name": "{{ payer_name }}", 15 | "email": "{{ payer_email }}", 16 | "order_id": "{{ order_id }}" 17 | }, 18 | "notes": {{ frappe.form_dict|json }} 19 | }; 20 | 21 | var rzp = new Razorpay(options); 22 | rzp.open(); 23 | // e.preventDefault(); 24 | })(); 25 | }) 26 | 27 | frappe.provide('razorpay'); 28 | 29 | razorpay.make_payment_log = function(response, options, doctype, docname, token){ 30 | $('.razorpay-loading').addClass('hidden'); 31 | $('.razorpay-confirming').removeClass('hidden'); 32 | 33 | frappe.call({ 34 | method:"payments.templates.pages.razorpay_checkout.make_payment", 35 | freeze:true, 36 | headers: {"X-Requested-With": "XMLHttpRequest"}, 37 | args: { 38 | "razorpay_payment_id": response.razorpay_payment_id, 39 | "options": options, 40 | "reference_doctype": doctype, 41 | "reference_docname": docname, 42 | "token": token 43 | }, 44 | callback: function(r){ 45 | if (r.message && r.message.status == 200) { 46 | window.location.href = r.message.redirect_to 47 | } 48 | else if (r.message && ([401,400,500].indexOf(r.message.status) > -1)) { 49 | window.location.href = r.message.redirect_to 50 | } 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:gateway_name", 4 | "creation": "2018-02-06 16:11:10.028249", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "gateway_name", 10 | "section_break_2", 11 | "access_token", 12 | "webhooks_secret", 13 | "use_sandbox", 14 | "header_img" 15 | ], 16 | "fields": [ 17 | { 18 | "fieldname": "gateway_name", 19 | "fieldtype": "Data", 20 | "in_list_view": 1, 21 | "label": "Payment Gateway Name", 22 | "reqd": 1, 23 | "unique": 1 24 | }, 25 | { 26 | "fieldname": "section_break_2", 27 | "fieldtype": "Section Break" 28 | }, 29 | { 30 | "fieldname": "access_token", 31 | "fieldtype": "Data", 32 | "in_list_view": 1, 33 | "label": "Access Token", 34 | "reqd": 1 35 | }, 36 | { 37 | "fieldname": "webhooks_secret", 38 | "fieldtype": "Data", 39 | "label": "Webhooks Secret" 40 | }, 41 | { 42 | "default": "0", 43 | "fieldname": "use_sandbox", 44 | "fieldtype": "Check", 45 | "label": "Use Sandbox" 46 | }, 47 | { 48 | "fieldname": "header_img", 49 | "fieldtype": "Attach Image", 50 | "label": "Header Image" 51 | } 52 | ], 53 | "links": [], 54 | "modified": "2024-05-14 16:18:26.154479", 55 | "modified_by": "Administrator", 56 | "module": "Payment Gateways", 57 | "name": "GoCardless Settings", 58 | "naming_rule": "By fieldname", 59 | "owner": "Administrator", 60 | "permissions": [ 61 | { 62 | "create": 1, 63 | "delete": 1, 64 | "email": 1, 65 | "export": 1, 66 | "print": 1, 67 | "read": 1, 68 | "report": 1, 69 | "role": "System Manager", 70 | "share": 1, 71 | "write": 1 72 | } 73 | ], 74 | "sort_field": "modified", 75 | "sort_order": "DESC", 76 | "states": [], 77 | "track_changes": 1 78 | } -------------------------------------------------------------------------------- /payments/templates/pages/braintree_checkout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | 4 | import json 5 | 6 | import frappe 7 | from frappe import _ 8 | from frappe.utils import flt 9 | 10 | from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( 11 | get_client_token, 12 | get_gateway_controller, 13 | ) 14 | 15 | no_cache = 1 16 | 17 | expected_keys = ( 18 | "amount", 19 | "title", 20 | "description", 21 | "reference_doctype", 22 | "reference_docname", 23 | "payer_name", 24 | "payer_email", 25 | "order_id", 26 | "currency", 27 | ) 28 | 29 | 30 | def get_context(context): 31 | context.no_cache = 1 32 | 33 | # all these keys exist in form_dict 34 | if not (set(expected_keys) - set(list(frappe.form_dict))): 35 | for key in expected_keys: 36 | context[key] = frappe.form_dict[key] 37 | 38 | context.client_token = get_client_token(context.reference_docname) 39 | 40 | context["amount"] = flt(context["amount"]) 41 | 42 | gateway_controller = get_gateway_controller(context.reference_docname) 43 | context["header_img"] = frappe.db.get_value("Braintree Settings", gateway_controller, "header_img") 44 | 45 | else: 46 | frappe.redirect_to_message( 47 | _("Some information is missing"), 48 | _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), 49 | ) 50 | frappe.local.flags.redirect_location = frappe.local.response.location 51 | raise frappe.Redirect 52 | 53 | 54 | @frappe.whitelist(allow_guest=True) 55 | def make_payment(payload_nonce, data, reference_doctype, reference_docname): 56 | data = json.loads(data) 57 | 58 | data.update({"payload_nonce": payload_nonce}) 59 | 60 | gateway_controller = get_gateway_controller(reference_docname) 61 | data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) 62 | frappe.db.commit() 63 | return data 64 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "payments" 3 | authors = [ 4 | { name = "Frappe Technologies Pvt Ltd", email = "hello@frappe.io"} 5 | ] 6 | description = "Payments app for frappe" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | "paytmchecksum~=1.7.0", 12 | "razorpay~=1.4.2", 13 | "stripe~=10.12.0", 14 | "braintree~=4.20.0", 15 | "pycryptodome>=3.18.0,<4.0.0", 16 | "gocardless-pro~=1.22.0", 17 | "setuptools==80.9.0", 18 | ] 19 | 20 | [build-system] 21 | requires = ["flit_core >=3.4,<4"] 22 | build-backend = "flit_core.buildapi" 23 | 24 | [tool.ruff] 25 | line-length = 110 26 | target-version = "py310" 27 | 28 | [tool.ruff.lint] 29 | select = [ 30 | "F", 31 | "E", 32 | "W", 33 | "I", 34 | "UP", 35 | "B", 36 | "RUF", 37 | ] 38 | ignore = [ 39 | "B017", # assertRaises(Exception) - should be more specific 40 | "B018", # useless expression, not assigned to anything 41 | "B023", # function doesn't bind loop variable - will have last iteration's value 42 | "B904", # raise inside except without from 43 | "E101", # indentation contains mixed spaces and tabs 44 | "E402", # module level import not at top of file 45 | "E501", # line too long 46 | "E741", # ambiguous variable name 47 | "F401", # "unused" imports 48 | "F403", # can't detect undefined names from * import 49 | "F405", # can't detect undefined names from * import 50 | "F722", # syntax error in forward type annotation 51 | "W191", # indentation contains tabs 52 | "RUF001", # string contains ambiguous unicode character 53 | ] 54 | typing-modules = ["frappe.types.DF"] 55 | 56 | [tool.ruff.format] 57 | quote-style = "double" 58 | indent-style = "tab" 59 | docstring-code-format = true 60 | 61 | [project.urls] 62 | Repository = "https://github.com/frappe/payments.git" 63 | "Bug Reports" = "https://github.com/frappe/payments/issues" 64 | -------------------------------------------------------------------------------- /payments/templates/pages/stripe_checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block title %} Payment {% endblock %} 4 | 5 | {%- block header -%} 6 | {% endblock %} 7 | 8 | {% block script %} 9 | 10 | 11 | {% endblock %} 12 | 13 | {%- block page_content -%} 14 | 15 |
16 |
17 | {% if image %} 18 | 19 | {% endif %} 20 |

21 | {{description}} 22 |

23 |
24 |
25 |
26 |
27 | 31 |
32 |
33 |
34 |
35 | 39 |
40 |
41 |
42 | 47 |
48 | 49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 |
57 |
58 | 59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [pre-commit] 3 | fail_fast: false 4 | 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.3.0 9 | hooks: 10 | - id: trailing-whitespace 11 | files: "payments.*" 12 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" 13 | - id: check-yaml 14 | - id: no-commit-to-branch 15 | args: ['--branch', 'develop'] 16 | - id: check-merge-conflict 17 | - id: check-ast 18 | - id: check-json 19 | - id: check-toml 20 | - id: check-yaml 21 | - id: debug-statements 22 | 23 | - repo: https://github.com/pre-commit/mirrors-prettier 24 | rev: v2.7.1 25 | hooks: 26 | - id: prettier 27 | types_or: [javascript, vue, scss] 28 | # Ignore any files that might contain jinja / bundles 29 | exclude: | 30 | (?x)^( 31 | payments/public/dist/.*| 32 | cypress/.*| 33 | .*node_modules.*| 34 | payments/templates/includes/.* 35 | )$ 36 | 37 | - repo: https://github.com/pre-commit/mirrors-eslint 38 | rev: v8.44.0 39 | hooks: 40 | - id: eslint 41 | types_or: [javascript] 42 | args: ['--quiet'] 43 | # Ignore any files that might contain jinja / bundles 44 | exclude: | 45 | (?x)^( 46 | payments/public/dist/.*| 47 | cypress/.*| 48 | .*node_modules.*| 49 | payments/templates/includes/.* 50 | )$ 51 | 52 | - repo: https://github.com/astral-sh/ruff-pre-commit 53 | rev: v0.2.0 54 | hooks: 55 | - id: ruff 56 | name: "Run ruff import sorter" 57 | args: ["--select=I", "--fix"] 58 | 59 | - id: ruff 60 | name: "Run ruff linter" 61 | 62 | - id: ruff-format 63 | name: "Run ruff formatter" 64 | 65 | ci: 66 | autoupdate_schedule: weekly 67 | skip: [] 68 | submodules: false 69 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paytm_settings/paytm_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2020-04-02 00:11:22.846697", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "merchant_id", 9 | "merchant_key", 10 | "staging", 11 | "column_break_4", 12 | "industry_type_id", 13 | "website" 14 | ], 15 | "fields": [ 16 | { 17 | "fieldname": "merchant_id", 18 | "fieldtype": "Data", 19 | "in_list_view": 1, 20 | "label": "Merchant ID", 21 | "reqd": 1, 22 | "show_days": 1, 23 | "show_seconds": 1 24 | }, 25 | { 26 | "fieldname": "merchant_key", 27 | "fieldtype": "Password", 28 | "in_list_view": 1, 29 | "label": "Merchant Key", 30 | "reqd": 1, 31 | "show_days": 1, 32 | "show_seconds": 1 33 | }, 34 | { 35 | "default": "0", 36 | "fieldname": "staging", 37 | "fieldtype": "Check", 38 | "label": "Staging", 39 | "show_days": 1, 40 | "show_seconds": 1 41 | }, 42 | { 43 | "depends_on": "eval: !doc.staging", 44 | "fieldname": "website", 45 | "fieldtype": "Data", 46 | "label": "Website", 47 | "mandatory_depends_on": "eval: !doc.staging", 48 | "show_days": 1, 49 | "show_seconds": 1 50 | }, 51 | { 52 | "fieldname": "column_break_4", 53 | "fieldtype": "Column Break", 54 | "show_days": 1, 55 | "show_seconds": 1 56 | }, 57 | { 58 | "depends_on": "eval: !doc.staging", 59 | "fieldname": "industry_type_id", 60 | "fieldtype": "Data", 61 | "label": "Industry Type ID", 62 | "mandatory_depends_on": "eval: !doc.staging", 63 | "show_days": 1, 64 | "show_seconds": 1 65 | } 66 | ], 67 | "issingle": 1, 68 | "links": [], 69 | "modified": "2022-07-24 13:36:09.703143", 70 | "modified_by": "Administrator", 71 | "module": "Payment Gateways", 72 | "name": "Paytm Settings", 73 | "owner": "Administrator", 74 | "permissions": [ 75 | { 76 | "create": 1, 77 | "delete": 1, 78 | "email": 1, 79 | "print": 1, 80 | "read": 1, 81 | "role": "System Manager", 82 | "share": 1, 83 | "write": 1 84 | } 85 | ], 86 | "sort_field": "modified", 87 | "sort_order": "DESC", 88 | "track_changes": 1 89 | } -------------------------------------------------------------------------------- /payments/templates/pages/razorpay_checkout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | import json 4 | 5 | import frappe 6 | from frappe import _ 7 | from frappe.utils import cint, flt 8 | 9 | from payments.utils.utils import validate_integration_request 10 | 11 | no_cache = 1 12 | 13 | expected_keys = ( 14 | "amount", 15 | "title", 16 | "description", 17 | "reference_doctype", 18 | "reference_docname", 19 | "payer_name", 20 | "payer_email", 21 | "order_id", 22 | "currency", 23 | ) 24 | 25 | 26 | def get_context(context): 27 | context.no_cache = 1 28 | context.api_key = get_api_key() 29 | 30 | try: 31 | validate_integration_request(frappe.form_dict["token"]) 32 | 33 | doc = frappe.get_doc("Integration Request", frappe.form_dict["token"]) 34 | 35 | payment_details = json.loads(doc.data) 36 | 37 | for key in expected_keys: 38 | context[key] = payment_details[key] 39 | 40 | context["token"] = frappe.form_dict["token"] 41 | context["amount"] = flt(context["amount"]) 42 | context["subscription_id"] = ( 43 | payment_details["subscription_id"] if payment_details.get("subscription_id") else "" 44 | ) 45 | 46 | except Exception: 47 | frappe.redirect_to_message( 48 | _("Invalid Token"), 49 | _("Seems token you are using is invalid!"), 50 | http_status_code=400, 51 | indicator_color="red", 52 | ) 53 | 54 | frappe.local.flags.redirect_location = frappe.local.response.location 55 | raise frappe.Redirect 56 | 57 | 58 | def get_api_key(): 59 | api_key = frappe.db.get_single_value("Razorpay Settings", "api_key") 60 | if cint(frappe.form_dict.get("use_sandbox")): 61 | api_key = frappe.conf.sandbox_api_key 62 | 63 | return api_key 64 | 65 | 66 | @frappe.whitelist(allow_guest=True) 67 | def make_payment(razorpay_payment_id, options, reference_doctype, reference_docname, token): 68 | data = {} 69 | 70 | if isinstance(options, str): 71 | data = json.loads(options) 72 | 73 | data.update( 74 | { 75 | "razorpay_payment_id": razorpay_payment_id, 76 | "reference_docname": reference_docname, 77 | "reference_doctype": reference_doctype, 78 | "token": token, 79 | } 80 | ) 81 | 82 | data = frappe.get_doc("Razorpay Settings").create_request(data) 83 | frappe.db.commit() 84 | return data 85 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/gocardless_settings/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies and contributors 2 | # For license information, please see license.txt 3 | 4 | 5 | import hashlib 6 | import hmac 7 | import json 8 | 9 | import frappe 10 | 11 | 12 | @frappe.whitelist(allow_guest=True) 13 | def webhooks(): 14 | r = frappe.request 15 | if not r: 16 | return 17 | 18 | if not authenticate_signature(r): 19 | raise frappe.AuthenticationError 20 | 21 | gocardless_events = json.loads(r.get_data()) or [] 22 | for event in gocardless_events["events"]: 23 | set_status(event) 24 | 25 | return 200 26 | 27 | 28 | def set_status(event): 29 | resource_type = event.get("resource_type", {}) 30 | 31 | if resource_type == "mandates": 32 | set_mandate_status(event) 33 | 34 | 35 | def set_mandate_status(event): 36 | mandates = [] 37 | if isinstance(event["links"], list): 38 | for link in event["links"]: 39 | mandates.append(link["mandate"]) 40 | else: 41 | mandates.append(event["links"]["mandate"]) 42 | 43 | if ( 44 | event["action"] == "pending_customer_approval" 45 | or event["action"] == "pending_submission" 46 | or event["action"] == "submitted" 47 | or event["action"] == "active" 48 | ): 49 | disabled = 0 50 | else: 51 | disabled = 1 52 | 53 | for mandate in mandates: 54 | frappe.db.set_value("GoCardless Mandate", mandate, "disabled", disabled) 55 | 56 | 57 | def authenticate_signature(r): 58 | """Returns True if the received signature matches the generated signature""" 59 | received_signature = frappe.get_request_header("Webhook-Signature") 60 | 61 | if not received_signature: 62 | return False 63 | 64 | for key in get_webhook_keys(): 65 | computed_signature = hmac.new(key.encode("utf-8"), r.get_data(), hashlib.sha256).hexdigest() 66 | if hmac.compare_digest(str(received_signature), computed_signature): 67 | return True 68 | 69 | return False 70 | 71 | 72 | def get_webhook_keys(): 73 | def _get_webhook_keys(): 74 | webhook_keys = [ 75 | d.webhooks_secret 76 | for d in frappe.get_all( 77 | "GoCardless Settings", 78 | fields=["webhooks_secret"], 79 | ) 80 | if d.webhooks_secret 81 | ] 82 | 83 | return webhook_keys 84 | 85 | return frappe.cache().get_value("gocardless_webhooks_secret", _get_webhook_keys) 86 | 87 | 88 | def clear_cache(): 89 | frappe.cache().delete_value("gocardless_webhooks_secret") 90 | -------------------------------------------------------------------------------- /.github/helper/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd ~ || exit 6 | 7 | sudo apt update 8 | sudo apt remove mysql-server mysql-client 9 | sudo apt install libcups2-dev redis-server mariadb-client 10 | 11 | pip install frappe-bench 12 | 13 | githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}} 14 | frappeuser=${FRAPPE_USER:-"frappe"} 15 | frappebranch=${FRAPPE_BRANCH:-$githubbranch} 16 | erpnextbranch=${ERPNEXT_BRANCH:-$githubbranch} 17 | 18 | git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1 19 | bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench 20 | 21 | mkdir ~/frappe-bench/sites/test_site 22 | cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ 23 | 24 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" 25 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 26 | 27 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" 28 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" 29 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" 30 | 31 | mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" 32 | 33 | install_whktml() { 34 | wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz 35 | tar -xf /tmp/wkhtmltox.tar.xz -C /tmp 36 | sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf 37 | sudo chmod o+x /usr/local/bin/wkhtmltopdf 38 | } 39 | install_whktml & 40 | 41 | cd ~/frappe-bench || exit 42 | 43 | sed -i 's/watch:/# watch:/g' Procfile 44 | sed -i 's/schedule:/# schedule:/g' Procfile 45 | sed -i 's/socketio:/# socketio:/g' Procfile 46 | sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile 47 | 48 | bench get-app "https://github.com/${frappeuser}/erpnext" --branch "$erpnextbranch" --resolve-deps 49 | bench get-app payments "${GITHUB_WORKSPACE}" 50 | bench setup requirements --dev 51 | 52 | bench start &>> ~/frappe-bench/bench_start.log & 53 | CI=Yes bench build --app frappe & 54 | bench --site test_site reinstall --yes 55 | 56 | bench --verbose --site test_site install-app payments 57 | -------------------------------------------------------------------------------- /payments/templates/includes/stripe_checkout.js: -------------------------------------------------------------------------------- 1 | var stripe = Stripe("{{ publishable_key }}"); 2 | 3 | var elements = stripe.elements(); 4 | 5 | var style = { 6 | base: { 7 | color: '#32325d', 8 | lineHeight: '18px', 9 | fontFamily: '"Helvetica Neue", Helvetica, sans-serif', 10 | fontSmoothing: 'antialiased', 11 | fontSize: '16px', 12 | '::placeholder': { 13 | color: '#aab7c4' 14 | } 15 | }, 16 | invalid: { 17 | color: '#fa755a', 18 | iconColor: '#fa755a' 19 | } 20 | }; 21 | 22 | var card = elements.create('card', { 23 | hidePostalCode: true, 24 | style: style 25 | }); 26 | 27 | card.mount('#card-element'); 28 | 29 | function setOutcome(result) { 30 | 31 | if (result.token) { 32 | $('#submit').prop('disabled', true) 33 | $('#submit').html(__('Processing...')) 34 | frappe.call({ 35 | method:"payments.templates.pages.stripe_checkout.make_payment", 36 | freeze:true, 37 | headers: {"X-Requested-With": "XMLHttpRequest"}, 38 | args: { 39 | "stripe_token_id": result.token.id, 40 | "data": JSON.stringify({{ frappe.form_dict|json }}), 41 | "reference_doctype": "{{ reference_doctype }}", 42 | "reference_docname": "{{ reference_docname }}", 43 | "payment_gateway": "{{ payment_gateway }}" 44 | }, 45 | callback: function(r) { 46 | if (r.message.status == "Completed") { 47 | $('#submit').hide() 48 | $('.success').show() 49 | setTimeout(function() { 50 | window.location.href = r.message.redirect_to 51 | }, 2000); 52 | } else { 53 | $('#submit').hide() 54 | $('.error').show() 55 | setTimeout(function() { 56 | window.location.href = r.message.redirect_to 57 | }, 2000); 58 | } 59 | } 60 | }); 61 | 62 | } else if (result.error) { 63 | $('.error').html(result.error.message); 64 | $('.error').show() 65 | } 66 | } 67 | 68 | card.on('change', function(event) { 69 | var displayError = document.getElementById('card-errors'); 70 | if (event.error) { 71 | displayError.textContent = event.error.message; 72 | } else { 73 | displayError.textContent = ''; 74 | } 75 | }); 76 | 77 | frappe.ready(function() { 78 | $('#submit').off("click").on("click", function(e) { 79 | e.preventDefault(); 80 | var extraDetails = { 81 | name: $('input[name=cardholder-name]').val(), 82 | email: $('input[name=cardholder-email]').val() 83 | } 84 | stripe.createToken(card, extraDetails).then(setOutcome); 85 | }) 86 | }); 87 | -------------------------------------------------------------------------------- /payments/templates/pages/stripe_checkout.css: -------------------------------------------------------------------------------- 1 | .StripeElement { 2 | background-color: white; 3 | height: 40px; 4 | padding: 10px 12px; 5 | border-radius: 4px; 6 | border: 1px solid transparent; 7 | box-shadow: 0 1px 3px 0 #e6ebf1; 8 | -webkit-transition: box-shadow 150ms ease; 9 | transition: box-shadow 150ms ease; 10 | } 11 | 12 | .StripeElement--focus { 13 | box-shadow: 0 1px 3px 0 #cfd7df; 14 | } 15 | 16 | .StripeElement--invalid { 17 | border-color: #fa755a; 18 | } 19 | 20 | .StripeElement--webkit-autofill { 21 | background-color: #fefde5; 22 | } 23 | 24 | .stripe #payment-form { 25 | margin-top: 2rem; 26 | } 27 | 28 | .stripe button { 29 | float: right; 30 | display: block; 31 | background: #171717; 32 | color: white; 33 | border-radius: 8px; 34 | border: 0; 35 | font-size: 14px; 36 | font-weight: 420; 37 | max-width: 40%; 38 | height: 40px; 39 | line-height: 38px; 40 | outline: none; 41 | } 42 | 43 | .stripe .group { 44 | background: white; 45 | border-radius: 4px; 46 | margin-bottom: 20px; 47 | } 48 | 49 | .stripe label { 50 | position: relative; 51 | color: #8898AA; 52 | font-weight: 300; 53 | height: 40px; 54 | line-height: 40px; 55 | margin-left: 20px; 56 | display: block; 57 | } 58 | 59 | .stripe .group label:not(:last-child) { 60 | border-bottom: 1px solid #F0F5FA; 61 | } 62 | 63 | .stripe label>span { 64 | width: 20%; 65 | float: left; 66 | color: #525252; 67 | font-size: 13px; 68 | font-weight: 420; 69 | letter-spacing: 0.02em; 70 | } 71 | 72 | .current-card { 73 | margin-left: 20px; 74 | } 75 | 76 | .field { 77 | border-radius: 8px; 78 | letter-spacing: 0.02em; 79 | font-size: 14px; 80 | font-weight: 420; 81 | width: 70%; 82 | float: right; 83 | border: none; 84 | height: 28px; 85 | padding: 6px 8px; 86 | color: #383838; 87 | background: #f3f3f3; 88 | background-clip: padding-box; 89 | } 90 | 91 | .field::-webkit-input-placeholder { 92 | color: #CFD7E0; 93 | } 94 | 95 | .field::-moz-placeholder { 96 | color: #CFD7E0; 97 | } 98 | 99 | .field:-ms-input-placeholder { 100 | color: #CFD7E0; 101 | } 102 | 103 | #payment-section { 104 | min-height: 50vh; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | flex-direction: column; 109 | padding: 10rem; 110 | } 111 | 112 | @media (max-width: 48rem) { 113 | #payment-section { 114 | padding: 0rem; 115 | min-height: 70vh; 116 | }, 117 | } -------------------------------------------------------------------------------- /payments/payment_gateways/stripe_integration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | import stripe 6 | from frappe import _ 7 | from frappe.integrations.utils import create_request_log 8 | 9 | 10 | def create_stripe_subscription(gateway_controller, data): 11 | stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller) 12 | stripe_settings.data = frappe._dict(data) 13 | 14 | stripe.api_key = stripe_settings.get_password(fieldname="secret_key", raise_exception=False) 15 | stripe.default_http_client = stripe.http_client.RequestsClient() 16 | 17 | try: 18 | stripe_settings.integration_request = create_request_log(stripe_settings.data, "Host", "Stripe") 19 | stripe_settings.payment_plans = frappe.get_doc( 20 | "Payment Request", stripe_settings.data.reference_docname 21 | ).subscription_plans 22 | return create_subscription_on_stripe(stripe_settings) 23 | 24 | except Exception: 25 | stripe_settings.log_error("Unable to create Stripe subscription") 26 | return { 27 | "redirect_to": frappe.redirect_to_message( 28 | _("Server Error"), 29 | _( 30 | "It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account." 31 | ), 32 | ), 33 | "status": 401, 34 | } 35 | 36 | 37 | def create_subscription_on_stripe(stripe_settings): 38 | items = [] 39 | for payment_plan in stripe_settings.payment_plans: 40 | plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id") 41 | items.append({"price": plan, "quantity": payment_plan.qty}) 42 | 43 | try: 44 | customer = stripe.Customer.create( 45 | source=stripe_settings.data.stripe_token_id, 46 | description=stripe_settings.data.payer_name, 47 | email=stripe_settings.data.payer_email, 48 | ) 49 | 50 | subscription = stripe.Subscription.create(customer=customer, items=items) 51 | 52 | if subscription.status == "active": 53 | stripe_settings.integration_request.db_set("status", "Completed", update_modified=False) 54 | stripe_settings.flags.status_changed_to = "Completed" 55 | 56 | else: 57 | stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) 58 | frappe.log_error(f"Stripe Subscription ID {subscription.id}: Payment failed") 59 | except Exception: 60 | stripe_settings.integration_request.db_set("status", "Failed", update_modified=False) 61 | stripe_settings.log_error("Unable to create Stripe subscription") 62 | 63 | return stripe_settings.finalize_request() 64 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/accept_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Union 3 | 4 | import frappe 5 | import requests 6 | 7 | from .connection import AcceptConnection 8 | from .paymob_urls import PaymobUrls 9 | from .response_codes import SUCCESS 10 | from .response_feedback_dataclass import ResponseFeedBack 11 | 12 | 13 | class AcceptAPI: 14 | def __init__(self) -> None: 15 | """Class for Accept APIs 16 | By Initializing an Instance from This class, an auth token is obtained automatically 17 | and You will be able to call The Following APIs: 18 | - Create Payment Intention 19 | - Get Transaction Details 20 | """ 21 | self.connection = AcceptConnection() 22 | self.paymob_settings = frappe.get_doc("Paymob Settings") 23 | self.paymob_urls = PaymobUrls() 24 | 25 | def retrieve_auth_token(self): 26 | """ 27 | Authentication Request: 28 | :return: token: Authentication token, which is valid for one hour from the creation time. 29 | """ 30 | return self.connection.auth_token 31 | 32 | def create_payment_intent(self, data: dict) -> tuple[str, dict | None, ResponseFeedBack]: 33 | """ 34 | Creates a Paymob Payment Intent 35 | :param data: Dictionary containing payment intent details (refer to Paymob documentation) 36 | :return: Tuple[str, Union[Dict, None], ResponseFeedBack]: (Code, Dict, ResponseFeedBack Instance) 37 | 38 | """ 39 | 40 | headers = { 41 | "Authorization": f"Token {self.paymob_settings.get_password('secret_key')}", 42 | "Content-Type": "application/json", 43 | } 44 | payload = json.dumps(data) 45 | code, feedback = self.connection.post( 46 | url=self.paymob_urls.get_url("intention"), headers=headers, data=payload 47 | ) 48 | 49 | payment_intent = frappe._dict() 50 | 51 | if code == SUCCESS: 52 | payment_intent = feedback.data 53 | feedback.message = "Payment Intention Created Successfully" 54 | 55 | return code, payment_intent, feedback 56 | 57 | def retrieve_transaction(self, transaction_id: int) -> tuple[str, dict | None, ResponseFeedBack]: 58 | """Retrieves Transaction Data by Transaction ID 59 | 60 | Args: 61 | transaction_id (int): Paymob's Transaction ID 62 | 63 | Returns: 64 | Tuple[str, Union[Dict, None], ResponseFeedBack]: (Code, Dict, ResponseFeedBack Instance) 65 | """ 66 | code, feedback = self.connection.get( 67 | url=self.paymob_urls.get_url("retrieve_transaction", id=transaction_id) 68 | ) 69 | transaction = None 70 | if code == SUCCESS: 71 | transaction = feedback.data 72 | feedback.message = f"Transaction with id {transaction_id} retrieved Scuccessfully" 73 | return code, transaction, feedback 74 | 75 | def retrieve_iframe(self, iframe_id, payment_token): 76 | iframe_url = self.paymob_urls.get_url( 77 | "iframe", iframe_id=self.paymob_settings.iframe, payment_token=payment_token 78 | ) 79 | return iframe_url 80 | -------------------------------------------------------------------------------- /payments/templates/pages/gocardless_checkout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: GNU General Public License v3. See license.txt 3 | 4 | import json 5 | 6 | import frappe 7 | from frappe import _ 8 | from frappe.utils import flt, get_url 9 | 10 | from payments.payment_gateways.doctype.gocardless_settings.gocardless_settings import ( 11 | get_gateway_controller, 12 | gocardless_initialization, 13 | ) 14 | 15 | no_cache = 1 16 | 17 | expected_keys = ( 18 | "amount", 19 | "title", 20 | "description", 21 | "reference_doctype", 22 | "reference_docname", 23 | "payer_name", 24 | "payer_email", 25 | "order_id", 26 | "currency", 27 | ) 28 | 29 | 30 | def get_context(context): 31 | context.no_cache = 1 32 | 33 | # all these keys exist in form_dict 34 | if not (set(expected_keys) - set(frappe.form_dict.keys())): 35 | for key in expected_keys: 36 | context[key] = frappe.form_dict[key] 37 | 38 | context["amount"] = flt(context["amount"]) 39 | 40 | gateway_controller = get_gateway_controller(context.reference_docname) 41 | context["header_img"] = frappe.db.get_value("GoCardless Settings", gateway_controller, "header_img") 42 | 43 | else: 44 | frappe.redirect_to_message( 45 | _("Some information is missing"), 46 | _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), 47 | ) 48 | frappe.local.flags.redirect_location = frappe.local.response.location 49 | raise frappe.Redirect 50 | 51 | 52 | @frappe.whitelist(allow_guest=True) 53 | def check_mandate(data, reference_doctype, reference_docname): 54 | data = json.loads(data) 55 | 56 | client = gocardless_initialization(reference_docname) 57 | 58 | payer = frappe.get_doc("Customer", data["payer_name"]) 59 | 60 | if payer.customer_type == "Individual" and payer.customer_primary_contact is not None: 61 | primary_contact = frappe.get_doc("Contact", payer.customer_primary_contact) 62 | prefilled_customer = { 63 | "company_name": payer.name, 64 | "given_name": primary_contact.first_name, 65 | } 66 | if primary_contact.last_name is not None: 67 | prefilled_customer.update({"family_name": primary_contact.last_name}) 68 | 69 | if primary_contact.email_id is not None: 70 | prefilled_customer.update({"email": primary_contact.email_id}) 71 | else: 72 | prefilled_customer.update({"email": frappe.session.user}) 73 | 74 | else: 75 | prefilled_customer = {"company_name": payer.name, "email": frappe.session.user} 76 | 77 | success_url = get_url( 78 | "gocardless_confirmation?reference_doctype=" 79 | + reference_doctype 80 | + "&reference_docname=" 81 | + reference_docname 82 | ) 83 | 84 | try: 85 | redirect_flow = client.redirect_flows.create( 86 | params={ 87 | "description": _("Pay {0} {1}").format(data["amount"], data["currency"]), 88 | "session_token": frappe.session.user, 89 | "success_redirect_url": success_url, 90 | "prefilled_customer": prefilled_customer, 91 | } 92 | ) 93 | 94 | return {"redirect_to": redirect_flow.redirect_url} 95 | 96 | except Exception: 97 | frappe.log_error("GoCardless Payment Error") 98 | return {"redirect_to": "payment-failed"} 99 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2022": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": "off", 13 | "brace-style": "off", 14 | "no-mixed-spaces-and-tabs": "off", 15 | "no-useless-escape": "off", 16 | "space-unary-ops": ["error", { "words": true }], 17 | "linebreak-style": "off", 18 | "quotes": ["off"], 19 | "semi": "off", 20 | "camelcase": "off", 21 | "no-unused-vars": "off", 22 | "no-console": ["warn"], 23 | "no-extra-boolean-cast": ["off"], 24 | "no-control-regex": ["off"] 25 | }, 26 | "root": true, 27 | "globals": { 28 | "frappe": true, 29 | "Vue": true, 30 | "SetVueGlobals": true, 31 | "erpnext": true, 32 | "hub": true, 33 | "$": true, 34 | "jQuery": true, 35 | "moment": true, 36 | "hljs": true, 37 | "Awesomplete": true, 38 | "CalHeatMap": true, 39 | "Sortable": true, 40 | "Showdown": true, 41 | "Taggle": true, 42 | "Gantt": true, 43 | "Slick": true, 44 | "PhotoSwipe": true, 45 | "PhotoSwipeUI_Default": true, 46 | "fluxify": true, 47 | "io": true, 48 | "c3": true, 49 | "__": true, 50 | "_p": true, 51 | "_f": true, 52 | "repl": true, 53 | "Class": true, 54 | "locals": true, 55 | "cint": true, 56 | "cstr": true, 57 | "cur_frm": true, 58 | "cur_dialog": true, 59 | "cur_page": true, 60 | "cur_list": true, 61 | "cur_tree": true, 62 | "cur_pos": true, 63 | "msg_dialog": true, 64 | "is_null": true, 65 | "in_list": true, 66 | "has_common": true, 67 | "posthog": true, 68 | "has_words": true, 69 | "validate_email": true, 70 | "open_web_template_values_editor": true, 71 | "get_number_format": true, 72 | "format_number": true, 73 | "format_currency": true, 74 | "round_based_on_smallest_currency_fraction": true, 75 | "roundNumber": true, 76 | "comment_when": true, 77 | "replace_newlines": true, 78 | "open_url_post": true, 79 | "toTitle": true, 80 | "lstrip": true, 81 | "strip": true, 82 | "strip_html": true, 83 | "replace_all": true, 84 | "flt": true, 85 | "precision": true, 86 | "md5": true, 87 | "CREATE": true, 88 | "AMEND": true, 89 | "CANCEL": true, 90 | "copy_dict": true, 91 | "get_number_format_info": true, 92 | "print_table": true, 93 | "Layout": true, 94 | "web_form_settings": true, 95 | "$c": true, 96 | "$a": true, 97 | "$i": true, 98 | "$bg": true, 99 | "$y": true, 100 | "$c_obj": true, 101 | "$c_obj_csv": true, 102 | "refresh_many": true, 103 | "refresh_field": true, 104 | "toggle_field": true, 105 | "get_field_obj": true, 106 | "get_query_params": true, 107 | "unhide_field": true, 108 | "hide_field": true, 109 | "set_field_options": true, 110 | "getCookie": true, 111 | "getCookies": true, 112 | "get_url_arg": true, 113 | "get_server_fields": true, 114 | "set_multiple": true, 115 | "QUnit": true, 116 | "Chart": true, 117 | "Cypress": true, 118 | "cy": true, 119 | "describe": true, 120 | "expect": true, 121 | "it": true, 122 | "context": true, 123 | "before": true, 124 | "beforeEach": true, 125 | "onScan": true, 126 | "extend_cscript": true, 127 | "localforage": true, 128 | "Plaid": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /payments/templates/pages/stripe_checkout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: MIT. See LICENSE 3 | import json 4 | 5 | import frappe 6 | from frappe import _ 7 | from frappe.utils import cint, fmt_money 8 | 9 | from payments.payment_gateways.doctype.stripe_settings.stripe_settings import ( 10 | get_gateway_controller, 11 | ) 12 | 13 | no_cache = 1 14 | 15 | expected_keys = ( 16 | "amount", 17 | "title", 18 | "description", 19 | "reference_doctype", 20 | "reference_docname", 21 | "payer_name", 22 | "payer_email", 23 | "currency", 24 | "payment_gateway", 25 | ) 26 | 27 | 28 | def get_context(context): 29 | context.no_cache = 1 30 | 31 | # all these keys exist in form_dict 32 | if not (set(expected_keys) - set(list(frappe.form_dict))): 33 | for key in expected_keys: 34 | context[key] = frappe.form_dict[key] 35 | gateway_controller = get_gateway_controller( 36 | context.reference_doctype, context.reference_docname, context.payment_gateway 37 | ) 38 | context.publishable_key = get_api_key(context.reference_docname, gateway_controller) 39 | context.image = get_header_image(context.reference_docname, gateway_controller) 40 | 41 | context["amount"] = fmt_money(amount=context["amount"], currency=context["currency"]) 42 | 43 | if is_a_subscription(context.reference_doctype, context.reference_docname): 44 | payment_plan = frappe.db.get_value( 45 | context.reference_doctype, context.reference_docname, "payment_plan" 46 | ) 47 | recurrence = frappe.db.get_value("Payment Plan", payment_plan, "recurrence") 48 | 49 | context["amount"] = context["amount"] + " " + _(recurrence) 50 | 51 | else: 52 | frappe.redirect_to_message( 53 | _("Some information is missing"), 54 | _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), 55 | ) 56 | frappe.local.flags.redirect_location = frappe.local.response.location 57 | raise frappe.Redirect 58 | 59 | 60 | def get_api_key(doc, gateway_controller): 61 | publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") 62 | if cint(frappe.form_dict.get("use_sandbox")): 63 | publishable_key = frappe.conf.sandbox_publishable_key 64 | 65 | return publishable_key 66 | 67 | 68 | def get_header_image(doc, gateway_controller): 69 | return frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") 70 | 71 | 72 | @frappe.whitelist(allow_guest=True) 73 | def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None): 74 | data = json.loads(data) 75 | 76 | data.update({"stripe_token_id": stripe_token_id}) 77 | 78 | gateway_controller = get_gateway_controller(reference_doctype, reference_docname, payment_gateway) 79 | 80 | if is_a_subscription(reference_doctype, reference_docname): 81 | reference = frappe.get_doc(reference_doctype, reference_docname) 82 | data = reference.create_subscription("stripe", gateway_controller, data) 83 | else: 84 | data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) 85 | 86 | frappe.db.commit() 87 | return data 88 | 89 | 90 | def is_a_subscription(reference_doctype, reference_docname): 91 | if not frappe.get_meta(reference_doctype).has_field("is_a_subscription"): 92 | return False 93 | return frappe.db.get_value(reference_doctype, reference_docname, "is_a_subscription") 94 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/paymob_settings/paymob_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2025-10-22 13:58:02.482023", 5 | "doctype": "DocType", 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "credentials_section", 9 | "api_key", 10 | "public_key", 11 | "hmac", 12 | "column_break_rdwp", 13 | "secret_key", 14 | "token", 15 | "expires_in", 16 | "payment_config", 17 | "iframe", 18 | "payment_integration", 19 | "column_break_qgqc", 20 | "redirect_to" 21 | ], 22 | "fields": [ 23 | { 24 | "fieldname": "credentials_section", 25 | "fieldtype": "Section Break", 26 | "label": "Credentials" 27 | }, 28 | { 29 | "fieldname": "api_key", 30 | "fieldtype": "Password", 31 | "in_list_view": 1, 32 | "label": "API Key", 33 | "reqd": 1 34 | }, 35 | { 36 | "fieldname": "public_key", 37 | "fieldtype": "Password", 38 | "in_list_view": 1, 39 | "label": "Public Key", 40 | "reqd": 1 41 | }, 42 | { 43 | "fieldname": "hmac", 44 | "fieldtype": "Password", 45 | "in_list_view": 1, 46 | "label": "HMAC", 47 | "reqd": 1 48 | }, 49 | { 50 | "fieldname": "column_break_rdwp", 51 | "fieldtype": "Column Break" 52 | }, 53 | { 54 | "fieldname": "token", 55 | "fieldtype": "Password", 56 | "label": "Token" 57 | }, 58 | { 59 | "fieldname": "secret_key", 60 | "fieldtype": "Password", 61 | "in_list_view": 1, 62 | "label": "Secret Key", 63 | "reqd": 1 64 | }, 65 | { 66 | "fieldname": "payment_config", 67 | "fieldtype": "Section Break", 68 | "label": "Payment Config" 69 | }, 70 | { 71 | "fieldname": "iframe", 72 | "fieldtype": "Data", 73 | "in_list_view": 1, 74 | "label": "Iframe", 75 | "reqd": 1 76 | }, 77 | { 78 | "fieldname": "payment_integration", 79 | "fieldtype": "Int", 80 | "label": "Payment Integration", 81 | "reqd": 1 82 | }, 83 | { 84 | "fieldname": "expires_in", 85 | "fieldtype": "Datetime", 86 | "label": "Expires In" 87 | }, 88 | { 89 | "fieldname": "column_break_qgqc", 90 | "fieldtype": "Column Break" 91 | }, 92 | { 93 | "description": "Mention transaction completion page URL\n\n", 94 | "fieldname": "redirect_to", 95 | "fieldtype": "Data", 96 | "label": "Redirect To" 97 | } 98 | ], 99 | "grid_page_length": 50, 100 | "index_web_pages_for_search": 1, 101 | "issingle": 1, 102 | "links": [], 103 | "modified": "2025-11-20 23:53:24.529932", 104 | "modified_by": "Administrator", 105 | "module": "Payment Gateways", 106 | "name": "Paymob Settings", 107 | "owner": "Administrator", 108 | "permissions": [ 109 | { 110 | "create": 1, 111 | "delete": 1, 112 | "email": 1, 113 | "print": 1, 114 | "read": 1, 115 | "role": "System Manager", 116 | "share": 1, 117 | "write": 1 118 | }, 119 | { 120 | "create": 1, 121 | "delete": 1, 122 | "email": 1, 123 | "print": 1, 124 | "read": 1, 125 | "role": "Accounts Manager", 126 | "share": 1, 127 | "write": 1 128 | }, 129 | { 130 | "create": 1, 131 | "delete": 1, 132 | "email": 1, 133 | "print": 1, 134 | "read": 1, 135 | "role": "Accounts User", 136 | "share": 1, 137 | "write": 1 138 | } 139 | ], 140 | "row_format": "Dynamic", 141 | "rows_threshold_for_grid_search": 20, 142 | "sort_field": "modified", 143 | "sort_order": "DESC", 144 | "states": [] 145 | } -------------------------------------------------------------------------------- /payments/templates/pages/gocardless_confirmation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors 2 | # License: GNU General Public License v3. See license.txt 3 | 4 | import frappe 5 | from frappe import _ 6 | 7 | from payments.payment_gateways.doctype.gocardless_settings.gocardless_settings import ( 8 | get_gateway_controller, 9 | gocardless_initialization, 10 | ) 11 | 12 | no_cache = 1 13 | 14 | expected_keys = ("redirect_flow_id", "reference_doctype", "reference_docname") 15 | 16 | 17 | def get_context(context): 18 | context.no_cache = 1 19 | 20 | # all these keys exist in form_dict 21 | if not (set(expected_keys) - set(frappe.form_dict.keys())): 22 | for key in expected_keys: 23 | context[key] = frappe.form_dict[key] 24 | 25 | else: 26 | frappe.redirect_to_message( 27 | _("Some information is missing"), 28 | _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), 29 | ) 30 | frappe.local.flags.redirect_location = frappe.local.response.location 31 | raise frappe.Redirect 32 | 33 | 34 | @frappe.whitelist(allow_guest=True) 35 | def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): 36 | client = gocardless_initialization(reference_docname) 37 | 38 | try: 39 | redirect_flow = client.redirect_flows.complete( 40 | redirect_flow_id, params={"session_token": frappe.session.user} 41 | ) 42 | 43 | confirmation_url = redirect_flow.confirmation_url 44 | gocardless_success_page = frappe.get_hooks("gocardless_success_page") 45 | if gocardless_success_page: 46 | confirmation_url = frappe.get_attr(gocardless_success_page[-1])( 47 | reference_doctype, reference_docname 48 | ) 49 | 50 | data = { 51 | "mandate": redirect_flow.links.mandate, 52 | "customer": redirect_flow.links.customer, 53 | "redirect_to": confirmation_url, 54 | "redirect_message": "Mandate successfully created", 55 | "reference_doctype": reference_doctype, 56 | "reference_docname": reference_docname, 57 | } 58 | 59 | try: 60 | create_mandate(data) 61 | except Exception: 62 | frappe.log_error("GoCardless Mandate Registration Error") 63 | 64 | gateway_controller = get_gateway_controller(reference_docname) 65 | frappe.get_doc("GoCardless Settings", gateway_controller).create_payment_request(data) 66 | 67 | return {"redirect_to": confirmation_url} 68 | 69 | except Exception: 70 | frappe.log_error("GoCardless Payment Error") 71 | return {"redirect_to": "payment-failed"} 72 | 73 | 74 | def create_mandate(data): 75 | data = frappe._dict(data) 76 | frappe.logger().debug(data) 77 | 78 | mandate = data.get("mandate") 79 | 80 | if frappe.db.exists("GoCardless Mandate", mandate): 81 | return 82 | 83 | else: 84 | reference_doc = frappe.db.get_value( 85 | data.get("reference_doctype"), 86 | data.get("reference_docname"), 87 | ["reference_doctype", "reference_name"], 88 | as_dict=1, 89 | ) 90 | erpnext_customer = frappe.db.get_value( 91 | reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1 92 | ) 93 | 94 | try: 95 | frappe.get_doc( 96 | { 97 | "doctype": "GoCardless Mandate", 98 | "mandate": mandate, 99 | "customer": erpnext_customer.customer_name, 100 | "gocardless_customer": data.get("customer"), 101 | } 102 | ).insert(ignore_permissions=True) 103 | 104 | except Exception: 105 | frappe.log_error("Gocardless: Unable to create mandate") 106 | -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 0, 4 | "allow_rename": 0, 5 | "beta": 0, 6 | "creation": "2016-09-20 03:44:03.799402", 7 | "custom": 0, 8 | "docstatus": 0, 9 | "doctype": "DocType", 10 | "document_type": "System", 11 | "editable_grid": 1, 12 | "fields": [ 13 | { 14 | "allow_on_submit": 0, 15 | "bold": 0, 16 | "collapsible": 0, 17 | "columns": 0, 18 | "fieldname": "api_key", 19 | "fieldtype": "Data", 20 | "hidden": 0, 21 | "ignore_user_permissions": 0, 22 | "ignore_xss_filter": 0, 23 | "in_filter": 0, 24 | "in_list_view": 0, 25 | "in_standard_filter": 0, 26 | "label": "API Key", 27 | "length": 0, 28 | "no_copy": 0, 29 | "permlevel": 0, 30 | "precision": "", 31 | "print_hide": 0, 32 | "print_hide_if_no_value": 0, 33 | "read_only": 0, 34 | "remember_last_selected_value": 0, 35 | "report_hide": 0, 36 | "reqd": 1, 37 | "search_index": 0, 38 | "set_only_once": 0, 39 | "unique": 0 40 | }, 41 | { 42 | "allow_on_submit": 0, 43 | "bold": 0, 44 | "collapsible": 0, 45 | "columns": 0, 46 | "fieldname": "api_secret", 47 | "fieldtype": "Password", 48 | "hidden": 0, 49 | "ignore_user_permissions": 0, 50 | "ignore_xss_filter": 0, 51 | "in_filter": 0, 52 | "in_list_view": 0, 53 | "in_standard_filter": 0, 54 | "label": "API Secret", 55 | "length": 0, 56 | "no_copy": 0, 57 | "permlevel": 0, 58 | "precision": "", 59 | "print_hide": 0, 60 | "print_hide_if_no_value": 0, 61 | "read_only": 0, 62 | "remember_last_selected_value": 0, 63 | "report_hide": 0, 64 | "reqd": 1, 65 | "search_index": 0, 66 | "set_only_once": 0, 67 | "unique": 0 68 | }, 69 | { 70 | "allow_on_submit": 0, 71 | "bold": 0, 72 | "collapsible": 0, 73 | "columns": 0, 74 | "description": "Mention transaction completion page URL", 75 | "fieldname": "redirect_to", 76 | "fieldtype": "Data", 77 | "hidden": 0, 78 | "ignore_user_permissions": 0, 79 | "ignore_xss_filter": 0, 80 | "in_filter": 0, 81 | "in_list_view": 0, 82 | "in_standard_filter": 0, 83 | "label": "Redirect To", 84 | "length": 0, 85 | "no_copy": 0, 86 | "permlevel": 0, 87 | "precision": "", 88 | "print_hide": 0, 89 | "print_hide_if_no_value": 0, 90 | "read_only": 0, 91 | "remember_last_selected_value": 0, 92 | "report_hide": 0, 93 | "reqd": 0, 94 | "search_index": 0, 95 | "set_only_once": 0, 96 | "unique": 0 97 | } 98 | ], 99 | "hide_heading": 0, 100 | "hide_toolbar": 0, 101 | "idx": 0, 102 | "image_view": 0, 103 | "in_create": 1, 104 | 105 | "is_submittable": 0, 106 | "issingle": 1, 107 | "istable": 0, 108 | "max_attachments": 0, 109 | "modified": "2022-07-24 14:40:31.658270", 110 | "modified_by": "Administrator", 111 | "module": "Payment Gateways", 112 | "name": "Razorpay Settings", 113 | "name_case": "", 114 | "owner": "Administrator", 115 | "permissions": [ 116 | { 117 | "amend": 0, 118 | "apply_user_permissions": 0, 119 | "cancel": 0, 120 | "create": 1, 121 | "delete": 1, 122 | "email": 1, 123 | "export": 1, 124 | "if_owner": 0, 125 | "import": 0, 126 | "is_custom": 0, 127 | "permlevel": 0, 128 | "print": 1, 129 | "read": 1, 130 | "report": 0, 131 | "role": "System Manager", 132 | "set_user_permissions": 0, 133 | "share": 1, 134 | "submit": 0, 135 | "write": 1 136 | } 137 | ], 138 | "quick_entry": 0, 139 | "read_only": 1, 140 | "read_only_onload": 0, 141 | "sort_field": "modified", 142 | "sort_order": "DESC", 143 | "track_changes": 1, 144 | "track_seen": 0 145 | } -------------------------------------------------------------------------------- /payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:payment_gateway_name", 4 | "creation": "2020-09-10 13:21:27.398088", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "payment_gateway_name", 10 | "consumer_key", 11 | "consumer_secret", 12 | "initiator_name", 13 | "till_number", 14 | "transaction_limit", 15 | "sandbox", 16 | "column_break_4", 17 | "business_shortcode", 18 | "online_passkey", 19 | "security_credential", 20 | "get_account_balance", 21 | "account_balance" 22 | ], 23 | "fields": [ 24 | { 25 | "fieldname": "payment_gateway_name", 26 | "fieldtype": "Data", 27 | "label": "Payment Gateway Name", 28 | "reqd": 1, 29 | "unique": 1 30 | }, 31 | { 32 | "fieldname": "consumer_key", 33 | "fieldtype": "Data", 34 | "in_list_view": 1, 35 | "label": "Consumer Key", 36 | "reqd": 1 37 | }, 38 | { 39 | "fieldname": "consumer_secret", 40 | "fieldtype": "Password", 41 | "in_list_view": 1, 42 | "label": "Consumer Secret", 43 | "reqd": 1 44 | }, 45 | { 46 | "fieldname": "till_number", 47 | "fieldtype": "Data", 48 | "in_list_view": 1, 49 | "label": "Till Number", 50 | "reqd": 1 51 | }, 52 | { 53 | "default": "0", 54 | "fieldname": "sandbox", 55 | "fieldtype": "Check", 56 | "label": "Sandbox" 57 | }, 58 | { 59 | "fieldname": "column_break_4", 60 | "fieldtype": "Column Break" 61 | }, 62 | { 63 | "fieldname": "online_passkey", 64 | "fieldtype": "Password", 65 | "label": " Online PassKey", 66 | "reqd": 1 67 | }, 68 | { 69 | "fieldname": "initiator_name", 70 | "fieldtype": "Data", 71 | "label": "Initiator Name" 72 | }, 73 | { 74 | "fieldname": "security_credential", 75 | "fieldtype": "Small Text", 76 | "label": "Security Credential" 77 | }, 78 | { 79 | "fieldname": "account_balance", 80 | "fieldtype": "Long Text", 81 | "hidden": 1, 82 | "label": "Account Balance", 83 | "read_only": 1 84 | }, 85 | { 86 | "fieldname": "get_account_balance", 87 | "fieldtype": "Button", 88 | "label": "Get Account Balance" 89 | }, 90 | { 91 | "depends_on": "eval:(doc.sandbox==0)", 92 | "fieldname": "business_shortcode", 93 | "fieldtype": "Data", 94 | "label": "Business Shortcode", 95 | "mandatory_depends_on": "eval:(doc.sandbox==0)" 96 | }, 97 | { 98 | "default": "150000", 99 | "fieldname": "transaction_limit", 100 | "fieldtype": "Float", 101 | "label": "Transaction Limit", 102 | "non_negative": 1 103 | } 104 | ], 105 | "links": [], 106 | "modified": "2023-09-25 10:55:33.879470", 107 | "modified_by": "Administrator", 108 | "module": "Payment Gateways", 109 | "name": "Mpesa Settings", 110 | "naming_rule": "By fieldname", 111 | "owner": "Administrator", 112 | "permissions": [ 113 | { 114 | "create": 1, 115 | "delete": 1, 116 | "email": 1, 117 | "export": 1, 118 | "print": 1, 119 | "read": 1, 120 | "report": 1, 121 | "role": "System Manager", 122 | "share": 1, 123 | "write": 1 124 | }, 125 | { 126 | "create": 1, 127 | "delete": 1, 128 | "email": 1, 129 | "export": 1, 130 | "print": 1, 131 | "read": 1, 132 | "report": 1, 133 | "role": "Accounts Manager", 134 | "share": 1, 135 | "write": 1 136 | }, 137 | { 138 | "create": 1, 139 | "delete": 1, 140 | "email": 1, 141 | "export": 1, 142 | "print": 1, 143 | "read": 1, 144 | "report": 1, 145 | "role": "Accounts User", 146 | "share": 1, 147 | "write": 1 148 | } 149 | ], 150 | "sort_field": "modified", 151 | "sort_order": "DESC", 152 | "states": [], 153 | "track_changes": 1 154 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "**.css" 7 | - "**.js" 8 | - "**.md" 9 | - "**.html" 10 | - "**.csv" 11 | schedule: 12 | # Run everday at midnight UTC / 5:30 IST 13 | - cron: "0 0 * * *" 14 | 15 | concurrency: 16 | group: develop-${{ github.event.number }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | tests: 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 60 23 | env: 24 | NODE_ENV: "production" 25 | WITH_COVERAGE: ${{ github.event_name != 'pull_request' }} 26 | 27 | strategy: 28 | fail-fast: false 29 | 30 | matrix: 31 | container: [1] 32 | 33 | name: Python Unit Tests 34 | 35 | services: 36 | mysql: 37 | image: mariadb:10.6 38 | env: 39 | MARIADB_ROOT_PASSWORD: 'root' 40 | ports: 41 | - 3306:3306 42 | options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 43 | 44 | steps: 45 | - name: Clone 46 | uses: actions/checkout@v4 47 | 48 | - name: Setup Python 49 | uses: actions/setup-python@v5 50 | with: 51 | python-version: '3.10' 52 | 53 | - name: Check for valid Python & Merge Conflicts 54 | run: | 55 | python -m compileall -f "${GITHUB_WORKSPACE}" 56 | if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" 57 | then echo "Found merge conflicts" 58 | exit 1 59 | fi 60 | 61 | - name: Setup Node 62 | uses: actions/setup-node@v4 63 | with: 64 | node-version: 18 65 | check-latest: true 66 | 67 | - name: Add to Hosts 68 | run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 69 | 70 | - name: Cache pip 71 | uses: actions/cache@v4 72 | with: 73 | path: ~/.cache/pip 74 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} 75 | restore-keys: | 76 | ${{ runner.os }}-pip- 77 | ${{ runner.os }}- 78 | 79 | - name: Cache node modules 80 | uses: actions/cache@v4 81 | env: 82 | cache-name: cache-node-modules 83 | with: 84 | path: ~/.npm 85 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 86 | restore-keys: | 87 | ${{ runner.os }}-build-${{ env.cache-name }}- 88 | ${{ runner.os }}-build- 89 | ${{ runner.os }}- 90 | 91 | - name: Get yarn cache directory path 92 | id: yarn-cache-dir-path 93 | run: echo "::set-output name=dir::$(yarn cache dir)" 94 | 95 | - uses: actions/cache@v4 96 | id: yarn-cache 97 | with: 98 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 99 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 100 | restore-keys: | 101 | ${{ runner.os }}-yarn- 102 | 103 | - name: Install 104 | run: | 105 | bash ${GITHUB_WORKSPACE}/.github/helper/install.sh 106 | env: 107 | FRAPPE_USER: ${{ github.event.inputs.user }} 108 | FRAPPE_BRANCH: ${{ github.event.inputs.branch }} 109 | 110 | - name: Run Tests 111 | run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app payments --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} 112 | env: 113 | TYPE: server 114 | CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }} 115 | 116 | - name: Upload coverage data 117 | uses: actions/upload-artifact@v4 118 | if: github.event_name != 'pull_request' 119 | with: 120 | name: coverage-${{ matrix.container }} 121 | path: /home/runner/frappe-bench/sites/coverage.xml 122 | 123 | coverage: 124 | name: Coverage Wrap Up 125 | needs: tests 126 | runs-on: ubuntu-latest 127 | if: ${{ github.event_name != 'pull_request' }} 128 | steps: 129 | - name: Clone 130 | uses: actions/checkout@v4 131 | 132 | - name: Download artifacts 133 | uses: actions/download-artifact@v4 134 | 135 | - name: Upload coverage data 136 | uses: codecov/codecov-action@v4 137 | with: 138 | token: ${{ secrets.CODECOV_TOKEN }} 139 | fail_ci_if_error: true 140 | verbose: true 141 | -------------------------------------------------------------------------------- /payments/payment_gateways/paymob/connection.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import requests 4 | from frappe.utils.password import get_decrypted_password 5 | from requests import HTTPError, JSONDecodeError, RequestException 6 | 7 | from .paymob_urls import PaymobUrls 8 | from .response_codes import ( 9 | HTTP_EXCEPTION, 10 | HTTP_EXCEPTION_MESSAGE, 11 | JSON_DECODE_EXCEPTION, 12 | JSON_DECODE_EXCEPTION_MESSAGE, 13 | REQUEST_EXCEPTION, 14 | REQUEST_EXCEPTION_MESSAGE, 15 | SUCCESS, 16 | SUCCESS_MESSAGE, 17 | UNHANDLED_EXCEPTION, 18 | UNHANDLED_EXCEPTION_MESSAGE, 19 | ) 20 | from .response_feedback_dataclass import ResponseFeedBack 21 | 22 | 23 | class AcceptConnection: 24 | def __init__(self) -> None: 25 | """Initializing the Following: 26 | 1- Requests Session 27 | 2- Auth Token 28 | 3- Set Headers 29 | 4- Paymob Urls 30 | """ 31 | self.session = requests.Session() 32 | self.paymob_urls = PaymobUrls() 33 | self.auth_token = self._get_auth_token() 34 | self.session.headers.update(self._get_headers()) 35 | 36 | def _get_headers(self) -> dict[str, Any]: 37 | """Initialize Header for Requests 38 | 39 | Returns: 40 | Dict[str, Any]: Initialized Header Dict 41 | """ 42 | return { 43 | "Content-Type": "application/json", 44 | "Authorization": f"{self.auth_token}", 45 | } 46 | 47 | def _get_auth_token(self) -> str | None: 48 | """Retrieve an Auth Token 49 | 50 | Returns: 51 | Union[str, None]: Auth Token 52 | """ 53 | api_key = get_decrypted_password("Paymob Settings", "Paymob Settings", "api_key") 54 | request_body = {"api_key": api_key} 55 | 56 | code, feedback = self.post( 57 | url=self.paymob_urls.get_url("auth"), 58 | json=request_body, 59 | ) 60 | 61 | token = None 62 | if code == SUCCESS: 63 | token = feedback.data.get("token") 64 | return token 65 | 66 | def _process_request(self, call, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]: 67 | """Process the Request 68 | 69 | Args: 70 | call (Session.get/Session.post): Session.get/Session.post 71 | *args, **kwargs: Same Args of requests.post/requests.get methods 72 | 73 | Returns: 74 | Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) 75 | """ 76 | 77 | reponse_data = None 78 | try: 79 | response = call(*args, timeout=90, **kwargs) 80 | reponse_data = response.json() 81 | response.raise_for_status() 82 | except JSONDecodeError as error: 83 | reponse_feedback = ResponseFeedBack( 84 | message=JSON_DECODE_EXCEPTION_MESSAGE, 85 | status_code=response.status_code, 86 | exception_error=error, 87 | ) 88 | return JSON_DECODE_EXCEPTION, reponse_feedback 89 | except HTTPError as error: 90 | reponse_feedback = ResponseFeedBack( 91 | message=HTTP_EXCEPTION_MESSAGE, 92 | data=reponse_data, 93 | status_code=response.status_code, 94 | exception_error=error, 95 | ) 96 | return HTTP_EXCEPTION, reponse_feedback 97 | except RequestException as error: 98 | reponse_feedback = ResponseFeedBack( 99 | message=REQUEST_EXCEPTION_MESSAGE, 100 | exception_error=error, 101 | ) 102 | return REQUEST_EXCEPTION, reponse_feedback 103 | except Exception as error: 104 | reponse_feedback = ResponseFeedBack( 105 | message=UNHANDLED_EXCEPTION_MESSAGE, 106 | exception_error=error, 107 | ) 108 | return UNHANDLED_EXCEPTION, reponse_feedback 109 | 110 | reponse_feedback = ResponseFeedBack( 111 | message=SUCCESS_MESSAGE, 112 | data=reponse_data, 113 | status_code=response.status_code, 114 | ) 115 | return SUCCESS, reponse_feedback 116 | 117 | def get(self, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]: 118 | """Wrapper for requests.get method 119 | 120 | Args: 121 | Same Args of requests.post/requests.get methods 122 | 123 | Returns: 124 | Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) 125 | """ 126 | return self._process_request(*args, call=self.session.get, **kwargs) 127 | 128 | def post(self, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]: 129 | """Wrapper for requests.get method 130 | 131 | Args: 132 | Same Args of requests.post/requests.get methods 133 | 134 | Returns: 135 | Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) 136 | """ 137 | return self._process_request(*args, call=self.session.post, **kwargs) 138 | -------------------------------------------------------------------------------- /payments/public/js/razorpay.js: -------------------------------------------------------------------------------- 1 | /* HOW-TO 2 | 3 | Razorpay Payment 4 | 5 | 1. Include checkout script in your code 6 | {{ include_script('/assets/payments/js/razorpay.js') }} 7 | 8 | 2. Create the Order controller in your backend 9 | def get_razorpay_order(self): 10 | controller = get_payment_gateway_controller("Razorpay") 11 | 12 | payment_details = { 13 | "amount": 300, 14 | ... 15 | "reference_doctype": "Conference Participant", 16 | "reference_docname": self.name, 17 | ... 18 | "receipt": self.name 19 | } 20 | 21 | return controller.create_order(**payment_details) 22 | 23 | 3. Inititate the payment in client using checkout API 24 | function make_payment(ticket) { 25 | var options = { 26 | "name": "", 27 | "description": "", 28 | "image": "", 29 | "prefill": { 30 | "name": "", 31 | "email": "", 32 | "contact": "" 33 | }, 34 | "theme": { 35 | "color": "" 36 | }, 37 | "doctype": "", 38 | "docname": " { 43 |