├── 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 |
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 |
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 | Confirming Payment
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 |
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 |
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 | | {{ __("Account Type") }} |
7 | {{ __("Current Balance") }} |
8 | {{ __("Available Balance") }} |
9 | {{ __("Reserved Balance") }} |
10 | {{ __("Uncleared Balance") }} |
11 |
12 |
13 |
14 | {% for(const [key, value] of Object.entries(data)) { %}
15 |
16 | | {%= key %} |
17 | {%= value["current_balance"] %} |
18 | {%= value["available_balance"] %} |
19 | {%= value["reserved_balance"] %} |
20 | {%= value["uncleared_balance"] %} |
21 |
22 | {% } %}
23 |
24 |
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 |
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 |
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 |