From 7d71eb4fdab168cab1f8ae0c484efafba6d6c251 Mon Sep 17 00:00:00 2001 From: Saullo Bretas Silva Date: Sun, 5 Nov 2023 17:08:55 -0300 Subject: [PATCH 001/138] fix: update stripe redirect URL to include reference doctype and docname --- .../doctype/stripe_settings/stripe_settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 0547a528..32adab85 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -255,7 +255,9 @@ def finalize_request(self): if custom_redirect_to: redirect_to = custom_redirect_to - redirect_url = "payment-success" + redirect_url = "payment-success?doctype={}&docname={}".format( + self.data.reference_doctype, self.data.reference_docname + ) if self.redirect_url: redirect_url = self.redirect_url From f009002c3adeeaa71852a479ed6a17f33f88456e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 23 Jan 2024 16:48:50 +0530 Subject: [PATCH 002/138] fix: payment sucess redirect error --- .../doctype/stripe_settings/stripe_settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 32adab85..816b77e9 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -265,8 +265,11 @@ def finalize_request(self): else: redirect_url = "payment-failed" - if redirect_to: + if redirect_to and "?" in redirect_url: + redirect_url += "&" + urlencode({"redirect_to": redirect_to}) + else: redirect_url += "?" + urlencode({"redirect_to": redirect_to}) + if redirect_message: redirect_url += "&" + urlencode({"redirect_message": redirect_message}) From cf3e69ac7f0f27e54060788c641778bd2a99b6d3 Mon Sep 17 00:00:00 2001 From: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:09:22 -0800 Subject: [PATCH 003/138] Update gocardless_settings.json add header_img field to gocardless_settings.json --- .../gocardless_settings.json | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json index cc7e36a0..5729eaa8 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json @@ -42,6 +42,36 @@ "fieldname": "use_sandbox", "fieldtype": "Check", "label": "Use Sandbox" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header_img", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "links": [], @@ -69,4 +99,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 45c63c4b747c7c1d49813cddf30aaae66db29f98 Mon Sep 17 00:00:00 2001 From: Kevin Shenk Date: Tue, 5 Mar 2024 15:11:49 -0500 Subject: [PATCH 004/138] fix: typo in GoCardless Settings name --- .../doctype/gocardless_settings/gocardless_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py index e95781c6..ac6b83a9 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py @@ -32,7 +32,7 @@ def on_update(self): from payments.utils import create_payment_gateway create_payment_gateway( - "GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name + "GoCardless-" + self.gateway_name, settings="GoCardless Settings", controller=self.gateway_name ) call_hook_method("payment_gateway_enabled", gateway="GoCardless-" + self.gateway_name) From f84edf918e52d07c1f612e6f8bbf6aafba1abd47 Mon Sep 17 00:00:00 2001 From: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com> Date: Tue, 14 May 2024 22:13:12 -0700 Subject: [PATCH 005/138] fix: header_img field schema (#83) --- .../gocardless_settings.json | 34 +++---------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json index 5729eaa8..7c1e5eed 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.json @@ -10,7 +10,8 @@ "section_break_2", "access_token", "webhooks_secret", - "use_sandbox" + "use_sandbox", + "header_img" ], "fields": [ { @@ -44,38 +45,13 @@ "label": "Use Sandbox" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "header_img", "fieldtype": "Attach Image", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Header Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "label": "Header Image" } ], "links": [], - "modified": "2023-09-22 13:33:42.225243", + "modified": "2024-05-14 16:18:26.154479", "modified_by": "Administrator", "module": "Payment Gateways", "name": "GoCardless Settings", @@ -99,4 +75,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From e87d8def18390a19500125344ed4eea7ed3bad33 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 30 Sep 2024 13:00:47 +0530 Subject: [PATCH 006/138] fix: stripe checkout --- .../doctype/stripe_settings/stripe_settings.py | 8 +++++--- payments/templates/includes/stripe_checkout.js | 3 ++- payments/templates/pages/stripe_checkout.py | 9 ++++----- pyproject.toml | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 816b77e9..a0414636 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -276,9 +276,11 @@ def finalize_request(self): return {"redirect_to": redirect_url, "status": status} -def get_gateway_controller(doctype, docname): - reference_doc = frappe.get_doc(doctype, docname) +def get_gateway_controller(doctype, docname, payment_gateway=None): + if not payment_gateway: + reference_doc = frappe.get_doc(doctype, docname) + payment_gateway = reference_doc.payment_gateway gateway_controller = frappe.db.get_value( - "Payment Gateway", reference_doc.payment_gateway, "gateway_controller" + "Payment Gateway", payment_gateway, "gateway_controller" ) return gateway_controller diff --git a/payments/templates/includes/stripe_checkout.js b/payments/templates/includes/stripe_checkout.js index 0a0aa706..7f37e96b 100644 --- a/payments/templates/includes/stripe_checkout.js +++ b/payments/templates/includes/stripe_checkout.js @@ -39,7 +39,8 @@ function setOutcome(result) { "stripe_token_id": result.token.id, "data": JSON.stringify({{ frappe.form_dict|json }}), "reference_doctype": "{{ reference_doctype }}", - "reference_docname": "{{ reference_docname }}" + "reference_docname": "{{ reference_docname }}", + "payment_gateway": "{{ payment_gateway }}" }, callback: function(r) { if (r.message.status == "Completed") { diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index c57d39c4..afc04221 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -20,8 +20,8 @@ "reference_docname", "payer_name", "payer_email", - "order_id", "currency", + "payment_gateway", ) @@ -32,8 +32,7 @@ def get_context(context): if not (set(expected_keys) - set(list(frappe.form_dict))): for key in expected_keys: context[key] = frappe.form_dict[key] - - gateway_controller = get_gateway_controller(context.reference_doctype, context.reference_docname) + gateway_controller = get_gateway_controller(context.reference_doctype, context.reference_docname, context.payment_gateway) context.publishable_key = get_api_key(context.reference_docname, gateway_controller) context.image = get_header_image(context.reference_docname, gateway_controller) @@ -71,12 +70,12 @@ def get_header_image(doc, gateway_controller): @frappe.whitelist(allow_guest=True) -def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): +def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None): data = json.loads(data) data.update({"stripe_token_id": stripe_token_id}) - gateway_controller = get_gateway_controller(reference_doctype, reference_docname) + gateway_controller = get_gateway_controller(reference_doctype, reference_docname, payment_gateway) if is_a_subscription(reference_doctype, reference_docname): reference = frappe.get_doc(reference_doctype, reference_docname) diff --git a/pyproject.toml b/pyproject.toml index 1dbe5220..a102875d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,8 @@ readme = "README.md" dynamic = ["version"] dependencies = [ "paytmchecksum~=1.7.0", - "razorpay~=1.2.0", - "stripe~=2.56.0", + "razorpay~=1.4.2", + "stripe~=10.12.0", "braintree~=4.20.0", "pycryptodome>=3.18.0,<4.0.0", "gocardless-pro~=1.22.0", From 4ea0c9331df787ef0de4d2b5fa704bb61a1ddc74 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 30 Sep 2024 13:15:10 +0530 Subject: [PATCH 007/138] chore: fixed linters --- .../doctype/stripe_settings/stripe_settings.py | 4 +--- payments/templates/pages/stripe_checkout.py | 8 ++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index a0414636..bb1bc677 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -280,7 +280,5 @@ def get_gateway_controller(doctype, docname, payment_gateway=None): if not payment_gateway: reference_doc = frappe.get_doc(doctype, docname) payment_gateway = reference_doc.payment_gateway - gateway_controller = frappe.db.get_value( - "Payment Gateway", payment_gateway, "gateway_controller" - ) + gateway_controller = frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") return gateway_controller diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index afc04221..7572002e 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -32,7 +32,9 @@ def get_context(context): if not (set(expected_keys) - set(list(frappe.form_dict))): for key in expected_keys: context[key] = frappe.form_dict[key] - gateway_controller = get_gateway_controller(context.reference_doctype, context.reference_docname, context.payment_gateway) + gateway_controller = get_gateway_controller( + context.reference_doctype, context.reference_docname, context.payment_gateway + ) context.publishable_key = get_api_key(context.reference_docname, gateway_controller) context.image = get_header_image(context.reference_docname, gateway_controller) @@ -70,7 +72,9 @@ def get_header_image(doc, gateway_controller): @frappe.whitelist(allow_guest=True) -def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None): +def make_payment( + stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None +): data = json.loads(data) data.update({"stripe_token_id": stripe_token_id}) From e8f3fa11f7e3034ef9e024ebe909b08e5cd59bdb Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Mon, 30 Sep 2024 13:21:34 +0530 Subject: [PATCH 008/138] chore: bumped up semgrep --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 6b504fa1..159d3ca3 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -50,5 +50,5 @@ jobs: - name: Run Semgrep rules run: | - pip install semgrep==0.97.0 + pip install semgrep==1.90.0 semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness From 5cb6239fbd2ad785128910b0b2ef326d409edffd Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 1 Oct 2024 09:50:27 +0530 Subject: [PATCH 009/138] fix: stripe page styling --- payments/templates/pages/stripe_checkout.css | 50 +++++++------------ payments/templates/pages/stripe_checkout.html | 14 +++--- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/payments/templates/pages/stripe_checkout.css b/payments/templates/pages/stripe_checkout.css index a42808aa..5aadc117 100644 --- a/payments/templates/pages/stripe_checkout.css +++ b/payments/templates/pages/stripe_checkout.css @@ -22,42 +22,26 @@ } .stripe #payment-form { - margin-top: 80px; + margin-top: 2rem; } .stripe button { float: right; display: block; - background: #5e64ff; + background: #171717; color: white; - box-shadow: 0 7px 14px 0 rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); - border-radius: 4px; + border-radius: 8px; border: 0; - margin-top: 20px; - font-size: 15px; - font-weight: 400; + font-size: 14px; + font-weight: 420; max-width: 40%; height: 40px; line-height: 38px; outline: none; } -.stripe button:hover, .stripe button:focus { - background: #2b33ff; - border-color: #0711ff; -} - -.stripe button:active { - background: #5e64ff; -} - -.stripe button:disabled { - background: #515e80; -} - .stripe .group { background: white; - box-shadow: 2px 7px 14px 2px rgba(49, 49, 93, 0.10), 0 3px 6px 0 rgba(0, 0, 0, 0.08); border-radius: 4px; margin-bottom: 20px; } @@ -78,8 +62,11 @@ .stripe label>span { width: 20%; - text-align: right; float: left; + color: #525252; + font-size: 13px; + font-weight: 420; + letter-spacing: 0.02em; } .current-card { @@ -87,17 +74,18 @@ } .field { - background: transparent; - font-weight: 300; - border: 0; - color: #31325F; - outline: none; - padding-right: 10px; - padding-left: 10px; - cursor: text; + border-radius: 8px; + letter-spacing: 0.02em; + font-size: 14px; + font-weight: 420; width: 70%; - height: 40px; float: right; + border: none; + height: 28px; + padding: 6px 8px; + color: #383838; + background: #f3f3f3; + background-clip: padding-box; } .field::-webkit-input-placeholder { diff --git a/payments/templates/pages/stripe_checkout.html b/payments/templates/pages/stripe_checkout.html index b038f6fd..9ce76a91 100644 --- a/payments/templates/pages/stripe_checkout.html +++ b/payments/templates/pages/stripe_checkout.html @@ -17,23 +17,25 @@ {% if image %} {% endif %} -

{{description}}

+

+ {{description}} +

+ +
+ +
@@ -45,7 +47,7 @@

{{description}}

- +
From 3b74e1844eb35644b3d3bbae139e9bf9c54984ad Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:19:24 +0200 Subject: [PATCH 010/138] chore: remove manifest --- MANIFEST.in | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 50b0f499..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,17 +0,0 @@ -include MANIFEST.in -include *.json -include *.md -include *.py -include *.txt -recursive-include payments *.css -recursive-include payments *.csv -recursive-include payments *.html -recursive-include payments *.ico -recursive-include payments *.js -recursive-include payments *.json -recursive-include payments *.md -recursive-include payments *.png -recursive-include payments *.py -recursive-include payments *.svg -recursive-include payments *.txt -recursive-exclude payments *.pyc From 55580c2b56e1932e70a04bb8b961f07b423b7322 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:20:17 +0200 Subject: [PATCH 011/138] chore: add tooling files to gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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 From 7fd091316f03eda9000e580e474be5c9199cef5f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:12:14 +0200 Subject: [PATCH 012/138] ci: adopt best practice --- .github/helper/install.sh | 56 ++++++++++++++++ .github/helper/site_config.json | 16 +++++ .github/workflows/ci.yml | 115 ++++++++++++++++++++++---------- .github/workflows/labeller.yml | 12 ++++ payments/tests/__init__.py | 0 5 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 .github/helper/install.sh create mode 100644 .github/helper/site_config.json create mode 100644 .github/workflows/labeller.yml create mode 100644 payments/tests/__init__.py diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 00000000..e1efaff1 --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e + +cd ~ || exit + +sudo apt update +sudo apt remove mysql-server mysql-client +sudo apt install libcups2-dev redis-server mariadb-client-10.6 + +pip install frappe-bench + +githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}} +frappeuser=${FRAPPE_USER:-"frappe"} +frappebranch=${FRAPPE_BRANCH:-$githubbranch} +erpnextbranch=${ERPNEXT_BRANCH:-$githubbranch} + +git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1 +bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench + +mkdir ~/frappe-bench/sites/test_site +cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" + +install_whktml() { + wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz + tar -xf /tmp/wkhtmltox.tar.xz -C /tmp + sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf + sudo chmod o+x /usr/local/bin/wkhtmltopdf +} +install_whktml & + +cd ~/frappe-bench || exit + +sed -i 's/watch:/# watch:/g' Procfile +sed -i 's/schedule:/# schedule:/g' Procfile +sed -i 's/socketio:/# socketio:/g' Procfile +sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile + +bench get-app "https://github.com/${frappeuser}/erpnext" --branch "$erpnextbranch" --resolve-deps +bench get-app payments "${GITHUB_WORKSPACE}" +bench setup requirements --dev + +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 diff --git a/.github/helper/site_config.json b/.github/helper/site_config.json new file mode 100644 index 00000000..21fdbf31 --- /dev/null +++ b/.github/helper/site_config.json @@ -0,0 +1,16 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_frappe", + "db_password": "test_frappe", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "root", + "host_name": "http://test_site:8000", + "install_apps": ["payments", "erpnext"], + "throttle_user_limit": 100 +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f99827fb..43fce713 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,62 +1,96 @@ - -name: Server +name: CI on: - push: - branches: - - develop pull_request: + paths-ignore: + - "**.css" + - "**.js" + - "**.md" + - "**.html" + - "**.csv" + schedule: + # Run everday at midnight UTC / 5:30 IST + - cron: "0 0 * * *" concurrency: - group: develop-payments-${{ github.event.number }} + group: develop-${{ github.event.number }} cancel-in-progress: true jobs: tests: runs-on: ubuntu-latest + timeout-minutes: 60 + env: + NODE_ENV: "production" + WITH_COVERAGE: ${{ github.event_name != 'pull_request' }} + strategy: fail-fast: false - name: Server + + matrix: + container: [1, 2] + + name: Python Unit Tests services: - mariadb: + mysql: image: mariadb:10.6 env: - MYSQL_ROOT_PASSWORD: root + MARIADB_ROOT_PASSWORD: 'root' ports: - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: - name: Clone uses: actions/checkout@v2 - - name: Install MariaDB Client - run: sudo apt-get -y install mariadb-client-10.6 - - name: Setup Python uses: actions/setup-python@v2 with: python-version: '3.10' + - name: Check for valid Python & Merge Conflicts + run: | + python -m compileall -f "${GITHUB_WORKSPACE}" + if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 + fi + - name: Setup Node uses: actions/setup-node@v2 with: node-version: 18 check-latest: true + - name: Add to Hosts + run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + - name: Cache pip uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Get yarn cache directory path id: yarn-cache-dir-path - run: 'echo "::set-output name=dir::$(yarn cache dir)"' + run: echo "::set-output name=dir::$(yarn cache dir)" - uses: actions/cache@v2 id: yarn-cache @@ -66,30 +100,41 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - name: Setup - run: | - pip install frappe-bench - bench init --skip-redis-config-generation --skip-assets --frappe-branch ${GITHUB_BASE_REF:-${GITHUB_REF##*/}} --python "$(which python)" ~/frappe-bench - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" - mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" - - name: Install - working-directory: /home/runner/frappe-bench run: | - bench get-app https://github.com/frappe/erpnext --branch "develop" --resolve-deps - bench get-app payments $GITHUB_WORKSPACE - bench setup requirements --dev - bench new-site --db-root-password root --admin-password admin test_site - bench start &> bench_start.log & - bench --site test_site install-app erpnext payments - bench build + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: - CI: 'Yes' + FRAPPE_USER: ${{ github.event.inputs.user }} + FRAPPE_BRANCH: ${{ github.event.inputs.branch }} - name: Run Tests - working-directory: /home/runner/frappe-bench - run: | - bench --site test_site set-config allow_tests true - bench --site test_site run-tests --app payments + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app payments --total-builds 1 --build-number ${{ matrix.container }} env: TYPE: server + CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }} + + - name: Upload coverage data + uses: actions/upload-artifact@v3 + if: github.event_name != 'pull_request' + with: + name: coverage-${{ matrix.container }} + path: /home/runner/frappe-bench/sites/coverage.xml + + coverage: + name: Coverage Wrap Up + needs: tests + runs-on: ubuntu-latest + if: ${{ github.event_name != 'pull_request' }} + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v3 + + - name: Upload coverage data + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + 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/payments/tests/__init__.py b/payments/tests/__init__.py new file mode 100644 index 00000000..e69de29b From c47ec6d8a943c4b6bb0c544fc128eae151d09314 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:41:03 +0200 Subject: [PATCH 013/138] ci: does it work? --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43fce713..bfe78a79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: - container: [1, 2] + container: [1] name: Python Unit Tests @@ -108,7 +108,7 @@ jobs: FRAPPE_BRANCH: ${{ github.event.inputs.branch }} - name: Run Tests - run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app payments --total-builds 1 --build-number ${{ matrix.container }} + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app payments --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} env: TYPE: server CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }} From a4380185a1bd7bd91cacf3740168917679227c19 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:23:18 +0200 Subject: [PATCH 014/138] chore: use ruff, prettier & eslint --- .eslintrc | 130 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 49 +++++++++++---- pyproject.toml | 49 +++++++++++---- 3 files changed, 205 insertions(+), 23 deletions(-) create mode 100644 .eslintrc 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index f552a81d..e9f5b27e 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/pyproject.toml b/pyproject.toml index a102875d..1eb59a56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,14 +20,43 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -[tool.black] -line-length = 99 +[tool.ruff] +line-length = 110 +target-version = "py310" -[tool.isort] -line_length = 99 -multi_line_output = 3 -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true -ensure_newline_before_comments = true -indent = "\t" +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "I", + "UP", + "B", + "RUF", +] +ignore = [ + "B017", # assertRaises(Exception) - should be more specific + "B018", # useless expression, not assigned to anything + "B023", # function doesn't bind loop variable - will have last iteration's value + "B904", # raise inside except without from + "E101", # indentation contains mixed spaces and tabs + "E402", # module level import not at top of file + "E501", # line too long + "E741", # ambiguous variable name + "F401", # "unused" imports + "F403", # can't detect undefined names from * import + "F405", # can't detect undefined names from * import + "F722", # syntax error in forward type annotation + "W191", # indentation contains tabs + "RUF001", # string contains ambiguous unicode character +] +typing-modules = ["frappe.types.DF"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "tab" +docstring-code-format = true + +[project.urls] +Repository = "https://github.com/frappe/payments.git" +"Bug Reports" = "https://github.com/frappe/payments/issues" From 63a4acf6c9fe9657fa6d7ad659465b0d5ef3d73f Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:27:58 +0200 Subject: [PATCH 015/138] chore: apply pre-commit --- commitlint.config.js | 46 ++-- .../braintree_settings/braintree_settings.js | 4 +- .../gocardless_mandate/gocardless_mandate.js | 3 +- .../gocardless_settings.js | 2 +- .../gocardless_settings.py | 11 +- .../doctype/mpesa_settings/mpesa_connector.py | 10 +- .../doctype/mpesa_settings/mpesa_settings.js | 60 +++--- .../doctype/mpesa_settings/mpesa_settings.py | 10 +- .../mpesa_settings/test_mpesa_settings.py | 9 +- .../paypal_settings/paypal_settings.js | 6 +- .../paypal_settings/paypal_settings.py | 6 +- .../doctype/paytm_settings/paytm_settings.js | 14 +- .../doctype/paytm_settings/paytm_settings.py | 5 +- .../razorpay_settings/razorpay_settings.js | 8 +- .../razorpay_settings/razorpay_settings.py | 8 +- .../stripe_settings/stripe_settings.js | 6 +- .../stripe_settings/stripe_settings.py | 4 +- .../payment_gateways/stripe_integration.py | 2 +- .../payment_gateway/payment_gateway.js | 6 +- payments/public/js/razorpay.js | 196 ++++++++++-------- .../templates/pages/braintree_checkout.py | 4 +- .../templates/pages/gocardless_checkout.py | 6 +- .../pages/gocardless_confirmation.py | 5 +- payments/templates/pages/razorpay_checkout.py | 2 +- payments/templates/pages/stripe_checkout.py | 4 +- payments/utils/__init__.py | 2 +- payments/utils/utils.py | 3 +- 27 files changed, 215 insertions(+), 227 deletions(-) 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/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/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/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 ac6b83a9..1bc384a2 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) @@ -64,7 +62,6 @@ def on_payment_request_submission(self, data): return True def check_mandate_validity(self, data): - if frappe.db.exists("GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0)): registered_mandate = frappe.db.get_value( "GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0), "mandate" @@ -124,9 +121,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: @@ -172,7 +167,7 @@ def create_charge_on_gocardless(self): frappe.log_error("Gocardless payment failed") self.integration_request.db_set("error", payment.status, update_modified=False) - except Exception as e: + except Exception: frappe.log_error("GoCardless Payment Error") if self.flags.status_changed_to == "Completed": 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 43d5348b..37ed9a52 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py @@ -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): @@ -318,9 +318,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 aab37d93..c0eb227e 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py @@ -5,17 +5,16 @@ from json import dumps import frappe - from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_customer from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice -from erpnext.stock.doctype.item.test_item import make_item from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.stock.doctype.item.test_item import make_item from payments.payment_gateways.doctype.mpesa_settings.mpesa_settings import ( + create_mode_of_payment, process_balance_info, verify_transaction, ) -from payments.payment_gateways.doctype.mpesa_settings.mpesa_settings import create_mode_of_payment class TestMpesaSettings(unittest.TestCase): @@ -120,9 +119,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/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..eb27db8a 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -108,11 +108,11 @@ class PayPalSettings(Document): ] 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 +171,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) 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 99e9f909..a51ddb32 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -75,7 +75,6 @@ def get_paytm_config(): def get_paytm_params(payment_details, order_id, paytm_config): - # initialize a dictionary paytm_params = dict() @@ -127,9 +126,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 6915c5c5..00a5ec9c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Razorpay Settings', { - refresh: function(frm) { - - } -}); \ No newline at end of file +frappe.ui.form.on("Razorpay Settings", { + refresh: function (frm) {}, +}); diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index ebf16056..a43fedd1 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -306,8 +306,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" @@ -393,7 +393,9 @@ def capture_payment(is_sandbox=False, sanbox_response=None): if resp.get("status") == "authorized": resp = make_post_request( - "https://api.razorpay.com/v1/payments/{}/capture".format(data.get("razorpay_payment_id")), + "https://api.razorpay.com/v1/payments/{}/capture".format( + data.get("razorpay_payment_id") + ), auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")}, ) 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.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index bb1bc677..88b54137 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -255,9 +255,7 @@ def finalize_request(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}" if self.redirect_url: redirect_url = self.redirect_url 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..a85717b9 100644 --- a/payments/public/js/razorpay.js +++ b/payments/public/js/razorpay.js @@ -54,95 +54,109 @@ 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() { + 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/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 12b01369..a0dd968f 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -40,9 +40,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/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index d0e77f6d..dab9a7ab 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -38,7 +38,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.py b/payments/templates/pages/stripe_checkout.py index 7572002e..8d8c1430 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -72,9 +72,7 @@ def get_header_image(doc, gateway_controller): @frappe.whitelist(allow_guest=True) -def make_payment( - stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None -): +def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None): data = json.loads(data) data.update({"stripe_token_id": stripe_token_id}) diff --git a/payments/utils/__init__.py b/payments/utils/__init__.py index fb540bd5..1a494cb7 100644 --- a/payments/utils/__init__.py +++ b/payments/utils/__init__.py @@ -2,7 +2,7 @@ before_install, create_payment_gateway, delete_custom_fields, + erpnext_app_import_guard, get_payment_gateway_controller, make_custom_fields, - erpnext_app_import_guard, ) diff --git a/payments/utils/utils.py b/payments/utils/utils.py index e56a89c2..fed9ac04 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -1,7 +1,8 @@ +from contextlib import contextmanager + import click import frappe from frappe import _ -from contextlib import contextmanager from frappe.custom.doctype.custom_field.custom_field import create_custom_fields From 3c8cf4819083d3f0d2ccd7ddd9b5699c6ed9772e Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 02:29:35 +0200 Subject: [PATCH 016/138] chore: add style commit to blame ignore --- .git-blame-ignore-revs | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..a901f83b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,12 @@ +# 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 From fe53c4ed4fa2c25e1c6407eb5bfa7436863baf09 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 03:02:57 +0200 Subject: [PATCH 017/138] ci: add labeler config --- .github/labeler.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/labeler.yml 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'] From cecf0bec9de2dcd176fc632e8a5348ab2f491cbe Mon Sep 17 00:00:00 2001 From: David Date: Mon, 16 Sep 2024 21:36:34 +0200 Subject: [PATCH 018/138] style: ruff format --- .../braintree_settings/braintree_settings.py | 4 +- .../doctype/gocardless_settings/__init__.py | 2 +- .../gocardless_settings.py | 2 +- .../doctype/mpesa_settings/mpesa_settings.py | 6 +-- .../paypal_settings/paypal_settings.py | 4 +- .../doctype/paytm_settings/paytm_settings.py | 2 +- .../razorpay_settings/razorpay_settings.py | 8 ++-- .../stripe_settings/stripe_settings.py | 45 ++++++++++--------- payments/templates/pages/payment_success.py | 1 - 9 files changed, 38 insertions(+), 36 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 717443e8..5c0fe801 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -14,7 +14,7 @@ class BraintreeSettings(Document): - supported_currencies = [ + supported_currencies = ( "AED", "AMD", "AOA", @@ -150,7 +150,7 @@ class BraintreeSettings(Document): "ZAR", "ZMK", "ZWD", - ] + ) def validate(self): if not self.flags.ignore_mandatory: 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.py b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py index 1bc384a2..4abb7e97 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py @@ -13,7 +13,7 @@ class GoCardlessSettings(Document): - supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"] + supported_currencies = ("EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD") def validate(self): self.initialize_client() diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py index 37ed9a52..97e9aacc 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: @@ -51,7 +51,7 @@ 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 ( @@ -197,7 +197,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") diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index eb27db8a..5bf4a2fb 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -79,7 +79,7 @@ def on_payment_authorized(payment_status): class PayPalSettings(Document): - supported_currencies = [ + supported_currencies = ( "AUD", "BRL", "CAD", @@ -105,7 +105,7 @@ class PayPalSettings(Document): "THB", "TRY", "USD", - ] + ) def __setup__(self): self.use_sandbox = 0 diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index a51ddb32..db879255 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -24,7 +24,7 @@ class PaytmSettings(Document): - supported_currencies = ["INR"] + supported_currencies = ("INR",) def validate(self): create_payment_gateway("Paytm") diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a43fedd1..7394456f 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -79,7 +79,7 @@ def on_payment_authorized(payment_status): class RazorpaySettings(Document): - supported_currencies = ["INR"] + supported_currencies = ("INR",) def init_client(self): if self.api_key: @@ -251,8 +251,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) @@ -341,7 +341,7 @@ def cancel_subscription(self, subscription_id): settings = self.get_settings({}) try: - resp = make_post_request( + make_post_request( f"https://api.razorpay.com/v1/subscriptions/{subscription_id}/cancel", auth=(settings.api_key, settings.api_secret), ) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 88b54137..ce3a1a02 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -1,6 +1,7 @@ # Copyright (c) 2017, Frappe Technologies and contributors # License: MIT. See LICENSE +from types import MappingProxyType from urllib.parse import urlencode import frappe @@ -11,9 +12,27 @@ from payments.utils import create_payment_gateway +currency_wise_minimum_charge_amount = { + "JPY": 50, + "MXN": 10, + "DKK": 2.50, + "HKD": 4.00, + "NOK": 3.00, + "SEK": 3.00, + "USD": 0.50, + "AUD": 0.50, + "BRL": 0.50, + "CAD": 0.50, + "CHF": 0.50, + "EUR": 0.50, + "GBP": 0.30, + "NZD": 0.50, + "SGD": 0.50, +} + class StripeSettings(Document): - supported_currencies = [ + supported_currencies = ( "AED", "ALL", "ANG", @@ -128,25 +147,9 @@ class StripeSettings(Document): "XPF", "YER", "ZAR", - ] - - currency_wise_minimum_charge_amount = { - "JPY": 50, - "MXN": 10, - "DKK": 2.50, - "HKD": 4.00, - "NOK": 3.00, - "SEK": 3.00, - "USD": 0.50, - "AUD": 0.50, - "BRL": 0.50, - "CAD": 0.50, - "CHF": 0.50, - "EUR": 0.50, - "GBP": 0.30, - "NZD": 0.50, - "SGD": 0.50, - } + ) + + currency_wise_minimum_charge_amount = MappingProxyType(currency_wise_minimum_charge_amount) def on_update(self): create_payment_gateway( @@ -225,7 +228,7 @@ def create_charge_on_stripe(self): receipt_email=self.data.payer_email, ) - if charge.captured == True: + if charge.captured is True: self.integration_request.db_set("status", "Completed", update_modified=False) self.flags.status_changed_to = "Completed" diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index 8985850a..e2d1115b 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -7,7 +7,6 @@ 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 = "" From 363f73d77ac088b87703a1fb1d74307a1d27fec5 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 03:04:45 +0200 Subject: [PATCH 019/138] chore: add style commit to blame ignore --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index a901f83b..56274db0 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -10,3 +10,6 @@ # pre-commit formatting ruff, eslint, prettier (automated) 63a4acf6c9fe9657fa6d7ad659465b0d5ef3d73f + +# pre-commit formatting ruff, eslint, prettier (manual fixup) +cecf0bec9de2dcd176fc632e8a5348ab2f491cbe \ No newline at end of file From d2d72785b4aa0982302e5a5431b83a048a6022c4 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 2 Oct 2024 03:07:37 +0200 Subject: [PATCH 020/138] chore: ignore undue lint error --- payments/public/js/razorpay.js | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/public/js/razorpay.js b/payments/public/js/razorpay.js index a85717b9..cf55b991 100644 --- a/payments/public/js/razorpay.js +++ b/payments/public/js/razorpay.js @@ -71,6 +71,7 @@ frappe.require("https://checkout.razorpay.com/v1/checkout.js").then(() => { } 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); From ef713a20f2e002cf756d45dc6e313162ac1e57ce Mon Sep 17 00:00:00 2001 From: David Arnold Date: Thu, 10 Oct 2024 09:15:36 +0200 Subject: [PATCH 021/138] docs: mention ongoing work --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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)) From 22581cca040835d6c451fe1da47e0a03d0b5cfa9 Mon Sep 17 00:00:00 2001 From: Oswin Dsouza <106068399+Oswin-san@users.noreply.github.com> Date: Thu, 10 Oct 2024 12:46:34 +0530 Subject: [PATCH 022/138] fix: type conversion #62 (#109) --- .../doctype/razorpay_settings/razorpay_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 7394456f..d86627d8 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -201,7 +201,7 @@ def create_order(self, **kwargs): # Creating Orders https://razorpay.com/docs/api/orders/ # convert rupees to paisa - kwargs["amount"] *= 100 + kwargs["amount"] = int(kwargs["amount"] * 100) # Create integration log integration_request = create_request_log(kwargs, service_name="Razorpay") From 1c3e0ae94f3610fe539e8446a1122d2ab0fcc7d8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Oct 2024 14:44:39 +0530 Subject: [PATCH 023/138] refactor: clear API details --- .../doctype/razorpay_settings/razorpay_settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index d86627d8..4e77afd8 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -362,6 +362,13 @@ def verify_signature(self, body, signature, key): 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() + def capture_payment(is_sandbox=False, sanbox_response=None): """ From dc92db56037e1918d0127a19e8b4e6a00ffa91fa Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Oct 2024 14:51:02 +0530 Subject: [PATCH 024/138] refactor: UI to clear razorpay api details --- .../doctype/razorpay_settings/razorpay_settings.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js index 00a5ec9c..04057e7a 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js @@ -2,5 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on("Razorpay Settings", { - refresh: function (frm) {}, + refresh: function (frm) { + frm.add_custom_button("Clear", function () { + frm.call({ + doc: frm.doc, + method: "clear", + callback: function (r) { + frm.refresh(); + }, + }); + }); + }, }); From 80272ceb66d6737c10fba22faeee7e44438d0a63 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Oct 2024 10:24:22 +0530 Subject: [PATCH 025/138] chore: translatable label --- .../doctype/razorpay_settings/razorpay_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js index 04057e7a..71fe56e2 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Razorpay Settings", { refresh: function (frm) { - frm.add_custom_button("Clear", function () { + frm.add_custom_button(__("Clear"), function () { frm.call({ doc: frm.doc, method: "clear", From a82733021f98d277f9eb9790e0bde296ffcfffdc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Nov 2024 17:58:43 +0530 Subject: [PATCH 026/138] refactor: throw for cancelled integration requests --- payments/templates/pages/paytm_checkout.py | 3 +++ payments/templates/pages/razorpay_checkout.py | 5 +++++ payments/utils/utils.py | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/payments/templates/pages/paytm_checkout.py b/payments/templates/pages/paytm_checkout.py index 216b0b22..f53648fc 100644 --- a/payments/templates/pages/paytm_checkout.py +++ b/payments/templates/pages/paytm_checkout.py @@ -9,6 +9,7 @@ get_paytm_config, get_paytm_params, ) +from payments.utils.utils import validate_integration_request def get_context(context): @@ -16,6 +17,8 @@ def get_context(context): paytm_config = get_paytm_config() try: + validate_integration_request(frappe.form_dict["order_id"]) + doc = frappe.get_doc("Integration Request", frappe.form_dict["order_id"]) context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index dab9a7ab..521a6d22 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -6,6 +6,8 @@ from frappe import _ from frappe.utils import cint, flt +from payments.utils.utils import validate_integration_request + no_cache = 1 expected_keys = ( @@ -26,7 +28,10 @@ def get_context(context): context.api_key = get_api_key() try: + validate_integration_request(frappe.form_dict["token"]) + doc = frappe.get_doc("Integration Request", frappe.form_dict["token"]) + payment_details = json.loads(doc.data) for key in expected_keys: diff --git a/payments/utils/utils.py b/payments/utils/utils.py index fed9ac04..5284b5e5 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -6,6 +6,11 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +def validate_integration_request(docname: str | None): + if frappe.db.get_value("Integration Request", docname, "status") == "Cancelled": + frappe.throw(_("Expired Token")) + + def get_payment_gateway_controller(payment_gateway): """Return payment gateway controller""" gateway = frappe.get_doc("Payment Gateway", payment_gateway) From 242293752fa71f09217b0c92e5f8dd3ce7dfd4d7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Nov 2024 13:46:14 +0530 Subject: [PATCH 027/138] fix: exception on braintree success page --- .../doctype/braintree_settings/braintree_settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 5c0fe801..6f694a90 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -262,7 +262,9 @@ def create_charge_on_braintree(self): if custom_redirect_to: redirect_to = custom_redirect_to - redirect_url = "payment-success" + redirect_url = ( + f"payment-success?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}" + ) else: status = "Error" redirect_url = "payment-failed" From cd32dddf07e1f8d6e35419becac63d46e1a5702f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Nov 2024 21:03:04 +0530 Subject: [PATCH 028/138] fix: get parameters on both success and failed page --- .../doctype/braintree_settings/braintree_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 6f694a90..5600acbd 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -267,10 +267,12 @@ def create_charge_on_braintree(self): ) else: status = "Error" - redirect_url = "payment-failed" + redirect_url = ( + f"payment-failed?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}" + ) if redirect_to: - redirect_url += "?" + urlencode({"redirect_to": redirect_to}) + redirect_url += "&" + urlencode({"redirect_to": redirect_to}) if redirect_message: redirect_url += "&" + urlencode({"redirect_message": redirect_message}) From e50bb16c6fb42c2f81bb4ed37fde9af87aa44929 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 22 Nov 2024 14:44:46 +0530 Subject: [PATCH 029/138] refactor: better handling of get parameters --- .../braintree_settings/braintree_settings.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 5600acbd..c5c971bd 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -262,20 +262,18 @@ def create_charge_on_braintree(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 = "payment-success" else: status = "Error" - redirect_url = ( - f"payment-failed?doctype={self.data.reference_doctype}&docname={self.data.reference_docname}" - ) + redirect_url = "payment-failed" + get_parameters = [("doctype", self.data.reference_doctype), ("docname", self.data.reference_docname)] if redirect_to: - redirect_url += "&" + urlencode({"redirect_to": redirect_to}) + get_parameters.append(("redirect_to", redirect_to)) if redirect_message: - redirect_url += "&" + urlencode({"redirect_message": redirect_message}) + get_parameters.append(("redirect_message", redirect_message)) + redirect_url += "?" + urlencode(get_parameters) return {"redirect_to": redirect_url, "status": status} From 941af4ddd40d96da75048ccef53e0319684f0290 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 9 Dec 2024 12:23:00 +0530 Subject: [PATCH 030/138] refactor: drop pytz Signed-off-by: Akhil Narang --- .../doctype/paypal_settings/paypal_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 5bf4a2fb..1410f397 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -64,9 +64,9 @@ def on_payment_authorized(payment_status): import json from urllib.parse import urlencode +from zoneinfo import ZoneInfo import frappe -import pytz from frappe import _ from frappe.integrations.utils import create_request_log, make_post_request from frappe.model.document import Document @@ -379,7 +379,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()}) From 70d8ac74937e2a7a5709b9d62204fda3305c1d26 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 6 May 2025 17:08:17 +0530 Subject: [PATCH 031/138] chore: fix broken CI bump all github actions and bring them to parity with erpnext --- .github/helper/install.sh | 2 +- .github/workflows/ci.yml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index e1efaff1..3e386d6b 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -6,7 +6,7 @@ cd ~ || exit sudo apt update sudo apt remove mysql-server mysql-client -sudo apt install libcups2-dev redis-server mariadb-client-10.6 +sudo apt install libcups2-dev redis-server mariadb-client pip install frappe-bench diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfe78a79..4d94a1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,10 @@ jobs: steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.10' @@ -59,7 +59,7 @@ jobs: fi - name: Setup Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: 18 check-latest: true @@ -68,7 +68,7 @@ jobs: run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} @@ -77,7 +77,7 @@ jobs: ${{ runner.os }}- - name: Cache node modules - uses: actions/cache@v2 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -92,7 +92,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v2 + - uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -114,7 +114,7 @@ jobs: CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }} - name: Upload coverage data - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: github.event_name != 'pull_request' with: name: coverage-${{ matrix.container }} @@ -130,7 +130,7 @@ jobs: uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Upload coverage data uses: codecov/codecov-action@v4 From 6fc587be4add20bc097466509fd63d25c0d0b1c8 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Fri, 9 May 2025 11:10:36 +0530 Subject: [PATCH 032/138] test(mpesa-settings): add opening entry for new POS profile --- .../doctype/mpesa_settings/test_mpesa_settings.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 c0eb227e..ce5dc88a 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py @@ -7,6 +7,7 @@ import frappe from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_customer from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice +from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.stock.doctype.item.test_item import make_item @@ -26,7 +27,7 @@ def setUp(self): self.customer = create_customer("_Test Customer", "USD") self.item = make_item(properties={"is_stock_item": 1}).name - self.pos_profile = make_pos_profile( + pos_profile = make_pos_profile( company="Wind Power LLC", cost_center="Main - WP", currency="USD", @@ -37,7 +38,9 @@ def setUp(self): warehouse="Stores - WP", write_off_account="Write Off - WP", write_off_cost_center="Main - WP", - ).name + ) + self.pos_profile = pos_profile.name + create_opening_entry(pos_profile, frappe.session.user) def tearDown(self): frappe.db.sql("delete from `tabMpesa Settings`") From 5b647546d5fefdaa94598d272df7db6209276ecb Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 6 May 2025 13:44:32 +0530 Subject: [PATCH 033/138] chore: extend supported currencies list for Razorpay. --- .../doctype/razorpay_settings/razorpay_settings.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 4e77afd8..bd455136 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -79,7 +79,19 @@ def on_payment_authorized(payment_status): class RazorpaySettings(Document): - supported_currencies = ("INR",) + supported_currencies = ("AED","ALL","AMD","ARS","AUD","AWG","AZN","BAM","BBD","BDT", + "BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP", + "BZD","CAD","CHF","CLP","CNY","COP","CRC","CUP","CVE","CZK", + "DJF","DKK","DOP","DZD","EGP","ETB","EUR","FJD","GBP","GHS", + "GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF", + "IDR","ILS","INR","IQD","ISK","JMD","JOD","JPY","KES","KGS", + "KHR","KMF","KRW","KWD","KYD","KZT","LAK","LKR","LRD","LSL", + "MAD","MDL","MGA","MKD","MMK","MNT","MOP","MUR","MVR","MWK", + "MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR", + "PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB", + "RWF","SAR","SCR","SEK","SGD","SLL","SOS","SSP","SVC","SZL", + "THB","TND","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU", + "UZS","VND","VUV","XAF","XCD","XOF","XPF","YER","ZAR","ZMW") def init_client(self): if self.api_key: From 58c92ad239c5b8f20afc1b55444c102697710af5 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 6 May 2025 16:20:20 +0530 Subject: [PATCH 034/138] chore: Update formatings --- .pre-commit-config.yaml | 2 +- .../razorpay_settings/razorpay_settings.py | 145 ++++++++++++++++-- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9f5b27e..2fb83911 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ exclude: 'node_modules|.git' -default_stages: [commit] +default_stages: [pre-commit] fail_fast: false diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index bd455136..20815477 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -79,19 +79,138 @@ def on_payment_authorized(payment_status): class RazorpaySettings(Document): - supported_currencies = ("AED","ALL","AMD","ARS","AUD","AWG","AZN","BAM","BBD","BDT", - "BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTN","BWP", - "BZD","CAD","CHF","CLP","CNY","COP","CRC","CUP","CVE","CZK", - "DJF","DKK","DOP","DZD","EGP","ETB","EUR","FJD","GBP","GHS", - "GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF", - "IDR","ILS","INR","IQD","ISK","JMD","JOD","JPY","KES","KGS", - "KHR","KMF","KRW","KWD","KYD","KZT","LAK","LKR","LRD","LSL", - "MAD","MDL","MGA","MKD","MMK","MNT","MOP","MUR","MVR","MWK", - "MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR", - "PEN","PGK","PHP","PKR","PLN","PYG","QAR","RON","RSD","RUB", - "RWF","SAR","SCR","SEK","SGD","SLL","SOS","SSP","SVC","SZL", - "THB","TND","TRY","TTD","TWD","TZS","UAH","UGX","USD","UYU", - "UZS","VND","VUV","XAF","XCD","XOF","XPF","YER","ZAR","ZMW") + supported_currencies = ( + "AED", + "ALL", + "AMD", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BZD", + "CAD", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ETB", + "EUR", + "FJD", + "GBP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LKR", + "LRD", + "LSL", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SCR", + "SEK", + "SGD", + "SLL", + "SOS", + "SSP", + "SVC", + "SZL", + "THB", + "TND", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VND", + "VUV", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", + ) def init_client(self): if self.api_key: From bc6cffe4aaacc58943ce76bb3c9bd618586ca4f2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Jun 2025 08:13:41 +0530 Subject: [PATCH 035/138] ci: trigger --- .../doctype/mpesa_settings/test_mpesa_settings.py | 3 +++ 1 file changed, 3 insertions(+) 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 ce5dc88a..cdfd0665 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py @@ -83,6 +83,7 @@ def test_processing_of_account_balance(self): integration_request.delete() def test_processing_of_callback_payload(self): + frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" ) @@ -144,6 +145,7 @@ def test_processing_of_callback_payload(self): pos_invoice.delete() def test_processing_of_multiple_callback_payload(self): + frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" ) @@ -214,6 +216,7 @@ def test_processing_of_multiple_callback_payload(self): pos_invoice.delete() def test_processing_of_only_one_succes_callback_payload(self): + frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" ) From e3a9d4c1e6c15a2fd76dda5bf678600203049a31 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 19 Jun 2025 09:11:28 +0530 Subject: [PATCH 036/138] chore: include setuptools --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1eb59a56..470810c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "braintree~=4.20.0", "pycryptodome>=3.18.0,<4.0.0", "gocardless-pro~=1.22.0", + "setuptools==80.9.0", ] [build-system] From 136787825d9a9babffe44f3087df693a86e0e821 Mon Sep 17 00:00:00 2001 From: ravibharathi656 Date: Thu, 19 Jun 2025 13:18:14 +0530 Subject: [PATCH 037/138] fix: make Stripe checkout page responsive --- payments/templates/pages/stripe_checkout.css | 16 ++++++++++++++++ payments/templates/pages/stripe_checkout.html | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/payments/templates/pages/stripe_checkout.css b/payments/templates/pages/stripe_checkout.css index 5aadc117..8842f1e7 100644 --- a/payments/templates/pages/stripe_checkout.css +++ b/payments/templates/pages/stripe_checkout.css @@ -99,3 +99,19 @@ .field:-ms-input-placeholder { color: #CFD7E0; } + +#payment-section { + min-height: 50vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 10rem; +} + +@media (max-width: 48rem) { + #payment-section { + padding: 0rem; + min-height: 70vh; + }, +} \ No newline at end of file diff --git a/payments/templates/pages/stripe_checkout.html b/payments/templates/pages/stripe_checkout.html index 9ce76a91..b8b56065 100644 --- a/payments/templates/pages/stripe_checkout.html +++ b/payments/templates/pages/stripe_checkout.html @@ -12,7 +12,7 @@ {%- block page_content -%} -
+
{% if image %} From 6db0e22d5d694e43b13565914b88153056a42f35 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 31 Jul 2025 21:25:51 +0530 Subject: [PATCH 038/138] fix(Razorpay): create order if not provided for payment link Closes #157 --- .../doctype/razorpay_settings/razorpay_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 20815477..6cbed70a 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -325,6 +325,10 @@ def prepare_subscription_details(self, settings, **kwargs): 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}") From 304713fba926053f9bfa7bb9321e29c9993129ee Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Aug 2025 13:51:04 +0530 Subject: [PATCH 039/138] fix: flaky mpesa tests --- .../doctype/mpesa_settings/mpesa_settings.py | 2 +- .../mpesa_settings/test_mpesa_settings.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py index 97e9aacc..ec0da3eb 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py @@ -44,8 +44,8 @@ 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) 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 cdfd0665..d712f94b 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/test_mpesa_settings.py @@ -39,10 +39,12 @@ def setUp(self): write_off_account="Write Off - WP", write_off_cost_center="Main - WP", ) - self.pos_profile = pos_profile.name - create_opening_entry(pos_profile, frappe.session.user) + self.pos_profile = pos_profile def tearDown(self): + frappe.db.rollback() + for x in frappe.db.get_all("POS Opening Entry"): + frappe.get_doc("POS Opening Entry", x.name).cancel().delete() frappe.db.sql("delete from `tabMpesa Settings`") frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'") @@ -83,6 +85,7 @@ def test_processing_of_account_balance(self): integration_request.delete() def test_processing_of_callback_payload(self): + create_opening_entry(self.pos_profile, frappe.session.user) frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" @@ -97,7 +100,7 @@ def test_processing_of_callback_payload(self): cost_center="Main - WP", company="Wind Power LLC", income_account="Sales - WP", - pos_profile=self.pos_profile, + pos_profile=self.pos_profile.name, account_for_change_amount="Cash - WP", expense_account="Cost of Goods Sold - WP", do_not_submit=1, @@ -145,6 +148,7 @@ def test_processing_of_callback_payload(self): pos_invoice.delete() def test_processing_of_multiple_callback_payload(self): + create_opening_entry(self.pos_profile, frappe.session.user) frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" @@ -161,7 +165,7 @@ def test_processing_of_multiple_callback_payload(self): cost_center="Main - WP", company="Wind Power LLC", income_account="Sales - WP", - pos_profile=self.pos_profile, + pos_profile=self.pos_profile.name, account_for_change_amount="Cash - WP", expense_account="Cost of Goods Sold - WP", do_not_submit=1, @@ -215,7 +219,8 @@ def test_processing_of_multiple_callback_payload(self): pr.delete() pos_invoice.delete() - def test_processing_of_only_one_succes_callback_payload(self): + def test_processing_of_only_one_success_callback_payload(self): + create_opening_entry(self.pos_profile, frappe.session.user) frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice") mpesa_account = frappe.db.get_value( "Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account" @@ -232,7 +237,7 @@ def test_processing_of_only_one_succes_callback_payload(self): cost_center="Main - WP", company="Wind Power LLC", income_account="Sales - WP", - pos_profile=self.pos_profile, + pos_profile=self.pos_profile.name, account_for_change_amount="Cash - WP", expense_account="Cost of Goods Sold - WP", do_not_submit=1, From 548978757d6e91c7fd3d4938f0ff352a5b66ce1e Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:44:03 +0200 Subject: [PATCH 040/138] fix: image condition (#162) --- payments/overrides/payment_webform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/overrides/payment_webform.py b/payments/overrides/payment_webform.py index 3b2d7afa..1b62e413 100644 --- a/payments/overrides/payment_webform.py +++ b/payments/overrides/payment_webform.py @@ -87,7 +87,7 @@ def accept(web_form, data, docname=None, for_payment=False): value = data.get(fieldname, None) if df and df.fieldtype in ("Attach", "Attach Image"): - if value and "data:" and "base64" in value: + if value and "data:" in value and "base64" in value: files.append((fieldname, value)) if not doc.name: doc.set(fieldname, "") From 957c5c5bc6e1a2f51eddd714154aa31f61cf677f Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:53:24 +0200 Subject: [PATCH 041/138] chore: bump actions/cache to v4 (#131) From 2c36c98ac0d708bbf7522047e493bdef683c0701 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:11:14 +0200 Subject: [PATCH 042/138] refactor: delete custom fields (#161) --- payments/utils/utils.py | 45 ++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 5284b5e5..64e79a99 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -162,26 +162,33 @@ def make_custom_fields(): def delete_custom_fields(): - if frappe.get_meta("Web Form").has_field("payments_tab"): - click.secho("* Uninstalling Payment Custom Fields from Web Form") - - fieldnames = ( - "payments_tab", - "accept_payment", - "payment_gateway", - "payment_button_label", - "payment_button_help", - "payments_cb", - "amount_field", - "amount_based_on_field", - "amount", - "currency", - ) - - for fieldname in fieldnames: - frappe.db.delete("Custom Field", {"name": "Web Form-" + fieldname}) + if not frappe.get_meta("Web Form").has_field("payments_tab"): + return + + click.secho("* Uninstalling Payment Custom Fields from Web Form") + frappe.db.delete( + "Custom Field", + { + "dt": "Web Form", + "fieldname": ( + "in", + ( + "payments_tab", + "accept_payment", + "payment_gateway", + "payment_button_label", + "payment_button_help", + "payments_cb", + "amount_field", + "amount_based_on_field", + "amount", + "currency", + ), + ), + }, + ) - frappe.clear_cache(doctype="Web Form") + frappe.clear_cache(doctype="Web Form") def before_install(): From c567e0620ceb223fc9f62e67f3fcd8c489a2db82 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:35:39 +0200 Subject: [PATCH 043/138] ci: bump github actions to supported versions (#169) (#171) (cherry picked from commit bbcde032a99b6ef5e1f383464f0bc52053a0dec8) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .github/workflows/linter.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 159d3ca3..d756d5f3 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -20,10 +20,10 @@ jobs: if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 200 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 16 check-latest: true @@ -39,8 +39,8 @@ jobs: if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - uses: pre-commit/action@v3.0.0 From ccca17c89e70a33e50e1be8cf8fefdaf9c5b86c1 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:41:13 +0200 Subject: [PATCH 044/138] refactor: inline immediately returned variable (#163) --- .../doctype/braintree_settings/braintree_settings.py | 5 +---- .../doctype/gocardless_settings/gocardless_settings.py | 8 ++------ .../doctype/mpesa_settings/mpesa_settings.py | 8 ++------ .../doctype/paytm_settings/paytm_settings.py | 5 +---- .../doctype/stripe_settings/stripe_settings.py | 3 +-- payments/templates/pages/stripe_checkout.py | 4 +--- 6 files changed, 8 insertions(+), 25 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index c5c971bd..936139be 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -279,10 +279,7 @@ def create_charge_on_braintree(self): def get_gateway_controller(doc): payment_request = frappe.get_doc("Payment Request", doc) - gateway_controller = frappe.db.get_value( - "Payment Gateway", payment_request.payment_gateway, "gateway_controller" - ) - return 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_settings/gocardless_settings.py b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py index 4abb7e97..b101503d 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py @@ -199,14 +199,10 @@ def create_charge_on_gocardless(self): def get_gateway_controller(doc): payment_request = frappe.get_doc("Payment Request", doc) - gateway_controller = frappe.db.get_value( - "Payment Gateway", payment_request.payment_gateway, "gateway_controller" - ) - return gateway_controller + return frappe.db.get_value("Payment Gateway", payment_request.payment_gateway, "gateway_controller") def gocardless_initialization(doc): gateway_controller = get_gateway_controller(doc) settings = frappe.get_doc("GoCardless Settings", gateway_controller) - client = settings.initialize_client() - return client + return settings.initialize_client() diff --git a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py index ec0da3eb..b0f8eb6e 100644 --- a/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py +++ b/payments/payment_gateways/doctype/mpesa_settings/mpesa_settings.py @@ -143,7 +143,7 @@ def generate_stk_push(**kwargs): mobile_number = sanitize_mobile_number(args.sender) - response = connector.stk_push( + return connector.stk_push( business_shortcode=business_shortcode, amount=args.request_amount, passcode=mpesa_settings.get_password("online_passkey"), @@ -152,9 +152,6 @@ def generate_stk_push(**kwargs): phone_number=mobile_number, description="POS Payment", ) - - return response - except Exception: frappe.log_error("Mpesa Express Transaction Error") frappe.throw( @@ -268,7 +265,7 @@ def get_account_balance(request_payload): + "/api/method/payments.payment_gateways.doctype.mpesa_settings.mpesa_settings.process_balance_info" ) - response = connector.get_balance( + return connector.get_balance( mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, @@ -277,7 +274,6 @@ def get_account_balance(request_payload): callback_url, callback_url, ) - return response except Exception: frappe.log_error("Mpesa: Failed to get account balance") frappe.throw(_("Please check your configuration and try again"), title=_("Error")) diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index db879255..41a49c63 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -180,7 +180,4 @@ def finalize_request(order_id, transaction_response): def get_gateway_controller(doctype, docname): reference_doc = frappe.get_doc(doctype, docname) - gateway_controller = frappe.db.get_value( - "Payment Gateway", reference_doc.payment_gateway, "gateway_controller" - ) - return gateway_controller + return frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index ce3a1a02..29dff12b 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -281,5 +281,4 @@ def get_gateway_controller(doctype, docname, payment_gateway=None): if not payment_gateway: reference_doc = frappe.get_doc(doctype, docname) payment_gateway = reference_doc.payment_gateway - gateway_controller = frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") - return gateway_controller + return frappe.db.get_value("Payment Gateway", payment_gateway, "gateway_controller") diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 8d8c1430..7f4a30e0 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -66,9 +66,7 @@ def get_api_key(doc, gateway_controller): def get_header_image(doc, gateway_controller): - header_image = frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") - - return header_image + return frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") @frappe.whitelist(allow_guest=True) From 7665c57350e04ffb41f7b8c4926e5692ab3b4897 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:54:40 +0200 Subject: [PATCH 045/138] fix(Web Form): extend instead of overriding (#164) --- payments/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 # --------------- From 49b4f63f8748daea6d1b0f262a617d824c456d52 Mon Sep 17 00:00:00 2001 From: Kevin Shenk Date: Mon, 15 Sep 2025 14:04:25 -0400 Subject: [PATCH 046/138] feat: incorporate GoCardless payment request transaction_date (#80) * feat: incorporate payment request transaction_date * feat: incorporate mandate's next_possible_charge_date --- .../gocardless_settings/gocardless_settings.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py index b101503d..3a9cd0fb 100644 --- a/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py +++ b/payments/payment_gateways/doctype/gocardless_settings/gocardless_settings.py @@ -50,12 +50,13 @@ def on_payment_request_submission(self, data): "payer_name": customer_data.customer_name, "order_id": data.name, "currency": data.currency, + "charge_date": data.transaction_date or frappe.utils.nowdate(), } - valid_mandate = self.check_mandate_validity(data) + valid_mandate, next_possible_charge_date = self.check_mandate_validity(data) if valid_mandate is not None: data.update(valid_mandate) - + data["charge_date"] = max(data.get("charge_date"), next_possible_charge_date) self.create_payment_request(data) return False else: @@ -75,11 +76,11 @@ def check_mandate_validity(self, data): or mandate.status == "submitted" or mandate.status == "active" ): - return {"mandate": registered_mandate} + return {"mandate": registered_mandate}, mandate.next_possible_charge_date else: - return None + return None, None else: - return None + return None, None def get_environment(self): if self.use_sandbox: @@ -128,6 +129,7 @@ def create_charge_on_gocardless(self): payment = self.client.payments.create( params={ "amount": cint(reference_doc.grand_total * 100), + "charge_date": self.data.get("charge_date"), "currency": reference_doc.currency, "links": {"mandate": self.data.get("mandate")}, "metadata": { From fcca37e1cb23dc6bc911531799b5624d7920fd3e Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 22 Oct 2025 11:18:27 +0000 Subject: [PATCH 047/138] feat: implement Paymob payment gateway integration supporting Payments v16 --- .../doctype/paymob_settings/__init__.py | 0 .../paymob_settings/paymob_settings.js | 27 ++ .../paymob_settings/paymob_settings.json | 132 +++++++++ .../paymob_settings/paymob_settings.py | 266 ++++++++++++++++++ .../paymob_settings/test_paymob_settings.py | 9 + .../payment_gateways/paymob/accept_api.py | 85 ++++++ .../payment_gateways/paymob/connection.py | 138 +++++++++ payments/payment_gateways/paymob/constants.py | 4 + .../payment_gateways/paymob/hmac_validator.py | 166 +++++++++++ .../payment_gateways/paymob/paymob_urls.py | 38 +++ .../payment_gateways/paymob/response_codes.py | 16 ++ .../paymob/response_feedback_dataclass.py | 11 + 12 files changed, 892 insertions(+) create mode 100644 payments/payment_gateways/doctype/paymob_settings/__init__.py create mode 100644 payments/payment_gateways/doctype/paymob_settings/paymob_settings.js create mode 100644 payments/payment_gateways/doctype/paymob_settings/paymob_settings.json create mode 100644 payments/payment_gateways/doctype/paymob_settings/paymob_settings.py create mode 100644 payments/payment_gateways/doctype/paymob_settings/test_paymob_settings.py create mode 100644 payments/payment_gateways/paymob/accept_api.py create mode 100644 payments/payment_gateways/paymob/connection.py create mode 100644 payments/payment_gateways/paymob/constants.py create mode 100644 payments/payment_gateways/paymob/hmac_validator.py create mode 100644 payments/payment_gateways/paymob/paymob_urls.py create mode 100644 payments/payment_gateways/paymob/response_codes.py create mode 100644 payments/payment_gateways/paymob/response_feedback_dataclass.py 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..f9850429 --- /dev/null +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js @@ -0,0 +1,27 @@ +// 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") + }).addClass("btn btn-primary") + }, + get_access_token: function (frm) { + try { + frm.call({ + method: "get_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); + } + } +}); \ No newline at end of file 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..f6703513 --- /dev/null +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json @@ -0,0 +1,132 @@ +{ + "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", + "token", + "secret_key", + "payment_config", + "iframe", + "column_break_bpek", + "payment_integration" + ], + "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": "column_break_bpek", + "fieldtype": "Column Break" + }, + { + "fieldname": "payment_integration", + "fieldtype": "Int", + "label": "Payment Integration", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2025-10-22 14:04:07.307698", + "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..5ed4f57a --- /dev/null +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py @@ -0,0 +1,266 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import _ +from payments.payment_gateways.paymob.accept_api import AcceptAPI +from payments.payment_gateways.paymob.connection import AcceptConnection +from payments.payment_gateways.paymob.paymob_urls import PaymobUrls +from payments.payment_gateways.paymob.hmac_validator import HMACValidator + +from frappe.integrations.utils import ( + create_request_log, + +) +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 get_access_token(self): + accept = AcceptAPI() + token = accept.retrieve_auth_token() + return token + + + def get_payment_url(self, **kwargs): + try: + connection = AcceptConnection() + 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 (Paymob requires this) + 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_password("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") + code, feedback = connection.post(url=url, json=payment_key_payload) + + if code != SUCCESS or "token" not in feedback.data: + frappe.throw(_("Failed to retrieve payment token from Paymob")) + + payment_token = feedback.data["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): + + # Create integration log + integration_request = create_request_log(kwargs, service_name="Paymob") + connection=AcceptConnection() + paymob_urls = PaymobUrls() + + # Get your API token + token = self.get_password("token") + + # Required fields by Paymob + 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", []) # Required field even if empty + + # Construct payload + 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") + code, feedback = connection.post(url=url, json=payload) + if code != SUCCESS: + frappe.throw(_("Failed to create order in Paymob")) + + order=feedback.data + paymob_order_id=order.get('id') + + import json + integration_request_dict=json.loads(integration_request.data) + integration_request_dict["paymob_order_id"]= str(paymob_order_id) + order["integration_request"] = integration_request.name + integration_request.data = json.dumps(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: + # Extract the HMAC from request parameters (query string or form data) + incoming_hmac = frappe.request.args.get("hmac") or frappe.request.form.get("hmac") + + if not incoming_hmac: + return "Missing HMAC" + + # Get the callback JSON data from request body + incoming_data_json = frappe.request.get_json() + print("JSON data:", incoming_data_json) + + if not incoming_data_json: + return "Invalid or missing data" + + # Validate the HMAC + validator = HMACValidator( + incoming_hmac=incoming_hmac, + callback_dict=incoming_data_json + ) + + if not validator.is_valid: + return "Invalid HMAC" + + # HMAC is valid, extract transaction data + 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") + paymob_payment_id = obj_data.get("id") + paymob_order_id = obj_data.get("order", {}).get("id") + + # Validate all success conditions + is_payment_successful = ( + success is True and + pending is False and + str(payment_status).upper() == "PAID" and + str(txn_response_code).upper() == "APPROVED" + ) + + # Find the Integration Request based on order_id + if not paymob_order_id: + return "Missing order ID" + + 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 + ) + + import json + integration_request_doc = frappe.get_doc("Integration Request", integration_requests[0].name) + integration_request_dict = json.loads(integration_request_doc.data) + integration_request_dict["paymob_payment_id"] = str(paymob_payment_id) + integration_request_dict["order_id"] = str(paymob_order_id) + + # Update the data field with additional information + integration_request_doc.data = json.dumps(integration_request_dict) + + # Check if transaction succeeded / paid + if is_payment_successful: + # Update integration request status + integration_request_doc.save(ignore_permissions=True) + frappe.db.commit() + + # Call on_payment_authorized on the reference document + 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(), "Paymob on_payment_authorized Error") + + if custom_redirect_to: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = custom_redirect_to + return + + return "Payment verified successfully" + else: + # Payment failed or was cancelled + integration_request_doc.status = "Failed" + integration_request_doc.error = f"Payment Status: {payment_status}, Response Code: {txn_response_code}" + integration_request_doc.save(ignore_permissions=True) + frappe.db.commit() + + return "Payment failed or was cancelled" + + except Exception as e: + frappe.log_error(frappe.get_traceback(), "Paymob Callback Error") + return "Server error" + + + +@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 as e: + return "Failed to Update Paymob Credentials" \ No newline at end of file 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/paymob/accept_api.py b/payments/payment_gateways/paymob/accept_api.py new file mode 100644 index 00000000..4aa21085 --- /dev/null +++ b/payments/payment_gateways/paymob/accept_api.py @@ -0,0 +1,85 @@ +from typing import Any, Dict, List, Tuple, Union +import frappe +import requests +import json + +from .paymob_urls import PaymobUrls +from .connection import AcceptConnection + +from .response_feedback_dataclass import ResponseFeedBack +from .response_codes import SUCCESS + + +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, Union[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, Union[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 \ No newline at end of file diff --git a/payments/payment_gateways/paymob/connection.py b/payments/payment_gateways/paymob/connection.py new file mode 100644 index 00000000..912fe8e3 --- /dev/null +++ b/payments/payment_gateways/paymob/connection.py @@ -0,0 +1,138 @@ +from typing import Any, Dict, Tuple, Union + +import requests +from requests import HTTPError, JSONDecodeError, RequestException + +from .response_feedback_dataclass import ResponseFeedBack +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 frappe.utils.password import get_decrypted_password +from .paymob_urls import PaymobUrls + + + +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) -> Union[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(timeout=90, *args, **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(call=self.session.get, *args, **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(call=self.session.post, *args, **kwargs) \ No newline at end of file diff --git a/payments/payment_gateways/paymob/constants.py b/payments/payment_gateways/paymob/constants.py new file mode 100644 index 00000000..78761378 --- /dev/null +++ b/payments/payment_gateways/paymob/constants.py @@ -0,0 +1,4 @@ +class AcceptCallbackTypes: + TRANSACTION = "TRANSACTION" + CARD_TOKEN = "TOKEN" + DELIVERY_STATUS = "DELIVERY_STATUS" \ No newline at end of file diff --git a/payments/payment_gateways/paymob/hmac_validator.py b/payments/payment_gateways/paymob/hmac_validator.py new file mode 100644 index 00000000..c67a4fea --- /dev/null +++ b/payments/payment_gateways/paymob/hmac_validator.py @@ -0,0 +1,166 @@ +import hashlib +import hmac +from typing import Any, Dict +from .constants import AcceptCallbackTypes +import frappe + + +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 \ No newline at end of file diff --git a/payments/payment_gateways/paymob/paymob_urls.py b/payments/payment_gateways/paymob/paymob_urls.py new file mode 100644 index 00000000..2198e23d --- /dev/null +++ b/payments/payment_gateways/paymob/paymob_urls.py @@ -0,0 +1,38 @@ +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") \ No newline at end of file diff --git a/payments/payment_gateways/paymob/response_codes.py b/payments/payment_gateways/paymob/response_codes.py new file mode 100644 index 00000000..eb1a8868 --- /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." \ No newline at end of file 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..8643ebae --- /dev/null +++ b/payments/payment_gateways/paymob/response_feedback_dataclass.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass +class ResponseFeedBack: + + message: Optional[str] + data: Any = None + status_code: int = None + exception_error: str = None \ No newline at end of file From 19ca29006931e740cdf41c684b6be8d9f1ed3226 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 23 Oct 2025 10:49:41 +0000 Subject: [PATCH 048/138] refactor: improve Paymob callback flow readability and structure --- .../paymob_settings/paymob_settings.py | 135 ++++++++++-------- 1 file changed, 73 insertions(+), 62 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py index 5ed4f57a..e32eada5 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py @@ -8,7 +8,7 @@ from payments.payment_gateways.paymob.connection import AcceptConnection from payments.payment_gateways.paymob.paymob_urls import PaymobUrls from payments.payment_gateways.paymob.hmac_validator import HMACValidator - +from urllib.parse import urlencode from frappe.integrations.utils import ( create_request_log, @@ -125,11 +125,12 @@ def create_order(self, **kwargs): order=feedback.data paymob_order_id=order.get('id') - import json - integration_request_dict=json.loads(integration_request.data) - integration_request_dict["paymob_order_id"]= str(paymob_order_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 = json.dumps(integration_request_dict) + + integration_request.data = frappe.as_json(integration_request_dict) integration_request.save(ignore_permissions=True) frappe.db.commit() @@ -142,18 +143,12 @@ def create_order(self, **kwargs): @frappe.whitelist(allow_guest=True) def callback(): try: - # Extract the HMAC from request parameters (query string or form data) incoming_hmac = frappe.request.args.get("hmac") or frappe.request.form.get("hmac") if not incoming_hmac: - return "Missing HMAC" + frappe.throw(_("Missing HMAC")) - # Get the callback JSON data from request body incoming_data_json = frappe.request.get_json() - print("JSON data:", incoming_data_json) - - if not incoming_data_json: - return "Invalid or missing data" # Validate the HMAC validator = HMACValidator( @@ -162,18 +157,18 @@ def callback(): ) if not validator.is_valid: - return "Invalid HMAC" + frappe.throw(_("Invalid HMAC")) - # HMAC is valid, extract transaction data 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") - # Validate all success conditions is_payment_successful = ( success is True and pending is False and @@ -181,11 +176,42 @@ def callback(): str(txn_response_code).upper() == "APPROVED" ) - # Find the Integration Request based on order_id if not paymob_order_id: - return "Missing 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 as e: + 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_requests = frappe.get_all( "Integration Request", filters={ "integration_request_service": "Paymob", @@ -195,55 +221,40 @@ def callback(): order_by="creation desc", limit=1 ) + if not integration_requests: + frappe.throw(_("No Integration Request found for this order")) - import json - integration_request_doc = frappe.get_doc("Integration Request", integration_requests[0].name) - integration_request_dict = json.loads(integration_request_doc.data) - integration_request_dict["paymob_payment_id"] = str(paymob_payment_id) - integration_request_dict["order_id"] = str(paymob_order_id) - - # Update the data field with additional information - integration_request_doc.data = json.dumps(integration_request_dict) - - # Check if transaction succeeded / paid - if is_payment_successful: - # Update integration request status - integration_request_doc.save(ignore_permissions=True) - frappe.db.commit() - - # Call on_payment_authorized on the reference document - 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(), "Paymob on_payment_authorized Error") - - if custom_redirect_to: - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = custom_redirect_to - return - - return "Payment verified successfully" - else: - # Payment failed or was cancelled - integration_request_doc.status = "Failed" - integration_request_doc.error = f"Payment Status: {payment_status}, Response Code: {txn_response_code}" - integration_request_doc.save(ignore_permissions=True) - frappe.db.commit() + 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") - return "Payment failed or was cancelled" - except Exception as e: - frappe.log_error(frappe.get_traceback(), "Paymob Callback Error") - return "Server error" + 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): From dd05aef6480727e281e386e637fe4164fdaeeb6d Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 24 Oct 2025 08:06:45 +0000 Subject: [PATCH 049/138] style: adjust Get Access Token button style to avoid multiple primary buttons --- .../paymob_settings/paymob_settings.js | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js index f9850429..5988d2a6 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js @@ -2,26 +2,31 @@ // 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") - }).addClass("btn btn-primary") - }, - get_access_token: function (frm) { - try { - frm.call({ - method: "get_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); - } - } -}); \ No newline at end of file + refresh(frm) { + frm.add_custom_button(__("Get Access Token"), () => { + frm.trigger("get_access_token"); + }); + }, + get_access_token: function (frm) { + try { + frm + .call({ + method: "get_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); + } + }, +}); From 8e2e0bd50eef701e7d15b28f8f25696d82aabb43 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 24 Oct 2025 09:05:21 +0000 Subject: [PATCH 050/138] chore: resolve ruff lint issues --- .../paymob_settings/paymob_settings.py | 126 +++++++++--------- .../payment_gateways/paymob/accept_api.py | 28 ++-- .../payment_gateways/paymob/connection.py | 43 +++--- payments/payment_gateways/paymob/constants.py | 6 +- .../payment_gateways/paymob/hmac_validator.py | 32 ++--- .../payment_gateways/paymob/paymob_urls.py | 8 +- .../payment_gateways/paymob/response_codes.py | 2 +- .../paymob/response_feedback_dataclass.py | 9 +- 8 files changed, 124 insertions(+), 130 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py index e32eada5..7edbac42 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py @@ -1,19 +1,22 @@ # Copyright (c) 2025, Frappe Technologies and contributors # For license information, please see license.txt +from urllib.parse import urlencode + import frappe -from frappe.model.document import Document from frappe import _ -from payments.payment_gateways.paymob.accept_api import AcceptAPI -from payments.payment_gateways.paymob.connection import AcceptConnection -from payments.payment_gateways.paymob.paymob_urls import PaymobUrls -from payments.payment_gateways.paymob.hmac_validator import HMACValidator -from urllib.parse import urlencode from frappe.integrations.utils import ( create_request_log, - ) +from frappe.model.document import Document + +from payments.payment_gateways.paymob.accept_api import AcceptAPI +from payments.payment_gateways.paymob.connection import AcceptConnection +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. @@ -31,13 +34,12 @@ class PaymobSettings(Document): secret_key: DF.Password token: DF.Password | None # end: auto-generated types - + @frappe.whitelist() def get_access_token(self): accept = AcceptAPI() token = accept.retrieve_auth_token() return token - def get_payment_url(self, **kwargs): try: @@ -55,13 +57,13 @@ def get_payment_url(self, **kwargs): "first_name": kwargs.get("payer_name").split()[0], "street": "NA", "building": "NA", - "phone_number": "+201111111111", + "phone_number": "+201111111111", "shipping_method": "NA", "postal_code": "NA", "city": "Cairo", "country": "EG", "last_name": kwargs.get("payer_name").split()[-1], - "state": "NA" + "state": "NA", } payment_key_payload = { @@ -70,7 +72,7 @@ def get_payment_url(self, **kwargs): "expiration": 3600, "order_id": kwargs.get("order_id"), "currency": kwargs.get("currency", "EGP"), - "billing_data":billing_data, + "billing_data": billing_data, "integration_id": self.payment_integration, } @@ -89,13 +91,10 @@ def get_payment_url(self, **kwargs): frappe.log_error(frappe.get_traceback()) frappe.throw(_("Could not generate Paymob payment URL")) - - def create_order(self, **kwargs): - # Create integration log integration_request = create_request_log(kwargs, service_name="Paymob") - connection=AcceptConnection() + connection = AcceptConnection() paymob_urls = PaymobUrls() # Get your API token @@ -117,13 +116,13 @@ def create_order(self, **kwargs): } try: - url=paymob_urls.get_url("order") + url = paymob_urls.get_url("order") code, feedback = connection.post(url=url, json=payload) if code != SUCCESS: frappe.throw(_("Failed to create order in Paymob")) - - order=feedback.data - paymob_order_id=order.get('id') + + order = feedback.data + 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) @@ -144,17 +143,14 @@ def create_order(self, **kwargs): 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 - ) + validator = HMACValidator(incoming_hmac=incoming_hmac, callback_dict=incoming_data_json) if not validator.is_valid: frappe.throw(_("Invalid HMAC")) @@ -170,41 +166,45 @@ def callback(): 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" + 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_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_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" - + 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.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 as e: + except Exception: frappe.log_error(frappe.get_traceback(), "Paymob Callback Error") @@ -212,15 +212,15 @@ 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 - ) + "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")) @@ -230,15 +230,13 @@ def get_integration_request(paymob_order_id): 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']: + 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'] + integration_request_dict["reference_doctype"], integration_request_dict["reference_docname"] ).run_method("on_payment_authorized", "Completed") - except Exception: frappe.log_error(frappe.get_traceback()) @@ -246,15 +244,13 @@ def handle_payment_success(integration_request_dict): 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']}" - ) + 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): @@ -263,15 +259,15 @@ def update_paymob_settings(**kwargs): { "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"), + "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 as e: - return "Failed to Update Paymob Credentials" \ No newline at end of file + except Exception: + return "Failed to Update Paymob Credentials" diff --git a/payments/payment_gateways/paymob/accept_api.py b/payments/payment_gateways/paymob/accept_api.py index 4aa21085..e692c0b2 100644 --- a/payments/payment_gateways/paymob/accept_api.py +++ b/payments/payment_gateways/paymob/accept_api.py @@ -1,13 +1,13 @@ -from typing import Any, Dict, List, Tuple, Union +import json +from typing import Any, Union, dict, list, tuple + import frappe import requests -import json -from .paymob_urls import PaymobUrls from .connection import AcceptConnection - -from .response_feedback_dataclass import ResponseFeedBack +from .paymob_urls import PaymobUrls from .response_codes import SUCCESS +from .response_feedback_dataclass import ResponseFeedBack class AcceptAPI: @@ -29,9 +29,7 @@ def retrieve_auth_token(self): """ return self.connection.auth_token - def create_payment_intent( - self, data: Dict - ) -> Tuple[str, Union[Dict, None], ResponseFeedBack]: + 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) @@ -56,16 +54,14 @@ def create_payment_intent( return code, payment_intent, feedback - def retrieve_transaction( - self, transaction_id: int - ) -> Tuple[str, Union[Dict, None], ResponseFeedBack]: + 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 + transaction_id (int): Paymob's Transaction ID Returns: - Tuple[str, Union[Dict, None], ResponseFeedBack]: (Code, Dict, ResponseFeedBack Instance) + 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) @@ -73,13 +69,11 @@ def retrieve_transaction( transaction = None if code == SUCCESS: transaction = feedback.data - feedback.message = ( - f"Transaction with id {transaction_id} retrieved Scuccessfully" - ) + 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 \ No newline at end of file + return iframe_url diff --git a/payments/payment_gateways/paymob/connection.py b/payments/payment_gateways/paymob/connection.py index 912fe8e3..87b27838 100644 --- a/payments/payment_gateways/paymob/connection.py +++ b/payments/payment_gateways/paymob/connection.py @@ -1,9 +1,10 @@ -from typing import Any, Dict, Tuple, Union +from typing import Any, Union, dict, tuple import requests +from frappe.utils.password import get_decrypted_password from requests import HTTPError, JSONDecodeError, RequestException -from .response_feedback_dataclass import ResponseFeedBack +from .paymob_urls import PaymobUrls from .response_codes import ( HTTP_EXCEPTION, HTTP_EXCEPTION_MESSAGE, @@ -16,9 +17,7 @@ UNHANDLED_EXCEPTION, UNHANDLED_EXCEPTION_MESSAGE, ) -from frappe.utils.password import get_decrypted_password -from .paymob_urls import PaymobUrls - +from .response_feedback_dataclass import ResponseFeedBack class AcceptConnection: @@ -34,22 +33,22 @@ def __init__(self) -> None: self.auth_token = self._get_auth_token() self.session.headers.update(self._get_headers()) - def _get_headers(self) -> Dict[str, Any]: + def _get_headers(self) -> dict[str, Any]: """Initialize Header for Requests Returns: - Dict[str, Any]: Initialized Header Dict + Dict[str, Any]: Initialized Header Dict """ return { "Content-Type": "application/json", "Authorization": f"{self.auth_token}", } - def _get_auth_token(self) -> Union[str, None]: + def _get_auth_token(self) -> str | None: """Retrieve an Auth Token Returns: - Union[str, None]: Auth Token + Union[str, None]: Auth Token """ api_key = get_decrypted_password("Paymob Settings", "Paymob Settings", "api_key") request_body = {"api_key": api_key} @@ -64,20 +63,20 @@ def _get_auth_token(self) -> Union[str, None]: token = feedback.data.get("token") return token - def _process_request(self, call, *args, **kwargs) -> Tuple[str, Dict[str, Any], ResponseFeedBack]: + 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 + 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) + Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) """ reponse_data = None try: - response = call(timeout=90, *args, **kwargs) + response = call(*args, timeout=90, **kwargs) reponse_data = response.json() response.raise_for_status() except JSONDecodeError as error: @@ -115,24 +114,24 @@ def _process_request(self, call, *args, **kwargs) -> Tuple[str, Dict[str, Any], ) return SUCCESS, reponse_feedback - def get(self, *args, **kwargs) -> Tuple[str, Dict[str, Any], ResponseFeedBack]: + 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 + Same Args of requests.post/requests.get methods Returns: - Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) + Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) """ - return self._process_request(call=self.session.get, *args, **kwargs) + return self._process_request(*args, call=self.session.get, **kwargs) - def post(self, *args, **kwargs) -> Tuple[str, Dict[str, Any], ResponseFeedBack]: + 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 + Same Args of requests.post/requests.get methods Returns: - Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) + Tuple[str, Dict[str, Any], ResponseFeedBack]: Tuple containes the Following (Code, Data, Success/Error Message) """ - return self._process_request(call=self.session.post, *args, **kwargs) \ No newline at end of file + 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 index 78761378..be88d47e 100644 --- a/payments/payment_gateways/paymob/constants.py +++ b/payments/payment_gateways/paymob/constants.py @@ -1,4 +1,4 @@ class AcceptCallbackTypes: - TRANSACTION = "TRANSACTION" - CARD_TOKEN = "TOKEN" - DELIVERY_STATUS = "DELIVERY_STATUS" \ No newline at end of file + 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 index c67a4fea..74f602c0 100644 --- a/payments/payment_gateways/paymob/hmac_validator.py +++ b/payments/payment_gateways/paymob/hmac_validator.py @@ -1,17 +1,19 @@ import hashlib import hmac -from typing import Any, Dict -from .constants import AcceptCallbackTypes +from typing import Any, dict + import frappe +from .constants import AcceptCallbackTypes + class HMACValidator: - def __init__(self, incoming_hmac: str, callback_dict: Dict[str, Any], **kwargs) -> None: + 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 + 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 @@ -25,10 +27,10 @@ def _calculate_hmac(message: str) -> str: """Calculates HMAC Args: - message (str): GeneratedHMAC Message + message (str): GeneratedHMAC Message Returns: - str: Calculated HMAC + str: Calculated HMAC """ hmac_secret = frappe.get_doc("Paymob Settings").get_password("hmac").encode("utf-8") return ( @@ -42,14 +44,14 @@ def _calculate_hmac(message: str) -> str: ) @classmethod - def _generate_processed_hmac(cls, hmac_dict: Dict[str, Any]) -> str: + 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 + hmac_dict (Dict[str, Any]): Hmac Dict Returns: - str: Generated HMAC + str: Generated HMAC """ if not isinstance(hmac_dict, dict): return "" @@ -68,7 +70,7 @@ def _generate_transaction_processed_hmac(self) -> str: """Creates HMAC from sent transaction callback self.callback_obj_dict Returns: - str: Generated HMAC + str: Generated HMAC """ if not isinstance(self.callback_obj_dict, dict): return "" @@ -102,7 +104,7 @@ def _generate_card_token_processed_hmac(self) -> str: """Creates HMAC from sent card token callback body_dic Returns: - str: Generated HMAC + str: Generated HMAC """ if not isinstance(self.callback_obj_dict, dict): return "" @@ -124,7 +126,7 @@ def _generate_delivery_status_processed_hmac(self) -> str: """Creates HMAC from sent Delivery Status callback body_dic Returns: - str: Generated HMAC + str: Generated HMAC """ if not isinstance(self.callback_obj_dict, dict): return "" @@ -145,7 +147,7 @@ def is_valid(self) -> bool: """Validates HMAC for processed callback Returns: - bool: True if HMAC is Valid, False otherwise + bool: True if HMAC is Valid, False otherwise """ if not isinstance(self.callback_dict, dict): return False @@ -163,4 +165,4 @@ def is_valid(self) -> bool: if calculated_hmac != self.incoming_hmac: return False - return True \ No newline at end of file + return True diff --git a/payments/payment_gateways/paymob/paymob_urls.py b/payments/payment_gateways/paymob/paymob_urls.py index 2198e23d..c866c032 100644 --- a/payments/payment_gateways/paymob/paymob_urls.py +++ b/payments/payment_gateways/paymob/paymob_urls.py @@ -1,6 +1,8 @@ from dataclasses import dataclass + import frappe + @dataclass class PaymobUrls: base_url: str = "https://accept.paymob.com/" @@ -21,7 +23,9 @@ class PaymobUrls: 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}" + 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/" @@ -35,4 +39,4 @@ def get_url(self, endpoint, **kwargs): # 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") \ No newline at end of file +# 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 index eb1a8868..43f90e08 100644 --- a/payments/payment_gateways/paymob/response_codes.py +++ b/payments/payment_gateways/paymob/response_codes.py @@ -13,4 +13,4 @@ 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." \ No newline at end of file +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 index 8643ebae..42e41eba 100644 --- a/payments/payment_gateways/paymob/response_feedback_dataclass.py +++ b/payments/payment_gateways/paymob/response_feedback_dataclass.py @@ -4,8 +4,7 @@ @dataclass class ResponseFeedBack: - - message: Optional[str] - data: Any = None - status_code: int = None - exception_error: str = None \ No newline at end of file + message: str | None + data: Any = None + status_code: int = None + exception_error: str = None From 6cf407fb86a4edfc9761de14c858753ffc94aba9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 24 Oct 2025 09:34:08 +0000 Subject: [PATCH 051/138] chore: fix ruff lint issues and update deprecated typing imports --- payments/payment_gateways/paymob/accept_api.py | 2 +- payments/payment_gateways/paymob/connection.py | 2 +- payments/payment_gateways/paymob/hmac_validator.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/paymob/accept_api.py b/payments/payment_gateways/paymob/accept_api.py index e692c0b2..ae3ee4e5 100644 --- a/payments/payment_gateways/paymob/accept_api.py +++ b/payments/payment_gateways/paymob/accept_api.py @@ -1,5 +1,5 @@ import json -from typing import Any, Union, dict, list, tuple +from typing import Any, Union import frappe import requests diff --git a/payments/payment_gateways/paymob/connection.py b/payments/payment_gateways/paymob/connection.py index 87b27838..8f1c865a 100644 --- a/payments/payment_gateways/paymob/connection.py +++ b/payments/payment_gateways/paymob/connection.py @@ -1,4 +1,4 @@ -from typing import Any, Union, dict, tuple +from typing import Any import requests from frappe.utils.password import get_decrypted_password diff --git a/payments/payment_gateways/paymob/hmac_validator.py b/payments/payment_gateways/paymob/hmac_validator.py index 74f602c0..30ec0635 100644 --- a/payments/payment_gateways/paymob/hmac_validator.py +++ b/payments/payment_gateways/paymob/hmac_validator.py @@ -1,6 +1,6 @@ import hashlib import hmac -from typing import Any, dict +from typing import Any import frappe From 61a718b3a721b6694abc6c9946064631d0b879d8 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 24 Oct 2025 10:18:22 +0000 Subject: [PATCH 052/138] refactor: use frappe utils for HTTP requests --- .../paymob_settings/paymob_settings.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py index 7edbac42..680272f3 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py @@ -5,13 +5,10 @@ import frappe from frappe import _ -from frappe.integrations.utils import ( - create_request_log, -) +from frappe.integrations.utils import create_request_log, make_get_request, make_post_request from frappe.model.document import Document from payments.payment_gateways.paymob.accept_api import AcceptAPI -from payments.payment_gateways.paymob.connection import AcceptConnection 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 @@ -43,7 +40,6 @@ def get_access_token(self): def get_payment_url(self, **kwargs): try: - connection = AcceptConnection() paymob_urls = PaymobUrls() if not kwargs.get("order_id") or not kwargs.get("amount"): @@ -77,12 +73,13 @@ def get_payment_url(self, **kwargs): } url = paymob_urls.get_url("payment_key") - code, feedback = connection.post(url=url, json=payment_key_payload) + headers = {"Content-Type": "application/json"} + response = make_post_request(url=url, json=payment_key_payload, headers=headers) - if code != SUCCESS or "token" not in feedback.data: + if not response or "token" not in response: frappe.throw(_("Failed to retrieve payment token from Paymob")) - payment_token = feedback.data["token"] + payment_token = response["token"] iframe_url = f"https://accept.paymob.com/api/acceptance/iframes/{self.iframe}?payment_token={payment_token}" return iframe_url @@ -94,7 +91,6 @@ def get_payment_url(self, **kwargs): def create_order(self, **kwargs): # Create integration log integration_request = create_request_log(kwargs, service_name="Paymob") - connection = AcceptConnection() paymob_urls = PaymobUrls() # Get your API token @@ -117,11 +113,12 @@ def create_order(self, **kwargs): try: url = paymob_urls.get_url("order") - code, feedback = connection.post(url=url, json=payload) - if code != SUCCESS: + 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")) - order = feedback.data paymob_order_id = order.get("id") integration_request_dict = frappe.parse_json(integration_request.data) From c850acaef3df04d261c4b254ec9f9190919f47b8 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 29 Oct 2025 22:16:24 +0000 Subject: [PATCH 053/138] chore: remove mandatory requirement for HMAC field --- .../doctype/paymob_settings/paymob_settings.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json index f6703513..34cc16fa 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json @@ -41,8 +41,7 @@ "fieldname": "hmac", "fieldtype": "Password", "in_list_view": 1, - "label": "HMAC", - "reqd": 1 + "label": "HMAC" }, { "fieldname": "column_break_rdwp", @@ -87,7 +86,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-10-22 14:04:07.307698", + "modified": "2025-10-30 01:14:02.729163", "modified_by": "Administrator", "module": "Payment Gateways", "name": "Paymob Settings", From 1d2e5f2ee6da1924848a19148ed7f33611ad9d9f Mon Sep 17 00:00:00 2001 From: meaziz Date: Wed, 26 Nov 2025 05:53:37 -0800 Subject: [PATCH 054/138] Merge pull request #186 from Axentorllc/feat/paymob-integration fix: paymob refresh token --- .../paymob_settings/paymob_settings.js | 2 +- .../paymob_settings/paymob_settings.json | 32 +++++++++++----- .../paymob_settings/paymob_settings.py | 37 ++++++++++++++----- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js index 5988d2a6..39d99cca 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Paymob Settings", { try { frm .call({ - method: "get_access_token", + method: "refresh_access_token", doc: frm.doc, freeze: true, freeze_message: __("Getting Access Token ..."), diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json index 34cc16fa..55c1fc61 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.json @@ -10,12 +10,14 @@ "public_key", "hmac", "column_break_rdwp", - "token", "secret_key", + "token", + "expires_in", "payment_config", "iframe", - "column_break_bpek", - "payment_integration" + "payment_integration", + "column_break_qgqc", + "redirect_to" ], "fields": [ { @@ -41,7 +43,8 @@ "fieldname": "hmac", "fieldtype": "Password", "in_list_view": 1, - "label": "HMAC" + "label": "HMAC", + "reqd": 1 }, { "fieldname": "column_break_rdwp", @@ -71,22 +74,33 @@ "label": "Iframe", "reqd": 1 }, - { - "fieldname": "column_break_bpek", - "fieldtype": "Column Break" - }, { "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-10-30 01:14:02.729163", + "modified": "2025-11-20 23:53:24.529932", "modified_by": "Administrator", "module": "Payment Gateways", "name": "Paymob Settings", diff --git a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py index 680272f3..04bff5b9 100644 --- a/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py +++ b/payments/payment_gateways/doctype/paymob_settings/paymob_settings.py @@ -1,12 +1,14 @@ # 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 @@ -33,11 +35,32 @@ class PaymobSettings(Document): # end: auto-generated types @frappe.whitelist() - def get_access_token(self): + 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() @@ -45,7 +68,7 @@ def get_payment_url(self, **kwargs): if not kwargs.get("order_id") or not kwargs.get("amount"): frappe.throw(_("Missing order ID or amount")) - # Build dummy billing data (Paymob requires this) + # Build dummy billing data billing_data = { "apartment": "NA", "email": kwargs.get("payer_email"), @@ -63,7 +86,7 @@ def get_payment_url(self, **kwargs): } payment_key_payload = { - "auth_token": self.get_password("token"), + "auth_token": self.get_valid_token(), "amount_cents": str(int(float(kwargs.get("amount")) * 100)), "expiration": 3600, "order_id": kwargs.get("order_id"), @@ -89,20 +112,16 @@ def get_payment_url(self, **kwargs): frappe.throw(_("Could not generate Paymob payment URL")) def create_order(self, **kwargs): - # Create integration log integration_request = create_request_log(kwargs, service_name="Paymob") paymob_urls = PaymobUrls() - # Get your API token - token = self.get_password("token") + token = self.get_valid_token() - # Required fields by Paymob 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", []) # Required field even if empty + items = kwargs.get("items", []) - # Construct payload payload = { "auth_token": token, "delivery_needed": str(delivery_needed).lower(), From 678321ab0cd8550a6458c564abc5d073043d1b5f Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Mon, 12 Jan 2026 18:18:26 +0530 Subject: [PATCH 055/138] feat: add PayPal error logging (#193) --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 1410f397..88d12fc5 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -212,6 +212,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 From 92a9cfbd739c54a8e4d3e84edb85eed0b64fb5b4 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 21 Jan 2026 21:52:28 +0530 Subject: [PATCH 056/138] fix: order_id should be top-level option --- payments/templates/includes/razorpay_checkout.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 }} }; From b10803774e4abf79497471c6a272cd97c0a96d9e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 9 Feb 2026 19:31:13 +0530 Subject: [PATCH 057/138] chore: bump razorpay --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 470810c3..12b522c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" dynamic = ["version"] dependencies = [ "paytmchecksum~=1.7.0", - "razorpay~=1.4.2", + "razorpay~=2.0.0", "stripe~=10.12.0", "braintree~=4.20.0", "pycryptodome>=3.18.0,<4.0.0", From edc10e2128afa49859f1b4d64c515d731f923e10 Mon Sep 17 00:00:00 2001 From: raizasafeel <89463672+raizasafeel@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:18:24 +0530 Subject: [PATCH 058/138] fix(stripe): improve mobile card input, error style --- payments/templates/pages/stripe_checkout.css | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/payments/templates/pages/stripe_checkout.css b/payments/templates/pages/stripe_checkout.css index 8842f1e7..20746bbb 100644 --- a/payments/templates/pages/stripe_checkout.css +++ b/payments/templates/pages/stripe_checkout.css @@ -109,9 +109,34 @@ padding: 10rem; } +#card-errors { + clear: both; + color: #fa755a; +} + @media (max-width: 48rem) { #payment-section { padding: 0rem; min-height: 70vh; - }, + } + + .stripe label { + height: auto; + line-height: normal; + display: flex; + flex-direction: column; + } + + .stripe label > span { + width: 100%; + margin-bottom: 6px; + } + + .field { + width: 100%; + } + + #card-errors { + margin-top: 6px; + } } \ No newline at end of file From 2ebef4d1de909f67907e742e6542c141414d957e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 10 Nov 2016 09:58:56 +0530 Subject: [PATCH 059/138] [fix] paypal sandboxing to test dummy payments (#2281) * [fix] paypal sandboxing to test dummy payments * [fix] sandbox testing for razorpay --- .../doctype/razorpay_settings/razorpay_settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 6cbed70a..89e291cb 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -505,6 +505,14 @@ def clear(self): self.save() + if data.get('notes', {}).get('use_sandbox'): + settings.update({ + "api_key": frappe.conf.sandbox_api_key, + "api_secret": frappe.conf.sandbox_api_secret, + }) + + return settings + def capture_payment(is_sandbox=False, sanbox_response=None): """ Verifies the purchase as complete by the merchant. From 304a7b7ace835df91e0d2e1547b372279c0d07ca Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 28 Nov 2016 12:37:33 +0530 Subject: [PATCH 060/138] [urgent][fix] convert use_sandbox param to integer to avoid false data sandboxing (#2384) --- .../doctype/razorpay_settings/razorpay_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 89e291cb..b8ed9cb5 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -505,7 +505,7 @@ def clear(self): self.save() - if data.get('notes', {}).get('use_sandbox'): + if cint(data.get('notes', {}).get('use_sandbox')): settings.update({ "api_key": frappe.conf.sandbox_api_key, "api_secret": frappe.conf.sandbox_api_secret, From 2ce789ffc25f99f347c774c03044be8922df6015 Mon Sep 17 00:00:00 2001 From: omkarghaisas Date: Thu, 27 Jul 2017 11:32:40 +0530 Subject: [PATCH 061/138] Third party apps portal (#3782) * Added third party apps portal page stub * [WIP] third party apps portal page * Added portal page third party apps Added page to manage OAuth 2.0 active sessions * [Fix] Typo me.html * frappe/www/third_party_apps. * [Fix] Added column for last log in --- payments/templates/includes/third_party_apps.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 payments/templates/includes/third_party_apps.js diff --git a/payments/templates/includes/third_party_apps.js b/payments/templates/includes/third_party_apps.js new file mode 100644 index 00000000..fe75f23d --- /dev/null +++ b/payments/templates/includes/third_party_apps.js @@ -0,0 +1,9 @@ +frappe.ready(() => { + $(".btn-delete-app").on("click", function(event) { + frappe.call({ + method:"frappe.www.third_party_apps.delete_client", + args: {"client_id": $(this).data("client_id"), + } + }).done(r => location.href="/third_party_apps"); + }); +}); From 4f42b20eb8727d8f35383fbdec95216d19b03fa6 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Wed, 9 Aug 2017 11:43:23 +0530 Subject: [PATCH 062/138] Replaced all instances of basestring with six.string_types --- payments/templates/pages/razorpay_checkout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 521a6d22..e474c9be 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json +from six import string_types import frappe from frappe import _ From 6c79e69ef5f92ec393bab5baf3639d40196e71fe Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 16 Feb 2018 10:46:23 +0100 Subject: [PATCH 063/138] Braintree integration (#4971) * Braintree Integration WIP * Braintree integration * Link in config + settings after save * Code cleanup * Code cleanup * JS beautify issue * Codacy corrections * Spaces to tabs --- .../test_braintree_settings.js | 23 +++++++++++++++++++ .../payment_gateway/test_payment_gateway.js | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js create mode 100644 payments/payments/doctype/payment_gateway/test_payment_gateway.js diff --git a/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js new file mode 100644 index 00000000..28e4202c --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Braintree Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Braintree Setting + () => frappe.tests.make('Braintree Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/payments/payments/doctype/payment_gateway/test_payment_gateway.js b/payments/payments/doctype/payment_gateway/test_payment_gateway.js new file mode 100644 index 00000000..36168ec8 --- /dev/null +++ b/payments/payments/doctype/payment_gateway/test_payment_gateway.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Payment Gateway", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Payment Gateway + () => frappe.tests.make('Payment Gateway', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); From 4a7d27fe6f19d0c09c10b8f016f3b7b1e994814d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 19 Mar 2018 18:28:53 +0530 Subject: [PATCH 064/138] [fix] validate minimum transaction amount while creating a charge on stripe (#5223) --- .../doctype/stripe_settings/stripe_settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 29dff12b..9f320faf 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -190,6 +190,12 @@ 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)}") From f9f8aaafc6328f3733cd1574b355c5e6458a856b Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 6 Apr 2018 07:13:49 +0200 Subject: [PATCH 065/138] [New Feature] Google Calendar Connector (#5266) * Addition of a filter for last sync timestamp * Google calendar connector wip * Google calendar integration * Add test for account creation * Codacy corrections * Remove unused import * New section Google Services * Add no_copy to migration custom field --- .../templates/pages/gcalendar-success.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 payments/templates/pages/gcalendar-success.html diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html new file mode 100644 index 00000000..b6d22300 --- /dev/null +++ b/payments/templates/pages/gcalendar-success.html @@ -0,0 +1,20 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Connection Success") }}{% endblock %} + +{%- block page_content -%} +
+
+ + {{ _("Success") }} +
+

{{ _("Your connection request to Google Calendar was successfully accepted") }}

+ +
+ +{% endblock %} From 42b007f43b2d299731d1923fc88ce949d7aca198 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 23 May 2018 07:46:21 +0200 Subject: [PATCH 066/138] Allow several Stripe accounts (#5573) * Allow several Stripe accounts * Addition of a sync patch * Remove unused dependancy --- .../stripe_settings/test_stripe_settings.js | 23 +++++++++++++++++++ payments/templates/pages/stripe_checkout.py | 1 + 2 files changed, 24 insertions(+) create mode 100644 payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js diff --git a/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js new file mode 100644 index 00000000..b491ba57 --- /dev/null +++ b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Stripe Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Stripe Settings + () => frappe.tests.make('Stripe Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 7f4a30e0..d88246a2 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json +from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller import frappe from frappe import _ From c4672926b3c529b05f154650529f38666e703b14 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 11 Jun 2018 11:43:29 +0200 Subject: [PATCH 067/138] Subscriptions + Elements for Stripe payments (#5602) * Stripe Payments Improvements * Automatically create a new stripe customer * Charge arguments correction * Move Stripe to ERPNext * Revert "Move Stripe to ERPNext" This reverts commit f71f9952997ea0fcdccafc2410abafb6185a6a1f. * Stripe Settings correction * Payment plan moved to ERPNext * Remove Reference to payment plan in Frappe * Remove references to payment plan in Frappe * Modify Stripe Checkout Pages to allow usage without ERPNext --- .../stripe_settings/stripe_settings.json | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json index b1bd1c3e..6f016c4e 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json @@ -265,6 +265,132 @@ "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, + "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 } ], "has_web_view": 0, From c7ba774b586e77d29d8cbc19f868b66954469039 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 10 Jul 2018 16:06:35 +0530 Subject: [PATCH 068/138] [test] [style] --- payments/templates/pages/gcalendar-success.html | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html index b6d22300..1ce9ba5b 100644 --- a/payments/templates/pages/gcalendar-success.html +++ b/payments/templates/pages/gcalendar-success.html @@ -16,5 +16,6 @@ .hero-and-content { background-color: #f5f7fa; } +{% include "templates/styles/card_style.css" %} {% endblock %} From 65ccd902e9ee0dbab1f9b8a9188af99ba1981240 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 30 Aug 2018 10:03:46 +0530 Subject: [PATCH 069/138] Support for razorpay subscription and addons --- .../razorpay_settings/razorpay_settings.py | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index b8ed9cb5..ace23fca 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -324,6 +324,59 @@ def prepare_subscription_details(self, settings, **kwargs): return kwargs + def before_get_payment_url(self, **kwargs): + if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): + return + + settings = self.get_settings(kwargs) + if kwargs.get('subscription_id') and kwargs.get('addons'): + return self.setup_addon(settings, **kwargs) + else: + return self.setup_subscription(settings, **kwargs) + + def setup_addon(self, settings, **kwargs): + url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) + + for addon in kwargs.get("addons"): + try: + 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(str(resp), 'Razorpay Failed while creating subscription') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + return {} + + def setup_subscription(self, settings, **kwargs): + try: + resp = make_post_request( + "https://api.razorpay.com/v1/subscriptions", + auth=(settings.api_key, settings.api_secret), + data=kwargs.get('subscription_details') + ) + + if resp.get('status') == 'created': + kwargs['subscription_details']['subscription_id'] = resp.get('id') + else: + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + + return kwargs + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + def get_payment_url(self, **kwargs): if not kwargs.get("order_id"): order = self.create_order(**kwargs) @@ -505,7 +558,7 @@ def clear(self): self.save() - if cint(data.get('notes', {}).get('use_sandbox')): + 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, From fc9c2b36c1d6184f5bc27c548929b41fde1f74bf Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 30 Aug 2018 19:26:04 +0530 Subject: [PATCH 070/138] [fix] razorpay subscription --- .../razorpay_settings/razorpay_settings.py | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index ace23fca..b187bcaf 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -324,46 +324,56 @@ def prepare_subscription_details(self, settings, **kwargs): return kwargs - def before_get_payment_url(self, **kwargs): - if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): - return + # def before_get_payment_url(self, **kwargs): + # if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): + # return + # + # settings = self.get_settings(kwargs) + # if kwargs.get('subscription_id') and kwargs.get('addons'): + # return self.setup_addon(settings, **kwargs) + # else: + # return self.setup_subscription(settings, **kwargs) + # + # def setup_addon(self, settings, **kwargs): + # url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) + # + # for addon in kwargs.get("addons"): + # try: + # 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(str(resp), 'Razorpay Failed while creating subscription') + # + # except: + # frappe.log_error(frappe.get_traceback()) + # # failed + # pass + # + # return {} - settings = self.get_settings(kwargs) - if kwargs.get('subscription_id') and kwargs.get('addons'): - return self.setup_addon(settings, **kwargs) - else: - return self.setup_subscription(settings, **kwargs) - - def setup_addon(self, settings, **kwargs): - url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - - for addon in kwargs.get("addons"): - try: - 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(str(resp), 'Razorpay Failed while creating subscription') - - except: - frappe.log_error(frappe.get_traceback()) - # 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 - return {} + subscription_details = { + "plan_id": kwargs.get('subscription_details').get("plan_id"), + "start_at": cint(start_date), + "total_count": kwargs.get('subscription_details').get("billing_frequency"), + "customer_notify": kwargs.get('subscription_details').get("customer_notify"), + } - def setup_subscription(self, settings, **kwargs): try: resp = make_post_request( "https://api.razorpay.com/v1/subscriptions", auth=(settings.api_key, settings.api_secret), - data=kwargs.get('subscription_details') + data=subscription_details ) if resp.get('status') == 'created': @@ -371,7 +381,6 @@ def setup_subscription(self, settings, **kwargs): else: frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - return kwargs except: frappe.log_error(frappe.get_traceback()) # failed From d5989795067f7e3cac5782726bd844a29382f21b Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 5 Sep 2018 11:55:03 +0530 Subject: [PATCH 071/138] setup upfront amount --- .../doctype/razorpay_settings/razorpay_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index b187bcaf..e25f925f 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -367,6 +367,7 @@ def setup_subscription(self, settings, **kwargs): "start_at": cint(start_date), "total_count": kwargs.get('subscription_details').get("billing_frequency"), "customer_notify": kwargs.get('subscription_details').get("customer_notify"), + "upfront_amount": kwargs.get('subscription_details').get("upfront_amount") } try: From 7bcddb3fa5539927edd561ac7854a8210bcdaa08 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 10 Sep 2018 18:50:17 +0530 Subject: [PATCH 072/138] Provision to cancel existing subscription --- .../paypal_settings/paypal_settings.py | 20 +++++++++++++++++++ .../razorpay_settings/razorpay_settings.py | 11 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 88d12fc5..4579ba17 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -216,6 +216,26 @@ def execute_set_express_checkout(self, **kwargs): frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) return response + + def cancel_recurring_profile(self, **kwargs): + params, url = self.get_paypal_params_and_url() + + if not kwargs.get('profile_id'): + frappe.throw(_("PayPal Recurring Profile ID is required")) + + params.update({ + "METHOD": "ManageRecurringPaymentsProfileStatus", + "PROFILEID": kwargs['profile_id'], + "ACTION": "Cancel" + }) + + params = urlencode(params) + + response = make_post_request(url, data=params.encode("utf-8")) + if response.get("ACK")[0] != "Success": + frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) + + return response def configure_recurring_payments(self, params, kwargs): # removing the params as we have to setup rucurring payments diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index e25f925f..92ff0591 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -575,6 +575,17 @@ def clear(self): }) return settings + + + def cancel_subscription(self, subscription_id): + settings = self.get_settings() + + try: + resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" + .format(subscription_id), auth=(settings.api_key, + settings.api_secret)) + except Exception: + frappe.log_error(frappe.get_traceback()) def capture_payment(is_sandbox=False, sanbox_response=None): """ From b135872680ff93f8fad16c3a61a6ba40379e3873 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 18 Sep 2018 17:16:53 +0530 Subject: [PATCH 073/138] setup addons on razorpay --- .../razorpay_settings/razorpay_settings.py | 82 +++++++++++-------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 92ff0591..aae68fcd 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -324,39 +324,42 @@ def prepare_subscription_details(self, settings, **kwargs): return kwargs - # def before_get_payment_url(self, **kwargs): - # if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): - # return - # - # settings = self.get_settings(kwargs) - # if kwargs.get('subscription_id') and kwargs.get('addons'): - # return self.setup_addon(settings, **kwargs) - # else: - # return self.setup_subscription(settings, **kwargs) - # - # def setup_addon(self, settings, **kwargs): - # url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - # - # for addon in kwargs.get("addons"): - # try: - # 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(str(resp), 'Razorpay Failed while creating subscription') - # - # except: - # frappe.log_error(frappe.get_traceback()) - # # failed - # pass - # - # return {} + 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')) + + for addon in kwargs.get("addons"): + try: + addon['item']['amount'] *= 100 #convert amount to paisa + + 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(str(resp), 'Razorpay Failed while creating subscription') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass def setup_subscription(self, settings, **kwargs): start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ @@ -378,7 +381,8 @@ def setup_subscription(self, settings, **kwargs): ) if resp.get('status') == 'created': - kwargs['subscription_details']['subscription_id'] = resp.get('id') + kwargs['subscription_id'] = resp.get('id') + return kwargs else: frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') @@ -387,6 +391,16 @@ def setup_subscription(self, settings, **kwargs): # failed pass + def prepare_subscription_details(self, settings, **kwargs): + if kwargs.get('subscription_details'): + kwargs = self.setup_subscription(settings, **kwargs) + + if kwargs.get("subscription_id") and kwargs.get("addons"): + self.setup_addon(settings, **kwargs) + kwargs['subscription_id'] = None + + return kwargs + def get_payment_url(self, **kwargs): if not kwargs.get("order_id"): order = self.create_order(**kwargs) From 5e9a1c857f1535dfca617c7b5d769182f2f28da5 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Sep 2018 16:36:03 +0530 Subject: [PATCH 074/138] finalize subscription razorpay --- .../razorpay_settings/razorpay_settings.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index aae68fcd..2a7079ba 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -339,11 +339,10 @@ def setup_addon(self, settings, **kwargs): """ url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - - for addon in kwargs.get("addons"): - try: + + try: + for addon in kwargs.get("addons"): addon['item']['amount'] *= 100 #convert amount to paisa - resp = make_post_request( url, auth=(settings.api_key, settings.api_secret), @@ -355,11 +354,10 @@ def setup_addon(self, settings, **kwargs): if not resp.get('id'): frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - - except: - frappe.log_error(frappe.get_traceback()) - # failed - pass + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass def setup_subscription(self, settings, **kwargs): start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ From 79b31ab91786559198e46c6f710471805b20f290 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Sep 2018 18:11:25 +0530 Subject: [PATCH 075/138] finalize recurring payments for paypal --- .../paypal_settings/paypal_settings.py | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 4579ba17..c82d8d01 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -78,6 +78,9 @@ def on_payment_authorized(payment_status): api_path = "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings" + +api_path = '/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings' + class PayPalSettings(Document): supported_currencies = ( "AUD", @@ -216,26 +219,44 @@ def execute_set_express_checkout(self, **kwargs): frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) return response - - def cancel_recurring_profile(self, **kwargs): - params, url = self.get_paypal_params_and_url() - if not kwargs.get('profile_id'): - frappe.throw(_("PayPal Recurring Profile ID is required")) + 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({ - "METHOD": "ManageRecurringPaymentsProfileStatus", - "PROFILEID": kwargs['profile_id'], - "ACTION": "Cancel" + "L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement + "L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] }) - params = urlencode(params) +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() - response = make_post_request(url, data=params.encode("utf-8")) - if response.get("ACK")[0] != "Success": - frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) + integration_request = frappe.get_doc("Integration Request", token) + data = json.loads(integration_request.data) - return response + 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 From 87a2f456644274dccca30e1f1e461dbb2b1b3345 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 25 Sep 2018 11:52:56 +0530 Subject: [PATCH 076/138] fix for immediate subscriptions --- .../razorpay_settings/razorpay_settings.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 2a7079ba..4588b8bc 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -337,12 +337,13 @@ def setup_addon(self, settings, **kwargs): "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"): - addon['item']['amount'] *= 100 #convert amount to paisa resp = make_post_request( url, auth=(settings.api_key, settings.api_secret), @@ -367,19 +368,28 @@ def setup_subscription(self, settings, **kwargs): "plan_id": kwargs.get('subscription_details').get("plan_id"), "start_at": cint(start_date), "total_count": kwargs.get('subscription_details').get("billing_frequency"), - "customer_notify": kwargs.get('subscription_details').get("customer_notify"), - "upfront_amount": kwargs.get('subscription_details').get("upfront_amount") + "customer_notify": kwargs.get('subscription_details').get("customer_notify") } + 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=subscription_details + 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(str(resp), 'Razorpay Failed while creating subscription') @@ -390,11 +400,10 @@ def setup_subscription(self, settings, **kwargs): pass def prepare_subscription_details(self, settings, **kwargs): - if kwargs.get('subscription_details'): + if not kwargs.get("subscription_id"): kwargs = self.setup_subscription(settings, **kwargs) - if kwargs.get("subscription_id") and kwargs.get("addons"): - self.setup_addon(settings, **kwargs) + if frappe.flags.status !='created': kwargs['subscription_id'] = None return kwargs From b405de70ddbcaee805a6fcc004db83489b02b2f1 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 2 Oct 2018 22:11:43 +0530 Subject: [PATCH 077/138] provision to handel paypal and razoapay subscription charge notifications --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 1 + .../doctype/razorpay_settings/razorpay_settings.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index c82d8d01..be3778ec 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -484,6 +484,7 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): ) response = make_post_request(url, data=args) + # error code 11556 indicates profile is not in active state(or already cancelled) # thus could not cancel the subscription. diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 4588b8bc..00a686f1 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -352,7 +352,6 @@ def setup_addon(self, settings, **kwargs): "content-type": "application/json" } ) - if not resp.get('id'): frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') except: From 9a7ee9966b7c5998f7fa3815091126bb35b9bf9f Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 5 Oct 2018 17:16:28 +0530 Subject: [PATCH 078/138] [fix] add validations on payment notification callback --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 1 - .../doctype/razorpay_settings/razorpay_settings.py | 1 - 2 files changed, 2 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index be3778ec..c82d8d01 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -484,7 +484,6 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): ) response = make_post_request(url, data=args) - # error code 11556 indicates profile is not in active state(or already cancelled) # thus could not cancel the subscription. diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 00a686f1..95ec0d14 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -721,7 +721,6 @@ def convert_rupee_to_paisa(**kwargs): frappe.conf.converted_rupee_to_paisa = True - @frappe.whitelist(allow_guest=True) def razorpay_subscription_callback(): try: From f1d1212f61c6bfb4fd034401c5b2db2f13340faf Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 10 Oct 2018 17:28:19 +0530 Subject: [PATCH 079/138] [fix] do not cache payment success page --- payments/templates/pages/payment_success.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index e2d1115b..2c48c602 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import frappe +no_cache = True no_cache = True From b09f0ad0d7260a8a9d7e560543d559fe92f59d2d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 17 Oct 2018 19:50:54 +0530 Subject: [PATCH 080/138] [fix] razorpay subscription cancellation --- .../doctype/razorpay_settings/razorpay_settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 95ec0d14..06bf35b5 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -595,13 +595,12 @@ def clear(self): }) return settings - - + def cancel_subscription(self, subscription_id): - settings = self.get_settings() - + settings = self.get_settings({}) + try: - resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" + resp = make_post_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" .format(subscription_id), auth=(settings.api_key, settings.api_secret)) except Exception: From 9e3f8b777e8823e4fdf4fee7fc19ca594b936587 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 29 Nov 2018 09:07:38 +0530 Subject: [PATCH 081/138] Customize payment success message (#6460) * Provision to set custom payment success message * typo fix * Fix formatting --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index c82d8d01..3e418c55 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -249,7 +249,7 @@ def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): redirect_to = custom_redirect_to if redirect_to: - redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + redirect_url += '&' + urlencode({'redirect_to': redirect_to}) if redirect_message: redirect_url += '&' + urlencode({'redirect_message': redirect_message}) From c94a7e5610ad21922699a304a2e126c80b14fa3d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 21 May 2019 16:22:57 +0530 Subject: [PATCH 082/138] fix: before capturing payment first check payment status --- .../doctype/razorpay_settings/razorpay_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 06bf35b5..bd2afed0 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -643,6 +643,10 @@ 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") == "captured": frappe.db.set_value("Integration Request", doc.name, "status", "Completed") From f3335bfb2a440b0cd955f76f4ce2b732eaba51bb Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 21 May 2019 16:27:07 +0530 Subject: [PATCH 083/138] fix: check payment status before capturing the payment --- .../doctype/razorpay_settings/razorpay_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index bd2afed0..d14b60db 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -647,6 +647,10 @@ def capture_payment(is_sandbox=False, sanbox_response=None): 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") From 4c4a26814d251f70c5c9d70a2894341d0c9a06a5 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 25 Jun 2019 14:41:27 +0530 Subject: [PATCH 084/138] fix: if start date exist only then pass it while creating subscription --- .../doctype/razorpay_settings/razorpay_settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index d14b60db..9fdfeda6 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -365,11 +365,13 @@ def setup_subscription(self, settings, **kwargs): subscription_details = { "plan_id": kwargs.get('subscription_details').get("plan_id"), - "start_at": cint(start_date), "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({ From 9eda1c2f8e18012ced661d35888685053c6842b2 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 13 Nov 2019 15:42:33 +0530 Subject: [PATCH 085/138] feat: added orders API --- .../razorpay_settings/razorpay_settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 9fdfeda6..80c2c26c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -449,6 +449,23 @@ def create_order(self, **kwargs): 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/ + # + # kwargs = { + # "amount": 3000, + # "currency": "INR", + # "receipt": "rcptid_11234", + # "payment_capture": 1 + # } + integration_request = create_request_log(kwargs, "Host", "Razorpay") + if self.api_key and self.api_secret: + try: + return make_post_request("https://api.razorpay.com/v1/orders", auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), data=kwargs) + except Exception: + frappe.log(frappe.get_traceback()) + frappe.throw(_("Could not create razorpay order")) + def create_request(self, data): self.data = frappe._dict(data) From 2bc20f94d67915ccfaff106c522c8a9d323ef5e1 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 13 Nov 2019 17:37:02 +0530 Subject: [PATCH 086/138] fix: fixed post request --- .../doctype/razorpay_settings/razorpay_settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 80c2c26c..c7a2c86c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -459,9 +459,15 @@ def create_order(self, **kwargs): # "payment_capture": 1 # } integration_request = create_request_log(kwargs, "Host", "Razorpay") + 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: - return make_post_request("https://api.razorpay.com/v1/orders", auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), data=kwargs) + return 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) except Exception: frappe.log(frappe.get_traceback()) frappe.throw(_("Could not create razorpay order")) From cc338991cf4a7ede9f91849fdca66e0b0c5f4195 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 13 Nov 2019 19:31:11 +0530 Subject: [PATCH 087/138] feat: updated client API --- .../razorpay_settings/razorpay_settings.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index c7a2c86c..3c72a695 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -467,7 +467,9 @@ def create_order(self, **kwargs): } if self.api_key and self.api_secret: try: - return 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 = 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 + return order except Exception: frappe.log(frappe.get_traceback()) frappe.throw(_("Could not create razorpay order")) @@ -747,6 +749,22 @@ def order_payment_failure(integration_request, params): integration.update_status(params, integration.status) +@frappe.whitelist(allow_guest=True) +def get_order(doctype, docname): + return frappe.get_doc(doctype, docname).run_method("get_razorpay_order") + +@frappe.whitelist(allow_guest=True) +def order_payment_success(self, integration_request, params): + print("SUCCESSSSSSS ------------------") + integration = frappe.get_doc("Integration Request", integration_request) + integration.update_status(update_dict) + +@frappe.whitelist(allow_guest=True) +def order_payment_failure(self, integration_request, params): + print("FAILLLLLL ------------------") + integration = frappe.get_doc("Integration Request", integration_request) + integration.update_status(update_dict) + def convert_rupee_to_paisa(**kwargs): for addon in kwargs.get("addons"): addon["item"]["amount"] *= 100 From 865e1739acda2d23ddf0ccc7baa44afd87c47aba Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 11:44:04 +0530 Subject: [PATCH 088/138] style: added comments and cleaned up API --- .../razorpay_settings/razorpay_settings.py | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 3c72a695..a38e74a5 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -451,14 +451,14 @@ def create_order(self, **kwargs): def create_order(self, **kwargs): # Creating Orders https://razorpay.com/docs/api/orders/ - # - # kwargs = { - # "amount": 3000, - # "currency": "INR", - # "receipt": "rcptid_11234", - # "payment_capture": 1 - # } + + # convert ruppes to paisa + kwargs['amount'] *= 100 + + # Create integration log integration_request = create_request_log(kwargs, "Host", "Razorpay") + + # Setup payment otptions payment_options = { "amount": kwargs.get('amount'), "currency": kwargs.get('currency', 'INR'), @@ -468,8 +468,8 @@ def create_order(self, **kwargs): 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 - return order + 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")) @@ -751,19 +751,55 @@ def order_payment_failure(integration_request, params): @frappe.whitelist(allow_guest=True) def get_order(doctype, docname): - return frappe.get_doc(doctype, docname).run_method("get_razorpay_order") + # 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: + error_log = 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(self, integration_request, params): - print("SUCCESSSSSSS ------------------") +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) - integration.update_status(update_dict) + + # 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(self, integration_request, params): - print("FAILLLLLL ------------------") +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(update_dict) + integration.update_status(params, integration.status) def convert_rupee_to_paisa(**kwargs): for addon in kwargs.get("addons"): From 5a9786cb46aa5be5aa4ccaa06fdf6659c60ce65a Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 11:46:57 +0530 Subject: [PATCH 089/138] style: use tabs --- .../doctype/razorpay_settings/razorpay_settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a38e74a5..3d109b04 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -767,8 +767,8 @@ def order_payment_success(integration_request, params): 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. + 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) @@ -792,8 +792,8 @@ 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 + integration_request (TYPE): Description + params (TYPE): error data to be updated """ frappe.log_error(params, 'Razorpay Payment Failure') From 524a502f4a421b68653ed807eddb9551ab037e04 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 11:59:14 +0530 Subject: [PATCH 090/138] refactor: auto fetch API key --- .../doctype/razorpay_settings/razorpay_settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 3d109b04..55d23de6 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -749,6 +749,12 @@ def order_payment_failure(integration_request, params): integration.update_status(params, integration.status) + +@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 From 71a0d247e75c5fd6d41cc2ec660af6fb5dc8bd5c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 15:57:04 +0530 Subject: [PATCH 091/138] style: liting fixes --- .../doctype/razorpay_settings/razorpay_settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 55d23de6..5c4ee80f 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -467,7 +467,9 @@ def create_order(self, **kwargs): } 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 = 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: @@ -763,7 +765,7 @@ def get_order(doctype, docname): # Do not use run_method here as it fails silently return doc.get_razorpay_order() except AttributeError: - error_log = frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) + 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) @@ -802,7 +804,6 @@ def order_payment_failure(integration_request, params): 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) From fb7173a7c7d53b889d7b557f306a01d9aff673e0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 16:07:15 +0530 Subject: [PATCH 092/138] style: whitespace fixes --- .../doctype/razorpay_settings/razorpay_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 5c4ee80f..14dd313b 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -467,8 +467,8 @@ def create_order(self, **kwargs): } 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)), + 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 @@ -798,7 +798,7 @@ def order_payment_success(integration_request, params): @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 From cb4406236c0990c3fc860da4d1c36a58d3cca7a2 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 14 Nov 2019 16:54:21 +0530 Subject: [PATCH 093/138] style: whitespace fixes --- .../doctype/razorpay_settings/razorpay_settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 14dd313b..cfd88dc2 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -454,10 +454,10 @@ def create_order(self, **kwargs): # convert ruppes to paisa kwargs['amount'] *= 100 - + # Create integration log integration_request = create_request_log(kwargs, "Host", "Razorpay") - + # Setup payment otptions payment_options = { "amount": kwargs.get('amount'), @@ -773,25 +773,25 @@ 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() From 4589d1024d6b6521a18c1e49d01df97ac19a71e8 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 19 Nov 2019 12:05:13 +0530 Subject: [PATCH 094/138] Update frappe/integrations/doctype/razorpay_settings/razorpay_settings.py Co-Authored-By: Faris Ansari --- .../doctype/razorpay_settings/razorpay_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index cfd88dc2..78894834 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -452,7 +452,7 @@ def create_order(self, **kwargs): def create_order(self, **kwargs): # Creating Orders https://razorpay.com/docs/api/orders/ - # convert ruppes to paisa + # convert rupees to paisa kwargs['amount'] *= 100 # Create integration log From b7b99b2d4cff43cf660c454c12574135e3542fb0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 19 Nov 2019 12:54:47 +0530 Subject: [PATCH 095/138] Update frappe/integrations/doctype/razorpay_settings/razorpay_settings.py Co-Authored-By: Faris Ansari --- .../doctype/razorpay_settings/razorpay_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 78894834..a6885ebf 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -458,7 +458,7 @@ def create_order(self, **kwargs): # Create integration log integration_request = create_request_log(kwargs, "Host", "Razorpay") - # Setup payment otptions + # Setup payment options payment_options = { "amount": kwargs.get('amount'), "currency": kwargs.get('currency', 'INR'), From eacb0e9e8aed7973219138dcc45d913bd5e87bb0 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 8 Apr 2020 18:49:48 +0530 Subject: [PATCH 096/138] feat: blink checkout --- .../doctype/paytm_settings/checksum.py | 143 ++++++++++++++++++ payments/templates/includes/paytm_checkout.js | 42 +++++ 2 files changed, 185 insertions(+) create mode 100644 payments/payment_gateways/doctype/paytm_settings/checksum.py create mode 100644 payments/templates/includes/paytm_checkout.js diff --git a/payments/payment_gateways/doctype/paytm_settings/checksum.py b/payments/payment_gateways/doctype/paytm_settings/checksum.py new file mode 100644 index 00000000..95fb7dc1 --- /dev/null +++ b/payments/payment_gateways/doctype/paytm_settings/checksum.py @@ -0,0 +1,143 @@ +import base64 +import string +import random +import hashlib + +from Crypto.Cipher import AES + + +IV = "@@@@&&&&####$$$$" +BLOCK_SIZE = 16 + + +def generate_checksum(param_dict, merchant_key, salt=None): + params_string = __get_param_string__(param_dict) + salt = salt if salt else __id_generator__(4) + final_string = '%s|%s' % (params_string, salt) + + hasher = hashlib.sha256(final_string.encode()) + hash_string = hasher.hexdigest() + + hash_string += salt + + return __encode__(hash_string, IV, merchant_key) + + +def generate_refund_checksum(param_dict, merchant_key, salt=None): + for i in param_dict: + if("|" in param_dict[i]): + param_dict = {} + exit() + params_string = __get_param_string__(param_dict) + salt = salt if salt else __id_generator__(4) + final_string = '%s|%s' % (params_string, salt) + + hasher = hashlib.sha256(final_string.encode()) + hash_string = hasher.hexdigest() + + hash_string += salt + + return __encode__(hash_string, IV, merchant_key) + + +def generate_checksum_by_str(param_str, merchant_key, salt=None): + params_string = param_str + salt = salt if salt else __id_generator__(4) + final_string = '%s|%s' % (params_string, salt) + + hasher = hashlib.sha256(final_string.encode()) + hash_string = hasher.hexdigest() + + hash_string += salt + + return __encode__(hash_string, IV, merchant_key) + + +def verify_checksum(param_dict, merchant_key, checksum): + # Remove checksum + if 'CHECKSUMHASH' in param_dict: + param_dict.pop('CHECKSUMHASH') + + # Get salt + paytm_hash = __decode__(checksum, IV, merchant_key) + salt = paytm_hash[-4:] + calculated_checksum = generate_checksum( + param_dict, merchant_key, salt=salt) + return calculated_checksum == checksum + + +def verify_checksum_by_str(param_str, merchant_key, checksum): + # Remove checksum + # if 'CHECKSUMHASH' in param_dict: + # param_dict.pop('CHECKSUMHASH') + + # Get salt + paytm_hash = __decode__(checksum, IV, merchant_key) + salt = paytm_hash[-4:] + calculated_checksum = generate_checksum_by_str( + param_str, merchant_key, salt=salt) + return calculated_checksum == checksum + + +def __id_generator__(size=6, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase): + return ''.join(random.choice(chars) for _ in range(size)) + + +def __get_param_string__(params): + params_string = [] + for key in sorted(params.keys()): + if("REFUND" in params[key] or "|" in params[key]): + respons_dict = {} + exit() + value = params[key] + params_string.append('' if value == 'null' else str(value)) + return '|'.join(params_string) + + +def __pad__(s): return s + (BLOCK_SIZE - len(s) % + BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) + + +def __unpad__(s): return s[0:-ord(s[-1])] + + +def __encode__(to_encode, iv, key): + # Pad + to_encode = __pad__(to_encode) + # Encrypt + c = AES.new(key, AES.MODE_CBC, iv) + to_encode = c.encrypt(to_encode) + # Encode + to_encode = base64.b64encode(to_encode) + return to_encode.decode("UTF-8") + + +def __decode__(to_decode, iv, key): + # Decode + to_decode = base64.b64decode(to_decode) + # Decrypt + c = AES.new(key, AES.MODE_CBC, iv) + to_decode = c.decrypt(to_decode) + if type(to_decode) == bytes: + # convert bytes array to str. + to_decode = to_decode.decode() + # remove pad + return __unpad__(to_decode) + + +if __name__ == "__main__": + params = { + "MID": "mid", + "ORDER_ID": "order_id", + "CUST_ID": "cust_id", + "TXN_AMOUNT": "1", + "CHANNEL_ID": "WEB", + "INDUSTRY_TYPE_ID": "Retail", + "WEBSITE": "xxxxxxxxxxx" + } + + print(verify_checksum( + params, 'xxxxxxxxxxxxxxxx', + "CD5ndX8VVjlzjWbbYoAtKQIlvtXPypQYOg0Fi2AUYKXZA5XSHiRF0FDj7vQu66S8MHx9NaDZ/uYm3WBOWHf+sDQAmTyxqUipA7i1nILlxrk=")) + + # print(generate_checksum(params, "xxxxxxxxxxxxxxxx")) diff --git a/payments/templates/includes/paytm_checkout.js b/payments/templates/includes/paytm_checkout.js new file mode 100644 index 00000000..4cf070b6 --- /dev/null +++ b/payments/templates/includes/paytm_checkout.js @@ -0,0 +1,42 @@ +function onScriptLoad() { + console.log('inside on load') + var config = { + root: '', + flow: 'DEFAULT', + data: { + orderId: '{{ order_id}}', + token: '{{ token }}', + tokenType: 'TXN_TOKEN', + amount: '{{ amount }}' + }, + handler: { + notifyMerchant: function(eventName, data) { + // notify about the state of the payment page ( invalid token , session expire , cancel transaction) + console.log('notifyMerchant handler function called'); + console.log('eventName => ', eventName); + console.log('data => ', data); + }, + transactionStatus: function transactionStatus(paymentStatus) { + // provide information to merchant about the payment status. + console.log('transaction status handler function called'); + console.log('paymentStatus => ', paymentStatus); + } + } + }; + + $('.paytm-loading').addClass('hidden'); + if (window.Paytm && window.Paytm.CheckoutJS) { + window.Paytm.CheckoutJS.onLoad(function excecuteAfterCompleteLoad() { + // initialze configuration using init method + window.Paytm.CheckoutJS.init(config) + .then(function onSuccess() { + // after successfully updating configuration, invoke Blink Checkout + window.Paytm.CheckoutJS.invoke(); + }) + .catch(function onError(error) { + console.log('inside the error window') + console.log('error => ', error); + }); + }); + } +} From 4fdfbb45b13ceb25708cb70c65a730f5a5176ce2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 22 Apr 2020 11:40:08 +0530 Subject: [PATCH 097/138] fix: add finalize request to complete the transaction --- payments/templates/includes/paytm_checkout.js | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 payments/templates/includes/paytm_checkout.js diff --git a/payments/templates/includes/paytm_checkout.js b/payments/templates/includes/paytm_checkout.js deleted file mode 100644 index 4cf070b6..00000000 --- a/payments/templates/includes/paytm_checkout.js +++ /dev/null @@ -1,42 +0,0 @@ -function onScriptLoad() { - console.log('inside on load') - var config = { - root: '', - flow: 'DEFAULT', - data: { - orderId: '{{ order_id}}', - token: '{{ token }}', - tokenType: 'TXN_TOKEN', - amount: '{{ amount }}' - }, - handler: { - notifyMerchant: function(eventName, data) { - // notify about the state of the payment page ( invalid token , session expire , cancel transaction) - console.log('notifyMerchant handler function called'); - console.log('eventName => ', eventName); - console.log('data => ', data); - }, - transactionStatus: function transactionStatus(paymentStatus) { - // provide information to merchant about the payment status. - console.log('transaction status handler function called'); - console.log('paymentStatus => ', paymentStatus); - } - } - }; - - $('.paytm-loading').addClass('hidden'); - if (window.Paytm && window.Paytm.CheckoutJS) { - window.Paytm.CheckoutJS.onLoad(function excecuteAfterCompleteLoad() { - // initialze configuration using init method - window.Paytm.CheckoutJS.init(config) - .then(function onSuccess() { - // after successfully updating configuration, invoke Blink Checkout - window.Paytm.CheckoutJS.invoke(); - }) - .catch(function onError(error) { - console.log('inside the error window') - console.log('error => ', error); - }); - }); - } -} From 1cf315db177db9c96c8801154abbca11c833d627 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 22 Apr 2020 16:12:49 +0530 Subject: [PATCH 098/138] chore: remove initial validation check --- payments/payment_gateways/doctype/paytm_settings/checksum.py | 1 - 1 file changed, 1 deletion(-) diff --git a/payments/payment_gateways/doctype/paytm_settings/checksum.py b/payments/payment_gateways/doctype/paytm_settings/checksum.py index 95fb7dc1..b2e67bb5 100644 --- a/payments/payment_gateways/doctype/paytm_settings/checksum.py +++ b/payments/payment_gateways/doctype/paytm_settings/checksum.py @@ -87,7 +87,6 @@ def __get_param_string__(params): params_string = [] for key in sorted(params.keys()): if("REFUND" in params[key] or "|" in params[key]): - respons_dict = {} exit() value = params[key] params_string.append('' if value == 'null' else str(value)) From 09c8bc9404a34d33c46a3f85b7a03197e530179c Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 22 May 2020 13:31:49 +0530 Subject: [PATCH 099/138] feat: added webhook hmac verification --- .../razorpay_settings/razorpay_settings.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a6885ebf..3a379e4b 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -635,6 +635,27 @@ def cancel_subscription(self, subscription_id): except Exception: frappe.log_error(frappe.get_traceback()) + def verify_signature(self, body, signature, key): + if sys.version_info[0] == 3: + key = bytes(key, 'utf-8') + body = bytes(body, 'utf-8') + + dig = hmac.new(key=key, + msg=body, + digestmod=hashlib.sha256) + + generated_signature = dig.hexdigest() + + if sys.version_info[0:3] < (2, 7, 7): + result = self.compare_string(generated_signature, signature) + else: + result = hmac.compare_digest(generated_signature, signature) + + if not result: + frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError) + + return result + def capture_payment(is_sandbox=False, sanbox_response=None): """ Verifies the purchase as complete by the merchant. From 778472666cb61200ab00b30a18074d66ddb0bc8f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 29 May 2020 13:22:52 +0530 Subject: [PATCH 100/138] refactor: use six instead of sys.version_info --- .../doctype/razorpay_settings/razorpay_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 3a379e4b..f2c5cbf0 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -636,7 +636,7 @@ def cancel_subscription(self, subscription_id): frappe.log_error(frappe.get_traceback()) def verify_signature(self, body, signature, key): - if sys.version_info[0] == 3: + if six.PY3: key = bytes(key, 'utf-8') body = bytes(body, 'utf-8') @@ -646,7 +646,7 @@ def verify_signature(self, body, signature, key): generated_signature = dig.hexdigest() - if sys.version_info[0:3] < (2, 7, 7): + if six.PY2: result = self.compare_string(generated_signature, signature) else: result = hmac.compare_digest(generated_signature, signature) From 164c5d9fb8a378345746aa8ddb6e261da0bf56ca Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 29 May 2020 13:24:04 +0530 Subject: [PATCH 101/138] style: linting fixes --- .../doctype/razorpay_settings/razorpay_settings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index f2c5cbf0..c6a4a09e 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -640,9 +640,7 @@ 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) + dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) generated_signature = dig.hexdigest() From 9cae27dee2f4ffed65d433616283e788350873e0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 29 May 2020 14:09:05 +0530 Subject: [PATCH 102/138] feat: make verification function python 3 only --- .../doctype/razorpay_settings/razorpay_settings.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index c6a4a09e..215aac0c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -636,18 +636,13 @@ def cancel_subscription(self, subscription_id): frappe.log_error(frappe.get_traceback()) def verify_signature(self, body, signature, key): - if six.PY3: - key = bytes(key, 'utf-8') - body = bytes(body, 'utf-8') + key = bytes(key, 'utf-8') + body = bytes(body, 'utf-8') dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) generated_signature = dig.hexdigest() - - if six.PY2: - result = self.compare_string(generated_signature, signature) - else: - result = hmac.compare_digest(generated_signature, signature) + result = hmac.compare_digest(generated_signature, signature) if not result: frappe.throw(_('Razorpay Signature Verification Failed'), exc=frappe.PermissionError) From d02700379925be73803e8ec312bd5ba3dae36ba9 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 8 Jun 2020 17:06:48 +0530 Subject: [PATCH 103/138] fix: update the checksum logic --- .../doctype/paytm_settings/checksum.py | 186 ++++++------------ 1 file changed, 62 insertions(+), 124 deletions(-) diff --git a/payments/payment_gateways/doctype/paytm_settings/checksum.py b/payments/payment_gateways/doctype/paytm_settings/checksum.py index b2e67bb5..32f976ae 100644 --- a/payments/payment_gateways/doctype/paytm_settings/checksum.py +++ b/payments/payment_gateways/doctype/paytm_settings/checksum.py @@ -2,141 +2,79 @@ import string import random import hashlib +import sys from Crypto.Cipher import AES -IV = "@@@@&&&&####$$$$" +iv = '@@@@&&&&####$$$$' BLOCK_SIZE = 16 - -def generate_checksum(param_dict, merchant_key, salt=None): - params_string = __get_param_string__(param_dict) - salt = salt if salt else __id_generator__(4) - final_string = '%s|%s' % (params_string, salt) - - hasher = hashlib.sha256(final_string.encode()) - hash_string = hasher.hexdigest() - - hash_string += salt - - return __encode__(hash_string, IV, merchant_key) - - -def generate_refund_checksum(param_dict, merchant_key, salt=None): - for i in param_dict: - if("|" in param_dict[i]): - param_dict = {} - exit() - params_string = __get_param_string__(param_dict) - salt = salt if salt else __id_generator__(4) - final_string = '%s|%s' % (params_string, salt) - - hasher = hashlib.sha256(final_string.encode()) - hash_string = hasher.hexdigest() - - hash_string += salt - - return __encode__(hash_string, IV, merchant_key) - - -def generate_checksum_by_str(param_str, merchant_key, salt=None): - params_string = param_str - salt = salt if salt else __id_generator__(4) - final_string = '%s|%s' % (params_string, salt) - - hasher = hashlib.sha256(final_string.encode()) - hash_string = hasher.hexdigest() - - hash_string += salt - - return __encode__(hash_string, IV, merchant_key) - - -def verify_checksum(param_dict, merchant_key, checksum): - # Remove checksum - if 'CHECKSUMHASH' in param_dict: - param_dict.pop('CHECKSUMHASH') - - # Get salt - paytm_hash = __decode__(checksum, IV, merchant_key) +if (sys.version_info > (3, 0)): + __pad__ = lambda s: bytes(s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE), 'utf-8') +else: + __pad__ = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) + +__unpad__ = lambda s: s[0:-ord(s[-1])] + +def encrypt(input, key): + input = __pad__(input) + c = AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")) + input = c.encrypt(input) + input = base64.b64encode(input) + return input.decode("UTF-8") + +def decrypt(encrypted, key): + encrypted = base64.b64decode(encrypted) + c = AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")) + param = c.decrypt(encrypted) + if type(param) == bytes: + param = param.decode() + return __unpad__(param) + +def generateSignature(params, key): + if not type(params) is dict and not type(params) is str: + raise Exception("string or dict expected, " + str(type(params)) + " given") + if type(params) is dict: + params = getStringByParams(params) + return generateSignatureByString(params, key) + +def verifySignature(params, key, checksum): + if not type(params) is dict and not type(params) is str: + raise Exception("string or dict expected, " + str(type(params)) + " given") + if "CHECKSUMHASH" in params: + del params["CHECKSUMHASH"] + + if type(params) is dict: + params = getStringByParams(params) + return verifySignatureByString(params, key, checksum) + +def generateSignatureByString(params, key): + salt = generateRandomString(4) + return calculateChecksum(params, key, salt) + +def verifySignatureByString(params, key, checksum): + paytm_hash = decrypt(checksum, key) salt = paytm_hash[-4:] - calculated_checksum = generate_checksum( - param_dict, merchant_key, salt=salt) - return calculated_checksum == checksum + return paytm_hash == calculateHash(params, salt) +def generateRandomString(length): + chars = string.ascii_uppercase + string.digits + string.ascii_lowercase + return ''.join(random.choice(chars) for _ in range(length)) -def verify_checksum_by_str(param_str, merchant_key, checksum): - # Remove checksum - # if 'CHECKSUMHASH' in param_dict: - # param_dict.pop('CHECKSUMHASH') - - # Get salt - paytm_hash = __decode__(checksum, IV, merchant_key) - salt = paytm_hash[-4:] - calculated_checksum = generate_checksum_by_str( - param_str, merchant_key, salt=salt) - return calculated_checksum == checksum - - -def __id_generator__(size=6, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase): - return ''.join(random.choice(chars) for _ in range(size)) - - -def __get_param_string__(params): +def getStringByParams(params): params_string = [] for key in sorted(params.keys()): - if("REFUND" in params[key] or "|" in params[key]): - exit() - value = params[key] - params_string.append('' if value == 'null' else str(value)) + value = params[key] if params[key] is not None and params[key].lower() != "null" else "" + params_string.append(str(value)) return '|'.join(params_string) +def calculateHash(params, salt): + finalString = '%s|%s' % (params, salt) + hasher = hashlib.sha256(finalString.encode()) + hashString = hasher.hexdigest() + salt + return hashString -def __pad__(s): return s + (BLOCK_SIZE - len(s) % - BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) - - -def __unpad__(s): return s[0:-ord(s[-1])] - - -def __encode__(to_encode, iv, key): - # Pad - to_encode = __pad__(to_encode) - # Encrypt - c = AES.new(key, AES.MODE_CBC, iv) - to_encode = c.encrypt(to_encode) - # Encode - to_encode = base64.b64encode(to_encode) - return to_encode.decode("UTF-8") - - -def __decode__(to_decode, iv, key): - # Decode - to_decode = base64.b64decode(to_decode) - # Decrypt - c = AES.new(key, AES.MODE_CBC, iv) - to_decode = c.decrypt(to_decode) - if type(to_decode) == bytes: - # convert bytes array to str. - to_decode = to_decode.decode() - # remove pad - return __unpad__(to_decode) - - -if __name__ == "__main__": - params = { - "MID": "mid", - "ORDER_ID": "order_id", - "CUST_ID": "cust_id", - "TXN_AMOUNT": "1", - "CHANNEL_ID": "WEB", - "INDUSTRY_TYPE_ID": "Retail", - "WEBSITE": "xxxxxxxxxxx" - } - - print(verify_checksum( - params, 'xxxxxxxxxxxxxxxx', - "CD5ndX8VVjlzjWbbYoAtKQIlvtXPypQYOg0Fi2AUYKXZA5XSHiRF0FDj7vQu66S8MHx9NaDZ/uYm3WBOWHf+sDQAmTyxqUipA7i1nILlxrk=")) - - # print(generate_checksum(params, "xxxxxxxxxxxxxxxx")) +def calculateChecksum(params, key, salt): + hashString = calculateHash(params, salt) + return encrypt(hashString, key) \ No newline at end of file From 1a49f9107c9c474c6d2037ea06258f34b0ee8824 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 20 Jul 2020 23:08:50 +0530 Subject: [PATCH 104/138] fix(paytm-integration): use checksum library to generate/verify checksum --- .../doctype/paytm_settings/checksum.py | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 payments/payment_gateways/doctype/paytm_settings/checksum.py diff --git a/payments/payment_gateways/doctype/paytm_settings/checksum.py b/payments/payment_gateways/doctype/paytm_settings/checksum.py deleted file mode 100644 index 32f976ae..00000000 --- a/payments/payment_gateways/doctype/paytm_settings/checksum.py +++ /dev/null @@ -1,80 +0,0 @@ -import base64 -import string -import random -import hashlib -import sys - -from Crypto.Cipher import AES - - -iv = '@@@@&&&&####$$$$' -BLOCK_SIZE = 16 - -if (sys.version_info > (3, 0)): - __pad__ = lambda s: bytes(s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE), 'utf-8') -else: - __pad__ = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) - -__unpad__ = lambda s: s[0:-ord(s[-1])] - -def encrypt(input, key): - input = __pad__(input) - c = AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")) - input = c.encrypt(input) - input = base64.b64encode(input) - return input.decode("UTF-8") - -def decrypt(encrypted, key): - encrypted = base64.b64decode(encrypted) - c = AES.new(key.encode("utf8"), AES.MODE_CBC, iv.encode("utf8")) - param = c.decrypt(encrypted) - if type(param) == bytes: - param = param.decode() - return __unpad__(param) - -def generateSignature(params, key): - if not type(params) is dict and not type(params) is str: - raise Exception("string or dict expected, " + str(type(params)) + " given") - if type(params) is dict: - params = getStringByParams(params) - return generateSignatureByString(params, key) - -def verifySignature(params, key, checksum): - if not type(params) is dict and not type(params) is str: - raise Exception("string or dict expected, " + str(type(params)) + " given") - if "CHECKSUMHASH" in params: - del params["CHECKSUMHASH"] - - if type(params) is dict: - params = getStringByParams(params) - return verifySignatureByString(params, key, checksum) - -def generateSignatureByString(params, key): - salt = generateRandomString(4) - return calculateChecksum(params, key, salt) - -def verifySignatureByString(params, key, checksum): - paytm_hash = decrypt(checksum, key) - salt = paytm_hash[-4:] - return paytm_hash == calculateHash(params, salt) - -def generateRandomString(length): - chars = string.ascii_uppercase + string.digits + string.ascii_lowercase - return ''.join(random.choice(chars) for _ in range(length)) - -def getStringByParams(params): - params_string = [] - for key in sorted(params.keys()): - value = params[key] if params[key] is not None and params[key].lower() != "null" else "" - params_string.append(str(value)) - return '|'.join(params_string) - -def calculateHash(params, salt): - finalString = '%s|%s' % (params, salt) - hasher = hashlib.sha256(finalString.encode()) - hashString = hasher.hexdigest() + salt - return hashString - -def calculateChecksum(params, key, salt): - hashString = calculateHash(params, salt) - return encrypt(hashString, key) \ No newline at end of file From b08162cdf86bf959d4f9d7a2f6d22b05959653ee Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 4 Sep 2020 15:52:21 +0530 Subject: [PATCH 105/138] feat: add init client API --- .../doctype/razorpay_settings/razorpay_settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 215aac0c..a42794ca 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -217,6 +217,11 @@ def init_client(self): secret = self.get_password(fieldname="api_secret", raise_exception=False) self.client = razorpay.Client(auth=(self.api_key, secret)) + def init_client(self): + if self.api_key: + self.secret = self.get_password(fieldname="api_secret", raise_exception=False) + self.client = razorpay.Client(auth=(self.api_key, self.secret)) + def validate(self): create_payment_gateway("Razorpay") call_hook_method("payment_gateway_enabled", gateway="Razorpay") From 84ef0de2647081ecacea37fb8d9ad060e112e160 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 7 Sep 2020 11:36:31 +0530 Subject: [PATCH 106/138] refactor: don't attach secret to self (#11428) --- .../doctype/razorpay_settings/razorpay_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a42794ca..b632d7c2 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -219,8 +219,8 @@ def init_client(self): def init_client(self): if self.api_key: - self.secret = self.get_password(fieldname="api_secret", raise_exception=False) - self.client = razorpay.Client(auth=(self.api_key, self.secret)) + secret = self.get_password(fieldname="api_secret", raise_exception=False) + self.client = razorpay.Client(auth=(self.api_key, secret)) def validate(self): create_payment_gateway("Razorpay") From b501af8c5664134b641b65ddbb0b2bb83cc5d0c0 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 11 Nov 2020 16:31:47 +0530 Subject: [PATCH 107/138] feat(app): move /desk to /app --- payments/templates/pages/gcalendar-success.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html index 1ce9ba5b..4138644c 100644 --- a/payments/templates/pages/gcalendar-success.html +++ b/payments/templates/pages/gcalendar-success.html @@ -9,7 +9,7 @@ {{ _("Success") }}

{{ _("Your connection request to Google Calendar was successfully accepted") }}

- -{% endblock %} From 4669b22dfbfceee9b3c03120ec92ff2656f198f4 Mon Sep 17 00:00:00 2001 From: phot0n Date: Sun, 24 Jul 2022 20:13:34 +0530 Subject: [PATCH 119/138] chore: update requirements --- requirements.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7668191f..0e430d99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,6 @@ -# frappe -- https://github.com/frappe/frappe is installed via 'bench init' \ No newline at end of file +# frappe -- https://github.com/frappe/frappe is installed via 'bench init' +paytmchecksum~=1.7.0 +razorpay~=1.2.0 +stripe~=2.56.0 +braintree~=4.8.0 + From c3ecc7a2e8a759d0bc6fb634768d2fc695836cdb Mon Sep 17 00:00:00 2001 From: phot0n Date: Sun, 24 Jul 2022 23:11:45 +0530 Subject: [PATCH 120/138] feat: add payment and custom field creation utils * did a bit of cleanup via pre-commit * added custom field creation/deletion via hooks * add scheduler event for razorpay --- .../paypal_settings/paypal_settings.py | 10 +++++++-- .../doctype/paytm_settings/paytm_settings.py | 2 ++ .../razorpay_settings/razorpay_settings.py | 21 ++++++++++++++----- .../templates/pages/braintree_checkout.py | 10 ++++++--- setup.py | 4 ++-- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 8a603885..977f3f51 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -78,6 +78,8 @@ 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/frappe.integrations.doctype.paypal_settings.paypal_settings" @@ -425,7 +427,9 @@ def create_recurring_profile(token, payerid): } ) - status_changed_to = "Completed" if data.get("starting_immediately") or updating else "Verified" + 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=ZoneInfo(get_system_timezone())).astimezone(ZoneInfo("UTC")) @@ -497,7 +501,9 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): # thus could not cancel the subscription. # thus raise an exception only if the error code is not equal to 11556 - if response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556": + if ( + response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556" + ): frappe.throw(_("Failed while amending subscription")) diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index f3aa047d..4debb63b 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -23,6 +23,8 @@ from payments.utils import create_payment_gateway +from payments.utils import create_payment_gateway + class PaytmSettings(Document): supported_currencies = ("INR",) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 88d34d0e..e7f349c6 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -262,7 +262,9 @@ def setup_addon(self, settings, **kwargs): "quantity": 1 (The total amount is calculated as item.amount * quantity) } """ - url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format(kwargs.get("subscription_id")) + url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format( + kwargs.get("subscription_id") + ) try: if not frappe.conf.converted_rupee_to_paisa: @@ -276,7 +278,9 @@ def setup_addon(self, settings, **kwargs): headers={"content-type": "application/json"}, ) if not resp.get("id"): - frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") + frappe.log_error( + message=str(resp), title="Razorpay Failed while creating subscription" + ) except Exception: frappe.log_error() # failed @@ -315,7 +319,9 @@ def setup_subscription(self, settings, **kwargs): frappe.flags.status = "created" return kwargs else: - frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") + frappe.log_error( + message=str(resp), title="Razorpay Failed while creating subscription" + ) except Exception: frappe.log_error() @@ -471,7 +477,10 @@ def create_order(self, **kwargs): try: order = make_post_request( "https://api.razorpay.com/v1/orders", - auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), + auth=( + self.api_key, + self.get_password(fieldname="api_secret", raise_exception=False), + ), data=payment_options, ) order["integration_request"] = integration_request.name @@ -730,7 +739,9 @@ def get_order(doctype, docname): # 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.log_error( + frappe.get_traceback(), _("Controller method get_razorpay_order missing") + ) frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 99f141af..02cc31f1 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.integrations.doctype.braintree_settings.braintree_settings import ( - get_client_token, + get_client_token, get_gateway_controller, ) from frappe.utils import flt @@ -49,7 +49,9 @@ def get_context(context): else: frappe.redirect_to_message( _("Some information is missing"), - _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + _( + "Looks like someone sent you to an incomplete URL. Please ask them to look into it." + ), ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect @@ -62,6 +64,8 @@ def make_payment(payload_nonce, data, reference_doctype, reference_docname): data.update({"payload_nonce": payload_nonce}) gateway_controller = get_gateway_controller(reference_docname) - data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) + data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request( + data + ) frappe.db.commit() return data diff --git a/setup.py b/setup.py index 1c54de5d..c6ebbc9c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup with open("requirements.txt") as f: install_requires = f.read().strip().split("\n") @@ -15,5 +15,5 @@ packages=find_packages(), zip_safe=False, include_package_data=True, - install_requires=install_requires + install_requires=install_requires, ) From 88147e44f986a8fdc53eb5f7195570b7466a90ba Mon Sep 17 00:00:00 2001 From: phot0n Date: Sun, 24 Jul 2022 23:56:02 +0530 Subject: [PATCH 121/138] fix: import paths and doctype modules --- .../paypal_settings/paypal_settings.py | 2 +- .../stripe_settings/stripe_settings.json | 236 +++++++++--------- .../templates/pages/braintree_checkout.py | 5 +- 3 files changed, 122 insertions(+), 121 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 977f3f51..7879ea01 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -80,7 +80,7 @@ def on_payment_authorized(payment_status): from payments.utils import create_payment_gateway -api_path = "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings" +api_path = "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings" class PayPalSettings(Document): diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json index 6f016c4e..17d94347 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json @@ -265,131 +265,131 @@ "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, + "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, + "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, + "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, - "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, + "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 } ], diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 02cc31f1..31fd8b89 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -5,11 +5,12 @@ import frappe from frappe import _ -from frappe.integrations.doctype.braintree_settings.braintree_settings import ( +from frappe.utils import flt + +from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( get_client_token, get_gateway_controller, ) -from frappe.utils import flt from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( get_client_token, From 245741f13f942d200c58278f0a979cdd08b4db14 Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 25 Jul 2022 00:43:45 +0530 Subject: [PATCH 122/138] fix: method paths to accomodate new structure --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e430d99..8f7867e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ paytmchecksum~=1.7.0 razorpay~=1.2.0 stripe~=2.56.0 braintree~=4.8.0 - From b95a1d90fb6da459e5672f52ad79e74fec628227 Mon Sep 17 00:00:00 2001 From: phot0n Date: Tue, 26 Jul 2022 19:28:16 +0530 Subject: [PATCH 123/138] fix: before install hook for not installing this is being done as erpnext's patch test needs a bit more thought it currently installs v14, restores the site to v10 then starts migrating --- payments/utils/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 64e79a99..7d79f422 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -3,6 +3,7 @@ import click import frappe from frappe import _ +from frappe.utils.data import cint from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -160,6 +161,8 @@ def make_custom_fields(): create_custom_fields(custom_fields) + frappe.clear_cache(doctype="Web Form") + def delete_custom_fields(): if not frappe.get_meta("Web Form").has_field("payments_tab"): From dc73f79bb5be47973cd551adc1a67f7cd2492c0a Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 1 Aug 2022 14:23:31 +0530 Subject: [PATCH 124/138] ci: add precommit and semantic commit workflow * ci: add commitlint config file * ci: add precommit config * style: lint code --- .../braintree_settings/braintree_settings.py | 4 +++- .../doctype/paypal_settings/paypal_settings.py | 17 +++++++++++++---- .../doctype/paytm_settings/paytm_settings.py | 14 +++++++++++--- .../razorpay_settings/razorpay_settings.py | 7 ++----- payments/templates/pages/braintree_checkout.py | 4 +--- payments/templates/pages/paytm_checkout.py | 4 +++- payments/templates/pages/razorpay_checkout.py | 4 +++- payments/templates/pages/stripe_checkout.py | 8 ++++++-- payments/utils/utils.py | 3 ++- 9 files changed, 44 insertions(+), 21 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 43ec8f84..d1700689 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -224,7 +224,9 @@ def create_charge_on_braintree(self): if result.is_success: self.integration_request.db_set("status", "Completed", update_modified=False) self.flags.status_changed_to = "Completed" - self.integration_request.db_set("output", result.transaction.status, update_modified=False) + self.integration_request.db_set( + "output", result.transaction.status, update_modified=False + ) elif result.transaction: self.integration_request.db_set("status", "Failed", update_modified=False) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 7879ea01..e4ab201f 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -67,6 +67,7 @@ def on_payment_authorized(payment_status): from zoneinfo import ZoneInfo import frappe +import pytz from frappe import _ from frappe.integrations.utils import create_request_log, make_post_request from frappe.model.document import Document @@ -80,7 +81,9 @@ def on_payment_authorized(payment_status): from payments.utils import create_payment_gateway -api_path = "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings" +api_path = ( + "/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings" +) class PayPalSettings(Document): @@ -181,7 +184,9 @@ def get_payment_url(self, **kwargs): response = self.execute_set_express_checkout(**kwargs) if self.paypal_sandbox or self.use_sandbox: - return_url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" + return_url = ( + "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" + ) else: return_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" @@ -345,7 +350,9 @@ def get_express_checkout_details(token): ) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) + frappe.local.response["location"] = get_redirect_uri( + doc, token, response.get("PAYERID")[0] + ) except Exception: frappe.log_error(frappe.get_traceback()) @@ -411,7 +418,9 @@ def create_recurring_profile(token, payerid): if data.get("subscription_id"): if addons: updating = True - manage_recurring_payment_profile_status(data["subscription_id"], "Cancel", params, url) + manage_recurring_payment_profile_status( + data["subscription_id"], "Cancel", params, url + ) params.update( { diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index 4debb63b..40a5977e 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -55,7 +55,11 @@ def get_paytm_config(): paytm_config = frappe.db.get_singles_dict("Paytm Settings") paytm_config.update( - dict(merchant_key=get_decrypted_password("Paytm Settings", "Paytm Settings", "merchant_key")) + dict( + merchant_key=get_decrypted_password( + "Paytm Settings", "Paytm Settings", "merchant_key" + ) + ) ) if cint(paytm_config.staging): @@ -118,7 +122,9 @@ def verify_transaction(**paytm_params): if paytm_params and paytm_config and paytm_checksum: # Verify checksum - is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) + is_valid_checksum = verifySignature( + paytm_params, paytm_config.merchant_key, paytm_checksum + ) if is_valid_checksum and paytm_params.get("RESPCODE") == "01": verify_transaction_status(paytm_config, paytm_params["ORDERID"]) @@ -142,7 +148,9 @@ def verify_transaction_status(paytm_config, order_id): post_data = json.dumps(paytm_params) url = paytm_config.transaction_status_url - response = requests.post(url, data=post_data, headers={"Content-type": "application/json"}).json() + response = requests.post( + url, data=post_data, headers={"Content-type": "application/json"} + ).json() finalize_request(order_id, response) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index e7f349c6..2c46a65a 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -67,11 +67,8 @@ def on_payment_authorized(payment_status): import frappe import razorpay from frappe import _ -from frappe.integrations.utils import ( - create_request_log, - make_get_request, - make_post_request, -) +from frappe.integrations.utils import (create_request_log, make_get_request, + make_post_request) from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_timestamp, get_url diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 31fd8b89..d13b7e2a 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -8,9 +8,7 @@ from frappe.utils import flt from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( - get_client_token, - get_gateway_controller, -) + get_client_token, get_gateway_controller) from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( get_client_token, diff --git a/payments/templates/pages/paytm_checkout.py b/payments/templates/pages/paytm_checkout.py index f53648fc..8b101e89 100644 --- a/payments/templates/pages/paytm_checkout.py +++ b/payments/templates/pages/paytm_checkout.py @@ -21,7 +21,9 @@ def get_context(context): doc = frappe.get_doc("Integration Request", frappe.form_dict["order_id"]) - context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) + context.payment_details = get_paytm_params( + json.loads(doc.data), doc.name, paytm_config + ) context.url = paytm_config.url diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 521a6d22..0bde4c6e 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -64,7 +64,9 @@ def get_api_key(): @frappe.whitelist(allow_guest=True) -def make_payment(razorpay_payment_id, options, reference_doctype, reference_docname, token): +def make_payment( + razorpay_payment_id, options, reference_doctype, reference_docname, token +): data = {} if isinstance(options, str): diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index ad5d0ba6..3350fec5 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -53,14 +53,18 @@ def get_context(context): else: frappe.redirect_to_message( _("Some information is missing"), - _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + _( + "Looks like someone sent you to an incomplete URL. Please ask them to look into it." + ), ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect def get_api_key(doc, gateway_controller): - publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") + publishable_key = frappe.db.get_value( + "Stripe Settings", gateway_controller, "publishable_key" + ) if cint(frappe.form_dict.get("use_sandbox")): publishable_key = frappe.conf.sandbox_publishable_key diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 7d79f422..9cf5f16a 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -3,8 +3,9 @@ import click import frappe from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import \ + create_custom_fields from frappe.utils.data import cint -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def validate_integration_request(docname: str | None): From 1af0bb20d17e75087b571502a186ce1e48bd501f Mon Sep 17 00:00:00 2001 From: phot0n Date: Mon, 1 Aug 2022 15:54:22 +0530 Subject: [PATCH 125/138] ci: remove isort step from pre-commit config --- .../doctype/razorpay_settings/razorpay_settings.py | 7 +++++-- payments/templates/pages/braintree_checkout.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 2c46a65a..e7f349c6 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -67,8 +67,11 @@ def on_payment_authorized(payment_status): import frappe import razorpay from frappe import _ -from frappe.integrations.utils import (create_request_log, make_get_request, - make_post_request) +from frappe.integrations.utils import ( + create_request_log, + make_get_request, + make_post_request, +) from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_timestamp, get_url diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index d13b7e2a..280f599e 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -8,7 +8,9 @@ from frappe.utils import flt from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( - get_client_token, get_gateway_controller) + get_client_token, + get_gateway_controller, +) from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( get_client_token, From 6930d49655bd1b7cb8ad417b32400ea2aaf14617 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Mar 2023 18:53:05 +0530 Subject: [PATCH 126/138] Revert "refactor: use renamed timezone utils" --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index e4ab201f..8c91354f 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -72,7 +72,6 @@ def on_payment_authorized(payment_status): from frappe.integrations.utils import create_request_log, make_post_request from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_datetime, get_url -from frappe.utils.data import get_system_timezone from payments.utils import create_payment_gateway From f910de2f8ce312bb6020728a95e9cefd88552049 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Mon, 13 Mar 2023 01:46:36 +0530 Subject: [PATCH 127/138] Revert "Revert "refactor: use renamed timezone utils"" --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 8c91354f..e4ab201f 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -72,6 +72,7 @@ def on_payment_authorized(payment_status): from frappe.integrations.utils import create_request_log, make_post_request from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_datetime, get_url +from frappe.utils.data import get_system_timezone from payments.utils import create_payment_gateway From 54fe386a549edfaa615fa42451b2961ea1e5bf5a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 27 Jun 2023 10:54:30 +0530 Subject: [PATCH 128/138] chore: bump braintree (#34) * chore: bump braintree * ci: bump node --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f7867e8..6537eed3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ paytmchecksum~=1.7.0 razorpay~=1.2.0 stripe~=2.56.0 -braintree~=4.8.0 +braintree~=4.20.0 From 8f203f0f5bcaebbaa9c704aa2637ee5d29410caa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 2 Sep 2023 16:27:13 +0530 Subject: [PATCH 129/138] build: add pycryptodome dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6537eed3..189dbaf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ paytmchecksum~=1.7.0 razorpay~=1.2.0 stripe~=2.56.0 braintree~=4.20.0 +pycryptodome~=3.18.0 From bb8d38c5727406e6f730ead0ae74cc108bfee0ee Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 13 Sep 2023 07:45:48 +0200 Subject: [PATCH 130/138] build: switch to pyproject & flint (#42) --- MANIFEST.in | 3 +-- requirements.txt | 6 ------ setup.py | 19 ------------------- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 58f49a95..50b0f499 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include MANIFEST.in -include requirements.txt include *.json include *.md include *.py @@ -15,4 +14,4 @@ recursive-include payments *.png recursive-include payments *.py recursive-include payments *.svg recursive-include payments *.txt -recursive-exclude payments *.pyc \ No newline at end of file +recursive-exclude payments *.pyc diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 189dbaf2..00000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# frappe -- https://github.com/frappe/frappe is installed via 'bench init' -paytmchecksum~=1.7.0 -razorpay~=1.2.0 -stripe~=2.56.0 -braintree~=4.20.0 -pycryptodome~=3.18.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index c6ebbc9c..00000000 --- a/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import find_packages, setup - -with open("requirements.txt") as f: - install_requires = f.read().strip().split("\n") - -# get version from __version__ variable in pay/__init__.py -from payments import __version__ as version - -setup( - name="payments", - version=version, - description="Payments app for frappe", - author="Frappe Technologies", - author_email="hello@frappe.io", - packages=find_packages(), - zip_safe=False, - include_package_data=True, - install_requires=install_requires, -) From 8533d315a1ca1b1f800d348650cd1e2944350776 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 25 Sep 2023 11:58:56 +0530 Subject: [PATCH 131/138] style: format code --- .../braintree_settings/braintree_settings.py | 4 +--- .../paypal_settings/paypal_settings.py | 20 +++++-------------- .../doctype/paytm_settings/paytm_settings.py | 14 +++---------- .../razorpay_settings/razorpay_settings.py | 20 +++++-------------- .../templates/pages/braintree_checkout.py | 8 ++------ payments/templates/pages/paytm_checkout.py | 4 +--- payments/templates/pages/razorpay_checkout.py | 4 +--- payments/templates/pages/stripe_checkout.py | 8 ++------ 8 files changed, 20 insertions(+), 62 deletions(-) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index d1700689..43ec8f84 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -224,9 +224,7 @@ def create_charge_on_braintree(self): if result.is_success: self.integration_request.db_set("status", "Completed", update_modified=False) self.flags.status_changed_to = "Completed" - self.integration_request.db_set( - "output", result.transaction.status, update_modified=False - ) + self.integration_request.db_set("output", result.transaction.status, update_modified=False) elif result.transaction: self.integration_request.db_set("status", "Failed", update_modified=False) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index e4ab201f..3c25bec8 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -184,9 +184,7 @@ def get_payment_url(self, **kwargs): response = self.execute_set_express_checkout(**kwargs) if self.paypal_sandbox or self.use_sandbox: - return_url = ( - "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" - ) + return_url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" else: return_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" @@ -350,9 +348,7 @@ def get_express_checkout_details(token): ) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_redirect_uri( - doc, token, response.get("PAYERID")[0] - ) + frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) except Exception: frappe.log_error(frappe.get_traceback()) @@ -418,9 +414,7 @@ def create_recurring_profile(token, payerid): if data.get("subscription_id"): if addons: updating = True - manage_recurring_payment_profile_status( - data["subscription_id"], "Cancel", params, url - ) + manage_recurring_payment_profile_status(data["subscription_id"], "Cancel", params, url) params.update( { @@ -436,9 +430,7 @@ def create_recurring_profile(token, payerid): } ) - status_changed_to = ( - "Completed" if data.get("starting_immediately") or updating else "Verified" - ) + 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=ZoneInfo(get_system_timezone())).astimezone(ZoneInfo("UTC")) @@ -510,9 +502,7 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): # thus could not cancel the subscription. # thus raise an exception only if the error code is not equal to 11556 - if ( - response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556" - ): + if response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556": frappe.throw(_("Failed while amending subscription")) diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index 40a5977e..4debb63b 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -55,11 +55,7 @@ def get_paytm_config(): paytm_config = frappe.db.get_singles_dict("Paytm Settings") paytm_config.update( - dict( - merchant_key=get_decrypted_password( - "Paytm Settings", "Paytm Settings", "merchant_key" - ) - ) + dict(merchant_key=get_decrypted_password("Paytm Settings", "Paytm Settings", "merchant_key")) ) if cint(paytm_config.staging): @@ -122,9 +118,7 @@ def verify_transaction(**paytm_params): if paytm_params and paytm_config and paytm_checksum: # Verify checksum - is_valid_checksum = verifySignature( - paytm_params, paytm_config.merchant_key, paytm_checksum - ) + is_valid_checksum = verifySignature(paytm_params, paytm_config.merchant_key, paytm_checksum) if is_valid_checksum and paytm_params.get("RESPCODE") == "01": verify_transaction_status(paytm_config, paytm_params["ORDERID"]) @@ -148,9 +142,7 @@ def verify_transaction_status(paytm_config, order_id): post_data = json.dumps(paytm_params) url = paytm_config.transaction_status_url - response = requests.post( - url, data=post_data, headers={"Content-type": "application/json"} - ).json() + response = requests.post(url, data=post_data, headers={"Content-type": "application/json"}).json() finalize_request(order_id, response) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index e7f349c6..f498b294 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -262,9 +262,7 @@ def setup_addon(self, settings, **kwargs): "quantity": 1 (The total amount is calculated as item.amount * quantity) } """ - url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format( - kwargs.get("subscription_id") - ) + url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format(kwargs.get("subscription_id")) try: if not frappe.conf.converted_rupee_to_paisa: @@ -278,9 +276,7 @@ def setup_addon(self, settings, **kwargs): headers={"content-type": "application/json"}, ) if not resp.get("id"): - frappe.log_error( - message=str(resp), title="Razorpay Failed while creating subscription" - ) + frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") except Exception: frappe.log_error() # failed @@ -319,9 +315,7 @@ def setup_subscription(self, settings, **kwargs): frappe.flags.status = "created" return kwargs else: - frappe.log_error( - message=str(resp), title="Razorpay Failed while creating subscription" - ) + frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") except Exception: frappe.log_error() @@ -696,9 +690,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None): if resp.get("status") == "authorized": resp = make_post_request( - "https://api.razorpay.com/v1/payments/{}/capture".format( - data.get("razorpay_payment_id") - ), + "https://api.razorpay.com/v1/payments/{}/capture".format(data.get("razorpay_payment_id")), auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")}, ) @@ -739,9 +731,7 @@ def get_order(doctype, docname): # 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.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 280f599e..bb7aa6dd 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -50,9 +50,7 @@ def get_context(context): else: frappe.redirect_to_message( _("Some information is missing"), - _( - "Looks like someone sent you to an incomplete URL. Please ask them to look into it." - ), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect @@ -65,8 +63,6 @@ def make_payment(payload_nonce, data, reference_doctype, reference_docname): data.update({"payload_nonce": payload_nonce}) gateway_controller = get_gateway_controller(reference_docname) - data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request( - data - ) + data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) frappe.db.commit() return data diff --git a/payments/templates/pages/paytm_checkout.py b/payments/templates/pages/paytm_checkout.py index 8b101e89..f53648fc 100644 --- a/payments/templates/pages/paytm_checkout.py +++ b/payments/templates/pages/paytm_checkout.py @@ -21,9 +21,7 @@ def get_context(context): doc = frappe.get_doc("Integration Request", frappe.form_dict["order_id"]) - context.payment_details = get_paytm_params( - json.loads(doc.data), doc.name, paytm_config - ) + context.payment_details = get_paytm_params(json.loads(doc.data), doc.name, paytm_config) context.url = paytm_config.url diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 0bde4c6e..521a6d22 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -64,9 +64,7 @@ def get_api_key(): @frappe.whitelist(allow_guest=True) -def make_payment( - razorpay_payment_id, options, reference_doctype, reference_docname, token -): +def make_payment(razorpay_payment_id, options, reference_doctype, reference_docname, token): data = {} if isinstance(options, str): diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 3350fec5..ad5d0ba6 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -53,18 +53,14 @@ def get_context(context): else: frappe.redirect_to_message( _("Some information is missing"), - _( - "Looks like someone sent you to an incomplete URL. Please ask them to look into it." - ), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect def get_api_key(doc, gateway_controller): - publishable_key = frappe.db.get_value( - "Stripe Settings", gateway_controller, "publishable_key" - ) + publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") if cint(frappe.form_dict.get("use_sandbox")): publishable_key = frappe.conf.sandbox_publishable_key From 68f53ce56b34c92e1d2e849090f2a438f5c1c948 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 27 Sep 2023 11:03:15 +0530 Subject: [PATCH 132/138] refactor: add import guard for erpnext --- payments/utils/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/utils/__init__.py b/payments/utils/__init__.py index 1a494cb7..21e8ffde 100644 --- a/payments/utils/__init__.py +++ b/payments/utils/__init__.py @@ -5,4 +5,5 @@ erpnext_app_import_guard, get_payment_gateway_controller, make_custom_fields, + erpnext_app_import_guard, ) From 5a8f607797384bf2df07afde9011cbaa8685ccdf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 18 Oct 2023 12:19:33 +0530 Subject: [PATCH 133/138] chore: loosen pycryptodome and install mariadb --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d94a1f5..782fb388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,9 @@ jobs: - name: Clone uses: actions/checkout@v4 + - name: Install MariaDB Client + run: sudo apt-get -y install mariadb-client-10.6 + - name: Setup Python uses: actions/setup-python@v5 with: From 0658ac195c47e68b2405f53f34676b1648352843 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Nov 2024 17:58:43 +0530 Subject: [PATCH 134/138] refactor: throw for cancelled integration requests --- payments/utils/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/payments/utils/utils.py b/payments/utils/utils.py index 9cf5f16a..7a155b49 100644 --- a/payments/utils/utils.py +++ b/payments/utils/utils.py @@ -8,6 +8,11 @@ from frappe.utils.data import cint +def validate_integration_request(docname: str | None): + if frappe.db.get_value("Integration Request", docname, "status") == "Cancelled": + frappe.throw(_("Expired Token")) + + def validate_integration_request(docname: str | None): if frappe.db.get_value("Integration Request", docname, "status") == "Cancelled": frappe.throw(_("Expired Token")) From a30697885b56058b1bd96f659ac6aca194a5e0e1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Oct 2024 14:44:39 +0530 Subject: [PATCH 135/138] refactor: clear API details --- .../doctype/razorpay_settings/razorpay_settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index f498b294..50d34742 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -659,6 +659,13 @@ def verify_signature(self, body, signature, key): 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() + def capture_payment(is_sandbox=False, sanbox_response=None): """ From 95e14adb24d6c0a968b1f532bb6741ec1e1b567d Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 18 Mar 2025 16:05:27 +0530 Subject: [PATCH 136/138] fix: stripe checkout --- payments/templates/pages/stripe_checkout.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index ad5d0ba6..4beb0b61 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -51,6 +51,11 @@ def get_context(context): context["amount"] = context["amount"] + " " + _(recurrence) else: + frappe.log_error( + "Missing keys in form_dict", + "Expected keys: {0}," + "Received keys: {1}".format(expected_keys, list(frappe.form_dict)), + ) frappe.redirect_to_message( _("Some information is missing"), _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), From bcd196eff731e9ece1a97fcbd2b9465d4afb4509 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Tue, 18 Mar 2025 16:09:03 +0530 Subject: [PATCH 137/138] chore: fixed linters --- payments/templates/pages/stripe_checkout.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 4beb0b61..e4256867 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -53,8 +53,7 @@ def get_context(context): else: frappe.log_error( "Missing keys in form_dict", - "Expected keys: {0}," - "Received keys: {1}".format(expected_keys, list(frappe.form_dict)), + "Expected keys: {}," "Received keys: {}".format(expected_keys, list(frappe.form_dict)), ) frappe.redirect_to_message( _("Some information is missing"), @@ -78,7 +77,9 @@ def get_header_image(doc, gateway_controller): @frappe.whitelist(allow_guest=True) -def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None): +def make_payment( + stripe_token_id, data, reference_doctype=None, reference_docname=None, payment_gateway=None +): data = json.loads(data) data.update({"stripe_token_id": stripe_token_id}) From a224c1b0172c3a78e4b3700e0225783f82e682d2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 6 May 2025 17:08:17 +0530 Subject: [PATCH 138/138] chore: fix broken CI bump all github actions and bring them to parity with erpnext (cherry picked from commit 70d8ac74937e2a7a5709b9d62204fda3305c1d26) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 782fb388..144dbbd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@v4 - name: Install MariaDB Client - run: sudo apt-get -y install mariadb-client-10.6 + run: sudo apt-get -y install mariadb-client - name: Setup Python uses: actions/setup-python@v5