@@ -0,0 +1,8 @@ | |||||
.DS_Store | |||||
*.pyc | |||||
*.egg-info | |||||
*.swp | |||||
tags | |||||
payments/docs/current | |||||
node_modules/ | |||||
__pycache__/ |
@@ -0,0 +1,45 @@ | |||||
exclude: 'node_modules|.git' | |||||
default_stages: [commit] | |||||
fail_fast: false | |||||
repos: | |||||
- repo: https://github.com/pre-commit/pre-commit-hooks | |||||
rev: v4.3.0 | |||||
hooks: | |||||
- id: trailing-whitespace | |||||
files: "payments.*" | |||||
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" | |||||
- id: check-yaml | |||||
- id: no-commit-to-branch | |||||
args: ['--branch', 'develop'] | |||||
- id: check-merge-conflict | |||||
- id: check-ast | |||||
- id: check-json | |||||
- id: check-toml | |||||
- id: check-yaml | |||||
- id: debug-statements | |||||
- repo: https://github.com/asottile/pyupgrade | |||||
rev: v2.34.0 | |||||
hooks: | |||||
- id: pyupgrade | |||||
args: ['--py310-plus'] | |||||
- repo: https://github.com/adityahase/black | |||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119 | |||||
hooks: | |||||
- id: black | |||||
additional_dependencies: ['click==8.0.4'] | |||||
- repo: https://gitlab.com/pycqa/flake8 | |||||
rev: 3.9.2 | |||||
hooks: | |||||
- id: flake8 | |||||
additional_dependencies: ['flake8-bugbear',] | |||||
args: ['--config', '.github/helper/flake8.conf'] | |||||
ci: | |||||
autoupdate_schedule: weekly | |||||
skip: [] | |||||
submodules: false |
@@ -0,0 +1,18 @@ | |||||
include MANIFEST.in | |||||
include requirements.txt | |||||
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 |
@@ -0,0 +1,33 @@ | |||||
# Payments | |||||
A payments app for influxframework. | |||||
## Installation | |||||
1. Install [bench & influxframework](https://influxframework.com/docs/v14/user/en/installation). | |||||
2. Once setup is complete, add the payments app to your bench by running | |||||
``` | |||||
$ bench get-app payments | |||||
``` | |||||
3. Install the payments app on the required site by running | |||||
``` | |||||
$ bench --site <sitename> install-app payments | |||||
``` | |||||
## App Structure & Details | |||||
App has 2 modules - Payments and Payment Gateways. | |||||
Payment Module contains the Payment Gateway DocType which creates links for the payment gateways and Payment Gateways Module contain all the Payment Gateway (Razorpay, Stripe, Braintree, Paypal, PayTM) DocTypes. | |||||
App adds custom fields to Web Form for facilitating payments upon installation and removes them upon uninstallation. | |||||
All general utils are stored in [utils](payments/utils) directory. The utils are written in [utils.py](payments/utils/utils.py) and then imported into the [`__init__.py`](payments/utils/__init__.py) file for easier importing/namespacing. | |||||
[overrides](payments/overrides) directory has all the overrides for overriding standard influxframework code. Currently it overrides WebForm DocType controller as well as a WebForm whitelisted method. | |||||
[templates](payments/templates) directory has all the payment gateways' custom checkout pages. | |||||
# | |||||
## License | |||||
MIT ([license.txt](license.txt)) |
@@ -0,0 +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', | |||||
], | |||||
], | |||||
}, | |||||
}; |
@@ -0,0 +1,21 @@ | |||||
The MIT License | |||||
Copyright (c) 2016-2021 InfluxFramework LLC <developers@influxframework.com> | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in | |||||
all copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
THE SOFTWARE. |
@@ -0,0 +1 @@ | |||||
__version__ = "0.0.1" |
@@ -0,0 +1,5 @@ | |||||
from influxframework import _ | |||||
def get_data(): | |||||
return [{"module_name": "Payments", "type": "module", "label": _("Payments")}] |
@@ -0,0 +1,11 @@ | |||||
""" | |||||
Configuration for docs | |||||
""" | |||||
# source_link = "https://github.com/[org_name]/payments" | |||||
# headline = "App that does everything" | |||||
# sub_heading = "Yes, you got that right the first time, everything" | |||||
def get_context(context): | |||||
context.brand_html = "Payments" |
@@ -0,0 +1,183 @@ | |||||
from . import __version__ as app_version | |||||
app_name = "payments" | |||||
app_title = "Payments" | |||||
app_publisher = "InfluxFramework LLC" | |||||
app_description = "Payments app for influxframework" | |||||
app_email = "hello@influxframework.com" | |||||
app_license = "MIT" | |||||
# Includes in <head> | |||||
# ------------------ | |||||
# include js, css files in header of desk.html | |||||
# app_include_css = "/assets/pay/css/pay.css" | |||||
# app_include_js = "/assets/pay/js/pay.js" | |||||
# include js, css files in header of web template | |||||
# web_include_css = "/assets/pay/css/pay.css" | |||||
# web_include_js = "/assets/pay/js/pay.js" | |||||
# include custom scss in every website theme (without file extension ".scss") | |||||
# website_theme_scss = "pay/public/scss/website" | |||||
# include js, css files in header of web form | |||||
# webform_include_js = {"doctype": "public/js/doctype.js"} | |||||
# webform_include_css = {"doctype": "public/css/doctype.css"} | |||||
# include js in page | |||||
# page_js = {"page" : "public/js/file.js"} | |||||
# include js in doctype views | |||||
# doctype_js = {"doctype" : "public/js/doctype.js"} | |||||
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} | |||||
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} | |||||
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} | |||||
# Home Pages | |||||
# ---------- | |||||
# application home page (will override Website Settings) | |||||
# home_page = "login" | |||||
# website user home page (by Role) | |||||
# role_home_page = { | |||||
# "Role": "home_page" | |||||
# } | |||||
# Generators | |||||
# ---------- | |||||
# automatically create page for each record of this doctype | |||||
# website_generators = ["Web Page"] | |||||
# Jinja | |||||
# ---------- | |||||
# add methods and filters to jinja environment | |||||
# jinja = { | |||||
# "methods": "pay.utils.jinja_methods", | |||||
# "filters": "pay.utils.jinja_filters" | |||||
# } | |||||
# Installation | |||||
# ------------ | |||||
before_install = "payments.utils.before_install" | |||||
after_install = "payments.utils.make_custom_fields" | |||||
# Uninstallation | |||||
# ------------ | |||||
before_uninstall = "payments.utils.delete_custom_fields" | |||||
# after_uninstall = "pay.uninstall.after_uninstall" | |||||
# Desk Notifications | |||||
# ------------------ | |||||
# See influxframework.core.notifications.get_notification_config | |||||
# notification_config = "pay.notifications.get_notification_config" | |||||
# Permissions | |||||
# ----------- | |||||
# Permissions evaluated in scripted ways | |||||
# permission_query_conditions = { | |||||
# "Event": "influxframework.desk.doctype.event.event.get_permission_query_conditions", | |||||
# } | |||||
# | |||||
# has_permission = { | |||||
# "Event": "influxframework.desk.doctype.event.event.has_permission", | |||||
# } | |||||
# DocType Class | |||||
# --------------- | |||||
# Override standard doctype classes | |||||
override_doctype_class = { | |||||
"Web Form": "payments.overrides.payment_webform.PaymentWebForm" | |||||
} | |||||
# Document Events | |||||
# --------------- | |||||
# Hook on document methods and events | |||||
# doc_events = { | |||||
# "*": { | |||||
# "on_update": "method", | |||||
# "on_cancel": "method", | |||||
# "on_trash": "method" | |||||
# } | |||||
# } | |||||
# Scheduled Tasks | |||||
# --------------- | |||||
scheduler_events = { | |||||
"all": [ | |||||
"payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.capture_payment", | |||||
], | |||||
} | |||||
# Testing | |||||
# ------- | |||||
# before_tests = "pay.install.before_tests" | |||||
# Overriding Methods | |||||
# ------------------------------ | |||||
# | |||||
override_whitelisted_methods = { | |||||
"influxframework.website.doctype.web_form.web_form.accept": "payments.overrides.payment_webform.accept" | |||||
} | |||||
# | |||||
# each overriding function accepts a `data` argument; | |||||
# generated from the base implementation of the doctype dashboard, | |||||
# along with any modifications made in other InfluxFramework apps | |||||
# override_doctype_dashboards = { | |||||
# "Task": "pay.task.get_dashboard_data" | |||||
# } | |||||
# exempt linked doctypes from being automatically cancelled | |||||
# | |||||
# auto_cancel_exempted_doctypes = ["Auto Repeat"] | |||||
# User Data Protection | |||||
# -------------------- | |||||
# user_data_fields = [ | |||||
# { | |||||
# "doctype": "{doctype_1}", | |||||
# "filter_by": "{filter_by}", | |||||
# "redact_fields": ["{field_1}", "{field_2}"], | |||||
# "partial": 1, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_2}", | |||||
# "filter_by": "{filter_by}", | |||||
# "partial": 1, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_3}", | |||||
# "strict": False, | |||||
# }, | |||||
# { | |||||
# "doctype": "{doctype_4}" | |||||
# } | |||||
# ] | |||||
# Authentication and authorization | |||||
# -------------------------------- | |||||
# auth_hooks = [ | |||||
# "pay.auth.validate" | |||||
# ] | |||||
# Translation | |||||
# -------------------------------- | |||||
# Make link fields search translated document names for these DocTypes | |||||
# Recommended only for DocTypes which have limited documents with untranslated names | |||||
# For example: Role, Gender, etc. | |||||
# translated_search_doctypes = [] |
@@ -0,0 +1,2 @@ | |||||
Payments | |||||
Payment Gateways |
@@ -0,0 +1,157 @@ | |||||
import json | |||||
import influxframework | |||||
from influxframework.core.doctype.file import remove_file_by_url | |||||
from influxframework.rate_limiter import rate_limit | |||||
from influxframework.website.doctype.web_form.web_form import WebForm | |||||
from payments.utils import get_payment_gateway_controller | |||||
class PaymentWebForm(WebForm): | |||||
def validate(self): | |||||
super().validate() | |||||
if getattr(self, "accept_payment", False): | |||||
self.validate_payment_amount() | |||||
def validate_payment_amount(self): | |||||
if self.amount_based_on_field and not self.amount_field: | |||||
influxframework.throw(influxframework._("Please select a Amount Field.")) | |||||
elif not self.amount_based_on_field and not self.amount > 0: | |||||
influxframework.throw(influxframework._("Amount must be greater than 0.")) | |||||
def get_payment_gateway_url(self, doc): | |||||
if getattr(self, "accept_payment", False): | |||||
controller = get_payment_gateway_controller(self.payment_gateway) | |||||
title = f"Payment for {doc.doctype} {doc.name}" | |||||
amount = self.amount | |||||
if self.amount_based_on_field: | |||||
amount = doc.get(self.amount_field) | |||||
from decimal import Decimal | |||||
if amount is None or Decimal(amount) <= 0: | |||||
return influxframework.utils.get_url(self.success_url or self.route) | |||||
payment_details = { | |||||
"amount": amount, | |||||
"title": title, | |||||
"description": title, | |||||
"reference_doctype": doc.doctype, | |||||
"reference_docname": doc.name, | |||||
"payer_email": influxframework.session.user, | |||||
"payer_name": influxframework.utils.get_fullname(influxframework.session.user), | |||||
"order_id": doc.name, | |||||
"currency": self.currency, | |||||
"redirect_to": influxframework.utils.get_url(self.success_url or self.route), | |||||
} | |||||
# Redirect the user to this url | |||||
return controller.get_payment_url(**payment_details) | |||||
@influxframework.whitelist(allow_guest=True) | |||||
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) | |||||
def accept(web_form, data, docname=None, for_payment=False): | |||||
"""Save the web form""" | |||||
data = influxframework._dict(json.loads(data)) | |||||
for_payment = influxframework.parse_json(for_payment) | |||||
files = [] | |||||
files_to_delete = [] | |||||
web_form = influxframework.get_doc("Web Form", web_form) | |||||
if data.name and not web_form.allow_edit: | |||||
influxframework.throw(influxframework._("You are not allowed to update this Web Form Document")) | |||||
influxframework.flags.in_web_form = True | |||||
meta = influxframework.get_meta(data.doctype) | |||||
if docname: | |||||
# update | |||||
doc = influxframework.get_doc(data.doctype, docname) | |||||
else: | |||||
# insert | |||||
doc = influxframework.new_doc(data.doctype) | |||||
# set values | |||||
for field in web_form.web_form_fields: | |||||
fieldname = field.fieldname | |||||
df = meta.get_field(fieldname) | |||||
value = data.get(fieldname, None) | |||||
if df and df.fieldtype in ("Attach", "Attach Image"): | |||||
if value and "data:" and "base64" in value: | |||||
files.append((fieldname, value)) | |||||
if not doc.name: | |||||
doc.set(fieldname, "") | |||||
continue | |||||
elif not value and doc.get(fieldname): | |||||
files_to_delete.append(doc.get(fieldname)) | |||||
doc.set(fieldname, value) | |||||
if for_payment: | |||||
web_form.validate_mandatory(doc) | |||||
doc.run_method("validate_payment") | |||||
if doc.name: | |||||
if web_form.has_web_form_permission(doc.doctype, doc.name, "write"): | |||||
doc.save(ignore_permissions=True) | |||||
else: | |||||
# only if permissions are present | |||||
doc.save() | |||||
else: | |||||
# insert | |||||
if web_form.login_required and influxframework.session.user == "Guest": | |||||
influxframework.throw(influxframework._("You must login to submit this form")) | |||||
ignore_mandatory = True if files else False | |||||
doc.insert(ignore_permissions=True, ignore_mandatory=ignore_mandatory) | |||||
# add files | |||||
if files: | |||||
for f in files: | |||||
fieldname, filedata = f | |||||
# remove earlier attached file (if exists) | |||||
if doc.get(fieldname): | |||||
remove_file_by_url(doc.get(fieldname), doctype=doc.doctype, name=doc.name) | |||||
# save new file | |||||
filename, dataurl = filedata.split(",", 1) | |||||
_file = influxframework.get_doc( | |||||
{ | |||||
"doctype": "File", | |||||
"file_name": filename, | |||||
"attached_to_doctype": doc.doctype, | |||||
"attached_to_name": doc.name, | |||||
"content": dataurl, | |||||
"decode": True, | |||||
} | |||||
) | |||||
_file.save() | |||||
# update values | |||||
doc.set(fieldname, _file.file_url) | |||||
doc.save(ignore_permissions=True) | |||||
if files_to_delete: | |||||
for f in files_to_delete: | |||||
if f: | |||||
remove_file_by_url(f, doctype=doc.doctype, name=doc.name) | |||||
influxframework.flags.web_form_doc = doc | |||||
if for_payment: | |||||
# this is needed for Payments app | |||||
return web_form.get_payment_gateway_url(doc) | |||||
else: | |||||
return doc |
@@ -0,0 +1,6 @@ | |||||
// Copyright (c) 2018, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('Braintree Settings', { | |||||
}); |
@@ -0,0 +1,273 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "field:gateway_name", | |||||
"beta": 0, | |||||
"creation": "2018-02-05 13:46:12.101852", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_name", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Payment Gateway Name", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Company", | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "section_break_2", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "merchant_id", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Merchant ID", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "public_key", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Public Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "private_key", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Private Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "use_sandbox", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Use Sandbox", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "header_img", | |||||
"fieldtype": "Attach Image", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Header Image", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
} | |||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2022-07-24 14:33:06.050377", | |||||
"modified_by": "Administrator", | |||||
"module": "Payment Gateways", | |||||
"name": "Braintree Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 0, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,293 @@ | |||||
# Copyright (c) 2018, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
from urllib.parse import urlencode | |||||
import braintree | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.integrations.utils import create_request_log | |||||
from influxframework.model.document import Document | |||||
from influxframework.utils import call_hook_method, get_url | |||||
from payments.utils import create_payment_gateway | |||||
class BraintreeSettings(Document): | |||||
supported_currencies = [ | |||||
"AED", | |||||
"AMD", | |||||
"AOA", | |||||
"ARS", | |||||
"AUD", | |||||
"AWG", | |||||
"AZN", | |||||
"BAM", | |||||
"BBD", | |||||
"BDT", | |||||
"BGN", | |||||
"BIF", | |||||
"BMD", | |||||
"BND", | |||||
"BOB", | |||||
"BRL", | |||||
"BSD", | |||||
"BWP", | |||||
"BYN", | |||||
"BZD", | |||||
"CAD", | |||||
"CHF", | |||||
"CLP", | |||||
"CNY", | |||||
"COP", | |||||
"CRC", | |||||
"CVE", | |||||
"CZK", | |||||
"DJF", | |||||
"DKK", | |||||
"DOP", | |||||
"DZD", | |||||
"EGP", | |||||
"ETB", | |||||
"EUR", | |||||
"FJD", | |||||
"FKP", | |||||
"GBP", | |||||
"GEL", | |||||
"GHS", | |||||
"GIP", | |||||
"GMD", | |||||
"GNF", | |||||
"GTQ", | |||||
"GYD", | |||||
"HKD", | |||||
"HNL", | |||||
"HRK", | |||||
"HTG", | |||||
"HUF", | |||||
"IDR", | |||||
"ILS", | |||||
"INR", | |||||
"ISK", | |||||
"JMD", | |||||
"JPY", | |||||
"KES", | |||||
"KGS", | |||||
"KHR", | |||||
"KMF", | |||||
"KRW", | |||||
"KYD", | |||||
"KZT", | |||||
"LAK", | |||||
"LBP", | |||||
"LKR", | |||||
"LRD", | |||||
"LSL", | |||||
"LTL", | |||||
"MAD", | |||||
"MDL", | |||||
"MKD", | |||||
"MNT", | |||||
"MOP", | |||||
"MUR", | |||||
"MVR", | |||||
"MWK", | |||||
"MXN", | |||||
"MYR", | |||||
"MZN", | |||||
"NAD", | |||||
"NGN", | |||||
"NIO", | |||||
"NOK", | |||||
"NPR", | |||||
"NZD", | |||||
"PAB", | |||||
"PEN", | |||||
"PGK", | |||||
"PHP", | |||||
"PKR", | |||||
"PLN", | |||||
"PYG", | |||||
"QAR", | |||||
"RON", | |||||
"RSD", | |||||
"RUB", | |||||
"RWF", | |||||
"SAR", | |||||
"SBD", | |||||
"SCR", | |||||
"SEK", | |||||
"SGD", | |||||
"SHP", | |||||
"SLL", | |||||
"SOS", | |||||
"SRD", | |||||
"STD", | |||||
"SVC", | |||||
"SYP", | |||||
"SZL", | |||||
"THB", | |||||
"TJS", | |||||
"TOP", | |||||
"TRY", | |||||
"TTD", | |||||
"TWD", | |||||
"TZS", | |||||
"UAH", | |||||
"UGX", | |||||
"USD", | |||||
"UYU", | |||||
"UZS", | |||||
"VEF", | |||||
"VND", | |||||
"VUV", | |||||
"WST", | |||||
"XAF", | |||||
"XCD", | |||||
"XOF", | |||||
"XPF", | |||||
"YER", | |||||
"ZAR", | |||||
"ZMK", | |||||
"ZWD", | |||||
] | |||||
def validate(self): | |||||
if not self.flags.ignore_mandatory: | |||||
self.configure_braintree() | |||||
def on_update(self): | |||||
create_payment_gateway( | |||||
"Braintree-" + self.gateway_name, | |||||
settings="Braintree Settings", | |||||
controller=self.gateway_name, | |||||
) | |||||
call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) | |||||
def configure_braintree(self): | |||||
if self.use_sandbox: | |||||
environment = "sandbox" | |||||
else: | |||||
environment = "production" | |||||
braintree.Configuration.configure( | |||||
environment=environment, | |||||
merchant_id=self.merchant_id, | |||||
public_key=self.public_key, | |||||
private_key=self.get_password(fieldname="private_key", raise_exception=False), | |||||
) | |||||
def validate_transaction_currency(self, currency): | |||||
if currency not in self.supported_currencies: | |||||
influxframework.throw( | |||||
_( | |||||
"Please select another payment method. Stripe does not support transactions in currency '{0}'" | |||||
).format(currency) | |||||
) | |||||
def get_payment_url(self, **kwargs): | |||||
return get_url(f"./braintree_checkout?{urlencode(kwargs)}") | |||||
def create_payment_request(self, data): | |||||
self.data = influxframework._dict(data) | |||||
try: | |||||
self.integration_request = create_request_log(self.data, service_name="Braintree") | |||||
return self.create_charge_on_braintree() | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
return { | |||||
"redirect_to": influxframework.redirect_to_message( | |||||
_("Server Error"), | |||||
_( | |||||
"There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account." | |||||
), | |||||
), | |||||
"status": 401, | |||||
} | |||||
def create_charge_on_braintree(self): | |||||
self.configure_braintree() | |||||
redirect_to = self.data.get("redirect_to") or None | |||||
redirect_message = self.data.get("redirect_message") or None | |||||
result = braintree.Transaction.sale( | |||||
{ | |||||
"amount": self.data.amount, | |||||
"payment_method_nonce": self.data.payload_nonce, | |||||
"options": {"submit_for_settlement": True}, | |||||
} | |||||
) | |||||
if result.is_success: | |||||
self.integration_request.db_set("status", "Completed", update_modified=False) | |||||
self.flags.status_changed_to = "Completed" | |||||
self.integration_request.db_set( | |||||
"output", result.transaction.status, update_modified=False | |||||
) | |||||
elif result.transaction: | |||||
self.integration_request.db_set("status", "Failed", update_modified=False) | |||||
error_log = influxframework.log_error( | |||||
"code: " | |||||
+ str(result.transaction.processor_response_code) | |||||
+ " | text: " | |||||
+ str(result.transaction.processor_response_text), | |||||
"Braintree Payment Error", | |||||
) | |||||
self.integration_request.db_set("error", error_log.error, update_modified=False) | |||||
else: | |||||
self.integration_request.db_set("status", "Failed", update_modified=False) | |||||
for error in result.errors.deep_errors: | |||||
error_log = influxframework.log_error( | |||||
"code: " + str(error.code) + " | message: " + str(error.message), | |||||
"Braintree Payment Error", | |||||
) | |||||
self.integration_request.db_set("error", error_log.error, update_modified=False) | |||||
if self.flags.status_changed_to == "Completed": | |||||
status = "Completed" | |||||
if self.data.reference_doctype and self.data.reference_docname: | |||||
custom_redirect_to = None | |||||
try: | |||||
custom_redirect_to = influxframework.get_doc( | |||||
self.data.reference_doctype, self.data.reference_docname | |||||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||||
braintree_success_page = influxframework.get_hooks("braintree_success_page") | |||||
if braintree_success_page: | |||||
custom_redirect_to = influxframework.get_attr(braintree_success_page[-1])(self.data) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = "payment-success" | |||||
else: | |||||
status = "Error" | |||||
redirect_url = "payment-failed" | |||||
if redirect_to: | |||||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||||
return {"redirect_to": redirect_url, "status": status} | |||||
def get_gateway_controller(doc): | |||||
payment_request = influxframework.get_doc("Payment Request", doc) | |||||
gateway_controller = influxframework.db.get_value( | |||||
"Payment Gateway", payment_request.payment_gateway, "gateway_controller" | |||||
) | |||||
return gateway_controller | |||||
def get_client_token(doc): | |||||
gateway_controller = get_gateway_controller(doc) | |||||
settings = influxframework.get_doc("Braintree Settings", gateway_controller) | |||||
settings.configure_braintree() | |||||
return braintree.ClientToken.generate() |
@@ -0,0 +1,7 @@ | |||||
# Copyright (c) 2018, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import unittest | |||||
class TestBraintreeSettings(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('PayPal Settings', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,202 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2016-09-21 08:03:01.009852", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "System", | |||||
"editable_grid": 1, | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "api_username", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "API Username", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "api_password", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "API Password", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "signature", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Signature", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Check this if you are testing your payment using the Sandbox API", | |||||
"fieldname": "paypal_sandbox", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Use Sandbox", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Mention transaction completion page URL", | |||||
"fieldname": "redirect_to", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Redirect To", | |||||
"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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 1, | |||||
"is_submittable": 0, | |||||
"issingle": 1, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2022-07-24 14:40:31.574789", | |||||
"modified_by": "Administrator", | |||||
"module": "Payment Gateways", | |||||
"name": "PayPal Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 1, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,532 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
""" | |||||
# Integrating PayPal | |||||
### 1. Validate Currency Support | |||||
Example: | |||||
from payments.utils import get_payment_gateway_controller | |||||
controller = get_payment_gateway_controller("PayPal") | |||||
controller().validate_transaction_currency(currency) | |||||
### 2. Redirect for payment | |||||
Example: | |||||
payment_details = { | |||||
"amount": 600, | |||||
"title": "Payment for bill : 111", | |||||
"description": "payment via cart", | |||||
"reference_doctype": "Payment Request", | |||||
"reference_docname": "PR0001", | |||||
"payer_email": "NuranVerkleij@example.com", | |||||
"payer_name": "Nuran Verkleij", | |||||
"order_id": "111", | |||||
"currency": "USD", | |||||
"payment_gateway": "Razorpay", | |||||
"subscription_details": { | |||||
"plan_id": "plan_12313", # if Required | |||||
"start_date": "2018-08-30", | |||||
"billing_period": "Month" #(Day, Week, SemiMonth, Month, Year), | |||||
"billing_frequency": 1, | |||||
"customer_notify": 1, | |||||
"upfront_amount": 1000 | |||||
} | |||||
} | |||||
# redirect the user to this url | |||||
url = controller().get_payment_url(**payment_details) | |||||
### 3. On Completion of Payment | |||||
Write a method for `on_payment_authorized` in the reference doctype | |||||
Example: | |||||
def on_payment_authorized(payment_status): | |||||
# your code to handle callback | |||||
##### Note: | |||||
payment_status - payment gateway will put payment status on callback. | |||||
For paypal payment status parameter is one from: [Completed, Cancelled, Failed] | |||||
More Details: | |||||
<div class="small">For details on how to get your API credentials, follow this link: <a href="https://developer.paypal.com/docs/classic/api/apiCredentials/" target="_blank">https://developer.paypal.com/docs/classic/api/apiCredentials/</a></div> | |||||
""" | |||||
import json | |||||
from urllib.parse import urlencode | |||||
import influxframework | |||||
import pytz | |||||
from influxframework import _ | |||||
from influxframework.integrations.utils import create_request_log, make_post_request | |||||
from influxframework.model.document import Document | |||||
from influxframework.utils import call_hook_method, cint, get_datetime, get_url | |||||
from payments.utils import create_payment_gateway | |||||
api_path = ( | |||||
"/api/method/payments.payment_gateways.doctype.paypal_settings.paypal_settings" | |||||
) | |||||
class PayPalSettings(Document): | |||||
supported_currencies = [ | |||||
"AUD", | |||||
"BRL", | |||||
"CAD", | |||||
"CZK", | |||||
"DKK", | |||||
"EUR", | |||||
"HKD", | |||||
"HUF", | |||||
"ILS", | |||||
"JPY", | |||||
"MYR", | |||||
"MXN", | |||||
"TWD", | |||||
"NZD", | |||||
"NOK", | |||||
"PHP", | |||||
"PLN", | |||||
"GBP", | |||||
"RUB", | |||||
"SGD", | |||||
"SEK", | |||||
"CHF", | |||||
"THB", | |||||
"TRY", | |||||
"USD", | |||||
] | |||||
def __setup__(self): | |||||
setattr(self, "use_sandbox", 0) | |||||
def setup_sandbox_env(self, token): | |||||
data = json.loads(influxframework.db.get_value("Integration Request", token, "data")) | |||||
setattr(self, "use_sandbox", cint(influxframework._dict(data).use_sandbox) or 0) | |||||
def validate(self): | |||||
create_payment_gateway("PayPal") | |||||
call_hook_method("payment_gateway_enabled", gateway="PayPal") | |||||
if not self.flags.ignore_mandatory: | |||||
self.validate_paypal_credentails() | |||||
def on_update(self): | |||||
pass | |||||
def validate_transaction_currency(self, currency): | |||||
if currency not in self.supported_currencies: | |||||
influxframework.throw( | |||||
_( | |||||
"Please select another payment method. PayPal does not support transactions in currency '{0}'" | |||||
).format(currency) | |||||
) | |||||
def get_paypal_params_and_url(self): | |||||
params = { | |||||
"USER": self.api_username, | |||||
"PWD": self.get_password(fieldname="api_password", raise_exception=False), | |||||
"SIGNATURE": self.signature, | |||||
"VERSION": "98", | |||||
"METHOD": "GetPalDetails", | |||||
} | |||||
if hasattr(self, "use_sandbox") and self.use_sandbox: | |||||
params.update( | |||||
{ | |||||
"USER": influxframework.conf.sandbox_api_username, | |||||
"PWD": influxframework.conf.sandbox_api_password, | |||||
"SIGNATURE": influxframework.conf.sandbox_signature, | |||||
} | |||||
) | |||||
api_url = ( | |||||
"https://api-3t.sandbox.paypal.com/nvp" | |||||
if (self.paypal_sandbox or self.use_sandbox) | |||||
else "https://api-3t.paypal.com/nvp" | |||||
) | |||||
return params, api_url | |||||
def validate_paypal_credentails(self): | |||||
params, url = self.get_paypal_params_and_url() | |||||
params = urlencode(params) | |||||
try: | |||||
res = make_post_request(url=url, data=params.encode("utf-8")) | |||||
if res["ACK"][0] == "Failure": | |||||
raise Exception | |||||
except Exception: | |||||
influxframework.throw(_("Invalid payment gateway credentials")) | |||||
def get_payment_url(self, **kwargs): | |||||
setattr(self, "use_sandbox", cint(kwargs.get("use_sandbox", 0))) | |||||
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}" | |||||
) | |||||
else: | |||||
return_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token={0}" | |||||
kwargs.update( | |||||
{ | |||||
"token": response.get("TOKEN")[0], | |||||
"correlation_id": response.get("CORRELATIONID")[0], | |||||
} | |||||
) | |||||
create_request_log(kwargs, service_name="PayPal", name=kwargs["token"]) | |||||
return return_url.format(kwargs["token"]) | |||||
def execute_set_express_checkout(self, **kwargs): | |||||
params, url = self.get_paypal_params_and_url() | |||||
params.update( | |||||
{ | |||||
"METHOD": "SetExpressCheckout", | |||||
"returnUrl": get_url(f"{api_path}.get_express_checkout_details"), | |||||
"cancelUrl": get_url("/payment-cancel"), | |||||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||||
"PAYMENTREQUEST_0_AMT": kwargs["amount"], | |||||
"PAYMENTREQUEST_0_CURRENCYCODE": kwargs["currency"].upper(), | |||||
} | |||||
) | |||||
if kwargs.get("subscription_details"): | |||||
self.configure_recurring_payments(params, kwargs) | |||||
params = urlencode(params) | |||||
response = make_post_request(url, data=params.encode("utf-8")) | |||||
if response.get("ACK")[0] != "Success": | |||||
influxframework.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 | |||||
for param in ( | |||||
"PAYMENTREQUEST_0_PAYMENTACTION", | |||||
"PAYMENTREQUEST_0_AMT", | |||||
"PAYMENTREQUEST_0_CURRENCYCODE", | |||||
): | |||||
del params[param] | |||||
params.update( | |||||
{ | |||||
"L_BILLINGTYPE0": "RecurringPayments", # The type of billing agreement | |||||
"L_BILLINGAGREEMENTDESCRIPTION0": kwargs["description"], | |||||
} | |||||
) | |||||
def get_paypal_and_transaction_details(token): | |||||
doc = influxframework.get_doc("PayPal Settings") | |||||
doc.setup_sandbox_env(token) | |||||
params, url = doc.get_paypal_params_and_url() | |||||
integration_request = influxframework.get_doc("Integration Request", token) | |||||
data = json.loads(integration_request.data) | |||||
return data, params, url | |||||
def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): | |||||
redirect_to = data.get("redirect_to") or None | |||||
redirect_message = data.get("redirect_message") or None | |||||
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: | |||||
influxframework.local.response["type"] = "redirect" | |||||
influxframework.local.response["location"] = get_url(redirect_url) | |||||
@influxframework.whitelist(allow_guest=True, xss_safe=True) | |||||
def get_express_checkout_details(token): | |||||
try: | |||||
doc = influxframework.get_doc("PayPal Settings") | |||||
doc.setup_sandbox_env(token) | |||||
params, url = doc.get_paypal_params_and_url() | |||||
params.update({"METHOD": "GetExpressCheckoutDetails", "TOKEN": token}) | |||||
response = make_post_request(url, data=params) | |||||
if response.get("ACK")[0] != "Success": | |||||
influxframework.respond_as_web_page( | |||||
_("Something went wrong"), | |||||
_( | |||||
"Looks like something went wrong during the transaction. Since we haven't confirmed the payment, Paypal will automatically refund you this amount. If it doesn't, please send us an email and mention the Correlation ID: {0}." | |||||
).format(response.get("CORRELATIONID", [None])[0]), | |||||
indicator_color="red", | |||||
http_status_code=influxframework.ValidationError.http_status_code, | |||||
) | |||||
return | |||||
doc = influxframework.get_doc("Integration Request", token) | |||||
update_integration_request_status( | |||||
token, | |||||
{"payerid": response.get("PAYERID")[0], "payer_email": response.get("EMAIL")[0]}, | |||||
"Authorized", | |||||
doc=doc, | |||||
) | |||||
influxframework.local.response["type"] = "redirect" | |||||
influxframework.local.response["location"] = get_redirect_uri( | |||||
doc, token, response.get("PAYERID")[0] | |||||
) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
@influxframework.whitelist(allow_guest=True, xss_safe=True) | |||||
def confirm_payment(token): | |||||
try: | |||||
custom_redirect_to = None | |||||
data, params, url = get_paypal_and_transaction_details(token) | |||||
params.update( | |||||
{ | |||||
"METHOD": "DoExpressCheckoutPayment", | |||||
"PAYERID": data.get("payerid"), | |||||
"TOKEN": token, | |||||
"PAYMENTREQUEST_0_PAYMENTACTION": "SALE", | |||||
"PAYMENTREQUEST_0_AMT": data.get("amount"), | |||||
"PAYMENTREQUEST_0_CURRENCYCODE": data.get("currency").upper(), | |||||
} | |||||
) | |||||
response = make_post_request(url, data=params) | |||||
if response.get("ACK")[0] == "Success": | |||||
update_integration_request_status( | |||||
token, | |||||
{ | |||||
"transaction_id": response.get("PAYMENTINFO_0_TRANSACTIONID")[0], | |||||
"correlation_id": response.get("CORRELATIONID")[0], | |||||
}, | |||||
"Completed", | |||||
) | |||||
if data.get("reference_doctype") and data.get("reference_docname"): | |||||
custom_redirect_to = influxframework.get_doc( | |||||
data.get("reference_doctype"), data.get("reference_docname") | |||||
).run_method("on_payment_authorized", "Completed") | |||||
influxframework.db.commit() | |||||
redirect_url = "payment-success?doctype={}&docname={}".format( | |||||
data.get("reference_doctype"), data.get("reference_docname") | |||||
) | |||||
else: | |||||
redirect_url = "payment-failed" | |||||
setup_redirect(data, redirect_url, custom_redirect_to) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
@influxframework.whitelist(allow_guest=True, xss_safe=True) | |||||
def create_recurring_profile(token, payerid): | |||||
try: | |||||
custom_redirect_to = None | |||||
updating = False | |||||
data, params, url = get_paypal_and_transaction_details(token) | |||||
addons = data.get("addons") | |||||
subscription_details = data.get("subscription_details") | |||||
if data.get("subscription_id"): | |||||
if addons: | |||||
updating = True | |||||
manage_recurring_payment_profile_status( | |||||
data["subscription_id"], "Cancel", params, url | |||||
) | |||||
params.update( | |||||
{ | |||||
"METHOD": "CreateRecurringPaymentsProfile", | |||||
"PAYERID": payerid, | |||||
"TOKEN": token, | |||||
"DESC": data.get("description"), | |||||
"BILLINGPERIOD": subscription_details.get("billing_period"), | |||||
"BILLINGFREQUENCY": subscription_details.get("billing_frequency"), | |||||
"AMT": data.get("amount") | |||||
if data.get("subscription_amount") == data.get("amount") | |||||
else data.get("subscription_amount"), | |||||
"CURRENCYCODE": data.get("currency").upper(), | |||||
"INITAMT": data.get("upfront_amount"), | |||||
} | |||||
) | |||||
status_changed_to = ( | |||||
"Completed" if data.get("starting_immediately") or updating else "Verified" | |||||
) | |||||
starts_at = ( | |||||
get_datetime(subscription_details.get("start_date")) or influxframework.utils.now_datetime() | |||||
) | |||||
starts_at = starts_at.replace( | |||||
tzinfo=pytz.timezone(influxframework.utils.get_time_zone()) | |||||
).astimezone(pytz.utc) | |||||
# "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() | |||||
params.update({"PROFILESTARTDATE": starts_at.isoformat()}) | |||||
response = make_post_request(url, data=params) | |||||
if response.get("ACK")[0] == "Success": | |||||
update_integration_request_status( | |||||
token, | |||||
{ | |||||
"profile_id": response.get("PROFILEID")[0], | |||||
}, | |||||
"Completed", | |||||
) | |||||
if data.get("reference_doctype") and data.get("reference_docname"): | |||||
data["subscription_id"] = response.get("PROFILEID")[0] | |||||
influxframework.flags.data = data | |||||
custom_redirect_to = influxframework.get_doc( | |||||
data.get("reference_doctype"), data.get("reference_docname") | |||||
).run_method("on_payment_authorized", status_changed_to) | |||||
influxframework.db.commit() | |||||
redirect_url = "payment-success?doctype={}&docname={}".format( | |||||
data.get("reference_doctype"), data.get("reference_docname") | |||||
) | |||||
else: | |||||
redirect_url = "payment-failed" | |||||
setup_redirect(data, redirect_url, custom_redirect_to) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
def update_integration_request_status(token, data, status, error=False, doc=None): | |||||
if not doc: | |||||
doc = influxframework.get_doc("Integration Request", token) | |||||
doc.update_status(data, status) | |||||
def get_redirect_uri(doc, token, payerid): | |||||
data = json.loads(doc.data) | |||||
if data.get("subscription_details") or data.get("subscription_id"): | |||||
return get_url(f"{api_path}.create_recurring_profile?token={token}&payerid={payerid}") | |||||
else: | |||||
return get_url(f"{api_path}.confirm_payment?token={token}") | |||||
def manage_recurring_payment_profile_status(profile_id, action, args, url): | |||||
args.update( | |||||
{ | |||||
"METHOD": "ManageRecurringPaymentsProfileStatus", | |||||
"PROFILEID": profile_id, | |||||
"ACTION": action, | |||||
} | |||||
) | |||||
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. | |||||
# 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" | |||||
): | |||||
influxframework.throw(_("Failed while amending subscription")) | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def ipn_handler(): | |||||
try: | |||||
data = influxframework.local.form_dict | |||||
validate_ipn_request(data) | |||||
data.update({"payment_gateway": "PayPal"}) | |||||
doc = influxframework.get_doc( | |||||
{ | |||||
"data": json.dumps(influxframework.local.form_dict), | |||||
"doctype": "Integration Request", | |||||
"request_description": "Subscription Notification", | |||||
"is_remote_request": 1, | |||||
"status": "Queued", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
influxframework.db.commit() | |||||
influxframework.enqueue( | |||||
method="payments.payment_gateways.doctype.paypal_settings.paypal_settings.handle_subscription_notification", | |||||
queue="long", | |||||
timeout=600, | |||||
is_async=True, | |||||
**{"doctype": "Integration Request", "docname": doc.name}, | |||||
) | |||||
except influxframework.InvalidStatusError: | |||||
pass | |||||
except Exception as e: | |||||
influxframework.log(influxframework.log_error(title=e)) | |||||
def validate_ipn_request(data): | |||||
def _throw(): | |||||
influxframework.throw(_("In Valid Request"), exc=influxframework.InvalidStatusError) | |||||
if not data.get("recurring_payment_id"): | |||||
_throw() | |||||
doc = influxframework.get_doc("PayPal Settings") | |||||
params, url = doc.get_paypal_params_and_url() | |||||
params.update( | |||||
{ | |||||
"METHOD": "GetRecurringPaymentsProfileDetails", | |||||
"PROFILEID": data.get("recurring_payment_id"), | |||||
} | |||||
) | |||||
params = urlencode(params) | |||||
res = make_post_request(url=url, data=params.encode("utf-8")) | |||||
if res["ACK"][0] != "Success": | |||||
_throw() | |||||
def handle_subscription_notification(doctype, docname): | |||||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2020, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('Paytm Settings', { | |||||
refresh: function(frm) { | |||||
frm.dashboard.set_headline(__("For more information, {0}.", [`<a href='https://influxerp.com/docs/user/manual/en/influxerp_integration/paytm-integration'>${__('Click here')}</a>`])); | |||||
} | |||||
}); |
@@ -0,0 +1,89 @@ | |||||
{ | |||||
"actions": [], | |||||
"creation": "2020-04-02 00:11:22.846697", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"merchant_id", | |||||
"merchant_key", | |||||
"staging", | |||||
"column_break_4", | |||||
"industry_type_id", | |||||
"website" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "merchant_id", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Merchant ID", | |||||
"reqd": 1, | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "merchant_key", | |||||
"fieldtype": "Password", | |||||
"in_list_view": 1, | |||||
"label": "Merchant Key", | |||||
"reqd": 1, | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "staging", | |||||
"fieldtype": "Check", | |||||
"label": "Staging", | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
}, | |||||
{ | |||||
"depends_on": "eval: !doc.staging", | |||||
"fieldname": "website", | |||||
"fieldtype": "Data", | |||||
"label": "Website", | |||||
"mandatory_depends_on": "eval: !doc.staging", | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "column_break_4", | |||||
"fieldtype": "Column Break", | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
}, | |||||
{ | |||||
"depends_on": "eval: !doc.staging", | |||||
"fieldname": "industry_type_id", | |||||
"fieldtype": "Data", | |||||
"label": "Industry Type ID", | |||||
"mandatory_depends_on": "eval: !doc.staging", | |||||
"show_days": 1, | |||||
"show_seconds": 1 | |||||
} | |||||
], | |||||
"issingle": 1, | |||||
"links": [], | |||||
"modified": "2022-07-24 13:36:09.703143", | |||||
"modified_by": "Administrator", | |||||
"module": "Payment Gateways", | |||||
"name": "Paytm Settings", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"role": "System Manager", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1 | |||||
} |
@@ -0,0 +1,197 @@ | |||||
# Copyright (c) 2020, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import json | |||||
from urllib.parse import urlencode | |||||
import influxframework | |||||
import requests | |||||
from influxframework import _ | |||||
from influxframework.integrations.utils import create_request_log | |||||
from influxframework.model.document import Document | |||||
from influxframework.utils import ( | |||||
call_hook_method, | |||||
cint, | |||||
cstr, | |||||
flt, | |||||
get_request_site_address, | |||||
get_url, | |||||
) | |||||
from influxframework.utils.password import get_decrypted_password | |||||
from paytmchecksum import generateSignature, verifySignature | |||||
from payments.utils import create_payment_gateway | |||||
class PaytmSettings(Document): | |||||
supported_currencies = ["INR"] | |||||
def validate(self): | |||||
create_payment_gateway("Paytm") | |||||
call_hook_method("payment_gateway_enabled", gateway="Paytm") | |||||
def validate_transaction_currency(self, currency): | |||||
if currency not in self.supported_currencies: | |||||
influxframework.throw( | |||||
_( | |||||
"Please select another payment method. Paytm does not support transactions in currency '{0}'" | |||||
).format(currency) | |||||
) | |||||
def get_payment_url(self, **kwargs): | |||||
"""Return payment url with several params""" | |||||
# create unique order id by making it equal to the integration request | |||||
integration_request = create_request_log(kwargs, service_name="Paytm") | |||||
kwargs.update(dict(order_id=integration_request.name)) | |||||
return get_url(f"./paytm_checkout?{urlencode(kwargs)}") | |||||
def get_paytm_config(): | |||||
"""Returns paytm config""" | |||||
paytm_config = influxframework.db.get_singles_dict("Paytm Settings") | |||||
paytm_config.update( | |||||
dict( | |||||
merchant_key=get_decrypted_password( | |||||
"Paytm Settings", "Paytm Settings", "merchant_key" | |||||
) | |||||
) | |||||
) | |||||
if cint(paytm_config.staging): | |||||
paytm_config.update( | |||||
dict( | |||||
website="WEBSTAGING", | |||||
url="https://securegw-stage.paytm.in/order/process", | |||||
transaction_status_url="https://securegw-stage.paytm.in/order/status", | |||||
industry_type_id="RETAIL", | |||||
) | |||||
) | |||||
else: | |||||
paytm_config.update( | |||||
dict( | |||||
url="https://securegw.paytm.in/order/process", | |||||
transaction_status_url="https://securegw.paytm.in/order/status", | |||||
) | |||||
) | |||||
return paytm_config | |||||
def get_paytm_params(payment_details, order_id, paytm_config): | |||||
# initialize a dictionary | |||||
paytm_params = dict() | |||||
redirect_uri = ( | |||||
get_request_site_address(True) | |||||
+ "/api/method/payments.payment_gateways.doctype.paytm_settings.paytm_settings.verify_transaction" | |||||
) | |||||
paytm_params.update( | |||||
{ | |||||
"MID": paytm_config.merchant_id, | |||||
"WEBSITE": paytm_config.website, | |||||
"INDUSTRY_TYPE_ID": paytm_config.industry_type_id, | |||||
"CHANNEL_ID": "WEB", | |||||
"ORDER_ID": order_id, | |||||
"CUST_ID": payment_details["payer_email"], | |||||
"EMAIL": payment_details["payer_email"], | |||||
"TXN_AMOUNT": cstr(flt(payment_details["amount"], 2)), | |||||
"CALLBACK_URL": redirect_uri, | |||||
} | |||||
) | |||||
checksum = generateSignature(paytm_params, paytm_config.merchant_key) | |||||
paytm_params.update({"CHECKSUMHASH": checksum}) | |||||
return paytm_params | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def verify_transaction(**paytm_params): | |||||
"""Verify checksum for received data in the callback and then verify the transaction""" | |||||
paytm_config = get_paytm_config() | |||||
is_valid_checksum = False | |||||
paytm_params.pop("cmd", None) | |||||
paytm_checksum = paytm_params.pop("CHECKSUMHASH", None) | |||||
if paytm_params and paytm_config and paytm_checksum: | |||||
# Verify 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"]) | |||||
else: | |||||
influxframework.respond_as_web_page( | |||||
"Payment Failed", | |||||
"Transaction failed to complete. In case of any deductions, deducted amount will get refunded to your account.", | |||||
http_status_code=401, | |||||
indicator_color="red", | |||||
) | |||||
influxframework.log_error( | |||||
"Order unsuccessful. Failed Response:" + cstr(paytm_params), "Paytm Payment Failed" | |||||
) | |||||
def verify_transaction_status(paytm_config, order_id): | |||||
"""Verify transaction completion after checksum has been verified""" | |||||
paytm_params = dict(MID=paytm_config.merchant_id, ORDERID=order_id) | |||||
checksum = generateSignature(paytm_params, paytm_config.merchant_key) | |||||
paytm_params["CHECKSUMHASH"] = checksum | |||||
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() | |||||
finalize_request(order_id, response) | |||||
def finalize_request(order_id, transaction_response): | |||||
request = influxframework.get_doc("Integration Request", order_id) | |||||
transaction_data = influxframework._dict(json.loads(request.data)) | |||||
redirect_to = transaction_data.get("redirect_to") or None | |||||
redirect_message = transaction_data.get("redirect_message") or None | |||||
if transaction_response["STATUS"] == "TXN_SUCCESS": | |||||
if transaction_data.reference_doctype and transaction_data.reference_docname: | |||||
custom_redirect_to = None | |||||
try: | |||||
custom_redirect_to = influxframework.get_doc( | |||||
transaction_data.reference_doctype, transaction_data.reference_docname | |||||
).run_method("on_payment_authorized", "Completed") | |||||
request.db_set("status", "Completed") | |||||
except Exception: | |||||
request.db_set("status", "Failed") | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = "payment-success" | |||||
else: | |||||
request.db_set("status", "Failed") | |||||
redirect_url = "payment-failed" | |||||
if redirect_to: | |||||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||||
influxframework.local.response["type"] = "redirect" | |||||
influxframework.local.response["location"] = redirect_url | |||||
def get_gateway_controller(doctype, docname): | |||||
reference_doc = influxframework.get_doc(doctype, docname) | |||||
gateway_controller = influxframework.db.get_value( | |||||
"Payment Gateway", reference_doc.payment_gateway, "gateway_controller" | |||||
) | |||||
return gateway_controller |
@@ -0,0 +1,8 @@ | |||||
# Copyright (c) 2020, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
# import influxframework | |||||
import unittest | |||||
class TestPaytmSettings(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('Razorpay Settings', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,145 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"beta": 0, | |||||
"creation": "2016-09-20 03:44:03.799402", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "System", | |||||
"editable_grid": 1, | |||||
"fields": [ | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "api_key", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "API Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "api_secret", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "API Secret", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Mention transaction completion page URL", | |||||
"fieldname": "redirect_to", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Redirect To", | |||||
"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 | |||||
} | |||||
], | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 1, | |||||
"is_submittable": 0, | |||||
"issingle": 1, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2022-07-24 14:40:31.658270", | |||||
"modified_by": "Administrator", | |||||
"module": "Payment Gateways", | |||||
"name": "Razorpay Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"apply_user_permissions": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 1, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"is_custom": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 0, | |||||
"read_only": 1, | |||||
"read_only_onload": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 1, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,546 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
""" | |||||
# Integrating RazorPay | |||||
### Validate Currency | |||||
Example: | |||||
from payments.utils import get_payment_gateway_controller | |||||
controller = get_payment_gateway_controller("Razorpay") | |||||
controller().validate_transaction_currency(currency) | |||||
### 2. Redirect for payment | |||||
Example: | |||||
payment_details = { | |||||
"amount": 600, | |||||
"title": "Payment for bill : 111", | |||||
"description": "payment via cart", | |||||
"reference_doctype": "Payment Request", | |||||
"reference_docname": "PR0001", | |||||
"payer_email": "NuranVerkleij@example.com", | |||||
"payer_name": "Nuran Verkleij", | |||||
"order_id": "111", | |||||
"currency": "INR", | |||||
"payment_gateway": "Razorpay", | |||||
"subscription_details": { | |||||
"plan_id": "plan_12313", # if Required | |||||
"start_date": "2018-08-30", | |||||
"billing_period": "Month" #(Day, Week, Month, Year), | |||||
"billing_frequency": 1, | |||||
"customer_notify": 1, | |||||
"upfront_amount": 1000 | |||||
} | |||||
} | |||||
# Redirect the user to this url | |||||
url = controller().get_payment_url(**payment_details) | |||||
### 3. On Completion of Payment | |||||
Write a method for `on_payment_authorized` in the reference doctype | |||||
Example: | |||||
def on_payment_authorized(payment_status): | |||||
# this method will be called when payment is complete | |||||
##### Notes: | |||||
payment_status - payment gateway will put payment status on callback. | |||||
For razorpay payment status is Authorized | |||||
""" | |||||
import hashlib | |||||
import hmac | |||||
import json | |||||
from urllib.parse import urlencode | |||||
import influxframework | |||||
import razorpay | |||||
from influxframework import _ | |||||
from influxframework.integrations.utils import ( | |||||
create_request_log, | |||||
make_get_request, | |||||
make_post_request, | |||||
) | |||||
from influxframework.model.document import Document | |||||
from influxframework.utils import call_hook_method, cint, get_timestamp, get_url | |||||
from payments.utils import create_payment_gateway | |||||
class RazorpaySettings(Document): | |||||
supported_currencies = ["INR"] | |||||
def init_client(self): | |||||
if self.api_key: | |||||
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") | |||||
call_hook_method("payment_gateway_enabled", gateway="Razorpay") | |||||
if not self.flags.ignore_mandatory: | |||||
self.validate_razorpay_credentails() | |||||
def validate_razorpay_credentails(self): | |||||
if self.api_key and self.api_secret: | |||||
try: | |||||
make_get_request( | |||||
url="https://api.razorpay.com/v1/payments", | |||||
auth=( | |||||
self.api_key, | |||||
self.get_password(fieldname="api_secret", raise_exception=False), | |||||
), | |||||
) | |||||
except Exception: | |||||
influxframework.throw(_("Seems API Key or API Secret is wrong !!!")) | |||||
def validate_transaction_currency(self, currency): | |||||
if currency not in self.supported_currencies: | |||||
influxframework.throw( | |||||
_( | |||||
"Please select another payment method. Razorpay does not support transactions in currency '{0}'" | |||||
).format(currency) | |||||
) | |||||
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/{}/addons".format( | |||||
kwargs.get("subscription_id") | |||||
) | |||||
try: | |||||
if not influxframework.conf.converted_rupee_to_paisa: | |||||
convert_rupee_to_paisa(**kwargs) | |||||
for addon in kwargs.get("addons"): | |||||
resp = make_post_request( | |||||
url, | |||||
auth=(settings.api_key, settings.api_secret), | |||||
data=json.dumps(addon), | |||||
headers={"content-type": "application/json"}, | |||||
) | |||||
if not resp.get("id"): | |||||
influxframework.log_error( | |||||
message=str(resp), title="Razorpay Failed while creating subscription" | |||||
) | |||||
except Exception: | |||||
influxframework.log_error() | |||||
# failed | |||||
pass | |||||
def setup_subscription(self, settings, **kwargs): | |||||
start_date = ( | |||||
get_timestamp(kwargs.get("subscription_details").get("start_date")) | |||||
if kwargs.get("subscription_details").get("start_date") | |||||
else None | |||||
) | |||||
subscription_details = { | |||||
"plan_id": kwargs.get("subscription_details").get("plan_id"), | |||||
"total_count": kwargs.get("subscription_details").get("billing_frequency"), | |||||
"customer_notify": kwargs.get("subscription_details").get("customer_notify"), | |||||
} | |||||
if start_date: | |||||
subscription_details["start_at"] = cint(start_date) | |||||
if kwargs.get("addons"): | |||||
convert_rupee_to_paisa(**kwargs) | |||||
subscription_details.update({"addons": kwargs.get("addons")}) | |||||
try: | |||||
resp = make_post_request( | |||||
"https://api.razorpay.com/v1/subscriptions", | |||||
auth=(settings.api_key, settings.api_secret), | |||||
data=json.dumps(subscription_details), | |||||
headers={"content-type": "application/json"}, | |||||
) | |||||
if resp.get("status") == "created": | |||||
kwargs["subscription_id"] = resp.get("id") | |||||
influxframework.flags.status = "created" | |||||
return kwargs | |||||
else: | |||||
influxframework.log_error( | |||||
message=str(resp), title="Razorpay Failed while creating subscription" | |||||
) | |||||
except Exception: | |||||
influxframework.log_error() | |||||
def prepare_subscription_details(self, settings, **kwargs): | |||||
if not kwargs.get("subscription_id"): | |||||
kwargs = self.setup_subscription(settings, **kwargs) | |||||
if influxframework.flags.status != "created": | |||||
kwargs["subscription_id"] = None | |||||
return kwargs | |||||
def get_payment_url(self, **kwargs): | |||||
integration_request = create_request_log(kwargs, service_name="Razorpay") | |||||
return get_url(f"./razorpay_checkout?token={integration_request.name}") | |||||
def create_order(self, **kwargs): | |||||
# Creating Orders https://razorpay.com/docs/api/orders/ | |||||
# convert rupees to paisa | |||||
kwargs["amount"] *= 100 | |||||
# Create integration log | |||||
integration_request = create_request_log(kwargs, service_name="Razorpay") | |||||
# Setup payment options | |||||
payment_options = { | |||||
"amount": kwargs.get("amount"), | |||||
"currency": kwargs.get("currency", "INR"), | |||||
"receipt": kwargs.get("receipt"), | |||||
"payment_capture": kwargs.get("payment_capture"), | |||||
} | |||||
if self.api_key and self.api_secret: | |||||
try: | |||||
order = make_post_request( | |||||
"https://api.razorpay.com/v1/orders", | |||||
auth=( | |||||
self.api_key, | |||||
self.get_password(fieldname="api_secret", raise_exception=False), | |||||
), | |||||
data=payment_options, | |||||
) | |||||
order["integration_request"] = integration_request.name | |||||
return order # Order returned to be consumed by razorpay.js | |||||
except Exception: | |||||
influxframework.log(influxframework.get_traceback()) | |||||
influxframework.throw(_("Could not create razorpay order")) | |||||
def create_request(self, data): | |||||
self.data = influxframework._dict(data) | |||||
try: | |||||
self.integration_request = influxframework.get_doc("Integration Request", self.data.token) | |||||
self.integration_request.update_status(self.data, "Queued") | |||||
return self.authorize_payment() | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
return { | |||||
"redirect_to": influxframework.redirect_to_message( | |||||
_("Server Error"), | |||||
_( | |||||
"Seems issue with server's razorpay config. Don't worry, in case of failure amount will get refunded to your account." | |||||
), | |||||
), | |||||
"status": 401, | |||||
} | |||||
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 | |||||
until it is explicitly captured by merchant. | |||||
""" | |||||
data = json.loads(self.integration_request.data) | |||||
settings = self.get_settings(data) | |||||
try: | |||||
resp = make_get_request( | |||||
f"https://api.razorpay.com/v1/payments/{self.data.razorpay_payment_id}", | |||||
auth=(settings.api_key, settings.api_secret), | |||||
) | |||||
if resp.get("status") == "authorized": | |||||
self.integration_request.update_status(data, "Authorized") | |||||
self.flags.status_changed_to = "Authorized" | |||||
if resp.get("status") == "captured": | |||||
self.integration_request.update_status(data, "Completed") | |||||
self.flags.status_changed_to = "Completed" | |||||
elif data.get("subscription_id"): | |||||
if resp.get("status") == "refunded": | |||||
# if subscription start date is in future then | |||||
# razorpay refunds the amount after authorizing the card details | |||||
# thus changing status to Verified | |||||
self.integration_request.update_status(data, "Completed") | |||||
self.flags.status_changed_to = "Verified" | |||||
else: | |||||
influxframework.log_error(message=str(resp), title="Razorpay Payment not authorized") | |||||
except Exception: | |||||
influxframework.log_error() | |||||
status = influxframework.flags.integration_request.status_code | |||||
redirect_to = data.get("redirect_to") or None | |||||
redirect_message = data.get("redirect_message") or None | |||||
if self.flags.status_changed_to in ("Authorized", "Verified", "Completed"): | |||||
if self.data.reference_doctype and self.data.reference_docname: | |||||
custom_redirect_to = None | |||||
try: | |||||
influxframework.flags.data = data | |||||
custom_redirect_to = influxframework.get_doc( | |||||
self.data.reference_doctype, self.data.reference_docname | |||||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = "payment-success?doctype={}&docname={}".format( | |||||
self.data.reference_doctype, self.data.reference_docname | |||||
) | |||||
else: | |||||
redirect_url = "payment-failed" | |||||
if redirect_to: | |||||
redirect_url += "&" + urlencode({"redirect_to": redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||||
return {"redirect_to": redirect_url, "status": status} | |||||
def get_settings(self, data): | |||||
settings = influxframework._dict( | |||||
{ | |||||
"api_key": self.api_key, | |||||
"api_secret": self.get_password(fieldname="api_secret", raise_exception=False), | |||||
} | |||||
) | |||||
if cint(data.get("notes", {}).get("use_sandbox")) or data.get("use_sandbox"): | |||||
settings.update( | |||||
{ | |||||
"api_key": influxframework.conf.sandbox_api_key, | |||||
"api_secret": influxframework.conf.sandbox_api_secret, | |||||
} | |||||
) | |||||
return settings | |||||
def cancel_subscription(self, subscription_id): | |||||
settings = self.get_settings({}) | |||||
try: | |||||
resp = make_post_request( | |||||
f"https://api.razorpay.com/v1/subscriptions/{subscription_id}/cancel", | |||||
auth=(settings.api_key, settings.api_secret), | |||||
) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
def verify_signature(self, body, signature, key): | |||||
key = bytes(key, "utf-8") | |||||
body = bytes(body, "utf-8") | |||||
dig = hmac.new(key=key, msg=body, digestmod=hashlib.sha256) | |||||
generated_signature = dig.hexdigest() | |||||
result = hmac.compare_digest(generated_signature, signature) | |||||
if not result: | |||||
influxframework.throw(_("Razorpay Signature Verification Failed"), exc=influxframework.PermissionError) | |||||
return result | |||||
def capture_payment(is_sandbox=False, sanbox_response=None): | |||||
""" | |||||
Verifies the purchase as complete by the merchant. | |||||
After capture, the amount is transferred to the merchant within T+3 days | |||||
where T is the day on which payment is captured. | |||||
Note: Attempting to capture a payment whose status is not authorized will produce an error. | |||||
""" | |||||
controller = influxframework.get_doc("Razorpay Settings") | |||||
for doc in influxframework.get_all( | |||||
"Integration Request", | |||||
filters={"status": "Authorized", "integration_request_service": "Razorpay"}, | |||||
fields=["name", "data"], | |||||
): | |||||
try: | |||||
if is_sandbox: | |||||
resp = sanbox_response | |||||
else: | |||||
data = json.loads(doc.data) | |||||
settings = controller.get_settings(data) | |||||
resp = make_get_request( | |||||
"https://api.razorpay.com/v1/payments/{}".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/{}/capture".format( | |||||
data.get("razorpay_payment_id") | |||||
), | |||||
auth=(settings.api_key, settings.api_secret), | |||||
data={"amount": data.get("amount")}, | |||||
) | |||||
if resp.get("status") == "captured": | |||||
influxframework.db.set_value("Integration Request", doc.name, "status", "Completed") | |||||
except Exception: | |||||
doc = influxframework.get_doc("Integration Request", doc.name) | |||||
doc.status = "Failed" | |||||
doc.error = influxframework.get_traceback() | |||||
doc.save() | |||||
influxframework.log_error(doc.error, f"{doc.name} Failed") | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def get_api_key(): | |||||
controller = influxframework.get_doc("Razorpay Settings") | |||||
return controller.api_key | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def get_order(doctype, docname): | |||||
# Order returned to be consumed by razorpay.js | |||||
doc = influxframework.get_doc(doctype, docname) | |||||
try: | |||||
# Do not use run_method here as it fails silently | |||||
return doc.get_razorpay_order() | |||||
except AttributeError: | |||||
influxframework.log_error( | |||||
influxframework.get_traceback(), _("Controller method get_razorpay_order missing") | |||||
) | |||||
influxframework.throw(_("Could not create Razorpay order. Please contact Administrator")) | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def order_payment_success(integration_request, params): | |||||
"""Called by razorpay.js on order payment success, the params | |||||
contains razorpay_payment_id, razorpay_order_id, razorpay_signature | |||||
that is updated in the data field of integration request | |||||
Args: | |||||
integration_request (string): Name for integration request doc | |||||
params (string): Params to be updated for integration request. | |||||
""" | |||||
params = json.loads(params) | |||||
integration = influxframework.get_doc("Integration Request", integration_request) | |||||
# Update integration request | |||||
integration.update_status(params, integration.status) | |||||
integration.reload() | |||||
data = json.loads(integration.data) | |||||
controller = influxframework.get_doc("Razorpay Settings") | |||||
# Update payment and integration data for payment controller object | |||||
controller.integration_request = integration | |||||
controller.data = influxframework._dict(data) | |||||
# Authorize payment | |||||
controller.authorize_payment() | |||||
@influxframework.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 | |||||
""" | |||||
influxframework.log_error(params, "Razorpay Payment Failure") | |||||
params = json.loads(params) | |||||
integration = influxframework.get_doc("Integration Request", integration_request) | |||||
integration.update_status(params, integration.status) | |||||
def convert_rupee_to_paisa(**kwargs): | |||||
for addon in kwargs.get("addons"): | |||||
addon["item"]["amount"] *= 100 | |||||
influxframework.conf.converted_rupee_to_paisa = True | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def razorpay_subscription_callback(): | |||||
try: | |||||
data = influxframework.local.form_dict | |||||
validate_payment_callback(data) | |||||
data.update({"payment_gateway": "Razorpay"}) | |||||
doc = influxframework.get_doc( | |||||
{ | |||||
"data": json.dumps(influxframework.local.form_dict), | |||||
"doctype": "Integration Request", | |||||
"request_description": "Subscription Notification", | |||||
"is_remote_request": 1, | |||||
"status": "Queued", | |||||
} | |||||
).insert(ignore_permissions=True) | |||||
influxframework.db.commit() | |||||
influxframework.enqueue( | |||||
method="payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.handle_subscription_notification", | |||||
queue="long", | |||||
timeout=600, | |||||
is_async=True, | |||||
**{"doctype": "Integration Request", "docname": doc.name}, | |||||
) | |||||
except influxframework.InvalidStatusError: | |||||
pass | |||||
except Exception as e: | |||||
influxframework.log(influxframework.log_error(title=e)) | |||||
def validate_payment_callback(data): | |||||
def _throw(): | |||||
influxframework.throw(_("Invalid Subscription"), exc=influxframework.InvalidStatusError) | |||||
subscription_id = data.get("payload").get("subscription").get("entity").get("id") | |||||
if not (subscription_id): | |||||
_throw() | |||||
controller = influxframework.get_doc("Razorpay Settings") | |||||
settings = controller.get_settings(data) | |||||
resp = make_get_request( | |||||
f"https://api.razorpay.com/v1/subscriptions/{subscription_id}", | |||||
auth=(settings.api_key, settings.api_secret), | |||||
) | |||||
if resp.get("status") != "active": | |||||
_throw() | |||||
def handle_subscription_notification(doctype, docname): | |||||
call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2017, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('Stripe Settings', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,315 @@ | |||||
{ | |||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 0, | |||||
"autoname": "field:gateway_name", | |||||
"beta": 0, | |||||
"creation": "2017-03-09 17:18:29.458397", | |||||
"custom": 0, | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "gateway_name", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Payment Gateway Name", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "publishable_key", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Publishable Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"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_3", | |||||
"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": "secret_key", | |||||
"fieldtype": "Password", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Secret Key", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"precision": "", | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"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, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"idx": 0, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2022-07-24 13:32:14.429916", | |||||
"modified_by": "Administrator", | |||||
"module": "Payment Gateways", | |||||
"name": "Stripe Settings", | |||||
"name_case": "", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "System Manager", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 0, | |||||
"show_name_in_global_search": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"track_changes": 0, | |||||
"track_seen": 0 | |||||
} |
@@ -0,0 +1,279 @@ | |||||
# Copyright (c) 2017, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
from urllib.parse import urlencode | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.integrations.utils import create_request_log, make_get_request | |||||
from influxframework.model.document import Document | |||||
from influxframework.utils import call_hook_method, cint, flt, get_url | |||||
from payments.utils import create_payment_gateway | |||||
class StripeSettings(Document): | |||||
supported_currencies = [ | |||||
"AED", | |||||
"ALL", | |||||
"ANG", | |||||
"ARS", | |||||
"AUD", | |||||
"AWG", | |||||
"BBD", | |||||
"BDT", | |||||
"BIF", | |||||
"BMD", | |||||
"BND", | |||||
"BOB", | |||||
"BRL", | |||||
"BSD", | |||||
"BWP", | |||||
"BZD", | |||||
"CAD", | |||||
"CHF", | |||||
"CLP", | |||||
"CNY", | |||||
"COP", | |||||
"CRC", | |||||
"CVE", | |||||
"CZK", | |||||
"DJF", | |||||
"DKK", | |||||
"DOP", | |||||
"DZD", | |||||
"EGP", | |||||
"ETB", | |||||
"EUR", | |||||
"FJD", | |||||
"FKP", | |||||
"GBP", | |||||
"GIP", | |||||
"GMD", | |||||
"GNF", | |||||
"GTQ", | |||||
"GYD", | |||||
"HKD", | |||||
"HNL", | |||||
"HRK", | |||||
"HTG", | |||||
"HUF", | |||||
"IDR", | |||||
"ILS", | |||||
"INR", | |||||
"ISK", | |||||
"JMD", | |||||
"JPY", | |||||
"KES", | |||||
"KHR", | |||||
"KMF", | |||||
"KRW", | |||||
"KYD", | |||||
"KZT", | |||||
"LAK", | |||||
"LBP", | |||||
"LKR", | |||||
"LRD", | |||||
"MAD", | |||||
"MDL", | |||||
"MNT", | |||||
"MOP", | |||||
"MRO", | |||||
"MUR", | |||||
"MVR", | |||||
"MWK", | |||||
"MXN", | |||||
"MYR", | |||||
"NAD", | |||||
"NGN", | |||||
"NIO", | |||||
"NOK", | |||||
"NPR", | |||||
"NZD", | |||||
"PAB", | |||||
"PEN", | |||||
"PGK", | |||||
"PHP", | |||||
"PKR", | |||||
"PLN", | |||||
"PYG", | |||||
"QAR", | |||||
"RUB", | |||||
"SAR", | |||||
"SBD", | |||||
"SCR", | |||||
"SEK", | |||||
"SGD", | |||||
"SHP", | |||||
"SLL", | |||||
"SOS", | |||||
"STD", | |||||
"SVC", | |||||
"SZL", | |||||
"THB", | |||||
"TOP", | |||||
"TTD", | |||||
"TWD", | |||||
"TZS", | |||||
"UAH", | |||||
"UGX", | |||||
"USD", | |||||
"UYU", | |||||
"UZS", | |||||
"VND", | |||||
"VUV", | |||||
"WST", | |||||
"XAF", | |||||
"XOF", | |||||
"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, | |||||
} | |||||
def on_update(self): | |||||
create_payment_gateway( | |||||
"Stripe-" + self.gateway_name, | |||||
settings="Stripe Settings", | |||||
controller=self.gateway_name, | |||||
) | |||||
call_hook_method("payment_gateway_enabled", gateway="Stripe-" + self.gateway_name) | |||||
if not self.flags.ignore_mandatory: | |||||
self.validate_stripe_credentails() | |||||
def validate_stripe_credentails(self): | |||||
if self.publishable_key and self.secret_key: | |||||
header = { | |||||
"Authorization": "Bearer {}".format( | |||||
self.get_password(fieldname="secret_key", raise_exception=False) | |||||
) | |||||
} | |||||
try: | |||||
make_get_request(url="https://api.stripe.com/v1/charges", headers=header) | |||||
except Exception: | |||||
influxframework.throw(_("Seems Publishable Key or Secret Key is wrong !!!")) | |||||
def validate_transaction_currency(self, currency): | |||||
if currency not in self.supported_currencies: | |||||
influxframework.throw( | |||||
_( | |||||
"Please select another payment method. Stripe does not support transactions in currency '{0}'" | |||||
).format(currency) | |||||
) | |||||
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): | |||||
influxframework.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)}") | |||||
def create_request(self, data): | |||||
import stripe | |||||
self.data = influxframework._dict(data) | |||||
stripe.api_key = self.get_password(fieldname="secret_key", raise_exception=False) | |||||
stripe.default_http_client = stripe.http_client.RequestsClient() | |||||
try: | |||||
self.integration_request = create_request_log(self.data, service_name="Stripe") | |||||
return self.create_charge_on_stripe() | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
return { | |||||
"redirect_to": influxframework.redirect_to_message( | |||||
_("Server Error"), | |||||
_( | |||||
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account." | |||||
), | |||||
), | |||||
"status": 401, | |||||
} | |||||
def create_charge_on_stripe(self): | |||||
import stripe | |||||
try: | |||||
charge = stripe.Charge.create( | |||||
amount=cint(flt(self.data.amount) * 100), | |||||
currency=self.data.currency, | |||||
source=self.data.stripe_token_id, | |||||
description=self.data.description, | |||||
receipt_email=self.data.payer_email, | |||||
) | |||||
if charge.captured == True: | |||||
self.integration_request.db_set("status", "Completed", update_modified=False) | |||||
self.flags.status_changed_to = "Completed" | |||||
else: | |||||
influxframework.log_error(charge.failure_message, "Stripe Payment not completed") | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
return self.finalize_request() | |||||
def finalize_request(self): | |||||
redirect_to = self.data.get("redirect_to") or None | |||||
redirect_message = self.data.get("redirect_message") or None | |||||
status = self.integration_request.status | |||||
if self.flags.status_changed_to == "Completed": | |||||
if self.data.reference_doctype and self.data.reference_docname: | |||||
custom_redirect_to = None | |||||
try: | |||||
custom_redirect_to = influxframework.get_doc( | |||||
self.data.reference_doctype, self.data.reference_docname | |||||
).run_method("on_payment_authorized", self.flags.status_changed_to) | |||||
except Exception: | |||||
influxframework.log_error(influxframework.get_traceback()) | |||||
if custom_redirect_to: | |||||
redirect_to = custom_redirect_to | |||||
redirect_url = "payment-success" | |||||
if self.redirect_url: | |||||
redirect_url = self.redirect_url | |||||
redirect_to = None | |||||
else: | |||||
redirect_url = "payment-failed" | |||||
if redirect_to: | |||||
redirect_url += "?" + urlencode({"redirect_to": redirect_to}) | |||||
if redirect_message: | |||||
redirect_url += "&" + urlencode({"redirect_message": redirect_message}) | |||||
return {"redirect_to": redirect_url, "status": status} | |||||
def get_gateway_controller(doctype, docname): | |||||
reference_doc = influxframework.get_doc(doctype, docname) | |||||
gateway_controller = influxframework.db.get_value( | |||||
"Payment Gateway", reference_doc.payment_gateway, "gateway_controller" | |||||
) | |||||
return gateway_controller |
@@ -0,0 +1,7 @@ | |||||
# Copyright (c) 2018, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import unittest | |||||
class TestStripeSettings(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,8 @@ | |||||
// Copyright (c) 2016, InfluxFramework LLC | |||||
// For license information, please see license.txt | |||||
influxframework.ui.form.on('Payment Gateway', { | |||||
refresh: function(frm) { | |||||
} | |||||
}); |
@@ -0,0 +1,55 @@ | |||||
{ | |||||
"actions": [], | |||||
"autoname": "field:gateway", | |||||
"creation": "2022-01-24 21:09:47.229371", | |||||
"doctype": "DocType", | |||||
"editable_grid": 1, | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"gateway", | |||||
"gateway_settings", | |||||
"gateway_controller" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "gateway", | |||||
"fieldtype": "Data", | |||||
"in_list_view": 1, | |||||
"label": "Gateway", | |||||
"reqd": 1, | |||||
"unique": 1 | |||||
}, | |||||
{ | |||||
"fieldname": "gateway_settings", | |||||
"fieldtype": "Link", | |||||
"label": "Gateway Settings", | |||||
"options": "DocType" | |||||
}, | |||||
{ | |||||
"fieldname": "gateway_controller", | |||||
"fieldtype": "Dynamic Link", | |||||
"label": "Gateway Controller", | |||||
"options": "gateway_settings" | |||||
} | |||||
], | |||||
"links": [], | |||||
"modified": "2022-07-24 21:17:03.864719", | |||||
"modified_by": "Administrator", | |||||
"module": "Payments", | |||||
"name": "Payment Gateway", | |||||
"naming_rule": "By fieldname", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"read": 1, | |||||
"role": "System Manager", | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"states": [] | |||||
} |
@@ -0,0 +1,8 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
from influxframework.model.document import Document | |||||
class PaymentGateway(Document): | |||||
pass |
@@ -0,0 +1,9 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import unittest | |||||
# test_records = influxframework.get_test_records('Payment Gateway') | |||||
class TestPaymentGateway(unittest.TestCase): | |||||
pass |
@@ -0,0 +1,148 @@ | |||||
/* HOW-TO | |||||
Razorpay Payment | |||||
1. Include checkout script in your code | |||||
{{ include_script('/assets/payments/js/razorpay.js') }} | |||||
2. Create the Order controller in your backend | |||||
def get_razorpay_order(self): | |||||
controller = get_payment_gateway_controller("Razorpay") | |||||
payment_details = { | |||||
"amount": 300, | |||||
... | |||||
"reference_doctype": "Conference Participant", | |||||
"reference_docname": self.name, | |||||
... | |||||
"receipt": self.name | |||||
} | |||||
return controller.create_order(**payment_details) | |||||
3. Inititate the payment in client using checkout API | |||||
function make_payment(ticket) { | |||||
var options = { | |||||
"name": "<CHECKOUT MODAL TITLE>", | |||||
"description": "<CHECKOUT MODAL DESCRIPTION>", | |||||
"image": "<CHECKOUT MODAL LOGO>", | |||||
"prefill": { | |||||
"name": "<CUSTOMER NAME>", | |||||
"email": "<CUSTOMER EMAIL>", | |||||
"contact": "<CUSTOMER PHONE>" | |||||
}, | |||||
"theme": { | |||||
"color": "<MODAL COLOR>" | |||||
}, | |||||
"doctype": "<REFERENCE DOCTYPE>", | |||||
"docname": "<REFERENCE DOCNAME" | |||||
}; | |||||
razorpay = new influxframework.checkout.razorpay(options) | |||||
razorpay.on_open = () => { | |||||
<SCRIPT TO RUN WHEN MODAL OPENS> | |||||
} | |||||
razorpay.on_success = () => { | |||||
<SCRIPT TO RUN ON PAYMENT SUCCESS> | |||||
} | |||||
razorpay.on_fail = () => { | |||||
<SCRIPT TO RUN ON PAYMENT FAILURE> | |||||
} | |||||
razorpay.init() // Creates the order and opens the modal | |||||
} | |||||
*/ | |||||
influxframework.provide("influxframework.checkout"); | |||||
influxframework.require('https://checkout.razorpay.com/v1/checkout.js').then(() => { | |||||
influxframework.checkout.razorpay = class RazorpayCheckout { | |||||
constructor(opts) { | |||||
Object.assign(this, opts); | |||||
} | |||||
init() { | |||||
influxframework.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 => { | |||||
influxframework.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 => { | |||||
influxframework.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) { | |||||
influxframework.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) { | |||||
influxframework.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); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}); |
@@ -0,0 +1,55 @@ | |||||
$(document).ready(function() { | |||||
var button = document.querySelector('#submit-button'); | |||||
var form = document.querySelector('#payment-form'); | |||||
var data = {{ influxframework.form_dict | json }}; | |||||
var doctype = "{{ reference_doctype }}" | |||||
var docname = "{{ reference_docname }}" | |||||
braintree.dropin.create({ | |||||
authorization: "{{ client_token }}", | |||||
container: '#bt-dropin', | |||||
paypal: { | |||||
flow: 'vault' | |||||
} | |||||
}, function(createErr, instance) { | |||||
form.addEventListener('submit', function(event) { | |||||
event.preventDefault(); | |||||
instance.requestPaymentMethod(function(err, payload) { | |||||
if (err) { | |||||
console.log('Error', err); | |||||
return; | |||||
} | |||||
influxframework.call({ | |||||
method: "payments.templates.pages.braintree_checkout.make_payment", | |||||
freeze: true, | |||||
headers: { | |||||
"X-Requested-With": "XMLHttpRequest" | |||||
}, | |||||
args: { | |||||
"payload_nonce": payload.nonce, | |||||
"data": JSON.stringify(data), | |||||
"reference_doctype": doctype, | |||||
"reference_docname": docname | |||||
}, | |||||
callback: function(r) { | |||||
if (r.message && r.message.status == "Completed") { | |||||
window.location.href = r.message.redirect_to | |||||
} else if (r.message && r.message.status == "Error") { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
} | |||||
}) | |||||
}); | |||||
}); | |||||
instance.on('paymentMethodRequestable', function (event) { | |||||
button.removeAttribute('disabled'); | |||||
}); | |||||
instance.on('noPaymentMethodRequestable', function () { | |||||
button.setAttribute('disabled', true); | |||||
}); | |||||
}); | |||||
}) |
@@ -0,0 +1,53 @@ | |||||
$(document).ready(function(){ | |||||
(function(e){ | |||||
var options = { | |||||
"key": "{{ api_key }}", | |||||
"amount": cint({{ amount }} * 100), // 2000 paise = INR 20 | |||||
"currency": "{{ currency }}", | |||||
"name": "{{ title }}", | |||||
"description": "{{ description }}", | |||||
"subscription_id": "{{ subscription_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 }}" | |||||
}, | |||||
"notes": {{ influxframework.form_dict|json }} | |||||
}; | |||||
var rzp = new Razorpay(options); | |||||
rzp.open(); | |||||
// e.preventDefault(); | |||||
})(); | |||||
}) | |||||
influxframework.provide('razorpay'); | |||||
razorpay.make_payment_log = function(response, options, doctype, docname, token){ | |||||
$('.razorpay-loading').addClass('hidden'); | |||||
$('.razorpay-confirming').removeClass('hidden'); | |||||
influxframework.call({ | |||||
method:"payments.templates.pages.razorpay_checkout.make_payment", | |||||
freeze:true, | |||||
headers: {"X-Requested-With": "XMLHttpRequest"}, | |||||
args: { | |||||
"razorpay_payment_id": response.razorpay_payment_id, | |||||
"options": options, | |||||
"reference_doctype": doctype, | |||||
"reference_docname": docname, | |||||
"token": token | |||||
}, | |||||
callback: function(r){ | |||||
if (r.message && r.message.status == 200) { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
else if (r.message && ([401,400,500].indexOf(r.message.status) > -1)) { | |||||
window.location.href = r.message.redirect_to | |||||
} | |||||
} | |||||
}) | |||||
} |
@@ -0,0 +1,85 @@ | |||||
var stripe = Stripe("{{ publishable_key }}"); | |||||
var elements = stripe.elements(); | |||||
var style = { | |||||
base: { | |||||
color: '#32325d', | |||||
lineHeight: '18px', | |||||
fontFamily: '"Helvetica Neue", Helvetica, sans-serif', | |||||
fontSmoothing: 'antialiased', | |||||
fontSize: '16px', | |||||
'::placeholder': { | |||||
color: '#aab7c4' | |||||
} | |||||
}, | |||||
invalid: { | |||||
color: '#fa755a', | |||||
iconColor: '#fa755a' | |||||
} | |||||
}; | |||||
var card = elements.create('card', { | |||||
hidePostalCode: true, | |||||
style: style | |||||
}); | |||||
card.mount('#card-element'); | |||||
function setOutcome(result) { | |||||
if (result.token) { | |||||
$('#submit').prop('disabled', true) | |||||
$('#submit').html(__('Processing...')) | |||||
influxframework.call({ | |||||
method:"payments.templates.pages.stripe_checkout.make_payment", | |||||
freeze:true, | |||||
headers: {"X-Requested-With": "XMLHttpRequest"}, | |||||
args: { | |||||
"stripe_token_id": result.token.id, | |||||
"data": JSON.stringify({{ influxframework.form_dict|json }}), | |||||
"reference_doctype": "{{ reference_doctype }}", | |||||
"reference_docname": "{{ reference_docname }}" | |||||
}, | |||||
callback: function(r) { | |||||
if (r.message.status == "Completed") { | |||||
$('#submit').hide() | |||||
$('.success').show() | |||||
setTimeout(function() { | |||||
window.location.href = r.message.redirect_to | |||||
}, 2000); | |||||
} else { | |||||
$('#submit').hide() | |||||
$('.error').show() | |||||
setTimeout(function() { | |||||
window.location.href = r.message.redirect_to | |||||
}, 2000); | |||||
} | |||||
} | |||||
}); | |||||
} else if (result.error) { | |||||
$('.error').html(result.error.message); | |||||
$('.error').show() | |||||
} | |||||
} | |||||
card.on('change', function(event) { | |||||
var displayError = document.getElementById('card-errors'); | |||||
if (event.error) { | |||||
displayError.textContent = event.error.message; | |||||
} else { | |||||
displayError.textContent = ''; | |||||
} | |||||
}); | |||||
influxframework.ready(function() { | |||||
$('#submit').off("click").on("click", function(e) { | |||||
e.preventDefault(); | |||||
var extraDetails = { | |||||
name: $('input[name=cardholder-name]').val(), | |||||
email: $('input[name=cardholder-email]').val() | |||||
} | |||||
stripe.createToken(card, extraDetails).then(setOutcome); | |||||
}) | |||||
}); |
@@ -0,0 +1,54 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%}{% endblock %} | |||||
{% block script %} | |||||
<script src="https://js.braintreegateway.com/web/dropin/1.9.3/js/dropin.min.js"></script> | |||||
<script>{% include "templates/includes/braintree_checkout.js" %}</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class="wrapper"> | |||||
<div class="checkout container"> | |||||
<header> | |||||
<div> | |||||
<img class="center" src="{{ header_img }}"></img> | |||||
</div> | |||||
</header> | |||||
<form id="payment-form"> | |||||
<section> | |||||
<div class="bt-drop-in-wrapper"> | |||||
<div id="bt-dropin"></div> | |||||
</div> | |||||
</section> | |||||
<button class="btn btn-primary" type="submit" id="submit-button" disabled><span>{{ _("Pay") }} {{ amount }} {{ currency }}</span></button> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
<style> | |||||
.checkout { | |||||
max-width: 60%; | |||||
} | |||||
.center { | |||||
margin:auto; | |||||
display: block; | |||||
} | |||||
#payment-form { | |||||
margin-top: 40px; | |||||
} | |||||
#submit-button { | |||||
float: right; | |||||
} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,69 @@ | |||||
# Copyright (c) 2021, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import json | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.utils import flt | |||||
from payments.payment_gateways.doctype.braintree_settings.braintree_settings import ( | |||||
get_client_token, | |||||
get_gateway_controller, | |||||
) | |||||
no_cache = 1 | |||||
expected_keys = ( | |||||
"amount", | |||||
"title", | |||||
"description", | |||||
"reference_doctype", | |||||
"reference_docname", | |||||
"payer_name", | |||||
"payer_email", | |||||
"order_id", | |||||
"currency", | |||||
) | |||||
def get_context(context): | |||||
context.no_cache = 1 | |||||
# all these keys exist in form_dict | |||||
if not (set(expected_keys) - set(list(influxframework.form_dict))): | |||||
for key in expected_keys: | |||||
context[key] = influxframework.form_dict[key] | |||||
context.client_token = get_client_token(context.reference_docname) | |||||
context["amount"] = flt(context["amount"]) | |||||
gateway_controller = get_gateway_controller(context.reference_docname) | |||||
context["header_img"] = influxframework.db.get_value( | |||||
"Braintree Settings", gateway_controller, "header_img" | |||||
) | |||||
else: | |||||
influxframework.redirect_to_message( | |||||
_("Some information is missing"), | |||||
_( | |||||
"Looks like someone sent you to an incomplete URL. Please ask them to look into it." | |||||
), | |||||
) | |||||
influxframework.local.flags.redirect_location = influxframework.local.response.location | |||||
raise influxframework.Redirect | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def make_payment(payload_nonce, data, reference_doctype, reference_docname): | |||||
data = json.loads(data) | |||||
data.update({"payload_nonce": payload_nonce}) | |||||
gateway_controller = get_gateway_controller(reference_docname) | |||||
data = influxframework.get_doc("Braintree Settings", gateway_controller).create_payment_request( | |||||
data | |||||
) | |||||
influxframework.db.commit() | |||||
return data |
@@ -0,0 +1,22 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %}{{ _("Payment Cancelled") }}{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class='page-card'> | |||||
<div class='page-card-head'> | |||||
<span class='indicator red'> | |||||
{{ _("Payment Cancelled") }}</span> | |||||
</div> | |||||
<p>{{ _("Your payment is cancelled.") }}</p> | |||||
<div><a href='{{ influxframework.form_dict.redirect_to or "/" }}' class='btn btn-primary btn-sm'> | |||||
{{ _("Continue") }}</a></div> | |||||
</div> | |||||
<style> | |||||
.hero-and-content { | |||||
background-color: #f5f7fa; | |||||
} | |||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,22 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %}{{ _("Payment Failed") }}{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class='page-card'> | |||||
<div class='page-card-head'> | |||||
<span class='indicator red'> | |||||
{{ _("Payment Failed") }}</span> | |||||
</div> | |||||
<p>{{ _("Your payment has failed.") }}</p> | |||||
<div><a href='{{ influxframework.form_dict.redirect_to or "/" }}' class='btn btn-primary btn-sm'> | |||||
{{ _("Continue") }}</a></div> | |||||
</div> | |||||
<style> | |||||
.hero-and-content { | |||||
background-color: #f5f7fa; | |||||
} | |||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,34 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %}{{ _("Payment Success") }}{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class='page-card'> | |||||
<div class='page-card-head'> | |||||
<span class='indicator green'> | |||||
{{ _("Success") }}</span> | |||||
</div> | |||||
<p>{{ payment_message or _("Your payment was successfully accepted") }}</p> | |||||
{% if not payment_message %} | |||||
<div> | |||||
<a | |||||
href='{{ influxframework.form_dict.redirect_to or "/" }}' | |||||
class='btn btn-primary btn-sm'> | |||||
{{ _("Continue") }} | |||||
</a> | |||||
</div> | |||||
{% endif %} | |||||
</div> | |||||
<style> | |||||
{% include "templates/styles/card_style.css" %} | |||||
</style> | |||||
<script> | |||||
influxframework.ready(function() { | |||||
if('{{ influxframework.form_dict.redirect_to or "" }}'){ | |||||
setTimeout(function(){ | |||||
window.location.href = '{{ influxframework.form_dict.redirect_to }}'; | |||||
}, 4000); | |||||
} | |||||
}) | |||||
</script> | |||||
{% endblock %} |
@@ -0,0 +1,12 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import influxframework | |||||
def get_context(context): | |||||
token = influxframework.local.form_dict.token | |||||
if token: | |||||
influxframework.db.set_value("Integration Request", token, "status", "Cancelled") | |||||
influxframework.db.commit() |
@@ -0,0 +1,15 @@ | |||||
# Copyright (c) 2015, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import influxframework | |||||
no_cache = True | |||||
def get_context(context): | |||||
token = influxframework.local.form_dict.token | |||||
doc = influxframework.get_doc(influxframework.local.form_dict.doctype, influxframework.local.form_dict.docname) | |||||
context.payment_message = "" | |||||
if hasattr(doc, "get_payment_success_message"): | |||||
context.payment_message = doc.get_payment_success_message() |
@@ -0,0 +1,43 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%} | |||||
<head> | |||||
<title>Merchant Checkout Page</title> | |||||
</head> | |||||
{% endblock %} | |||||
{% block script %} | |||||
<script defer type="text/javascript"> | |||||
document.paytm_form.submit(); | |||||
</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<body> | |||||
<div class="centered"> | |||||
<h2>Please do not refresh this page...</h2> | |||||
<form method="post" action="{{ url }}" name="paytm_form"> | |||||
{% for name, value in payment_details.items() %} | |||||
<input type="hidden" name="{{ name }}" value="{{ value }}"> | |||||
{% endfor %} | |||||
</form> | |||||
</div> | |||||
</body> | |||||
{% endblock %} | |||||
{% block style %} | |||||
<style> | |||||
.centered { | |||||
position: absolute; | |||||
top: 50%; | |||||
left: 50%; | |||||
transform: translate(-50%, -50%); | |||||
} | |||||
.web-footer { | |||||
display: none; | |||||
} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,37 @@ | |||||
# Copyright (c) 2021, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import json | |||||
import influxframework | |||||
from influxframework import _ | |||||
from payments.payment_gateways.doctype.paytm_settings.paytm_settings import ( | |||||
get_paytm_config, | |||||
get_paytm_params, | |||||
) | |||||
def get_context(context): | |||||
context.no_cache = 1 | |||||
paytm_config = get_paytm_config() | |||||
try: | |||||
doc = influxframework.get_doc("Integration Request", influxframework.form_dict["order_id"]) | |||||
context.payment_details = get_paytm_params( | |||||
json.loads(doc.data), doc.name, paytm_config | |||||
) | |||||
context.url = paytm_config.url | |||||
except Exception: | |||||
influxframework.log_error() | |||||
influxframework.redirect_to_message( | |||||
_("Invalid Token"), | |||||
_("Seems token you are using is invalid!"), | |||||
http_status_code=400, | |||||
indicator_color="red", | |||||
) | |||||
influxframework.local.flags.redirect_location = influxframework.local.response.location | |||||
raise influxframework.Redirect |
@@ -0,0 +1,28 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%}{% endblock %} | |||||
{% block script %} | |||||
<script src="https://checkout.razorpay.com/v1/checkout.js"></script> | |||||
<script>{% include "templates/includes/razorpay_checkout.js" %}</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<p class='lead text-center centered'> | |||||
<span class='razorpay-loading'>Loading Payment System</span> | |||||
<span class='razorpay-confirming hidden'>Confirming Payment</span> | |||||
</p> | |||||
{% endblock %} | |||||
{% block style %} | |||||
<style> | |||||
header, footer { | |||||
display: none; | |||||
} | |||||
</style> | |||||
{% endblock %} |
@@ -0,0 +1,81 @@ | |||||
# Copyright (c) 2021, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import json | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.utils import cint, flt | |||||
no_cache = 1 | |||||
expected_keys = ( | |||||
"amount", | |||||
"title", | |||||
"description", | |||||
"reference_doctype", | |||||
"reference_docname", | |||||
"payer_name", | |||||
"payer_email", | |||||
"order_id", | |||||
"currency", | |||||
) | |||||
def get_context(context): | |||||
context.no_cache = 1 | |||||
context.api_key = get_api_key() | |||||
try: | |||||
doc = influxframework.get_doc("Integration Request", influxframework.form_dict["token"]) | |||||
payment_details = json.loads(doc.data) | |||||
for key in expected_keys: | |||||
context[key] = payment_details[key] | |||||
context["token"] = influxframework.form_dict["token"] | |||||
context["amount"] = flt(context["amount"]) | |||||
context["subscription_id"] = ( | |||||
payment_details["subscription_id"] if payment_details.get("subscription_id") else "" | |||||
) | |||||
except Exception as e: | |||||
influxframework.redirect_to_message( | |||||
_("Invalid Token"), | |||||
_("Seems token you are using is invalid!"), | |||||
http_status_code=400, | |||||
indicator_color="red", | |||||
) | |||||
influxframework.local.flags.redirect_location = influxframework.local.response.location | |||||
raise influxframework.Redirect | |||||
def get_api_key(): | |||||
api_key = influxframework.db.get_single_value("Razorpay Settings", "api_key") | |||||
if cint(influxframework.form_dict.get("use_sandbox")): | |||||
api_key = influxframework.conf.sandbox_api_key | |||||
return api_key | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def make_payment( | |||||
razorpay_payment_id, options, reference_doctype, reference_docname, token | |||||
): | |||||
data = {} | |||||
if isinstance(options, str): | |||||
data = json.loads(options) | |||||
data.update( | |||||
{ | |||||
"razorpay_payment_id": razorpay_payment_id, | |||||
"reference_docname": reference_docname, | |||||
"reference_doctype": reference_doctype, | |||||
"token": token, | |||||
} | |||||
) | |||||
data = influxframework.get_doc("Razorpay Settings").create_request(data) | |||||
influxframework.db.commit() | |||||
return data |
@@ -0,0 +1,113 @@ | |||||
.StripeElement { | |||||
background-color: white; | |||||
height: 40px; | |||||
padding: 10px 12px; | |||||
border-radius: 4px; | |||||
border: 1px solid transparent; | |||||
box-shadow: 0 1px 3px 0 #e6ebf1; | |||||
-webkit-transition: box-shadow 150ms ease; | |||||
transition: box-shadow 150ms ease; | |||||
} | |||||
.StripeElement--focus { | |||||
box-shadow: 0 1px 3px 0 #cfd7df; | |||||
} | |||||
.StripeElement--invalid { | |||||
border-color: #fa755a; | |||||
} | |||||
.StripeElement--webkit-autofill { | |||||
background-color: #fefde5; | |||||
} | |||||
.stripe #payment-form { | |||||
margin-top: 80px; | |||||
} | |||||
.stripe button { | |||||
float: right; | |||||
display: block; | |||||
background: #5e64ff; | |||||
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: 0; | |||||
margin-top: 20px; | |||||
font-size: 15px; | |||||
font-weight: 400; | |||||
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; | |||||
} | |||||
.stripe label { | |||||
position: relative; | |||||
color: #8898AA; | |||||
font-weight: 300; | |||||
height: 40px; | |||||
line-height: 40px; | |||||
margin-left: 20px; | |||||
display: block; | |||||
} | |||||
.stripe .group label:not(:last-child) { | |||||
border-bottom: 1px solid #F0F5FA; | |||||
} | |||||
.stripe label>span { | |||||
width: 20%; | |||||
text-align: right; | |||||
float: left; | |||||
} | |||||
.current-card { | |||||
margin-left: 20px; | |||||
} | |||||
.field { | |||||
background: transparent; | |||||
font-weight: 300; | |||||
border: 0; | |||||
color: #31325F; | |||||
outline: none; | |||||
padding-right: 10px; | |||||
padding-left: 10px; | |||||
cursor: text; | |||||
width: 70%; | |||||
height: 40px; | |||||
float: right; | |||||
} | |||||
.field::-webkit-input-placeholder { | |||||
color: #CFD7E0; | |||||
} | |||||
.field::-moz-placeholder { | |||||
color: #CFD7E0; | |||||
} | |||||
.field:-ms-input-placeholder { | |||||
color: #CFD7E0; | |||||
} |
@@ -0,0 +1,58 @@ | |||||
{% extends "templates/web.html" %} | |||||
{% block title %} Payment {% endblock %} | |||||
{%- block header -%} | |||||
{% endblock %} | |||||
{% block script %} | |||||
<script src="https://js.stripe.com/v3/"></script> | |||||
<script>{% include "templates/includes/stripe_checkout.js" %}</script> | |||||
{% endblock %} | |||||
{%- block page_content -%} | |||||
<div class="stripe" style="min-height: 400px; padding-bottom: 50px; margin-top:100px;margin-left:250px;"> | |||||
<div class="col-sm-10 col-sm-offset-2"> | |||||
{% if image %} | |||||
<img src={{image}}> | |||||
{% endif %} | |||||
<h2 class="text-center">{{description}}</h2> | |||||
<form id="payment-form"> | |||||
<div class="form-row row"> | |||||
<div class="group col-12"> | |||||
<div> | |||||
<label> | |||||
<span>{{ _("Name") }}</span> | |||||
<input id="cardholder-name" name="cardholder-name" class="field" placeholder="{{ _('John Doe') }}" value="{{payer_name}}"/> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="group col-12"> | |||||
<div> | |||||
<label> | |||||
<span>{{ _("Email") }}</span> | |||||
<input id="cardholder-email" name="cardholder-email" class="field" placeholder="{{ _('john@doe.com') }}" value="{{payer_email}}"/> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<div class="group col-12"> | |||||
<label> | |||||
<span>{{ _("Card Details") }}</span> | |||||
<div id="card-element" name="card-element" class="field"></div> | |||||
<div id="card-errors" role="alert"></div> | |||||
</label> | |||||
</div> | |||||
</div> | |||||
<button type="submit" class="submit" id="submit">{{_('Pay')}} {{amount}}</button> | |||||
<div class="outcome text-center"> | |||||
<div class="error" hidden>{{ _("An error occured during the payment process. Please contact us.") }}</div> | |||||
<div class="success" hidden>{{ _("Your payment has been successfully registered.") }}</div> | |||||
</div> | |||||
</form> | |||||
</div> | |||||
</div> | |||||
{% endblock %} |
@@ -0,0 +1,100 @@ | |||||
# Copyright (c) 2021, InfluxFramework LLC | |||||
# License: MIT. See LICENSE | |||||
import json | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.utils import cint, fmt_money | |||||
from payments.payment_gateways.doctype.stripe_settings.stripe_settings import ( | |||||
get_gateway_controller, | |||||
) | |||||
no_cache = 1 | |||||
expected_keys = ( | |||||
"amount", | |||||
"title", | |||||
"description", | |||||
"reference_doctype", | |||||
"reference_docname", | |||||
"payer_name", | |||||
"payer_email", | |||||
"order_id", | |||||
"currency", | |||||
) | |||||
def get_context(context): | |||||
context.no_cache = 1 | |||||
# all these keys exist in form_dict | |||||
if not (set(expected_keys) - set(list(influxframework.form_dict))): | |||||
for key in expected_keys: | |||||
context[key] = influxframework.form_dict[key] | |||||
gateway_controller = get_gateway_controller( | |||||
context.reference_doctype, context.reference_docname | |||||
) | |||||
context.publishable_key = get_api_key(context.reference_docname, gateway_controller) | |||||
context.image = get_header_image(context.reference_docname, gateway_controller) | |||||
context["amount"] = fmt_money(amount=context["amount"], currency=context["currency"]) | |||||
if is_a_subscription(context.reference_doctype, context.reference_docname): | |||||
payment_plan = influxframework.db.get_value( | |||||
context.reference_doctype, context.reference_docname, "payment_plan" | |||||
) | |||||
recurrence = influxframework.db.get_value("Payment Plan", payment_plan, "recurrence") | |||||
context["amount"] = context["amount"] + " " + _(recurrence) | |||||
else: | |||||
influxframework.redirect_to_message( | |||||
_("Some information is missing"), | |||||
_( | |||||
"Looks like someone sent you to an incomplete URL. Please ask them to look into it." | |||||
), | |||||
) | |||||
influxframework.local.flags.redirect_location = influxframework.local.response.location | |||||
raise influxframework.Redirect | |||||
def get_api_key(doc, gateway_controller): | |||||
publishable_key = influxframework.db.get_value( | |||||
"Stripe Settings", gateway_controller, "publishable_key" | |||||
) | |||||
if cint(influxframework.form_dict.get("use_sandbox")): | |||||
publishable_key = influxframework.conf.sandbox_publishable_key | |||||
return publishable_key | |||||
def get_header_image(doc, gateway_controller): | |||||
header_image = influxframework.db.get_value("Stripe Settings", gateway_controller, "header_img") | |||||
return header_image | |||||
@influxframework.whitelist(allow_guest=True) | |||||
def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): | |||||
data = json.loads(data) | |||||
data.update({"stripe_token_id": stripe_token_id}) | |||||
gateway_controller = get_gateway_controller(reference_doctype, reference_docname) | |||||
if is_a_subscription(reference_doctype, reference_docname): | |||||
reference = influxframework.get_doc(reference_doctype, reference_docname) | |||||
data = reference.create_subscription("stripe", gateway_controller, data) | |||||
else: | |||||
data = influxframework.get_doc("Stripe Settings", gateway_controller).create_request(data) | |||||
influxframework.db.commit() | |||||
return data | |||||
def is_a_subscription(reference_doctype, reference_docname): | |||||
if not influxframework.get_meta(reference_doctype).has_field("is_a_subscription"): | |||||
return False | |||||
return influxframework.db.get_value(reference_doctype, reference_docname, "is_a_subscription") |
@@ -0,0 +1,7 @@ | |||||
from payments.utils.utils import ( | |||||
before_install, | |||||
create_payment_gateway, | |||||
delete_custom_fields, | |||||
get_payment_gateway_controller, | |||||
make_custom_fields, | |||||
) |
@@ -0,0 +1,173 @@ | |||||
import click | |||||
import influxframework | |||||
from influxframework import _ | |||||
from influxframework.custom.doctype.custom_field.custom_field import create_custom_fields | |||||
def get_payment_gateway_controller(payment_gateway): | |||||
"""Return payment gateway controller""" | |||||
gateway = influxframework.get_doc("Payment Gateway", payment_gateway) | |||||
if gateway.gateway_controller is None: | |||||
try: | |||||
return influxframework.get_doc(f"{payment_gateway} Settings") | |||||
except Exception: | |||||
influxframework.throw(_("{0} Settings not found").format(payment_gateway)) | |||||
else: | |||||
try: | |||||
return influxframework.get_doc(gateway.gateway_settings, gateway.gateway_controller) | |||||
except Exception: | |||||
influxframework.throw(_("{0} Settings not found").format(payment_gateway)) | |||||
@influxframework.whitelist(allow_guest=True, xss_safe=True) | |||||
def get_checkout_url(**kwargs): | |||||
try: | |||||
if kwargs.get("payment_gateway"): | |||||
doc = influxframework.get_doc("{} Settings".format(kwargs.get("payment_gateway"))) | |||||
return doc.get_payment_url(**kwargs) | |||||
else: | |||||
raise Exception | |||||
except Exception: | |||||
influxframework.respond_as_web_page( | |||||
_("Something went wrong"), | |||||
_( | |||||
"Looks like something is wrong with this site's payment gateway configuration. No payment has been made." | |||||
), | |||||
indicator_color="red", | |||||
http_status_code=influxframework.ValidationError.http_status_code, | |||||
) | |||||
def create_payment_gateway(gateway, settings=None, controller=None): | |||||
# NOTE: we don't translate Payment Gateway name because it is an internal doctype | |||||
if not influxframework.db.exists("Payment Gateway", gateway): | |||||
payment_gateway = influxframework.get_doc( | |||||
{ | |||||
"doctype": "Payment Gateway", | |||||
"gateway": gateway, | |||||
"gateway_settings": settings, | |||||
"gateway_controller": controller, | |||||
} | |||||
) | |||||
payment_gateway.insert(ignore_permissions=True) | |||||
def make_custom_fields(): | |||||
if not influxframework.get_meta("Web Form").has_field("payments_tab"): | |||||
click.secho("* Installing Payment Custom Fields in Web Form") | |||||
create_custom_fields( | |||||
{ | |||||
"Web Form": [ | |||||
{ | |||||
"fieldname": "payments_tab", | |||||
"fieldtype": "Tab Break", | |||||
"label": "Payments", | |||||
"insert_after": "custom_css", | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"fieldname": "accept_payment", | |||||
"fieldtype": "Check", | |||||
"label": "Accept Payment", | |||||
"insert_after": "payments", | |||||
}, | |||||
{ | |||||
"depends_on": "accept_payment", | |||||
"fieldname": "payment_gateway", | |||||
"fieldtype": "Link", | |||||
"label": "Payment Gateway", | |||||
"options": "Payment Gateway", | |||||
"insert_after": "accept_payment", | |||||
}, | |||||
{ | |||||
"default": "Buy Now", | |||||
"depends_on": "accept_payment", | |||||
"fieldname": "payment_button_label", | |||||
"fieldtype": "Data", | |||||
"label": "Button Label", | |||||
"insert_after": "payment_gateway", | |||||
}, | |||||
{ | |||||
"depends_on": "accept_payment", | |||||
"fieldname": "payment_button_help", | |||||
"fieldtype": "Text", | |||||
"label": "Button Help", | |||||
"insert_after": "payment_button_label", | |||||
}, | |||||
{ | |||||
"fieldname": "payments_cb", | |||||
"fieldtype": "Column Break", | |||||
"insert_after": "payment_button_help", | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"depends_on": "accept_payment", | |||||
"fieldname": "amount_based_on_field", | |||||
"fieldtype": "Check", | |||||
"label": "Amount Based On Field", | |||||
"insert_after": "payments_cb", | |||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.accept_payment && doc.amount_based_on_field", | |||||
"fieldname": "amount_field", | |||||
"fieldtype": "Select", | |||||
"label": "Amount Field", | |||||
"insert_after": "amount_based_on_field", | |||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.accept_payment && !doc.amount_based_on_field", | |||||
"fieldname": "amount", | |||||
"fieldtype": "Currency", | |||||
"label": "Amount", | |||||
"insert_after": "amount_field", | |||||
}, | |||||
{ | |||||
"depends_on": "accept_payment", | |||||
"fieldname": "currency", | |||||
"fieldtype": "Link", | |||||
"label": "Currency", | |||||
"options": "Currency", | |||||
"insert_after": "amount", | |||||
}, | |||||
] | |||||
} | |||||
) | |||||
influxframework.clear_cache(doctype="Web Form") | |||||
def delete_custom_fields(): | |||||
if influxframework.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: | |||||
influxframework.db.delete("Custom Field", {"name": "Web Form-" + fieldname}) | |||||
influxframework.clear_cache(doctype="Web Form") | |||||
def before_install(): | |||||
# TODO: remove this | |||||
# This is done for influxerp CI patch test | |||||
# | |||||
# Since we follow a flow like install v14 -> restore v10 site | |||||
# -> migrate to v12, v13 and then v14 again | |||||
# | |||||
# This app fails installing when the site is restored to v10 as | |||||
# a lot of apis don;t exist in v10 and this is a (at the moment) required app for influxerp. | |||||
if not influxframework.get_meta("Module Def").has_field("custom"): | |||||
return False |
@@ -0,0 +1,5 @@ | |||||
# influxframework -- https://github.com/influxframework/influxframework is installed via 'bench init' | |||||
paytmchecksum~=1.7.0 | |||||
razorpay~=1.2.0 | |||||
stripe~=2.56.0 | |||||
braintree~=4.8.0 |
@@ -0,0 +1,19 @@ | |||||
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 influxframework", | |||||
author="InfluxFramework LLC", | |||||
author_email="hello@influxframework.com", | |||||
packages=find_packages(), | |||||
zip_safe=False, | |||||
include_package_data=True, | |||||
install_requires=install_requires, | |||||
) |