diff --git a/payments/payment_gateways/doctype/braintree_settings/__init__.py b/payments/payment_gateways/doctype/braintree_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js new file mode 100644 index 0000000..c844022 --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.js @@ -0,0 +1,6 @@ +// Copyright (c) 2018, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Braintree Settings', { + +}); diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.json b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.json new file mode 100644 index 0000000..eebf64d --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.json @@ -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": "2018-02-05 14:33:06.050377", + "modified_by": "Administrator", + "module": "Integrations", + "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 +} \ No newline at end of file diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py new file mode 100644 index 0000000..768f58c --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +import braintree +from frappe import _ +from six.moves.urllib.parse import urlencode +from frappe.utils import get_url, call_hook_method +from frappe.integrations.utils import create_request_log, 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: + frappe.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("./integrations/braintree_checkout?{0}".format(urlencode(kwargs))) + + def create_payment_request(self, data): + self.data = frappe._dict(data) + + try: + self.integration_request = create_request_log(self.data, "Host", "Braintree") + return self.create_charge_on_braintree() + + except Exception: + frappe.log_error(frappe.get_traceback()) + return{ + "redirect_to": frappe.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 = frappe.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 = frappe.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 = frappe.get_doc(self.data.reference_doctype, + self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) + braintree_success_page = frappe.get_hooks('braintree_success_page') + if braintree_success_page: + custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) + except Exception: + frappe.log_error(frappe.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 = frappe.get_doc("Payment Request", doc) + gateway_controller = frappe.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 = frappe.get_doc("Braintree Settings", gateway_controller) + settings.configure_braintree() + + return braintree.ClientToken.generate() diff --git a/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js new file mode 100644 index 0000000..28e4202 --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Braintree Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Braintree Setting + () => frappe.tests.make('Braintree Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py new file mode 100644 index 0000000..80fa3c5 --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestBraintreeSettings(unittest.TestCase): + pass diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.json b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.json index eb95f4c..8d48496 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.json +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.json @@ -158,7 +158,7 @@ "idx": 0, "image_view": 0, "in_create": 1, - "in_dialog": 0, + "is_submittable": 0, "issingle": 1, "istable": 0, diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index eb28b99..62e0555 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -28,7 +28,15 @@ Example: "payer_name": "Nuran Verkleij", "order_id": "111", "currency": "USD", - "payment_gateway": "Razorpay" + "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 @@ -58,11 +66,15 @@ More Details: from __future__ import unicode_literals import frappe import json +import pytz from frappe import _ -from frappe.utils import get_url, call_hook_method, cint from six.moves.urllib.parse import urlencode from frappe.model.document import Document from frappe.integrations.utils import create_request_log, make_post_request, create_payment_gateway +from frappe.utils import get_url, call_hook_method, cint, get_datetime + + +api_path = '/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings' class PayPalSettings(Document): supported_currencies = ["AUD", "BRL", "CAD", "CZK", "DKK", "EUR", "HKD", "HUF", "ILS", "JPY", "MYR", "MXN", @@ -124,7 +136,7 @@ class PayPalSettings(Document): def get_payment_url(self, **kwargs): setattr(self, "use_sandbox", cint(kwargs.get("use_sandbox", 0))) - response = self.execute_set_express_checkout(kwargs["amount"], kwargs["currency"]) + 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}" @@ -135,30 +147,71 @@ class PayPalSettings(Document): "token": response.get("TOKEN")[0], "correlation_id": response.get("CORRELATIONID")[0] }) - self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) return return_url.format(kwargs["token"]) - def execute_set_express_checkout(self, amount, currency): + def execute_set_express_checkout(self, **kwargs): params, url = self.get_paypal_params_and_url() + params.update({ "METHOD": "SetExpressCheckout", + "returnUrl": get_url("{0}.get_express_checkout_details".format(api_path)), + "cancelUrl": get_url("/payment-cancel"), "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", - "PAYMENTREQUEST_0_AMT": amount, - "PAYMENTREQUEST_0_CURRENCYCODE": currency.upper(), - "returnUrl": get_url("/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.get_express_checkout_details"), - "cancelUrl": get_url("/payment-cancel") + "PAYMENTREQUEST_0_AMT": kwargs['amount'], + "PAYMENTREQUEST_0_CURRENCYCODE": kwargs['currency'].upper() }) - params = urlencode(params) + 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": frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) return response + def configure_recurring_payments(self, params, kwargs): + # removing the params as we have to setup rucurring payments + for param in ('PAYMENTREQUEST_0_PAYMENTACTION', 'PAYMENTREQUEST_0_AMT', + 'PAYMENTREQUEST_0_CURRENCYCODE'): + del params[param] + + params.update({ + "L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement + "L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] + }) + +def get_paypal_and_transaction_details(token): + doc = frappe.get_doc("PayPal Settings") + doc.setup_sandbox_env(token) + params, url = doc.get_paypal_params_and_url() + + integration_request = frappe.get_doc("Integration Request", token) + data = json.loads(integration_request.data) + + return data, params, url + +def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): + redirect_to = data.get('redirect_to') or None + redirect_message = data.get('redirect_message') or None + + if custom_redirect_to: + redirect_to = custom_redirect_to + + if redirect_to: + redirect_url += '&' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + # this is done so that functions called via hooks can update flags.redirect_to + if redirect: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) + @frappe.whitelist(allow_guest=True, xss_safe=True) def get_express_checkout_details(token): try: @@ -181,14 +234,14 @@ def get_express_checkout_details(token): return + doc = frappe.get_doc("Integration Request", token) update_integration_request_status(token, { "payerid": response.get("PAYERID")[0], "payer_email": response.get("EMAIL")[0] - }, "Authorized") + }, "Authorized", doc=doc) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url( \ - "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.confirm_payment?token={0}".format(token)) + frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) except Exception: frappe.log_error(frappe.get_traceback()) @@ -196,19 +249,9 @@ def get_express_checkout_details(token): @frappe.whitelist(allow_guest=True, xss_safe=True) def confirm_payment(token): try: - redirect = True - status_changed_to, redirect_to = None, None + custom_redirect_to = None + data, params, url = get_paypal_and_transaction_details(token) - doc = frappe.get_doc("PayPal Settings") - doc.setup_sandbox_env(token) - - integration_request = frappe.get_doc("Integration Request", token) - data = json.loads(integration_request.data) - - redirect_to = data.get('redirect_to') or None - redirect_message = data.get('redirect_message') or None - - params, url = doc.get_paypal_params_and_url() params.update({ "METHOD": "DoExpressCheckoutPayment", "PAYERID": data.get("payerid"), @@ -231,25 +274,148 @@ def confirm_payment(token): data.get("reference_docname")).run_method("on_payment_authorized", "Completed") frappe.db.commit() - if custom_redirect_to: - redirect_to = custom_redirect_to - - redirect_url = '/integrations/payment-success' + redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) else: redirect_url = "/integrations/payment-failed" - if redirect_to: - redirect_url += '?' + urlencode({'redirect_to': redirect_to}) - if redirect_message: - redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + setup_redirect(data, redirect_url, custom_redirect_to) + + except Exception: + frappe.log_error(frappe.get_traceback()) + +@frappe.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') and 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' - # this is done so that functions called via hooks can update flags.redirect_to - if redirect: - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url(redirect_url) + 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(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] + + frappe.flags.data = data + custom_redirect_to = frappe.get_doc(data.get("reference_doctype"), + data.get("reference_docname")).run_method("on_payment_authorized", status_changed_to) + frappe.db.commit() + + redirect_url = '/integrations/payment-success?doctype={0}&docname={1}'.format(data.get("reference_doctype"), data.get("reference_docname")) + else: + redirect_url = "/integrations/payment-failed" + + setup_redirect(data, redirect_url, custom_redirect_to) except Exception: frappe.log_error(frappe.get_traceback()) -def update_integration_request_status(token, data, status, error=False): - frappe.get_doc("Integration Request", token).update_status(data, status) +def update_integration_request_status(token, data, status, error=False, doc=None): + if not doc: + doc = frappe.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("{0}.create_recurring_profile?token={1}&payerid={2}".format(api_path, token, payerid)) + else: + return get_url("{0}.confirm_payment?token={1}".format(api_path, 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) + + if response.get("ACK")[0] != "Success": + frappe.throw(_("Failed while amending subscription")) + +@frappe.whitelist(allow_guest=True) +def ipn_handler(): + try: + data = frappe.local.form_dict + + validate_ipn_request(data) + + data.update({ + "payment_gateway": "PayPal" + }) + + doc = frappe.get_doc({ + "data": json.dumps(frappe.local.form_dict), + "doctype": "Integration Request", + "integration_type": "Subscription Notification", + "status": "Queued" + }).insert(ignore_permissions=True) + frappe.db.commit() + + frappe.enqueue(method='frappe.integrations.doctype.paypal_settings.paypal_settings.handle_subscription_notification', + queue='long', timeout=600, is_async=True, **{"doctype": "Integration Request", "docname": doc.name}) + + except frappe.InvalidStatusError: + pass + except Exception as e: + frappe.log(frappe.log_error(title=e)) + +def validate_ipn_request(data): + def _throw(): + frappe.throw(_("In Valid Request"), exc=frappe.InvalidStatusError) + + if not data.get("recurring_payment_id"): + _throw() + + doc = frappe.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) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.json b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.json index 838d5ac..3fdea79 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.json +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.json @@ -101,7 +101,7 @@ "idx": 0, "image_view": 0, "in_create": 1, - "in_dialog": 0, + "is_submittable": 0, "issingle": 1, "istable": 0, diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 4d6c5d5..cb66ed7 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -28,7 +28,15 @@ Example: "payer_name": "Nuran Verkleij", "order_id": "111", "currency": "INR", - "payment_gateway": "Razorpay" + "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 @@ -58,8 +66,9 @@ from frappe import _ import json from six.moves.urllib.parse import urlencode from frappe.model.document import Document -from frappe.utils import get_url, call_hook_method, cint -from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway +from frappe.utils import get_url, call_hook_method, cint, get_timestamp +from frappe.integrations.utils import (make_get_request, make_post_request, create_request_log, + create_payment_gateway) class RazorpaySettings(Document): supported_currencies = ["INR"] @@ -82,6 +91,89 @@ class RazorpaySettings(Document): if currency not in self.supported_currencies: frappe.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/{0}/addons".format(kwargs.get('subscription_id')) + + try: + if not frappe.conf.converted_rupee_to_paisa: + convert_rupee_to_paisa(**kwargs) + + for addon in kwargs.get("addons"): + resp = make_post_request( + url, + auth=(settings.api_key, settings.api_secret), + data=json.dumps(addon), + headers={ + "content-type": "application/json" + } + ) + if not resp.get('id'): + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + def setup_subscription(self, settings, **kwargs): + start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ + if kwargs.get('subscription_details').get("start_date") else None + + subscription_details = { + "plan_id": kwargs.get('subscription_details').get("plan_id"), + "start_at": cint(start_date), + "total_count": kwargs.get('subscription_details').get("billing_frequency"), + "customer_notify": kwargs.get('subscription_details').get("customer_notify") + } + + if kwargs.get('addons'): + convert_rupee_to_paisa(**kwargs) + subscription_details.update({ + "addons": kwargs.get('addons') + }) + + try: + resp = make_post_request( + "https://api.razorpay.com/v1/subscriptions", + auth=(settings.api_key, settings.api_secret), + data=json.dumps(subscription_details), + headers={ + "content-type": "application/json" + } + ) + + if resp.get('status') == 'created': + kwargs['subscription_id'] = resp.get('id') + frappe.flags.status = 'created' + return kwargs + else: + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + def prepare_subscription_details(self, settings, **kwargs): + if not kwargs.get("subscription_id"): + kwargs = self.setup_subscription(settings, **kwargs) + + if frappe.flags.status !='created': + kwargs['subscription_id'] = None + + return kwargs + def get_payment_url(self, **kwargs): integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) @@ -119,6 +211,23 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" + 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" + + if resp.get("status") == "captured": + # if subscription starts immediately then + # razorpay charge the actual amount + # thus changing status to Completed + + self.integration_request.update_status(data, 'Completed') + self.flags.status_changed_to = "Completed" + else: frappe.log_error(str(resp), 'Razorpay Payment not authorized') @@ -132,24 +241,26 @@ class RazorpaySettings(Document): redirect_to = data.get('notes', {}).get('redirect_to') or None redirect_message = data.get('notes', {}).get('redirect_message') or None - if self.flags.status_changed_to == "Authorized": + 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: + frappe.flags.data = data custom_redirect_to = frappe.get_doc(self.data.reference_doctype, self.data.reference_docname).run_method("on_payment_authorized", self.flags.status_changed_to) + except Exception: frappe.log_error(frappe.get_traceback()) if custom_redirect_to: redirect_to = custom_redirect_to - redirect_url = 'payment-success' + redirect_url = 'payment-success?doctype={0}&docname={1}'.format(self.data.reference_doctype, self.data.reference_docname) else: redirect_url = 'payment-failed' if redirect_to: - redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + redirect_url += '&' + urlencode({'redirect_to': redirect_to}) if redirect_message: redirect_url += '&' + urlencode({'redirect_message': redirect_message}) @@ -164,7 +275,7 @@ class RazorpaySettings(Document): "api_secret": self.get_password(fieldname="api_secret", raise_exception=False) }) - if cint(data.get('notes', {}).get('use_sandbox')): + if cint(data.get('notes', {}).get('use_sandbox')) or data.get("use_sandbox"): settings.update({ "api_key": frappe.conf.sandbox_api_key, "api_secret": frappe.conf.sandbox_api_secret, @@ -172,6 +283,16 @@ class RazorpaySettings(Document): return settings + def cancel_subscription(self, subscription_id): + settings = self.get_settings({}) + + try: + resp = make_post_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" + .format(subscription_id), auth=(settings.api_key, + settings.api_secret)) + except Exception: + frappe.log_error(frappe.get_traceback()) + def capture_payment(is_sandbox=False, sanbox_response=None): """ Verifies the purchase as complete by the merchant. @@ -201,4 +322,59 @@ def capture_payment(is_sandbox=False, sanbox_response=None): doc = frappe.get_doc("Integration Request", doc.name) doc.status = "Failed" doc.error = frappe.get_traceback() - frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) \ No newline at end of file + frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) + +def convert_rupee_to_paisa(**kwargs): + for addon in kwargs.get('addons'): + addon['item']['amount'] *= 100 + + frappe.conf.converted_rupee_to_paisa = True + +@frappe.whitelist(allow_guest=True) +def razorpay_subscription_callback(): + try: + data = frappe.local.form_dict + + validate_payment_callback(data) + + data.update({ + "payment_gateway": "Razorpay" + }) + + doc = frappe.get_doc({ + "data": json.dumps(frappe.local.form_dict), + "doctype": "Integration Request", + "integration_type": "Subscription Notification", + "status": "Queued" + }).insert(ignore_permissions=True) + frappe.db.commit() + + frappe.enqueue(method='frappe.integrations.doctype.razorpay_settings.razorpay_settings.handle_subscription_notification', + queue='long', timeout=600, is_async=True, **{"doctype": "Integration Request", "docname": doc.name}) + + except frappe.InvalidStatusError: + pass + except Exception as e: + frappe.log(frappe.log_error(title=e)) + +def validate_payment_callback(data): + def _throw(): + frappe.throw(_("Invalid Subscription"), exc=frappe.InvalidStatusError) + + subscription_id = data.get('payload').get("subscription").get("entity").get("id") + + if not(subscription_id): + _throw() + + controller = frappe.get_doc("Razorpay Settings") + + settings = controller.get_settings(data) + + resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}".format(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) \ No newline at end of file diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json index adf8a82..3063553 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json @@ -3,6 +3,7 @@ "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, @@ -13,6 +14,40 @@ "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, @@ -24,7 +59,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Publishable Key", "length": 0, @@ -39,9 +74,43 @@ "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, @@ -53,7 +122,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Secret Key", "length": 0, @@ -68,6 +137,133 @@ "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 } ], @@ -78,10 +274,10 @@ "image_view": 0, "in_create": 0, "is_submittable": 0, - "issingle": 1, + "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-03-09 17:19:25.087475", + "modified": "2018-05-23 13:32:14.429916", "modified_by": "Administrator", "module": "Integrations", "name": "Stripe Settings", @@ -90,7 +286,6 @@ "permissions": [ { "amend": 0, - "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 62d90cb..35beef7 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -29,9 +29,9 @@ class StripeSettings(Document): 'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 } - def validate(self): - create_payment_gateway('Stripe') - call_hook_method('payment_gateway_enabled', gateway='Stripe') + 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() @@ -55,50 +55,46 @@ class StripeSettings(Document): def get_payment_url(self, **kwargs): return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) - + def create_request(self, data): + import stripe self.data = frappe._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, "Host", "Stripe") return self.create_charge_on_stripe() + except Exception: frappe.log_error(frappe.get_traceback()) return{ - "redirect_to": frappe.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.")), + "redirect_to": frappe.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): - headers = {"Authorization": - "Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} - - data = { - "amount": cint(flt(self.data.amount)*100), - "currency": self.data.currency, - "source": self.data.stripe_token_id, - "description": self.data.description - } - - redirect_to = self.data.get('redirect_to') or None - redirect_message = self.data.get('redirect_message') or None + def create_charge_on_stripe(self): + import stripe try: - resp = make_post_request(url="https://api.stripe.com/v1/charges", headers=headers, data=data) - - if resp.get("captured") == True: + 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) + + if charge.captured == True: self.integration_request.db_set('status', 'Completed', update_modified=False) self.flags.status_changed_to = "Completed" else: - frappe.log_error(str(resp), 'Stripe Payment not completed') + frappe.log_error(charge.failure_message, 'Stripe Payment not completed') - except: + except Exception: frappe.log_error(frappe.get_traceback()) - # failed - pass - status = frappe.flags.integration_request.status_code + 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: @@ -112,7 +108,11 @@ class StripeSettings(Document): if custom_redirect_to: redirect_to = custom_redirect_to - redirect_url = 'payment-success' + redirect_url = 'payment-success' + + if self.redirect_url: + redirect_url = self.redirect_url + redirect_to = None else: redirect_url = 'payment-failed' @@ -125,3 +125,8 @@ class StripeSettings(Document): "redirect_to": redirect_url, "status": status } + +def get_gateway_controller(doctype, docname): + reference_doc = frappe.get_doc(doctype, docname) + gateway_controller = frappe.db.get_value("Payment Gateway", reference_doc.payment_gateway, "gateway_controller") + return gateway_controller diff --git a/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js new file mode 100644 index 0000000..b491ba5 --- /dev/null +++ b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Stripe Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Stripe Settings + () => frappe.tests.make('Stripe Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.py new file mode 100644 index 0000000..39e1281 --- /dev/null +++ b/payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestStripeSettings(unittest.TestCase): + pass diff --git a/payments/payments/doctype/payment_gateway/payment_gateway.json b/payments/payments/doctype/payment_gateway/payment_gateway.json index c7c551a..b97d72c 100644 --- a/payments/payments/doctype/payment_gateway/payment_gateway.json +++ b/payments/payments/doctype/payment_gateway/payment_gateway.json @@ -13,6 +13,7 @@ "editable_grid": 1, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -24,7 +25,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Gateway", "length": 0, @@ -40,6 +41,68 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "gateway_settings", + "fieldtype": "Link", + "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": "Gateway Settings", + "length": 0, + "no_copy": 0, + "options": "DocType", + "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": "gateway_controller", + "fieldtype": "Dynamic Link", + "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": "Gateway Controller", + "length": 0, + "no_copy": 0, + "options": "gateway_settings", + "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, @@ -52,7 +115,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-03-09 12:40:56.176464", + "modified": "2018-02-05 14:24:33.526645", "modified_by": "Administrator", "module": "Core", "name": "Payment Gateway", diff --git a/payments/payments/doctype/payment_gateway/test_payment_gateway.js b/payments/payments/doctype/payment_gateway/test_payment_gateway.js new file mode 100644 index 0000000..36168ec --- /dev/null +++ b/payments/payments/doctype/payment_gateway/test_payment_gateway.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Payment Gateway", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Payment Gateway + () => frappe.tests.make('Payment Gateway', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/payments/templates/includes/braintree_checkout.js b/payments/templates/includes/braintree_checkout.js new file mode 100644 index 0000000..6e6ec4c --- /dev/null +++ b/payments/templates/includes/braintree_checkout.js @@ -0,0 +1,55 @@ +$(document).ready(function() { + + var button = document.querySelector('#submit-button'); + var form = document.querySelector('#payment-form'); + var data = {{ frappe.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; + } + frappe.call({ + method: "frappe.templates.pages.integrations.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); + }); + }); + +}) diff --git a/payments/templates/includes/razorpay_checkout.js b/payments/templates/includes/razorpay_checkout.js index 07bdcd9..2986fcb 100644 --- a/payments/templates/includes/razorpay_checkout.js +++ b/payments/templates/includes/razorpay_checkout.js @@ -5,6 +5,7 @@ $(document).ready(function(){ "amount": cint({{ amount }} * 100), // 2000 paise = INR 20 "name": "{{ title }}", "description": "{{ description }}", + "subscription_id": "{{ subscription_id }}", "handler": function (response){ razorpay.make_payment_log(response, options, "{{ reference_doctype }}", "{{ reference_docname }}", "{{ token }}"); }, diff --git a/payments/templates/includes/stripe_checkout.js b/payments/templates/includes/stripe_checkout.js index b662012..a3cd5d6 100644 --- a/payments/templates/includes/stripe_checkout.js +++ b/payments/templates/includes/stripe_checkout.js @@ -1,47 +1,85 @@ -$(document).ready(function(){ - (function(e){ - var handler = StripeCheckout.configure({ - key: "{{ publishable_key }}", - token: function(token) { - // You can access the token ID with `token.id`. - // Get the token ID to your server-side code for use. - stripe.make_payment_log(token, {{ frappe.form_dict|json }}, "{{ reference_doctype }}", "{{ reference_docname }}"); +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...')) + frappe.call({ + method:"frappe.templates.pages.integrations.stripe_checkout.make_payment", + freeze:true, + headers: {"X-Requested-With": "XMLHttpRequest"}, + args: { + "stripe_token_id": result.token.id, + "data": JSON.stringify({{ frappe.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); + } } }); - - handler.open({ - name: "{{payer_name}}", - description: "{{description}}", - amount: cint("{{ amount }}" * 100), // 2000 paise = INR 20 - email: "{{payer_email}}", - currency: "{{currency}}" - }); - - })(); -}) - -frappe.provide('stripe'); - -stripe.make_payment_log = function(token, data, doctype, docname){ - $('.stripe-loading').addClass('hidden'); - $('.stripe-confirming').removeClass('hidden'); - frappe.call({ - method:"frappe.templates.pages.integrations.stripe_checkout.make_payment", - freeze:true, - headers: {"X-Requested-With": "XMLHttpRequest"}, - args: { - "stripe_token_id": token.id, - "data": JSON.stringify(data), - "reference_doctype": doctype, - "reference_docname": docname - }, - 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 - } + + } 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 = ''; + } +}); + +frappe.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); }) -} +}); diff --git a/payments/templates/pages/braintree_checkout.html b/payments/templates/pages/braintree_checkout.html new file mode 100644 index 0000000..bdc7b46 --- /dev/null +++ b/payments/templates/pages/braintree_checkout.html @@ -0,0 +1,54 @@ +{% extends "templates/web.html" %} + +{% block title %} Payment {% endblock %} + +{%- block header -%}{% endblock %} + +{% block script %} + + +{% endblock %} + +{%- block page_content -%} +
{{ _("Your connection request to Google Calendar was successfully accepted") }}
+ +{{ _("Your payment was successfully accepted") }}
- +{{ payment_message or _("Your payment was successfully accepted") }}
+ {% if not payment_message %} + + {% endif %} + {% endblock %} diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index 655767e..bdc14db 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -4,6 +4,13 @@ from __future__ import unicode_literals import frappe +no_cache = True def get_context(context): - token = frappe.local.form_dict.token + token = frappe.local.form_dict.token + doc = frappe.get_doc(frappe.local.form_dict.doctype, frappe.local.form_dict.docname) + + context.payment_message = '' + if hasattr(doc, 'get_payment_success_message'): + context.payment_message = doc.get_payment_success_message() + diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 1d988e9..c45279c 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -26,8 +26,10 @@ def get_context(context): context['token'] = frappe.form_dict['token'] context['amount'] = flt(context['amount']) + context['subscription_id'] = payment_details['subscription_id'] \ + if payment_details.get('subscription_id') else '' - except Exception: + except Exception as e: frappe.redirect_to_message(_('Invalid Token'), _('Seems token you are using is invalid!'), http_status_code=400, indicator_color='red') diff --git a/payments/templates/pages/stripe_checkout.css b/payments/templates/pages/stripe_checkout.css new file mode 100644 index 0000000..a42808a --- /dev/null +++ b/payments/templates/pages/stripe_checkout.css @@ -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; +} diff --git a/payments/templates/pages/stripe_checkout.html b/payments/templates/pages/stripe_checkout.html index 0fe5dc0..a4906af 100644 --- a/payments/templates/pages/stripe_checkout.html +++ b/payments/templates/pages/stripe_checkout.html @@ -2,27 +2,57 @@ {% block title %} Payment {% endblock %} -{%- block header -%}{% endblock %} +{%- block header -%} +{% endblock %} {% block script %} - + {% endblock %} {%- block page_content -%} -- Loading Payment System - -
- -{% endblock %} +