diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..1f225527
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,130 @@
+{
+ "env": {
+ "browser": true,
+ "node": true,
+ "es2022": true
+ },
+ "parserOptions": {
+ "sourceType": "module"
+ },
+ "extends": "eslint:recommended",
+ "rules": {
+ "indent": "off",
+ "brace-style": "off",
+ "no-mixed-spaces-and-tabs": "off",
+ "no-useless-escape": "off",
+ "space-unary-ops": ["error", { "words": true }],
+ "linebreak-style": "off",
+ "quotes": ["off"],
+ "semi": "off",
+ "camelcase": "off",
+ "no-unused-vars": "off",
+ "no-console": ["warn"],
+ "no-extra-boolean-cast": ["off"],
+ "no-control-regex": ["off"]
+ },
+ "root": true,
+ "globals": {
+ "frappe": true,
+ "Vue": true,
+ "SetVueGlobals": true,
+ "erpnext": true,
+ "hub": true,
+ "$": true,
+ "jQuery": true,
+ "moment": true,
+ "hljs": true,
+ "Awesomplete": true,
+ "CalHeatMap": true,
+ "Sortable": true,
+ "Showdown": true,
+ "Taggle": true,
+ "Gantt": true,
+ "Slick": true,
+ "PhotoSwipe": true,
+ "PhotoSwipeUI_Default": true,
+ "fluxify": true,
+ "io": true,
+ "c3": true,
+ "__": true,
+ "_p": true,
+ "_f": true,
+ "repl": true,
+ "Class": true,
+ "locals": true,
+ "cint": true,
+ "cstr": true,
+ "cur_frm": true,
+ "cur_dialog": true,
+ "cur_page": true,
+ "cur_list": true,
+ "cur_tree": true,
+ "cur_pos": true,
+ "msg_dialog": true,
+ "is_null": true,
+ "in_list": true,
+ "has_common": true,
+ "posthog": true,
+ "has_words": true,
+ "validate_email": true,
+ "open_web_template_values_editor": true,
+ "get_number_format": true,
+ "format_number": true,
+ "format_currency": true,
+ "round_based_on_smallest_currency_fraction": true,
+ "roundNumber": true,
+ "comment_when": true,
+ "replace_newlines": true,
+ "open_url_post": true,
+ "toTitle": true,
+ "lstrip": true,
+ "strip": true,
+ "strip_html": true,
+ "replace_all": true,
+ "flt": true,
+ "precision": true,
+ "md5": true,
+ "CREATE": true,
+ "AMEND": true,
+ "CANCEL": true,
+ "copy_dict": true,
+ "get_number_format_info": true,
+ "print_table": true,
+ "Layout": true,
+ "web_form_settings": true,
+ "$c": true,
+ "$a": true,
+ "$i": true,
+ "$bg": true,
+ "$y": true,
+ "$c_obj": true,
+ "$c_obj_csv": true,
+ "refresh_many": true,
+ "refresh_field": true,
+ "toggle_field": true,
+ "get_field_obj": true,
+ "get_query_params": true,
+ "unhide_field": true,
+ "hide_field": true,
+ "set_field_options": true,
+ "getCookie": true,
+ "getCookies": true,
+ "get_url_arg": true,
+ "get_server_fields": true,
+ "set_multiple": true,
+ "QUnit": true,
+ "Chart": true,
+ "Cypress": true,
+ "cy": true,
+ "describe": true,
+ "expect": true,
+ "it": true,
+ "context": true,
+ "before": true,
+ "beforeEach": true,
+ "onScan": true,
+ "extend_cscript": true,
+ "localforage": true,
+ "Plaid": true
+ }
+}
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 00000000..56274db0
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,15 @@
+# Since version 2.23 (released in August 2019), git-blame has a feature
+# to ignore or bypass certain commits.
+#
+# This file contains a list of commits that are not likely what you
+# are looking for in a blame, such as mass reformatting or renaming.
+# You can set this file as a default ignore file for blame by running
+# the following command.
+#
+# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
+
+# pre-commit formatting ruff, eslint, prettier (automated)
+63a4acf6c9fe9657fa6d7ad659465b0d5ef3d73f
+
+# pre-commit formatting ruff, eslint, prettier (manual fixup)
+cecf0bec9de2dcd176fc632e8a5348ab2f491cbe
\ No newline at end of file
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 632e6069..3e386d6b 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -53,4 +53,4 @@ bench start &>> ~/frappe-bench/bench_start.log &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
-bench --verbose --site test_site install-app payments
\ No newline at end of file
+bench --verbose --site test_site install-app payments
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 00000000..65bc68f2
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,4 @@
+# Any python files modifed but no test files modified
+needs-tests:
+- any: ['payments/**/*.py']
+ all: ['!payments/**/test*.py']
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 832fce93..4d94a1f5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -137,4 +137,4 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- verbose: true
\ No newline at end of file
+ verbose: true
diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml
new file mode 100644
index 00000000..97fa4a1a
--- /dev/null
+++ b/.github/workflows/labeller.yml
@@ -0,0 +1,12 @@
+name: "Pull Request Labeler"
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ triage:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v4
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.gitignore b/.gitignore
index 175055b1..a4cd298d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ tags
payments/docs/current
node_modules/
__pycache__/
+.aider*
+.helix
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bd9bf1af..2fb83911 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,24 +20,47 @@ repos:
- id: check-yaml
- id: debug-statements
- - repo: https://github.com/asottile/pyupgrade
- rev: v2.34.0
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v2.7.1
hooks:
- - id: pyupgrade
- args: ['--py310-plus']
+ - id: prettier
+ types_or: [javascript, vue, scss]
+ # Ignore any files that might contain jinja / bundles
+ exclude: |
+ (?x)^(
+ payments/public/dist/.*|
+ cypress/.*|
+ .*node_modules.*|
+ payments/templates/includes/.*
+ )$
- - repo: https://github.com/adityahase/black
- rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
+ - repo: https://github.com/pre-commit/mirrors-eslint
+ rev: v8.44.0
hooks:
- - id: black
- additional_dependencies: ['click==8.0.4']
+ - id: eslint
+ types_or: [javascript]
+ args: ['--quiet']
+ # Ignore any files that might contain jinja / bundles
+ exclude: |
+ (?x)^(
+ payments/public/dist/.*|
+ cypress/.*|
+ .*node_modules.*|
+ payments/templates/includes/.*
+ )$
- - repo: https://github.com/PyCQA/flake8
- rev: 6.0.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.2.0
hooks:
- - id: flake8
- additional_dependencies: ['flake8-bugbear',]
- args: ['--config', '.github/helper/flake8.conf']
+ - id: ruff
+ name: "Run ruff import sorter"
+ args: ["--select=I", "--fix"]
+
+ - id: ruff
+ name: "Run ruff linter"
+
+ - id: ruff-format
+ name: "Run ruff formatter"
ci:
autoupdate_schedule: weekly
diff --git a/README.md b/README.md
index 7772261f..cf0d0659 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,9 @@ All general utils are stored in [utils](payments/utils) directory. The utils are
[templates](payments/templates) directory has all the payment gateways' custom checkout pages.
-#
+## Ongoing Work
+- New API design: https://github.com/frappe/payments/pull/53
+- Mollie Integration: https://github.com/frappe/payments/pull/68 (awaiting the former, but you may use the branc)
## License
MIT ([license.txt](license.txt))
diff --git a/commitlint.config.js b/commitlint.config.js
index 8847564e..bd5cebce 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -1,25 +1,25 @@
module.exports = {
- parserPreset: 'conventional-changelog-conventionalcommits',
- rules: {
- 'subject-empty': [2, 'never'],
- 'type-case': [2, 'always', 'lower-case'],
- 'type-empty': [2, 'never'],
- 'type-enum': [
- 2,
- 'always',
- [
- 'build',
- 'chore',
- 'ci',
- 'docs',
- 'feat',
- 'fix',
- 'perf',
- 'refactor',
- 'revert',
- 'style',
- 'test',
- ],
- ],
- },
+ parserPreset: "conventional-changelog-conventionalcommits",
+ rules: {
+ "subject-empty": [2, "never"],
+ "type-case": [2, "always", "lower-case"],
+ "type-empty": [2, "never"],
+ "type-enum": [
+ 2,
+ "always",
+ [
+ "build",
+ "chore",
+ "ci",
+ "docs",
+ "feat",
+ "fix",
+ "perf",
+ "refactor",
+ "revert",
+ "style",
+ "test",
+ ],
+ ],
+ },
};
diff --git a/payments/hooks.py b/payments/hooks.py
index caa07d27..c6039c77 100644
--- a/payments/hooks.py
+++ b/payments/hooks.py
@@ -92,9 +92,9 @@
# DocType Class
# ---------------
-# Override standard doctype classes
+# Extend standard doctype classes
-override_doctype_class = {"Web Form": "payments.overrides.payment_webform.PaymentWebForm"}
+extend_doctype_class = {"Web Form": "payments.overrides.payment_webform.PaymentWebForm"}
# Document Events
# ---------------
diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js
index c844022c..f9d77bf7 100644
--- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js
+++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js
@@ -1,6 +1,4 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Braintree Settings', {
-
-});
+frappe.ui.form.on("Braintree Settings", {});
diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py
index 17800436..43ec8f84 100644
--- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py
+++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py
@@ -267,10 +267,7 @@ def create_charge_on_braintree(self):
status = "Error"
redirect_url = "payment-failed"
- get_parameters = [
- ("doctype", self.data.reference_doctype),
- ("docname", self.data.reference_docname),
- ]
+ get_parameters = [("doctype", self.data.reference_doctype), ("docname", self.data.reference_docname)]
if redirect_to:
get_parameters.append(("redirect_to", redirect_to))
if redirect_message:
@@ -282,9 +279,8 @@ def create_charge_on_braintree(self):
def get_gateway_controller(doc):
payment_request = frappe.get_doc("Payment Request", doc)
- return frappe.db.get_value(
- "Payment Gateway", payment_request.payment_gateway, "gateway_controller"
- )
+ return frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller")
+
def get_client_token(doc):
diff --git a/payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.js b/payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.js
index 37f9f7b9..9f8a22bb 100644
--- a/payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.js
+++ b/payments/payment_gateways/doctype/gocardless_mandate/gocardless_mandate.js
@@ -1,5 +1,4 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('GoCardless Mandate', {
-});
+frappe.ui.form.on("GoCardless Mandate", {});
diff --git a/payments/payment_gateways/doctype/gocardless_settings/__init__.py b/payments/payment_gateways/doctype/gocardless_settings/__init__.py
index 65be5993..d8f8a3d3 100644
--- a/payments/payment_gateways/doctype/gocardless_settings/__init__.py
+++ b/payments/payment_gateways/doctype/gocardless_settings/__init__.py
@@ -34,7 +34,7 @@ def set_status(event):
def set_mandate_status(event):
mandates = []
- if isinstance(event["links"], (list,)):
+ if isinstance(event["links"], list):
for link in event["links"]:
mandates.append(link["mandate"])
else:
diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.js b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.js
index ef1b97f6..a4db118d 100644
--- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.js
+++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.js
@@ -5,4 +5,4 @@
// refresh(frm) {
// },
-// });
\ No newline at end of file
+// });
diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py
index d732244f..3a9cd0fb 100644
--- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py
+++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py
@@ -21,9 +21,7 @@ def validate(self):
def initialize_client(self):
self.environment = self.get_environment()
try:
- self.client = gocardless_pro.Client(
- access_token=self.access_token, environment=self.environment
- )
+ self.client = gocardless_pro.Client(access_token=self.access_token, environment=self.environment)
return self.client
except Exception as e:
frappe.throw(e)
@@ -124,9 +122,7 @@ def create_charge_on_gocardless(self):
redirect_to = self.data.get("redirect_to") or None
redirect_message = self.data.get("redirect_message") or None
- reference_doc = frappe.get_doc(
- self.data.get("reference_doctype"), self.data.get("reference_docname")
- )
+ reference_doc = frappe.get_doc(self.data.get("reference_doctype"), self.data.get("reference_docname"))
self.initialize_client()
try:
@@ -205,9 +201,7 @@ def create_charge_on_gocardless(self):
def get_gateway_controller(doc):
payment_request = frappe.get_doc("Payment Request", doc)
- return frappe.db.get_value(
- "Payment Gateway", payment_request.payment_gateway, "gateway_controller"
- )
+ return frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller")
def gocardless_initialization(doc):
diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_connector.py b/payments/payment_gateways/doctype/mpesa_settings/mpesa_connector.py
index 7eb8b9c0..ce6df833 100644
--- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_connector.py
+++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_connector.py
@@ -119,10 +119,8 @@ def stk_push(
errorMessage(str): This is a predefined code that indicates the reason for request failure.
"""
- time = (
- str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
- )
- password = f"{str(business_shortcode)}{str(passcode)}{time}"
+ time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
+ password = f"{business_shortcode!s}{passcode!s}{time}"
encoded = base64.b64encode(bytes(password, encoding="utf8"))
payload = {
"BusinessShortCode": business_shortcode,
@@ -135,9 +133,7 @@ def stk_push(
"CallBackURL": callback_url,
"AccountReference": reference_code,
"TransactionDesc": description,
- "TransactionType": "CustomerPayBillOnline"
- if self.env == "sandbox"
- else "CustomerBuyGoodsOnline",
+ "TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline",
}
headers = {
"Authorization": f"Bearer {self.authentication_token}",
diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.js b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.js
index 9d625736..491223da 100644
--- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.js
+++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.js
@@ -1,36 +1,38 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Mpesa Settings', {
- onload_post_render: function(frm) {
- frm.events.setup_account_balance_html(frm);
- },
+frappe.ui.form.on("Mpesa Settings", {
+ onload_post_render: function (frm) {
+ frm.events.setup_account_balance_html(frm);
+ },
- refresh: function(frm) {
- frappe.realtime.on("refresh_mpesa_dashboard", function(){
- frm.reload_doc();
- frm.events.setup_account_balance_html(frm);
- });
- },
+ refresh: function (frm) {
+ frappe.realtime.on("refresh_mpesa_dashboard", function () {
+ frm.reload_doc();
+ frm.events.setup_account_balance_html(frm);
+ });
+ },
- get_account_balance: function(frm) {
- if (!frm.doc.initiator_name && !frm.doc.security_credential) {
- frappe.throw(__("Please set the initiator name and the security credential"));
- }
- frappe.call({
- method: "get_account_balance_info",
- doc: frm.doc
- });
- },
+ get_account_balance: function (frm) {
+ if (!frm.doc.initiator_name && !frm.doc.security_credential) {
+ frappe.throw(
+ __("Please set the initiator name and the security credential")
+ );
+ }
+ frappe.call({
+ method: "get_account_balance_info",
+ doc: frm.doc,
+ });
+ },
- setup_account_balance_html: function(frm) {
- if (!frm.doc.account_balance) return;
- $("div").remove(".form-dashboard-section.custom");
- frm.dashboard.add_section(
- frappe.render_template('account_balance', {
- data: JSON.parse(frm.doc.account_balance)
- })
- );
- frm.dashboard.show();
- }
+ setup_account_balance_html: function (frm) {
+ if (!frm.doc.account_balance) return;
+ $("div").remove(".form-dashboard-section.custom");
+ frm.dashboard.add_section(
+ frappe.render_template("account_balance", {
+ data: JSON.parse(frm.doc.account_balance),
+ })
+ );
+ frm.dashboard.show();
+ },
});
diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py
index 640ecb4c..b0f8eb6e 100644
--- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py
+++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py
@@ -18,7 +18,7 @@
class MpesaSettings(Document):
- supported_currencies = ["KES"]
+ supported_currencies = ("KES",)
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
@@ -44,14 +44,14 @@ def on_update(self):
)
# required to fetch the bank account details from the payment gateway account
- frappe.db.commit() # nosemgrep
create_mode_of_payment("Mpesa-" + self.payment_gateway_name, payment_type="Phone")
+ frappe.db.commit() # nosemgrep
def request_for_payment(self, **kwargs):
args = frappe._dict(kwargs)
request_amounts = self.split_request_amount_according_to_transaction_limit(args)
- for i, amount in enumerate(request_amounts):
+ for _i, amount in enumerate(request_amounts):
args.request_amount = amount
if frappe.flags.in_test:
from payments.payment_gateways.doctype.mpesa_settings.test_mpesa_settings import (
@@ -104,8 +104,8 @@ def get_account_balance_info(self):
def handle_api_response(self, global_id, request_dict, response):
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
# check error response
- if getattr(response, "requestId"):
- req_name = getattr(response, "requestId")
+ if response.requestId:
+ req_name = response.requestId
error = response
else:
# global checkout id used as request name
@@ -116,7 +116,7 @@ def handle_api_response(self, global_id, request_dict, response):
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if error:
- frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
+ frappe.throw(_(response.errorMessage), title=_("Transaction Error"))
def generate_stk_push(**kwargs):
@@ -194,7 +194,7 @@ def verify_transaction(**kwargs):
)
total_paid = amount + sum(completed_payments)
- mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt])
+ mpesa_receipts = ", ".join([*mpesa_receipts, mpesa_receipt])
if total_paid >= pr.grand_total:
pr.run_method("on_payment_authorized", "Completed")
@@ -314,9 +314,7 @@ def process_balance_info(**kwargs):
)
except Exception:
request.handle_failure(account_balance_response)
- frappe.log_error(
- title="Mpesa Account Balance Processing Error", message=account_balance_response
- )
+ frappe.log_error(title="Mpesa Account Balance Processing Error", message=account_balance_response)
else:
request.handle_failure(account_balance_response)
diff --git a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py
index 729d4cb6..d712f94b 100644
--- a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py
+++ b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py
@@ -126,9 +126,7 @@ def test_processing_of_callback_payload(self):
pluck="name",
)
- callback_response = get_payment_callback_payload(
- Amount=500, CheckoutRequestID=integration_req_ids[0]
- )
+ callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
verify_transaction(**callback_response)
# test creation of integration request
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
diff --git a/payments/payment_gateways/doctype/paymob_settings/__init__.py b/payments/payment_gateways/doctype/paymob_settings/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js
new file mode 100644
index 00000000..39d99cca
--- /dev/null
+++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js
@@ -0,0 +1,32 @@
+// Copyright (c) 2025, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Paymob Settings", {
+ refresh(frm) {
+ frm.add_custom_button(__("Get Access Token"), () => {
+ frm.trigger("get_access_token");
+ });
+ },
+ get_access_token: function (frm) {
+ try {
+ frm
+ .call({
+ method: "refresh_access_token",
+ doc: frm.doc,
+ freeze: true,
+ freeze_message: __("Getting Access Token ..."),
+ })
+ .then((r) => {
+ if (!r.exc && r.message) {
+ frm.set_value("token", r.message);
+ frappe.show_alert({
+ message: __("Access Token Updated"),
+ indicator: "green",
+ });
+ }
+ });
+ } catch (e) {
+ console.log(e);
+ }
+ },
+});
diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json
new file mode 100644
index 00000000..55c1fc61
--- /dev/null
+++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json
@@ -0,0 +1,145 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-10-22 13:58:02.482023",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "credentials_section",
+ "api_key",
+ "public_key",
+ "hmac",
+ "column_break_rdwp",
+ "secret_key",
+ "token",
+ "expires_in",
+ "payment_config",
+ "iframe",
+ "payment_integration",
+ "column_break_qgqc",
+ "redirect_to"
+ ],
+ "fields": [
+ {
+ "fieldname": "credentials_section",
+ "fieldtype": "Section Break",
+ "label": "Credentials"
+ },
+ {
+ "fieldname": "api_key",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "API Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "public_key",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Public Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "hmac",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "HMAC",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_rdwp",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "token",
+ "fieldtype": "Password",
+ "label": "Token"
+ },
+ {
+ "fieldname": "secret_key",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Secret Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_config",
+ "fieldtype": "Section Break",
+ "label": "Payment Config"
+ },
+ {
+ "fieldname": "iframe",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Iframe",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_integration",
+ "fieldtype": "Int",
+ "label": "Payment Integration",
+ "reqd": 1
+ },
+ {
+ "fieldname": "expires_in",
+ "fieldtype": "Datetime",
+ "label": "Expires In"
+ },
+ {
+ "fieldname": "column_break_qgqc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Mention transaction completion page URL\n\n",
+ "fieldname": "redirect_to",
+ "fieldtype": "Data",
+ "label": "Redirect To"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2025-11-20 23:53:24.529932",
+ "modified_by": "Administrator",
+ "module": "Payment Gateways",
+ "name": "Paymob Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py
new file mode 100644
index 00000000..04bff5b9
--- /dev/null
+++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py
@@ -0,0 +1,289 @@
+# Copyright (c) 2025, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from datetime import timedelta
+from urllib.parse import urlencode
+
+import frappe
+from frappe import _
+from frappe.integrations.utils import create_request_log, make_get_request, make_post_request
+from frappe.model.document import Document
+from frappe.utils import get_datetime, now_datetime
+
+from payments.payment_gateways.paymob.accept_api import AcceptAPI
+from payments.payment_gateways.paymob.hmac_validator import HMACValidator
+from payments.payment_gateways.paymob.paymob_urls import PaymobUrls
+from payments.payment_gateways.paymob.response_codes import SUCCESS
+
+
+class PaymobSettings(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ api_key: DF.Password
+ hmac: DF.Password
+ iframe: DF.Data
+ payment_integration: DF.Int
+ public_key: DF.Password
+ secret_key: DF.Password
+ token: DF.Password | None
+ # end: auto-generated types
+
+ @frappe.whitelist()
+ def refresh_access_token(self):
+ """
+ If existing token expired → fetch new one
+ """
+
+ accept = AcceptAPI()
+ token = accept.retrieve_auth_token()
+ self.token = token
+ self.expires_in = now_datetime() + timedelta(minutes=50)
+ self.save(ignore_permissions=True)
+
+ return token
+
+ def get_valid_token(self):
+ token = self.get_password("token") if self.token else None
+
+ buffer = timedelta(minutes=2)
+ if token and self.expires_in:
+ expires_in = (
+ get_datetime(self.expires_in) if isinstance(self.expires_in, str) else self.expires_in
+ )
+ if now_datetime() + buffer < expires_in:
+ return token
+
+ return self.refresh_access_token()
+
+ def get_payment_url(self, **kwargs):
+ try:
+ paymob_urls = PaymobUrls()
+
+ if not kwargs.get("order_id") or not kwargs.get("amount"):
+ frappe.throw(_("Missing order ID or amount"))
+
+ # Build dummy billing data
+ billing_data = {
+ "apartment": "NA",
+ "email": kwargs.get("payer_email"),
+ "floor": "NA",
+ "first_name": kwargs.get("payer_name").split()[0],
+ "street": "NA",
+ "building": "NA",
+ "phone_number": "+201111111111",
+ "shipping_method": "NA",
+ "postal_code": "NA",
+ "city": "Cairo",
+ "country": "EG",
+ "last_name": kwargs.get("payer_name").split()[-1],
+ "state": "NA",
+ }
+
+ payment_key_payload = {
+ "auth_token": self.get_valid_token(),
+ "amount_cents": str(int(float(kwargs.get("amount")) * 100)),
+ "expiration": 3600,
+ "order_id": kwargs.get("order_id"),
+ "currency": kwargs.get("currency", "EGP"),
+ "billing_data": billing_data,
+ "integration_id": self.payment_integration,
+ }
+
+ url = paymob_urls.get_url("payment_key")
+ headers = {"Content-Type": "application/json"}
+ response = make_post_request(url=url, json=payment_key_payload, headers=headers)
+
+ if not response or "token" not in response:
+ frappe.throw(_("Failed to retrieve payment token from Paymob"))
+
+ payment_token = response["token"]
+
+ iframe_url = f"https://accept.paymob.com/api/acceptance/iframes/{self.iframe}?payment_token={payment_token}"
+ return iframe_url
+
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+ frappe.throw(_("Could not generate Paymob payment URL"))
+
+ def create_order(self, **kwargs):
+ integration_request = create_request_log(kwargs, service_name="Paymob")
+ paymob_urls = PaymobUrls()
+
+ token = self.get_valid_token()
+
+ amount_cents = int(kwargs.get("amount")) * 100 # Paymob uses cents
+ currency = kwargs.get("currency", "EGP")
+ delivery_needed = kwargs.get("delivery_needed", False)
+ items = kwargs.get("items", [])
+
+ payload = {
+ "auth_token": token,
+ "delivery_needed": str(delivery_needed).lower(),
+ "amount_cents": str(amount_cents),
+ "currency": currency,
+ "items": items,
+ }
+
+ try:
+ url = paymob_urls.get_url("order")
+ headers = {"Content-Type": "application/json"}
+ order = make_post_request(url=url, json=payload, headers=headers)
+
+ if not order or not order.get("id"):
+ frappe.throw(_("Failed to create order in Paymob"))
+
+ paymob_order_id = order.get("id")
+
+ integration_request_dict = frappe.parse_json(integration_request.data)
+ integration_request_dict["paymob_order_id"] = str(paymob_order_id)
+
+ order["integration_request"] = integration_request.name
+
+ integration_request.data = frappe.as_json(integration_request_dict)
+ integration_request.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ return order
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+ frappe.throw(_("Could not create Paymob order"))
+
+
+@frappe.whitelist(allow_guest=True)
+def callback():
+ try:
+ incoming_hmac = frappe.request.args.get("hmac") or frappe.request.form.get("hmac")
+
+ if not incoming_hmac:
+ frappe.throw(_("Missing HMAC"))
+
+ incoming_data_json = frappe.request.get_json()
+
+ # Validate the HMAC
+ validator = HMACValidator(incoming_hmac=incoming_hmac, callback_dict=incoming_data_json)
+
+ if not validator.is_valid:
+ frappe.throw(_("Invalid HMAC"))
+
+ obj_data = incoming_data_json.get("obj", {})
+ success = obj_data.get("success")
+ pending = obj_data.get("pending")
+ payment_status = obj_data.get("order", {}).get("payment_status")
+ txn_response_code = obj_data.get("data", {}).get("txn_response_code")
+ migs_data = obj_data.get("data", {}).get("migs_order", {})
+ capture_status = migs_data.get("status")
+ paymob_payment_id = obj_data.get("id")
+ paymob_order_id = obj_data.get("order", {}).get("id")
+
+ is_payment_successful = (
+ success is True
+ and pending is False
+ and str(payment_status).upper() == "PAID"
+ and str(txn_response_code).upper() == "APPROVED"
+ )
+
+ if not paymob_order_id:
+ frappe.throw(_("Missing order ID"))
+
+ integration_request_doc = get_integration_request(paymob_order_id)
+ integration_request_dict = frappe.parse_json(integration_request_doc.data)
+
+ integration_request_dict.update(
+ {
+ "paymob_payment_id": str(paymob_payment_id),
+ "order_id": str(paymob_order_id),
+ }
+ )
+
+ integration_request_doc.data = frappe.as_json(integration_request_dict)
+
+ if is_payment_successful:
+ if capture_status == "CAPTURED":
+ integration_request_doc.status = "Completed"
+
+ integration_request_doc.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ handle_payment_success(integration_request_dict)
+
+ else:
+ integration_request_doc.error = (
+ f"Payment Status: {payment_status}, Response Code: {txn_response_code}"
+ )
+ integration_request_doc.save(ignore_permissions=True)
+ frappe.db.commit()
+ frappe.log_error(frappe.get_traceback(), "Paymob Payment not authorized")
+
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), "Paymob Callback Error")
+
+
+def get_integration_request(paymob_order_id):
+ """Fetch Integration Request linked to Paymob order."""
+
+ integration_requests = frappe.get_all(
+ "Integration Request",
+ filters={
+ "integration_request_service": "Paymob",
+ "data": ["like", f'%"paymob_order_id": "{paymob_order_id}"%'],
+ },
+ fields=["name", "data", "reference_doctype", "reference_docname"],
+ order_by="creation desc",
+ limit=1,
+ )
+ if not integration_requests:
+ frappe.throw(_("No Integration Request found for this order"))
+
+ return frappe.get_doc("Integration Request", integration_requests[0].name)
+
+
+def handle_payment_success(integration_request_dict):
+ """Handle post-success payments"""
+
+ redirect_to = integration_request_dict["redirect_to"]
+ if integration_request_dict["reference_doctype"] and integration_request_dict["reference_docname"]:
+ custom_redirect_to = None
+ try:
+ custom_redirect_to = frappe.get_doc(
+ integration_request_dict["reference_doctype"], integration_request_dict["reference_docname"]
+ ).run_method("on_payment_authorized", "Completed")
+
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+
+ if custom_redirect_to:
+ redirect_to = custom_redirect_to
+
+ redirect_url = f"payment-success?doctype={integration_request_dict['reference_doctype']}&docname={integration_request_dict['reference_docname']}"
+
+ if redirect_to:
+ redirect_url += "&" + urlencode({"redirect_to": redirect_to})
+
+ return {"redirect_to": redirect_url, "status": "Completed"}
+
+
+@frappe.whitelist()
+def update_paymob_settings(**kwargs):
+ args = frappe._dict(kwargs)
+ fields = frappe._dict(
+ {
+ "api_key": args.get("api_key"),
+ "secret_key": args.get("secret_key"),
+ "public_key": args.get("public_key"),
+ "hmac": args.get("hmac"),
+ "iframe": args.get("iframe"),
+ "payment_integration": args.get("payment_integration"),
+ }
+ )
+ try:
+ paymob_settings = frappe.get_doc("Paymob Settings").update(fields)
+ paymob_settings.save()
+ return "Paymob Credentials Successfully"
+ except Exception:
+ return "Failed to Update Paymob Credentials"
diff --git a/payments/payment_gateways/doctype/paymob_settings/test_paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/test_paymob_settings.py
new file mode 100644
index 00000000..b8e019e3
--- /dev/null
+++ b/payments/payment_gateways/doctype/paymob_settings/test_paymob_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2025, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestPaymobSettings(FrappeTestCase):
+ pass
diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.js b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.js
index 63480bc9..9f8ad92a 100644
--- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.js
+++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.js
@@ -1,8 +1,6 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('PayPal Settings', {
- refresh: function(frm) {
-
- }
+frappe.ui.form.on("PayPal Settings", {
+ refresh: function (frm) {},
});
diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py
index 04943a5c..3c25bec8 100644
--- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py
+++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py
@@ -64,6 +64,7 @@ def on_payment_authorized(payment_status):
import json
from urllib.parse import urlencode
+from zoneinfo import ZoneInfo
import frappe
import pytz
@@ -78,8 +79,15 @@ def on_payment_authorized(payment_status):
api_path = "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings"
+from payments.utils import create_payment_gateway
+
+api_path = (
+ "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings"
+)
+
+
class PayPalSettings(Document):
- supported_currencies = [
+ supported_currencies = (
"AUD",
"BRL",
"CAD",
@@ -105,14 +113,14 @@ class PayPalSettings(Document):
"THB",
"TRY",
"USD",
- ]
+ )
def __setup__(self):
- setattr(self, "use_sandbox", 0)
+ self.use_sandbox = 0
def setup_sandbox_env(self, token):
data = json.loads(frappe.db.get_value("Integration Request", token, "data"))
- setattr(self, "use_sandbox", cint(frappe._dict(data).use_sandbox) or 0)
+ self.use_sandbox = cint(frappe._dict(data).use_sandbox) or 0
def validate(self):
create_payment_gateway("PayPal")
@@ -171,7 +179,7 @@ def validate_paypal_credentails(self):
frappe.throw(_("Invalid payment gateway credentials"))
def get_payment_url(self, **kwargs):
- setattr(self, "use_sandbox", cint(kwargs.get("use_sandbox", 0)))
+ self.use_sandbox = cint(kwargs.get("use_sandbox", 0))
response = self.execute_set_express_checkout(**kwargs)
@@ -212,6 +220,7 @@ def execute_set_express_checkout(self, **kwargs):
response = make_post_request(url, data=params.encode("utf-8"))
if response.get("ACK")[0] != "Success":
+ create_request_log(response, service_name="PayPal", status="Failed")
frappe.throw(_("Looks like something is wrong with this site's Paypal configuration."))
return response
@@ -244,6 +253,51 @@ def get_paypal_and_transaction_details(token):
return data, params, url
+def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True):
+ redirect_to = data.get("redirect_to") or None
+ redirect_message = data.get("redirect_message") or None
+
+ if custom_redirect_to:
+ redirect_to = custom_redirect_to
+
+ if redirect_to:
+ redirect_url += "&" + urlencode({"redirect_to": redirect_to})
+ if redirect_message:
+ redirect_url += "&" + urlencode({"redirect_message": redirect_message})
+
+ # this is done so that functions called via hooks can update flags.redirect_to
+ if redirect:
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = get_url(redirect_url)
+
+ def configure_recurring_payments(self, params, kwargs):
+ # removing the params as we have to setup rucurring payments
+ for param in (
+ "PAYMENTREQUEST_0_PAYMENTACTION",
+ "PAYMENTREQUEST_0_AMT",
+ "PAYMENTREQUEST_0_CURRENCYCODE",
+ ):
+ del params[param]
+
+ params.update(
+ {
+ "L_BILLINGTYPE0": "RecurringPayments", # The type of billing agreement
+ "L_BILLINGAGREEMENTDESCRIPTION0": kwargs["description"],
+ }
+ )
+
+
+def get_paypal_and_transaction_details(token):
+ doc = frappe.get_doc("PayPal Settings")
+ doc.setup_sandbox_env(token)
+ params, url = doc.get_paypal_params_and_url()
+
+ integration_request = frappe.get_doc("Integration Request", token)
+ data = json.loads(integration_request.data)
+
+ return data, params, url
+
+
def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True):
redirect_to = data.get("redirect_to") or None
redirect_message = data.get("redirect_message") or None
@@ -379,7 +433,7 @@ def create_recurring_profile(token, payerid):
status_changed_to = "Completed" if data.get("starting_immediately") or updating else "Verified"
starts_at = get_datetime(subscription_details.get("start_date")) or frappe.utils.now_datetime()
- starts_at = starts_at.replace(tzinfo=pytz.timezone(get_system_timezone())).astimezone(pytz.utc)
+ starts_at = starts_at.replace(tzinfo=ZoneInfo(get_system_timezone())).astimezone(ZoneInfo("UTC"))
# "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat()
params.update({"PROFILESTARTDATE": starts_at.isoformat()})
@@ -432,6 +486,7 @@ def get_redirect_uri(doc, token, payerid):
return get_url(f"{api_path}.confirm_payment?token={token}")
+
def manage_recurring_payment_profile_status(profile_id, action, args, url):
args.update(
{
diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js
index fe2ee7c9..e561698e 100644
--- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js
+++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.js
@@ -1,8 +1,14 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Paytm Settings', {
- refresh: function(frm) {
- frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`]));
- }
+frappe.ui.form.on("Paytm Settings", {
+ refresh: function (frm) {
+ frm.dashboard.set_headline(
+ __("For more information, {0}.", [
+ `${__(
+ "Click here"
+ )}`,
+ ])
+ );
+ },
});
diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py
index 1010d050..4debb63b 100644
--- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py
+++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py
@@ -23,8 +23,11 @@
from payments.utils import create_payment_gateway
+from payments.utils import create_payment_gateway
+
+
class PaytmSettings(Document):
- supported_currencies = ["INR"]
+ supported_currencies = ("INR",)
def validate(self):
create_payment_gateway("Paytm")
@@ -75,7 +78,6 @@ def get_paytm_config():
def get_paytm_params(payment_details, order_id, paytm_config):
-
# initialize a dictionary
paytm_params = dict()
@@ -127,9 +129,7 @@ def verify_transaction(**paytm_params):
http_status_code=401,
indicator_color="red",
)
- frappe.log_error(
- "Order unsuccessful. Failed Response:" + cstr(paytm_params), "Paytm Payment Failed"
- )
+ frappe.log_error("Order unsuccessful. Failed Response:" + cstr(paytm_params), "Paytm Payment Failed")
def verify_transaction_status(paytm_config, order_id):
diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js
index bf2a955c..71fe56e2 100644
--- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js
+++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js
@@ -13,4 +13,4 @@ frappe.ui.form.on("Razorpay Settings", {
});
});
},
-});
\ No newline at end of file
+});
diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py
index 434518cd..3234e653 100644
--- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py
+++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py
@@ -324,10 +324,128 @@ def prepare_subscription_details(self, settings, **kwargs):
return kwargs
+ def setup_addon(self, settings, **kwargs):
+ """
+ Addon template:
+ {
+ "item": {
+ "name": row.upgrade_type,
+ "amount": row.amount,
+ "currency": currency,
+ "description": "add-on description"
+ },
+ "quantity": 1 (The total amount is calculated as item.amount * quantity)
+ }
+ """
+ url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(
+ kwargs.get("subscription_id")
+ )
+
+ try:
+ if not frappe.conf.converted_rupee_to_paisa:
+ convert_rupee_to_paisa(**kwargs)
+
+ for addon in kwargs.get("addons"):
+ resp = make_post_request(
+ url,
+ auth=(settings.api_key, settings.api_secret),
+ data=json.dumps(addon),
+ headers={"content-type": "application/json"},
+ )
+ if not resp.get("id"):
+ frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription")
+ except Exception:
+ frappe.log_error()
+ # failed
+ pass
+
+ def setup_subscription(self, settings, **kwargs):
+ start_date = (
+ get_timestamp(kwargs.get("subscription_details").get("start_date"))
+ if kwargs.get("subscription_details").get("start_date")
+ else None
+ )
+
+ subscription_details = {
+ "plan_id": kwargs.get("subscription_details").get("plan_id"),
+ "total_count": kwargs.get("subscription_details").get("billing_frequency"),
+ "customer_notify": kwargs.get("subscription_details").get("customer_notify"),
+ }
+
+ if start_date:
+ subscription_details["start_at"] = cint(start_date)
+
+ if kwargs.get("addons"):
+ convert_rupee_to_paisa(**kwargs)
+ subscription_details.update({"addons": kwargs.get("addons")})
+
+ try:
+ resp = make_post_request(
+ "https://api.razorpay.com/v1/subscriptions",
+ auth=(settings.api_key, settings.api_secret),
+ data=json.dumps(subscription_details),
+ headers={"content-type": "application/json"},
+ )
+
+ if resp.get("status") == "created":
+ kwargs["subscription_id"] = resp.get("id")
+ frappe.flags.status = "created"
+ return kwargs
+ else:
+ frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription")
+
+ except Exception:
+ frappe.log_error()
+
+ def prepare_subscription_details(self, settings, **kwargs):
+ if not kwargs.get("subscription_id"):
+ kwargs = self.setup_subscription(settings, **kwargs)
+
+ if frappe.flags.status != "created":
+ kwargs["subscription_id"] = None
+
+ return kwargs
+
def get_payment_url(self, **kwargs):
+ if not kwargs.get("order_id"):
+ order = self.create_order(**kwargs)
+ kwargs.update({"order_id": order.get("id")})
+
integration_request = create_request_log(kwargs, service_name="Razorpay")
return get_url(f"./razorpay_checkout?token={integration_request.name}")
+ def create_order(self, **kwargs):
+ # Creating Orders https://razorpay.com/docs/api/orders/
+
+ # convert rupees to paisa
+ kwargs["amount"] = int(kwargs["amount"] * 100)
+
+ # Create integration log
+ integration_request = create_request_log(kwargs, service_name="Razorpay")
+
+ # Setup payment options
+ payment_options = {
+ "amount": kwargs.get("amount"),
+ "currency": kwargs.get("currency", "INR"),
+ "receipt": kwargs.get("receipt"),
+ "payment_capture": kwargs.get("payment_capture"),
+ }
+ if self.api_key and self.api_secret:
+ try:
+ order = make_post_request(
+ "https://api.razorpay.com/v1/orders",
+ auth=(
+ self.api_key,
+ self.get_password(fieldname="api_secret", raise_exception=False),
+ ),
+ data=payment_options,
+ )
+ order["integration_request"] = integration_request.name
+ return order # Order returned to be consumed by razorpay.js
+ except Exception:
+ frappe.log(frappe.get_traceback())
+ frappe.throw(_("Could not create razorpay order"))
+
def create_order(self, **kwargs):
# Creating Orders https://razorpay.com/docs/api/orders/
@@ -382,8 +500,8 @@ def create_request(self, data):
def authorize_payment(self):
"""
- An authorization is performed when user’s payment details are successfully authenticated by the bank.
- The money is deducted from the customer’s account, but will not be transferred to the merchant’s account
+ An authorization is performed when user's payment details are successfully authenticated by the bank.
+ The money is deducted from the customer's account, but will not be transferred to the merchant's account
until it is explicitly captured by merchant.
"""
data = json.loads(self.integration_request.data)
@@ -437,8 +555,8 @@ def authorize_payment(self):
if custom_redirect_to:
redirect_to = custom_redirect_to
- redirect_url = "payment-success?doctype={}&docname={}".format(
- self.data.reference_doctype, self.data.reference_docname
+ redirect_url = (
+ f"payment-success?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}"
)
else:
redirect_url = "payment-failed"
@@ -458,6 +576,49 @@ def get_settings(self, data):
}
)
+ if cint(data.get("notes", {}).get("use_sandbox")) or data.get("use_sandbox"):
+ settings.update(
+ {
+ "api_key": frappe.conf.sandbox_api_key,
+ "api_secret": frappe.conf.sandbox_api_secret,
+ }
+ )
+
+ return settings
+
+ def cancel_subscription(self, subscription_id):
+ settings = self.get_settings({})
+
+ try:
+ make_post_request(
+ f"https://api.razorpay.com/v1/subscriptions/{subscription_id}/cancel",
+ auth=(settings.api_key, settings.api_secret),
+ )
+ except Exception:
+ frappe.log_error(frappe.get_traceback())
+
+ def verify_signature(self, body, signature, key):
+ key = bytes(key, "utf-8")
+ body = bytes(body, "utf-8")
+
+ dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256)
+
+ generated_signature = dig.hexdigest()
+ result = hmac.compare_digest(generated_signature, signature)
+
+ if not result:
+ frappe.throw(_("Razorpay Signature Verification Failed"), exc=frappe.PermissionError)
+
+ return result
+
+ @frappe.whitelist()
+ def clear(self):
+ self.api_key = self.api_secret = None
+ self.redirect_url = None
+ self.flags.ignore_mandatory = True
+ self.save()
+
+
if cint(data.get("notes", {}).get("use_sandbox")) or data.get("use_sandbox"):
settings.update(
{
@@ -536,6 +697,17 @@ def capture_payment(is_sandbox=False, sanbox_response=None):
data={"amount": data.get("amount")},
)
+ if resp.get('status') == "authorized":
+ resp = make_post_request("https://api.razorpay.com/v1/payments/{0}/capture".format(data.get("razorpay_payment_id")),
+ auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")})
+
+ if resp.get("status") == "authorized":
+ resp = make_post_request(
+ "https://api.razorpay.com/v1/payments/{0}/capture".format(data.get("razorpay_payment_id")),
+ auth=(settings.api_key, settings.api_secret),
+ data={"amount": data.get("amount")},
+ )
+
if resp.get("status") == "captured":
frappe.db.set_value("Integration Request", doc.name, "status", "Completed")
@@ -547,6 +719,67 @@ def capture_payment(is_sandbox=False, sanbox_response=None):
frappe.log_error(doc.error, f"{doc.name} Failed")
+@frappe.whitelist(allow_guest=True)
+def get_api_key():
+ controller = frappe.get_doc("Razorpay Settings")
+ return controller.api_key
+
+
+@frappe.whitelist(allow_guest=True)
+def get_order(doctype, docname):
+ # Order returned to be consumed by razorpay.js
+ doc = frappe.get_doc(doctype, docname)
+ try:
+ # Do not use run_method here as it fails silently
+ return doc.get_razorpay_order()
+ except AttributeError:
+ frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing"))
+ frappe.throw(_("Could not create Razorpay order. Please contact Administrator"))
+
+
+@frappe.whitelist(allow_guest=True)
+def order_payment_success(integration_request, params):
+ """Called by razorpay.js on order payment success, the params
+ contains razorpay_payment_id, razorpay_order_id, razorpay_signature
+ that is updated in the data field of integration request
+
+ Args:
+ integration_request (string): Name for integration request doc
+ params (string): Params to be updated for integration request.
+ """
+ params = json.loads(params)
+ integration = frappe.get_doc("Integration Request", integration_request)
+
+ # Update integration request
+ integration.update_status(params, integration.status)
+ integration.reload()
+
+ data = json.loads(integration.data)
+ controller = frappe.get_doc("Razorpay Settings")
+
+ # Update payment and integration data for payment controller object
+ controller.integration_request = integration
+ controller.data = frappe._dict(data)
+
+ # Authorize payment
+ controller.authorize_payment()
+
+
+@frappe.whitelist(allow_guest=True)
+def order_payment_failure(integration_request, params):
+ """Called by razorpay.js on failure
+
+ Args:
+ integration_request (TYPE): Description
+ params (TYPE): error data to be updated
+ """
+ frappe.log_error(params, "Razorpay Payment Failure")
+ params = json.loads(params)
+ integration = frappe.get_doc("Integration Request", integration_request)
+ integration.update_status(params, integration.status)
+
+
+
@frappe.whitelist(allow_guest=True)
def get_api_key():
controller = frappe.get_doc("Razorpay Settings")
diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.js b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.js
index 578ae949..31636949 100644
--- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.js
+++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.js
@@ -1,8 +1,6 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Stripe Settings', {
- refresh: function(frm) {
-
- }
+frappe.ui.form.on("Stripe Settings", {
+ refresh: function (frm) {},
});
diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json
index b1bd1c3e..17d94347 100644
--- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json
+++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json
@@ -234,6 +234,132 @@
"translatable": 0,
"unique": 0
},
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "redirect_url",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Redirect URL",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "header_img",
+ "fieldtype": "Attach Image",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Header Image",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py
index f8e8c9f6..4356d2ce 100644
--- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py
+++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py
@@ -190,6 +190,15 @@ def validate_minimum_transaction_amount(self, currency, amount):
)
)
+ def validate_minimum_transaction_amount(self, currency, amount):
+ if currency in self.currency_wise_minimum_charge_amount:
+ if flt(amount) < self.currency_wise_minimum_charge_amount.get(currency, 0.0):
+ frappe.throw(
+ _("For currency {0}, the minimum transaction amount should be {1}").format(
+ currency, self.currency_wise_minimum_charge_amount.get(currency, 0.0)
+ )
+ )
+
def get_payment_url(self, **kwargs):
return get_url(f"./stripe_checkout?{urlencode(kwargs)}")
@@ -258,9 +267,7 @@ def finalize_request(self):
if custom_redirect_to:
redirect_to = custom_redirect_to
- redirect_url = (
- f"payment-success?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}"
- )
+ redirect_url = f"payment-success?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}"
if self.redirect_url:
redirect_url = self.redirect_url
diff --git a/payments/payment_gateways/paymob/accept_api.py b/payments/payment_gateways/paymob/accept_api.py
new file mode 100644
index 00000000..ae3ee4e5
--- /dev/null
+++ b/payments/payment_gateways/paymob/accept_api.py
@@ -0,0 +1,79 @@
+import json
+from typing import Any, Union
+
+import frappe
+import requests
+
+from .connection import AcceptConnection
+from .paymob_urls import PaymobUrls
+from .response_codes import SUCCESS
+from .response_feedback_dataclass import ResponseFeedBack
+
+
+class AcceptAPI:
+ def __init__(self) -> None:
+ """Class for Accept APIs
+ By Initializing an Instance from This class, an auth token is obtained automatically
+ and You will be able to call The Following APIs:
+ - Create Payment Intention
+ - Get Transaction Details
+ """
+ self.connection = AcceptConnection()
+ self.paymob_settings = frappe.get_doc("Paymob Settings")
+ self.paymob_urls = PaymobUrls()
+
+ def retrieve_auth_token(self):
+ """
+ Authentication Request:
+ :return: token: Authentication token, which is valid for one hour from the creation time.
+ """
+ return self.connection.auth_token
+
+ def create_payment_intent(self, data: dict) -> tuple[str, dict | None, ResponseFeedBack]:
+ """
+ Creates a Paymob Payment Intent
+ :param data: Dictionary containing payment intent details (refer to Paymob documentation)
+ :return: Tuple[str, Union[Dict, None], ResponseFeedBack]: (Code, Dict, ResponseFeedBack Instance)
+
+ """
+
+ headers = {
+ "Authorization": f"Token {self.paymob_settings.get_password('secret_key')}",
+ "Content-Type": "application/json",
+ }
+ payload = json.dumps(data)
+ code, feedback = self.connection.post(
+ url=self.paymob_urls.get_url("intention"), headers=headers, data=payload
+ )
+
+ payment_intent = frappe._dict()
+
+ if code == SUCCESS:
+ payment_intent = feedback.data
+ feedback.message = "Payment Intention Created Successfully"
+
+ return code, payment_intent, feedback
+
+ def retrieve_transaction(self, transaction_id: int) -> tuple[str, dict | None, ResponseFeedBack]:
+ """Retrieves Transaction Data by Transaction ID
+
+ Args:
+ transaction_id (int): Paymob's Transaction ID
+
+ Returns:
+ Tuple[str, Union[Dict, None], ResponseFeedBack]: (Code, Dict, ResponseFeedBack Instance)
+ """
+ code, feedback = self.connection.get(
+ url=self.paymob_urls.get_url("retrieve_transaction", id=transaction_id)
+ )
+ transaction = None
+ if code == SUCCESS:
+ transaction = feedback.data
+ feedback.message = f"Transaction with id {transaction_id} retrieved Scuccessfully"
+ return code, transaction, feedback
+
+ def retrieve_iframe(self, iframe_id, payment_token):
+ iframe_url = self.paymob_urls.get_url(
+ "iframe", iframe_id=self.paymob_settings.iframe, payment_token=payment_token
+ )
+ return iframe_url
diff --git a/payments/payment_gateways/paymob/connection.py b/payments/payment_gateways/paymob/connection.py
new file mode 100644
index 00000000..8f1c865a
--- /dev/null
+++ b/payments/payment_gateways/paymob/connection.py
@@ -0,0 +1,137 @@
+from typing import Any
+
+import requests
+from frappe.utils.password import get_decrypted_password
+from requests import HTTPError, JSONDecodeError, RequestException
+
+from .paymob_urls import PaymobUrls
+from .response_codes import (
+ HTTP_EXCEPTION,
+ HTTP_EXCEPTION_MESSAGE,
+ JSON_DECODE_EXCEPTION,
+ JSON_DECODE_EXCEPTION_MESSAGE,
+ REQUEST_EXCEPTION,
+ REQUEST_EXCEPTION_MESSAGE,
+ SUCCESS,
+ SUCCESS_MESSAGE,
+ UNHANDLED_EXCEPTION,
+ UNHANDLED_EXCEPTION_MESSAGE,
+)
+from .response_feedback_dataclass import ResponseFeedBack
+
+
+class AcceptConnection:
+ def __init__(self) -> None:
+ """Initializing the Following:
+ 1- Requests Session
+ 2- Auth Token
+ 3- Set Headers
+ 4- Paymob Urls
+ """
+ self.session = requests.Session()
+ self.paymob_urls = PaymobUrls()
+ self.auth_token = self._get_auth_token()
+ self.session.headers.update(self._get_headers())
+
+ def _get_headers(self) -> dict[str, Any]:
+ """Initialize Header for Requests
+
+ Returns:
+ Dict[str, Any]: Initialized Header Dict
+ """
+ return {
+ "Content-Type": "application/json",
+ "Authorization": f"{self.auth_token}",
+ }
+
+ def _get_auth_token(self) -> str | None:
+ """Retrieve an Auth Token
+
+ Returns:
+ Union[str, None]: Auth Token
+ """
+ api_key = get_decrypted_password("Paymob Settings", "Paymob Settings", "api_key")
+ request_body = {"api_key": api_key}
+
+ code, feedback = self.post(
+ url=self.paymob_urls.get_url("auth"),
+ json=request_body,
+ )
+
+ token = None
+ if code == SUCCESS:
+ token = feedback.data.get("token")
+ return token
+
+ def _process_request(self, call, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]:
+ """Process the Request
+
+ Args:
+ call (Session.get/Session.post): Session.get/Session.post
+ *args, **kwargs: Same Args of requests.post/requests.get methods
+
+ Returns:
+ Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message)
+ """
+
+ reponse_data = None
+ try:
+ response = call(*args, timeout=90, **kwargs)
+ reponse_data = response.json()
+ response.raise_for_status()
+ except JSONDecodeError as error:
+ reponse_feedback = ResponseFeedBack(
+ message=JSON_DECODE_EXCEPTION_MESSAGE,
+ status_code=response.status_code,
+ exception_error=error,
+ )
+ return JSON_DECODE_EXCEPTION, reponse_feedback
+ except HTTPError as error:
+ reponse_feedback = ResponseFeedBack(
+ message=HTTP_EXCEPTION_MESSAGE,
+ data=reponse_data,
+ status_code=response.status_code,
+ exception_error=error,
+ )
+ return HTTP_EXCEPTION, reponse_feedback
+ except RequestException as error:
+ reponse_feedback = ResponseFeedBack(
+ message=REQUEST_EXCEPTION_MESSAGE,
+ exception_error=error,
+ )
+ return REQUEST_EXCEPTION, reponse_feedback
+ except Exception as error:
+ reponse_feedback = ResponseFeedBack(
+ message=UNHANDLED_EXCEPTION_MESSAGE,
+ exception_error=error,
+ )
+ return UNHANDLED_EXCEPTION, reponse_feedback
+
+ reponse_feedback = ResponseFeedBack(
+ message=SUCCESS_MESSAGE,
+ data=reponse_data,
+ status_code=response.status_code,
+ )
+ return SUCCESS, reponse_feedback
+
+ def get(self, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]:
+ """Wrapper for requests.get method
+
+ Args:
+ Same Args of requests.post/requests.get methods
+
+ Returns:
+ Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message)
+ """
+ return self._process_request(*args, call=self.session.get, **kwargs)
+
+ def post(self, *args, **kwargs) -> tuple[str, dict[str, Any], ResponseFeedBack]:
+ """Wrapper for requests.get method
+
+ Args:
+ Same Args of requests.post/requests.get methods
+
+ Returns:
+ Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message)
+ """
+ return self._process_request(*args, call=self.session.post, **kwargs)
diff --git a/payments/payment_gateways/paymob/constants.py b/payments/payment_gateways/paymob/constants.py
new file mode 100644
index 00000000..be88d47e
--- /dev/null
+++ b/payments/payment_gateways/paymob/constants.py
@@ -0,0 +1,4 @@
+class AcceptCallbackTypes:
+ TRANSACTION = "TRANSACTION"
+ CARD_TOKEN = "TOKEN"
+ DELIVERY_STATUS = "DELIVERY_STATUS"
diff --git a/payments/payment_gateways/paymob/hmac_validator.py b/payments/payment_gateways/paymob/hmac_validator.py
new file mode 100644
index 00000000..30ec0635
--- /dev/null
+++ b/payments/payment_gateways/paymob/hmac_validator.py
@@ -0,0 +1,168 @@
+import hashlib
+import hmac
+from typing import Any
+
+import frappe
+
+from .constants import AcceptCallbackTypes
+
+
+class HMACValidator:
+ def __init__(self, incoming_hmac: str, callback_dict: dict[str, Any], **kwargs) -> None:
+ """Initialize HMAC Attributes
+
+ Args:
+ incoming_hmac (str): Incoming Paymob's HMAC
+ callback_dict Dict[str, Any]: Incoming Callback Dict
+ """
+ self.incoming_hmac = incoming_hmac
+ self.callback_dict = callback_dict
+ if isinstance(self.callback_dict, dict):
+ self.callback_obj_dict = self.callback_dict.get("obj")
+
+ super().__init__(**kwargs)
+
+ @staticmethod
+ def _calculate_hmac(message: str) -> str:
+ """Calculates HMAC
+
+ Args:
+ message (str): GeneratedHMAC Message
+
+ Returns:
+ str: Calculated HMAC
+ """
+ hmac_secret = frappe.get_doc("Paymob Settings").get_password("hmac").encode("utf-8")
+ return (
+ hmac.new(
+ hmac_secret,
+ message.encode("utf-8"),
+ hashlib.sha512,
+ )
+ .hexdigest()
+ .lower()
+ )
+
+ @classmethod
+ def _generate_processed_hmac(cls, hmac_dict: dict[str, Any]) -> str:
+ """Creates HMAC from sent self.callback_obj_dict
+
+ Args:
+ hmac_dict (Dict[str, Any]): Hmac Dict
+
+ Returns:
+ str: Generated HMAC
+ """
+ if not isinstance(hmac_dict, dict):
+ return ""
+
+ message = ""
+ for value in hmac_dict.values():
+ if isinstance(value, bool):
+ value = str(value).lower()
+ if value is None:
+ value = ""
+ message += str(value)
+
+ return cls._calculate_hmac(message=message)
+
+ def _generate_transaction_processed_hmac(self) -> str:
+ """Creates HMAC from sent transaction callback self.callback_obj_dict
+
+ Returns:
+ str: Generated HMAC
+ """
+ if not isinstance(self.callback_obj_dict, dict):
+ return ""
+
+ hmac_dict = {
+ "amount_cents": self.callback_obj_dict.get("amount_cents"),
+ "created_at": self.callback_obj_dict.get("created_at"),
+ "currency": self.callback_obj_dict.get("currency"),
+ "error_occured": self.callback_obj_dict.get("error_occured"),
+ "has_parent_transaction": self.callback_obj_dict.get("has_parent_transaction"),
+ "id": self.callback_obj_dict.get("id"),
+ "integration_id": self.callback_obj_dict.get("integration_id"),
+ "is_3d_secure": self.callback_obj_dict.get("is_3d_secure"),
+ "is_auth": self.callback_obj_dict.get("is_auth"),
+ "is_capture": self.callback_obj_dict.get("is_capture"),
+ "is_refunded": self.callback_obj_dict.get("is_refunded"),
+ "is_standalone_payment": self.callback_obj_dict.get("is_standalone_payment"),
+ "is_voided": self.callback_obj_dict.get("is_voided"),
+ "order.id": self.callback_obj_dict.get("order", {}).get("id"),
+ "owner": self.callback_obj_dict.get("owner"),
+ "pending": self.callback_obj_dict.get("pending"),
+ "source_data.pan": self.callback_obj_dict.get("source_data", {}).get("pan"),
+ "source_data.sub_type": self.callback_obj_dict.get("source_data", {}).get("sub_type"),
+ "source_data.type": self.callback_obj_dict.get("source_data", {}).get("type"),
+ "success": self.callback_obj_dict.get("success"),
+ }
+
+ return self._generate_processed_hmac(hmac_dict=hmac_dict)
+
+ def _generate_card_token_processed_hmac(self) -> str:
+ """Creates HMAC from sent card token callback body_dic
+
+ Returns:
+ str: Generated HMAC
+ """
+ if not isinstance(self.callback_obj_dict, dict):
+ return ""
+
+ hmac_dict = {
+ "card_subtype": self.callback_obj_dict.get("card_subtype"),
+ "created_at": self.callback_obj_dict.get("created_at"),
+ "email": self.callback_obj_dict.get("email"),
+ "id": self.callback_obj_dict.get("id"),
+ "masked_pan": self.callback_obj_dict.get("masked_pan"),
+ "merchant_id": self.callback_obj_dict.get("merchant_id"),
+ "order_id": self.callback_obj_dict.get("order_id"),
+ "token": self.callback_obj_dict.get("token"),
+ }
+
+ return self._generate_processed_hmac(hmac_dict=hmac_dict)
+
+ def _generate_delivery_status_processed_hmac(self) -> str:
+ """Creates HMAC from sent Delivery Status callback body_dic
+
+ Returns:
+ str: Generated HMAC
+ """
+ if not isinstance(self.callback_obj_dict, dict):
+ return ""
+
+ hmac_dict = {
+ "order_id": self.callback_obj_dict.get("order_id"),
+ "order_delivery_status": self.callback_obj_dict.get("order_delivery_status"),
+ "merchant_id": self.callback_obj_dict.get("merchant_id"),
+ "merchant_name": self.callback_obj_dict.get("merchant_name"),
+ "updated_at": self.callback_obj_dict.get("updated_at"),
+ }
+
+ return self._generate_processed_hmac(hmac_dict=hmac_dict)
+
+ # Public Method that can be used Directly to Validate HMAC
+ @property
+ def is_valid(self) -> bool:
+ """Validates HMAC for processed callback
+
+ Returns:
+ bool: True if HMAC is Valid, False otherwise
+ """
+ if not isinstance(self.callback_dict, dict):
+ return False
+
+ callback_type = self.callback_dict.get("type")
+ if callback_type == AcceptCallbackTypes.TRANSACTION:
+ calculated_hmac = self._generate_transaction_processed_hmac()
+ elif callback_type == AcceptCallbackTypes.CARD_TOKEN:
+ calculated_hmac = self._generate_card_token_processed_hmac()
+ elif callback_type == AcceptCallbackTypes.DELIVERY_STATUS:
+ calculated_hmac = self._generate_delivery_status_processed_hmac()
+ else:
+ return False
+
+ if calculated_hmac != self.incoming_hmac:
+ return False
+
+ return True
diff --git a/payments/payment_gateways/paymob/paymob_urls.py b/payments/payment_gateways/paymob/paymob_urls.py
new file mode 100644
index 00000000..c866c032
--- /dev/null
+++ b/payments/payment_gateways/paymob/paymob_urls.py
@@ -0,0 +1,42 @@
+from dataclasses import dataclass
+
+import frappe
+
+
+@dataclass
+class PaymobUrls:
+ base_url: str = "https://accept.paymob.com/"
+
+ # Auth
+ auth: str = "api/auth/tokens"
+
+ # Ecommerce
+ order: str = "api/ecommerce/orders"
+ inquire_transaction: str = "api/ecommerce/orders/transaction_inquiry"
+ tracking: str = "api/ecommerce/orders/{order_id}/delivery_status?token={token}"
+ preparing_package: str = "api/ecommerce/orders/{order_id}/airway_bill?token={token}"
+
+ # Acceptance
+ payment_key: str = "api/acceptance/payment_keys"
+ payment: str = "api/acceptance/payments/pay"
+ capture: str = "api/acceptance/capture"
+ refund: str = "api/acceptance/void_refund/refund"
+ void: str = "api/acceptance/void_refund/void?token={token}"
+ retrieve_transaction: str = "api/acceptance/transactions/{id}"
+ retrieve_transactions: str = (
+ "api/acceptance/portal-transactions?page={from_page}&page_size={page_size}&token={token}"
+ )
+ loyalty_checkout: str = "api/acceptance/loyalty_checkout"
+ iframe: str = "api/acceptance/iframes/{iframe_id}?payment_token={payment_token}"
+ intention: str = "v1/intention/"
+
+ def get_url(self, endpoint, **kwargs):
+ # based on available attributes and passed keyword arguments
+ return f"{self.base_url}{getattr(self, endpoint)}".format(**kwargs)
+
+
+# Example usage
+# paymob_urls = PaymobUrls()
+# order_registration_url = paymob_urls.get_url("order")
+# void_transaction_url = paymob_urls.get_url("void", token="your_token")
+# tracking_url = paymob_urls.get_url("tracking", order_id="123", token="your_token")
diff --git a/payments/payment_gateways/paymob/response_codes.py b/payments/payment_gateways/paymob/response_codes.py
new file mode 100644
index 00000000..43f90e08
--- /dev/null
+++ b/payments/payment_gateways/paymob/response_codes.py
@@ -0,0 +1,16 @@
+# Response Codes
+SUCCESS = 10
+
+# Request Related Error Codes
+JSON_DECODE_EXCEPTION = 20
+REQUEST_EXCEPTION = 21
+HTTP_EXCEPTION = 22
+UNHANDLED_EXCEPTION = 23
+
+
+# Error Messages Templates
+JSON_DECODE_EXCEPTION_MESSAGE = "An Error Occurred While Parsing the Response into JSON"
+REQUEST_EXCEPTION_MESSAGE = "An Error Occurred During the Request"
+HTTP_EXCEPTION_MESSAGE = "Non 2xx Status Code Returned."
+UNHANDLED_EXCEPTION_MESSAGE = "Unhandled Exception"
+SUCCESS_MESSAGE = "API Successfully Called."
diff --git a/payments/payment_gateways/paymob/response_feedback_dataclass.py b/payments/payment_gateways/paymob/response_feedback_dataclass.py
new file mode 100644
index 00000000..42e41eba
--- /dev/null
+++ b/payments/payment_gateways/paymob/response_feedback_dataclass.py
@@ -0,0 +1,10 @@
+from dataclasses import dataclass
+from typing import Any, Optional
+
+
+@dataclass
+class ResponseFeedBack:
+ message: str | None
+ data: Any = None
+ status_code: int = None
+ exception_error: str = None
diff --git a/payments/payment_gateways/stripe_integration.py b/payments/payment_gateways/stripe_integration.py
index 35c63c55..2d7e8a5d 100644
--- a/payments/payment_gateways/stripe_integration.py
+++ b/payments/payment_gateways/stripe_integration.py
@@ -1,8 +1,8 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-import stripe
import frappe
+import stripe
from frappe import _
from frappe.integrations.utils import create_request_log
diff --git a/payments/payments/doctype/payment_gateway/payment_gateway.js b/payments/payments/doctype/payment_gateway/payment_gateway.js
index 0eff5a56..3e74b9cb 100644
--- a/payments/payments/doctype/payment_gateway/payment_gateway.js
+++ b/payments/payments/doctype/payment_gateway/payment_gateway.js
@@ -1,8 +1,6 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Payment Gateway', {
- refresh: function(frm) {
-
- }
+frappe.ui.form.on("Payment Gateway", {
+ refresh: function (frm) {},
});
diff --git a/payments/public/js/razorpay.js b/payments/public/js/razorpay.js
index c74d1be7..cf55b991 100644
--- a/payments/public/js/razorpay.js
+++ b/payments/public/js/razorpay.js
@@ -54,95 +54,110 @@ Razorpay Payment
frappe.provide("frappe.checkout");
-frappe.require('https://checkout.razorpay.com/v1/checkout.js').then(() => {
- frappe.checkout.razorpay = class RazorpayCheckout {
- constructor(opts) {
- Object.assign(this, opts);
- }
-
- init() {
- frappe.run_serially([
- () => this.get_key(),
- () => this.make_order(),
- () => this.prepare_options(),
- () => this.setup_handler(),
- () => this.show()
- ]);
- }
-
- show() {
- this.razorpay = new Razorpay(this.options);
- this.razorpay.once('ready', (response) => {
- this.on_open && this.on_open(response);
- })
- this.razorpay.open();
- }
-
- get_key() {
- return new Promise(resolve => {
- frappe.call("payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.get_api_key").then(res => {
- this.key = res.message;
- resolve(true);
- })
- });
- }
-
- make_order() {
- return new Promise(resolve => {
- frappe.call("payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.get_order", {
- doctype: this.doctype,
- docname: this.docname
- }).then(res => {
- this.order = res.message;
- resolve(true);
- })
- });
- }
-
- order_success(response) {
- frappe.call("payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.order_payment_success", {
- integration_request: this.order.integration_request,
- params: {
- razorpay_payment_id: response.razorpay_payment_id,
- razorpay_order_id: response.razorpay_order_id,
- razorpay_signature: response.razorpay_signature
- }
- })
- }
-
- order_fail(response) {
- frappe.call( "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.order_payment_failure", {
- integration_request: this.order.integration_request,
- params: response
- })
- }
-
- prepare_options() {
- this.options = {
- "key": this.key,
- "amount": this.order.amount_due,
- "currency": this.order.currency,
- "name": this.name,
- "description": this.description,
- "image": this.image,
- "order_id": this.order.id,
- "prefill": this.prefill,
- "theme": this.theme,
- "modal": this.modal
- };
- }
-
- setup_handler() {
- this.options.handler = (response) => {
- if (response.error) {
- this.order_fail(response);
- this.on_fail && this.on_fail(response);
- }
- else if (response.razorpay_payment_id) {
- this.order_success(response);
- this.on_success && this.on_success(response);
- }
- }
- }
- }
+frappe.require("https://checkout.razorpay.com/v1/checkout.js").then(() => {
+ frappe.checkout.razorpay = class RazorpayCheckout {
+ constructor(opts) {
+ Object.assign(this, opts);
+ }
+
+ init() {
+ frappe.run_serially([
+ () => this.get_key(),
+ () => this.make_order(),
+ () => this.prepare_options(),
+ () => this.setup_handler(),
+ () => this.show(),
+ ]);
+ }
+
+ show() {
+ // eslint-disable-next-line no-undef
+ this.razorpay = new Razorpay(this.options);
+ this.razorpay.once("ready", (response) => {
+ this.on_open && this.on_open(response);
+ });
+ this.razorpay.open();
+ }
+
+ get_key() {
+ return new Promise((resolve) => {
+ frappe
+ .call(
+ "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.get_api_key"
+ )
+ .then((res) => {
+ this.key = res.message;
+ resolve(true);
+ });
+ });
+ }
+
+ make_order() {
+ return new Promise((resolve) => {
+ frappe
+ .call(
+ "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.get_order",
+ {
+ doctype: this.doctype,
+ docname: this.docname,
+ }
+ )
+ .then((res) => {
+ this.order = res.message;
+ resolve(true);
+ });
+ });
+ }
+
+ order_success(response) {
+ frappe.call(
+ "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.order_payment_success",
+ {
+ integration_request: this.order.integration_request,
+ params: {
+ razorpay_payment_id: response.razorpay_payment_id,
+ razorpay_order_id: response.razorpay_order_id,
+ razorpay_signature: response.razorpay_signature,
+ },
+ }
+ );
+ }
+
+ order_fail(response) {
+ frappe.call(
+ "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.order_payment_failure",
+ {
+ integration_request: this.order.integration_request,
+ params: response,
+ }
+ );
+ }
+
+ prepare_options() {
+ this.options = {
+ key: this.key,
+ amount: this.order.amount_due,
+ currency: this.order.currency,
+ name: this.name,
+ description: this.description,
+ image: this.image,
+ order_id: this.order.id,
+ prefill: this.prefill,
+ theme: this.theme,
+ modal: this.modal,
+ };
+ }
+
+ setup_handler() {
+ this.options.handler = (response) => {
+ if (response.error) {
+ this.order_fail(response);
+ this.on_fail && this.on_fail(response);
+ } else if (response.razorpay_payment_id) {
+ this.order_success(response);
+ this.on_success && this.on_success(response);
+ }
+ };
+ }
+ };
});
diff --git a/payments/templates/includes/razorpay_checkout.js b/payments/templates/includes/razorpay_checkout.js
index a79a3dec..efbd2191 100644
--- a/payments/templates/includes/razorpay_checkout.js
+++ b/payments/templates/includes/razorpay_checkout.js
@@ -7,13 +7,13 @@ $(document).ready(function(){
"name": "{{ title }}",
"description": "{{ description }}",
"subscription_id": "{{ subscription_id }}",
+ "order_id": "{{ order_id }}",
"handler": function (response){
razorpay.make_payment_log(response, options, "{{ reference_doctype }}", "{{ reference_docname }}", "{{ token }}");
},
"prefill": {
"name": "{{ payer_name }}",
- "email": "{{ payer_email }}",
- "order_id": "{{ order_id }}"
+ "email": "{{ payer_email }}"
},
"notes": {{ frappe.form_dict|json }}
};
diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py
index 12b01369..bb7aa6dd 100644
--- a/payments/templates/pages/braintree_checkout.py
+++ b/payments/templates/pages/braintree_checkout.py
@@ -12,6 +12,11 @@
get_gateway_controller,
)
+from payments.payment_gateways.doctype.braintree_settings.braintree_settings import (
+ get_client_token,
+ get_gateway_controller,
+)
+
no_cache = 1
expected_keys = (
@@ -40,9 +45,7 @@ def get_context(context):
context["amount"] = flt(context["amount"])
gateway_controller = get_gateway_controller(context.reference_docname)
- context["header_img"] = frappe.db.get_value(
- "Braintree Settings", gateway_controller, "header_img"
- )
+ context["header_img"] = frappe.db.get_value("Braintree Settings", gateway_controller, "header_img")
else:
frappe.redirect_to_message(
diff --git a/payments/templates/pages/gocardless_checkout.py b/payments/templates/pages/gocardless_checkout.py
index fa780d23..89665995 100644
--- a/payments/templates/pages/gocardless_checkout.py
+++ b/payments/templates/pages/gocardless_checkout.py
@@ -38,9 +38,7 @@ def get_context(context):
context["amount"] = flt(context["amount"])
gateway_controller = get_gateway_controller(context.reference_docname)
- context["header_img"] = frappe.db.get_value(
- "GoCardless Settings", gateway_controller, "header_img"
- )
+ context["header_img"] = frappe.db.get_value("GoCardless Settings", gateway_controller, "header_img")
else:
frappe.redirect_to_message(
@@ -95,6 +93,6 @@ def check_mandate(data, reference_doctype, reference_docname):
return {"redirect_to": redirect_flow.redirect_url}
- except Exception as e:
+ except Exception:
frappe.log_error("GoCardless Payment Error")
return {"redirect_to": "payment-failed"}
diff --git a/payments/templates/pages/gocardless_confirmation.py b/payments/templates/pages/gocardless_confirmation.py
index 3fe7d99b..02420641 100644
--- a/payments/templates/pages/gocardless_confirmation.py
+++ b/payments/templates/pages/gocardless_confirmation.py
@@ -33,7 +33,6 @@ def get_context(context):
@frappe.whitelist(allow_guest=True)
def confirm_payment(redirect_flow_id, reference_doctype, reference_docname):
-
client = gocardless_initialization(reference_docname)
try:
@@ -59,7 +58,7 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname):
try:
create_mandate(data)
- except Exception as e:
+ except Exception:
frappe.log_error("GoCardless Mandate Registration Error")
gateway_controller = get_gateway_controller(reference_docname)
@@ -67,7 +66,7 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname):
return {"redirect_to": confirmation_url}
- except Exception as e:
+ except Exception:
frappe.log_error("GoCardless Payment Error")
return {"redirect_to": "payment-failed"}
diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py
index 8985850a..700d258c 100644
--- a/payments/templates/pages/payment_success.py
+++ b/payments/templates/pages/payment_success.py
@@ -5,9 +5,10 @@
no_cache = True
+no_cache = True
+
def get_context(context):
- token = frappe.local.form_dict.token
doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname)
context.payment_message = ""
diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py
index 0b2e9adf..521a6d22 100644
--- a/payments/templates/pages/razorpay_checkout.py
+++ b/payments/templates/pages/razorpay_checkout.py
@@ -43,7 +43,7 @@ def get_context(context):
payment_details["subscription_id"] if payment_details.get("subscription_id") else ""
)
- except Exception as e:
+ except Exception:
frappe.redirect_to_message(
_("Invalid Token"),
_("Seems token you are using is invalid!"),
diff --git a/payments/templates/pages/stripe_checkout.html b/payments/templates/pages/stripe_checkout.html
index e2cd58bb..b8b56065 100644
--- a/payments/templates/pages/stripe_checkout.html
+++ b/payments/templates/pages/stripe_checkout.html
@@ -17,23 +17,25 @@
{% if image %}
{% endif %}
-