diff --git a/payments/__init__.py b/payments/__init__.py index 7a0660b..f102a9c 100644 --- a/payments/__init__.py +++ b/payments/__init__.py @@ -1,3 +1 @@ - -__version__ = '0.0.1' - +__version__ = "0.0.1" diff --git a/payments/hooks.py b/payments/hooks.py index daf05ec..ffe6f3d 100644 --- a/payments/hooks.py +++ b/payments/hooks.py @@ -64,12 +64,12 @@ app_license = "MIT" # ------------ # before_install = "pay.install.before_install" -# after_install = "pay.install.after_install" +after_install = "payments.utils.make_custom_fields" # Uninstallation # ------------ -# before_uninstall = "pay.uninstall.before_uninstall" +before_uninstall = "payments.utils.delete_custom_fields" # after_uninstall = "pay.uninstall.after_uninstall" # Desk Notifications @@ -94,9 +94,9 @@ app_license = "MIT" # --------------- # Override standard doctype classes -# override_doctype_class = { -# "ToDo": "custom_app.overrides.CustomToDo" -# } +override_doctype_class = { + "Web Form": "payments.overrides.paymentwebform.PaymentWebForm" +} # Document Events # --------------- @@ -113,23 +113,11 @@ app_license = "MIT" # Scheduled Tasks # --------------- -# scheduler_events = { -# "all": [ -# "pay.tasks.all" -# ], -# "daily": [ -# "pay.tasks.daily" -# ], -# "hourly": [ -# "pay.tasks.hourly" -# ], -# "weekly": [ -# "pay.tasks.weekly" -# ], -# "monthly": [ -# "pay.tasks.monthly" -# ], -# } +scheduler_events = { + "all": [ + "payments.payment_gateways.doctype.razorpay_settings.razorpay_settings.capture_payment", + ], +} # Testing # ------- diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index 35481c6..d72ff94 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -7,10 +7,12 @@ import braintree import frappe from frappe import _ -from frappe.integrations.utils import create_payment_gateway, create_request_log +from frappe.integrations.utils import create_request_log from frappe.model.document import Document from frappe.utils import call_hook_method, get_url +from payments.utils import create_payment_gateway + class BraintreeSettings(Document): supported_currencies = [ @@ -157,7 +159,9 @@ class BraintreeSettings(Document): def on_update(self): create_payment_gateway( - "Braintree-" + self.gateway_name, settings="Braintree Settings", controller=self.gateway_name + "Braintree-" + self.gateway_name, + settings="Braintree Settings", + controller=self.gateway_name, ) call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) @@ -237,7 +241,8 @@ class BraintreeSettings(Document): self.integration_request.db_set("status", "Failed", update_modified=False) for error in result.errors.deep_errors: error_log = frappe.log_error( - "code: " + str(error.code) + " | message: " + str(error.message), "Braintree Payment Error" + "code: " + str(error.code) + " | message: " + str(error.message), + "Braintree Payment Error", ) self.integration_request.db_set("error", error_log.error, update_modified=False) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 99c4992..0aed94f 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -8,7 +8,7 @@ Example: - from frappe.integrations.utils import get_payment_gateway_controller + from payments.utils import get_payment_gateway_controller controller = get_payment_gateway_controller("PayPal") controller().validate_transaction_currency(currency) @@ -69,10 +69,12 @@ import pytz import frappe from frappe import _ -from frappe.integrations.utils import create_payment_gateway, create_request_log, make_post_request +from frappe.integrations.utils import create_request_log, make_post_request from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_datetime, get_url +from payments.utils import create_payment_gateway + api_path = "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings" @@ -179,7 +181,10 @@ class PayPalSettings(Document): 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]} + { + "token": response.get("TOKEN")[0], + "correlation_id": response.get("CORRELATIONID")[0], + } ) create_request_log(kwargs, service_name="PayPal", name=kwargs["token"]) @@ -373,7 +378,9 @@ def create_recurring_profile(token, payerid): } ) - status_changed_to = "Completed" if data.get("starting_immediately") or updating else "Verified" + status_changed_to = ( + "Completed" if data.get("starting_immediately") or updating else "Verified" + ) starts_at = get_datetime(subscription_details.get("start_date")) or frappe.utils.now_datetime() starts_at = starts_at.replace(tzinfo=pytz.timezone(frappe.utils.get_time_zone())).astimezone( @@ -433,7 +440,11 @@ def get_redirect_uri(doc, token, payerid): def manage_recurring_payment_profile_status(profile_id, action, args, url): args.update( - {"METHOD": "ManageRecurringPaymentsProfileStatus", "PROFILEID": profile_id, "ACTION": action} + { + "METHOD": "ManageRecurringPaymentsProfileStatus", + "PROFILEID": profile_id, + "ACTION": action, + } ) response = make_post_request(url, data=args) @@ -442,7 +453,9 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): # thus could not cancel the subscription. # thus raise an exception only if the error code is not equal to 11556 - if response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556": + if ( + response.get("ACK")[0] != "Success" and response.get("L_ERRORCODE0", [])[0] != "11556" + ): frappe.throw(_("Failed while amending subscription")) @@ -491,7 +504,10 @@ def validate_ipn_request(data): params, url = doc.get_paypal_params_and_url() params.update( - {"METHOD": "GetRecurringPaymentsProfileDetails", "PROFILEID": data.get("recurring_payment_id")} + { + "METHOD": "GetRecurringPaymentsProfileDetails", + "PROFILEID": data.get("recurring_payment_id"), + } ) params = urlencode(params) diff --git a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py index 81a5f45..4521f1c 100644 --- a/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py +++ b/payments/payment_gateways/doctype/paytm_settings/paytm_settings.py @@ -6,14 +6,15 @@ from urllib.parse import urlencode import requests from paytmchecksum import generateSignature, verifySignature - import frappe from frappe import _ -from frappe.integrations.utils import create_payment_gateway, create_request_log +from frappe.integrations.utils import create_request_log from frappe.model.document import Document from frappe.utils import call_hook_method, cint, cstr, flt, get_request_site_address, get_url from frappe.utils.password import get_decrypted_password +from payments.utils import create_payment_gateway + class PaytmSettings(Document): supported_currencies = ["INR"] diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a79e626..5b25112 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -8,7 +8,7 @@ Example: - from frappe.integrations.utils import get_payment_gateway_controller + from payments.utils import get_payment_gateway_controller controller = get_payment_gateway_controller("Razorpay") controller().validate_transaction_currency(currency) @@ -64,19 +64,16 @@ import hmac import json from urllib.parse import urlencode -import razorpay - import frappe +import razorpay from frappe import _ -from frappe.integrations.utils import ( - create_payment_gateway, - create_request_log, - make_get_request, - make_post_request, -) +from frappe.integrations.utils import (create_request_log, make_get_request, + make_post_request) from frappe.model.document import Document from frappe.utils import call_hook_method, cint, get_timestamp, get_url +from payments.utils import create_payment_gateway + class RazorpaySettings(Document): supported_currencies = ["INR"] @@ -97,7 +94,10 @@ class RazorpaySettings(Document): try: make_get_request( url="https://api.razorpay.com/v1/payments", - auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), + auth=( + self.api_key, + self.get_password(fieldname="api_secret", raise_exception=False), + ), ) except Exception: frappe.throw(_("Seems API Key or API Secret is wrong !!!")) @@ -123,7 +123,9 @@ class RazorpaySettings(Document): "quantity": 1 (The total amount is calculated as item.amount * quantity) } """ - url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format(kwargs.get("subscription_id")) + url = "https://api.razorpay.com/v1/subscriptions/{}/addons".format( + kwargs.get("subscription_id") + ) try: if not frappe.conf.converted_rupee_to_paisa: @@ -137,7 +139,9 @@ class RazorpaySettings(Document): headers={"content-type": "application/json"}, ) if not resp.get("id"): - frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") + frappe.log_error( + message=str(resp), title="Razorpay Failed while creating subscription" + ) except Exception: frappe.log_error() # failed @@ -176,7 +180,9 @@ class RazorpaySettings(Document): frappe.flags.status = "created" return kwargs else: - frappe.log_error(message=str(resp), title="Razorpay Failed while creating subscription") + frappe.log_error( + message=str(resp), title="Razorpay Failed while creating subscription" + ) except Exception: frappe.log_error() @@ -214,7 +220,10 @@ class RazorpaySettings(Document): try: order = make_post_request( "https://api.razorpay.com/v1/orders", - auth=(self.api_key, self.get_password(fieldname="api_secret", raise_exception=False)), + auth=( + self.api_key, + self.get_password(fieldname="api_secret", raise_exception=False), + ), data=payment_options, ) order["integration_request"] = integration_request.name @@ -387,7 +396,9 @@ def capture_payment(is_sandbox=False, sanbox_response=None): if resp.get("status") == "authorized": resp = make_post_request( - "https://api.razorpay.com/v1/payments/{}/capture".format(data.get("razorpay_payment_id")), + "https://api.razorpay.com/v1/payments/{}/capture".format( + data.get("razorpay_payment_id") + ), auth=(settings.api_key, settings.api_secret), data={"amount": data.get("amount")}, ) @@ -417,7 +428,9 @@ def get_order(doctype, docname): # Do not use run_method here as it fails silently return doc.get_razorpay_order() except AttributeError: - frappe.log_error(frappe.get_traceback(), _("Controller method get_razorpay_order missing")) + frappe.log_error( + frappe.get_traceback(), _("Controller method get_razorpay_order missing") + ) frappe.throw(_("Could not create Razorpay order. Please contact Administrator")) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 8e1d383..a22a568 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -5,15 +5,12 @@ from urllib.parse import urlencode import frappe from frappe import _ -from frappe.integrations.utils import ( - create_payment_gateway, - create_request_log, - make_get_request, - make_post_request, -) +from frappe.integrations.utils import create_request_log, make_get_request from frappe.model.document import Document from frappe.utils import call_hook_method, cint, flt, get_url +from payments.utils import create_payment_gateway + class StripeSettings(Document): supported_currencies = [ @@ -153,7 +150,9 @@ class StripeSettings(Document): def on_update(self): create_payment_gateway( - "Stripe-" + self.gateway_name, settings="Stripe Settings", controller=self.gateway_name + "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: diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index c4c79ea..ec4e434 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -6,7 +6,7 @@ import json import frappe from frappe import _ from frappe.integrations.doctype.braintree_settings.braintree_settings import ( - get_client_token, + get_client_token, get_gateway_controller, ) from frappe.utils import flt @@ -46,7 +46,9 @@ def get_context(context): else: frappe.redirect_to_message( _("Some information is missing"), - _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + _( + "Looks like someone sent you to an incomplete URL. Please ask them to look into it." + ), ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect @@ -59,6 +61,8 @@ def make_payment(payload_nonce, data, reference_doctype, reference_docname): data.update({"payload_nonce": payload_nonce}) gateway_controller = get_gateway_controller(reference_docname) - data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) + data = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request( + data + ) frappe.db.commit() return data diff --git a/payments/utils/__init__.py b/payments/utils/__init__.py new file mode 100644 index 0000000..0b7b520 --- /dev/null +++ b/payments/utils/__init__.py @@ -0,0 +1,6 @@ +from utils import ( + get_payment_gateway_controller, + create_payment_gateway, + make_custom_fields, + delete_custom_fields, +) diff --git a/payments/utils/utils.py b/payments/utils/utils.py new file mode 100644 index 0000000..1493ae5 --- /dev/null +++ b/payments/utils/utils.py @@ -0,0 +1,158 @@ +import click + +import frappe +from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +def get_payment_gateway_controller(payment_gateway): + """Return payment gateway controller""" + gateway = frappe.get_doc("Payment Gateway", payment_gateway) + if gateway.gateway_controller is None: + try: + return frappe.get_doc(f"{payment_gateway} Settings") + except Exception: + frappe.throw(_("{0} Settings not found").format(payment_gateway)) + else: + try: + return frappe.get_doc(gateway.gateway_settings, gateway.gateway_controller) + except Exception: + frappe.throw(_("{0} Settings not found").format(payment_gateway)) + + +@frappe.whitelist(allow_guest=True, xss_safe=True) +def get_checkout_url(**kwargs): + try: + if kwargs.get("payment_gateway"): + doc = frappe.get_doc("{} Settings".format(kwargs.get("payment_gateway"))) + return doc.get_payment_url(**kwargs) + else: + raise Exception + except Exception: + frappe.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=frappe.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 frappe.db.exists("Payment Gateway", gateway): + payment_gateway = frappe.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 frappe.get_meta("Web Form").has_field("payments"): + 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" + } + ] + }) + + +def delete_custom_fields(): + if frappe.get_meta("Web Form").has_field("payments_tab"): + click.secho("* Uninstalling Payment Custom Fields from Web Form") + + fieldnames = ( + "payments_tab", + "accept_payment", + "payment_gateway", + "payment_button_label", + "payment_button_help", + "payments_cb", + "amount_field", + "amount_based_on_field", + "amount", + "currency" + ) + + for fieldname in fieldnames: + frappe.db.delete( + "Custom Field", + {"name": "Web Form-" + fieldname} + ) diff --git a/setup.py b/setup.py index 1c54de5..c6ebbc9 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup with open("requirements.txt") as f: install_requires = f.read().strip().split("\n") @@ -15,5 +15,5 @@ setup( packages=find_packages(), zip_safe=False, include_package_data=True, - install_requires=install_requires + install_requires=install_requires, )