From 771d37a238b31a759ccd6308055a3325c9a43d6e Mon Sep 17 00:00:00 2001 From: Britlog Date: Tue, 23 Jan 2018 10:04:11 +0100 Subject: [PATCH 01/35] Stripe translations (#4892) --- payments/templates/includes/stripe_checkout.js | 3 ++- payments/templates/pages/stripe_checkout.html | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/payments/templates/includes/stripe_checkout.js b/payments/templates/includes/stripe_checkout.js index b662012..f711ca7 100644 --- a/payments/templates/includes/stripe_checkout.js +++ b/payments/templates/includes/stripe_checkout.js @@ -14,7 +14,8 @@ $(document).ready(function(){ description: "{{description}}", amount: cint("{{ amount }}" * 100), // 2000 paise = INR 20 email: "{{payer_email}}", - currency: "{{currency}}" + currency: "{{currency}}", + locale: "auto" }); })(); diff --git a/payments/templates/pages/stripe_checkout.html b/payments/templates/pages/stripe_checkout.html index 0fe5dc0..d448ff8 100644 --- a/payments/templates/pages/stripe_checkout.html +++ b/payments/templates/pages/stripe_checkout.html @@ -12,8 +12,8 @@ {%- block page_content -%}

- Loading Payment System - + {{ _("Loading Payment System") }} +

{% endblock %} From 800cd733a6344a7a75070b8fd547611a00dcdca8 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 16 Feb 2018 10:46:23 +0100 Subject: [PATCH 02/35] Braintree integration (#4971) * Braintree Integration WIP * Braintree integration * Link in config + settings after save * Code cleanup * Code cleanup * JS beautify issue * Codacy corrections * Spaces to tabs --- .../doctype/braintree_settings/__init__.py | 0 .../braintree_settings/braintree_settings.js | 6 + .../braintree_settings.json | 273 ++++++++++++++++++ .../braintree_settings/braintree_settings.py | 136 +++++++++ .../test_braintree_settings.js | 23 ++ .../test_braintree_settings.py | 9 + .../payment_gateway/payment_gateway.json | 67 ++++- .../payment_gateway/test_payment_gateway.js | 23 ++ .../templates/includes/braintree_checkout.js | 47 +++ .../templates/pages/braintree_checkout.html | 54 ++++ .../templates/pages/braintree_checkout.py | 48 +++ 11 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 payments/payment_gateways/doctype/braintree_settings/__init__.py create mode 100644 payments/payment_gateways/doctype/braintree_settings/braintree_settings.js create mode 100644 payments/payment_gateways/doctype/braintree_settings/braintree_settings.json create mode 100644 payments/payment_gateways/doctype/braintree_settings/braintree_settings.py create mode 100644 payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js create mode 100644 payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py create mode 100644 payments/payments/doctype/payment_gateway/test_payment_gateway.js create mode 100644 payments/templates/includes/braintree_checkout.js create mode 100644 payments/templates/pages/braintree_checkout.html create mode 100644 payments/templates/pages/braintree_checkout.py 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..c2a00cd --- /dev/null +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -0,0 +1,136 @@ +# -*- 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) + 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/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..96bc9c3 --- /dev/null +++ b/payments/templates/includes/braintree_checkout.js @@ -0,0 +1,47 @@ +$(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 + } + } + }) + }); + }); + }); + +}) diff --git a/payments/templates/pages/braintree_checkout.html b/payments/templates/pages/braintree_checkout.html new file mode 100644 index 0000000..a9fec0e --- /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 -%} +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ +
+
+ + +{% endblock %} diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py new file mode 100644 index 0000000..57213db --- /dev/null +++ b/payments/templates/pages/braintree_checkout.py @@ -0,0 +1,48 @@ +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt +import json +from frappe.integrations.doctype.braintree_settings.braintree_settings import get_client_token, get_gateway_controller + +no_cache = 1 +no_sitemap = 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(frappe.form_dict.keys())): + for key in expected_keys: + context[key] = frappe.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'] = frappe.db.get_value("Braintree Settings", gateway_controller, "header_img") + + 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.')) + frappe.local.flags.redirect_location = frappe.local.response.location + raise frappe.Redirect + +@frappe.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 = frappe.get_doc("Braintree Settings", gateway_controller).create_payment_request(data) + frappe.db.commit() + return data From d5632034de8ec1343eb61b1bb06c8cae983dd097 Mon Sep 17 00:00:00 2001 From: Achilles Rasquinha Date: Sat, 31 Mar 2018 18:15:43 +0530 Subject: [PATCH 03/35] python 3 fixes --- payments/templates/pages/braintree_checkout.py | 2 +- payments/templates/pages/stripe_checkout.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/payments/templates/pages/braintree_checkout.py b/payments/templates/pages/braintree_checkout.py index 57213db..7ebb2d4 100644 --- a/payments/templates/pages/braintree_checkout.py +++ b/payments/templates/pages/braintree_checkout.py @@ -17,7 +17,7 @@ def get_context(context): context.no_cache = 1 # all these keys exist in form_dict - if not (set(expected_keys) - set(frappe.form_dict.keys())): + if not (set(expected_keys) - set(list(frappe.form_dict))): for key in expected_keys: context[key] = frappe.form_dict[key] diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index e79a10f..e2f2a77 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -17,7 +17,7 @@ def get_context(context): context.publishable_key = get_api_key() # all these keys exist in form_dict - if not (set(expected_keys) - set(frappe.form_dict.keys())): + if not (set(expected_keys) - set(list(frappe.form_dict))): for key in expected_keys: context[key] = frappe.form_dict[key] From 589b998f3f8108279a2b720a989828ce67209c8d Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 6 Apr 2018 07:13:49 +0200 Subject: [PATCH 04/35] [New Feature] Google Calendar Connector (#5266) * Addition of a filter for last sync timestamp * Google calendar connector wip * Google calendar integration * Add test for account creation * Codacy corrections * Remove unused import * New section Google Services * Add no_copy to migration custom field --- .../templates/pages/gcalendar-success.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 payments/templates/pages/gcalendar-success.html diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html new file mode 100644 index 0000000..b6d2230 --- /dev/null +++ b/payments/templates/pages/gcalendar-success.html @@ -0,0 +1,20 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Connection Success") }}{% endblock %} + +{%- block page_content -%} +
+
+ + {{ _("Success") }} +
+

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

+ +
+ +{% endblock %} From f6385c602e16431be4ffb1c519851e7a3e37ae26 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 16 May 2018 12:51:01 +0530 Subject: [PATCH 05/35] [minor] remove in_dialog old property --- .../doctype/paypal_settings/paypal_settings.json | 2 +- .../doctype/razorpay_settings/razorpay_settings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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, From 6b81c3cefa07fff2670e02db215485d51bbd3a87 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Wed, 23 May 2018 07:46:21 +0200 Subject: [PATCH 06/35] Allow several Stripe accounts (#5573) * Allow several Stripe accounts * Addition of a sync patch * Remove unused dependancy --- .../stripe_settings/stripe_settings.json | 45 ++++++++++++++++--- .../stripe_settings/stripe_settings.py | 21 +++++---- .../stripe_settings/test_stripe_settings.js | 23 ++++++++++ .../stripe_settings/test_stripe_settings.py | 9 ++++ payments/templates/pages/stripe_checkout.py | 14 +++--- 5 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.js create mode 100644 payments/payment_gateways/doctype/stripe_settings/test_stripe_settings.py diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json index adf8a82..bdaf881 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,38 @@ "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": 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_on_submit": 0, "bold": 0, "collapsible": 0, @@ -24,7 +57,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 +72,11 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -53,7 +88,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 +103,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -78,10 +114,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-18 16:18:32.584083", "modified_by": "Administrator", "module": "Integrations", "name": "Stripe Settings", @@ -90,7 +126,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..85c7484 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,7 +55,7 @@ class StripeSettings(Document): def get_payment_url(self, **kwargs): return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) - + def create_request(self, data): self.data = frappe._dict(data) @@ -68,24 +68,24 @@ class StripeSettings(Document): "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.")), "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 try: resp = make_post_request(url="https://api.stripe.com/v1/charges", headers=headers, data=data) - + if resp.get("captured") == True: self.integration_request.db_set('status', 'Completed', update_modified=False) self.flags.status_changed_to = "Completed" @@ -125,3 +125,8 @@ class StripeSettings(Document): "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 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/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index e2f2a77..216fe5f 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt, cint import json +from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller no_cache = 1 no_sitemap = 1 @@ -14,13 +15,14 @@ expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'referen def get_context(context): context.no_cache = 1 - context.publishable_key = get_api_key() # all these keys exist in form_dict if not (set(expected_keys) - set(list(frappe.form_dict))): for key in expected_keys: context[key] = frappe.form_dict[key] + context.publishable_key = get_api_key(context.reference_docname) + context['amount'] = flt(context['amount']) else: @@ -29,8 +31,9 @@ def get_context(context): frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect -def get_api_key(): - publishable_key = frappe.db.get_value("Stripe Settings", None, "publishable_key") +def get_api_key(doc): + gateway_controller = get_gateway_controller(doc) + publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") if cint(frappe.form_dict.get("use_sandbox")): publishable_key = frappe.conf.sandbox_publishable_key @@ -44,6 +47,7 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam "stripe_token_id": stripe_token_id }) - data = frappe.get_doc("Stripe Settings").create_request(data) + gateway_controller = get_gateway_controller(reference_docname) + data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) frappe.db.commit() - return data \ No newline at end of file + return data From e7cf9d737c3cce1111e2188460404e220010c81c Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 11 Jun 2018 11:43:29 +0200 Subject: [PATCH 07/35] Subscriptions + Elements for Stripe payments (#5602) * Stripe Payments Improvements * Automatically create a new stripe customer * Charge arguments correction * Move Stripe to ERPNext * Revert "Move Stripe to ERPNext" This reverts commit f71f9952997ea0fcdccafc2410abafb6185a6a1f. * Stripe Settings correction * Payment plan moved to ERPNext * Remove Reference to payment plan in Frappe * Remove references to payment plan in Frappe * Modify Stripe Checkout Pages to allow usage without ERPNext --- .../stripe_settings/stripe_settings.json | 162 +++++++++++++++++- .../stripe_settings/stripe_settings.py | 43 +++-- .../templates/includes/stripe_checkout.js | 125 +++++++++----- payments/templates/pages/stripe_checkout.css | 113 ++++++++++++ payments/templates/pages/stripe_checkout.html | 60 +++++-- payments/templates/pages/stripe_checkout.py | 30 +++- 6 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 payments/templates/pages/stripe_checkout.css diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json index bdaf881..3063553 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.json @@ -15,6 +15,7 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -46,6 +47,7 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -77,6 +79,38 @@ }, { "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, @@ -105,6 +139,132 @@ "set_only_once": 0, "translatable": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header_img", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "redirect_url", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Redirect URL", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 } ], "has_web_view": 0, @@ -117,7 +277,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-05-18 16:18:32.584083", + "modified": "2018-05-23 13:32:14.429916", "modified_by": "Administrator", "module": "Integrations", "name": "Stripe Settings", diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 85c7484..1fbcb60 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -9,6 +9,7 @@ from frappe import _ from six.moves.urllib.parse import urlencode from frappe.utils import get_url, call_hook_method, cint, flt from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway +import stripe class StripeSettings(Document): supported_currencies = [ @@ -58,47 +59,41 @@ class StripeSettings(Document): def create_request(self, data): 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 - try: - resp = make_post_request(url="https://api.stripe.com/v1/charges", headers=headers, data=data) + 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 resp.get("captured") == True: + 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 +107,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' diff --git a/payments/templates/includes/stripe_checkout.js b/payments/templates/includes/stripe_checkout.js index f711ca7..07c0946 100644 --- a/payments/templates/includes/stripe_checkout.js +++ b/payments/templates/includes/stripe_checkout.js @@ -1,48 +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}}", - locale: "auto" - }); - - })(); -}) - -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/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 d448ff8..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 %} +
+
+ {% if image %} + + {% endif %} +

{{description}}

+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ + +
+
+
+
-{% block style %} - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 216fe5f..802b44f 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import cint, fmt_money import json from frappe.integrations.doctype.stripe_settings.stripe_settings import get_gateway_controller @@ -21,9 +21,17 @@ def get_context(context): for key in expected_keys: context[key] = frappe.form_dict[key] - context.publishable_key = get_api_key(context.reference_docname) + gateway_controller = get_gateway_controller(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'] = flt(context['amount']) + context['amount'] = fmt_money(amount=context['amount'], currency=context['currency']) + + if frappe.db.get_value(context.reference_doctype, context.reference_docname, "is_a_subscription"): + payment_plan = frappe.db.get_value(context.reference_doctype, context.reference_docname, "payment_plan") + recurrence = frappe.db.get_value("Payment Plan", payment_plan, "recurrence") + + context['amount'] = context['amount'] + " " + _(recurrence) else: frappe.redirect_to_message(_('Some information is missing'), @@ -31,14 +39,18 @@ def get_context(context): frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect -def get_api_key(doc): - gateway_controller = get_gateway_controller(doc) +def get_api_key(doc, gateway_controller): publishable_key = frappe.db.get_value("Stripe Settings", gateway_controller, "publishable_key") if cint(frappe.form_dict.get("use_sandbox")): publishable_key = frappe.conf.sandbox_publishable_key return publishable_key +def get_header_image(doc, gateway_controller): + header_image = frappe.db.get_value("Stripe Settings", gateway_controller, "header_img") + + return header_image + @frappe.whitelist(allow_guest=True) def make_payment(stripe_token_id, data, reference_doctype=None, reference_docname=None): data = json.loads(data) @@ -48,6 +60,12 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam }) gateway_controller = get_gateway_controller(reference_docname) - data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) + + if frappe.db.get_value(reference_doctype, reference_docname, 'is_a_subscription'): + reference = frappe.get_doc(reference_doctype, reference_docname) + data = reference.create_subscription("stripe", gateway_controller, data) + else: + data = frappe.get_doc("Stripe Settings", gateway_controller).create_request(data) + frappe.db.commit() return data From 1aa73ddd46548d2d92cb62fc9ef79338fba59a0a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 11 Jun 2018 17:19:59 +0530 Subject: [PATCH 08/35] [fix] [test] for notification --- .../doctype/stripe_settings/stripe_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 1fbcb60..90bb845 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -9,7 +9,6 @@ from frappe import _ from six.moves.urllib.parse import urlencode from frappe.utils import get_url, call_hook_method, cint, flt from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway -import stripe class StripeSettings(Document): supported_currencies = [ @@ -58,6 +57,7 @@ class StripeSettings(Document): 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() @@ -74,6 +74,7 @@ class StripeSettings(Document): } 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) From 1b4b6737eecb61b05d75f7827cc59d9d55fbcf17 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 21 Jun 2018 05:39:05 +0200 Subject: [PATCH 09/35] Stripe corrections (#5700) --- .../doctype/stripe_settings/stripe_settings.py | 6 +++--- payments/templates/includes/stripe_checkout.js | 2 +- payments/templates/pages/stripe_checkout.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py index 90bb845..35beef7 100644 --- a/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py +++ b/payments/payment_gateways/doctype/stripe_settings/stripe_settings.py @@ -126,7 +126,7 @@ class StripeSettings(Document): "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") +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/templates/includes/stripe_checkout.js b/payments/templates/includes/stripe_checkout.js index 07c0946..a3cd5d6 100644 --- a/payments/templates/includes/stripe_checkout.js +++ b/payments/templates/includes/stripe_checkout.js @@ -59,7 +59,7 @@ function setOutcome(result) { }); } else if (result.error) { - $('.error').html() = result.error.message; + $('.error').html(result.error.message); $('.error').show() } } diff --git a/payments/templates/pages/stripe_checkout.py b/payments/templates/pages/stripe_checkout.py index 802b44f..8ba4042 100644 --- a/payments/templates/pages/stripe_checkout.py +++ b/payments/templates/pages/stripe_checkout.py @@ -21,7 +21,7 @@ def get_context(context): for key in expected_keys: context[key] = frappe.form_dict[key] - gateway_controller = get_gateway_controller(context.reference_docname) + 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) @@ -59,7 +59,7 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam "stripe_token_id": stripe_token_id }) - gateway_controller = get_gateway_controller(reference_docname) + gateway_controller = get_gateway_controller(reference_doctype,reference_docname) if frappe.db.get_value(reference_doctype, reference_docname, 'is_a_subscription'): reference = frappe.get_doc(reference_doctype, reference_docname) From 79887b33742a1e486ca4435648ef4d91dd6a782c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 10 Jul 2018 16:06:35 +0530 Subject: [PATCH 10/35] [test] [style] --- payments/templates/pages/gcalendar-success.html | 1 + payments/templates/pages/payment-cancel.html | 1 + payments/templates/pages/payment-success.html | 1 + 3 files changed, 3 insertions(+) diff --git a/payments/templates/pages/gcalendar-success.html b/payments/templates/pages/gcalendar-success.html index b6d2230..1ce9ba5 100644 --- a/payments/templates/pages/gcalendar-success.html +++ b/payments/templates/pages/gcalendar-success.html @@ -16,5 +16,6 @@ .hero-and-content { background-color: #f5f7fa; } +{% include "templates/styles/card_style.css" %} {% endblock %} diff --git a/payments/templates/pages/payment-cancel.html b/payments/templates/pages/payment-cancel.html index 8d107fd..a7c3275 100644 --- a/payments/templates/pages/payment-cancel.html +++ b/payments/templates/pages/payment-cancel.html @@ -16,6 +16,7 @@ .hero-and-content { background-color: #f5f7fa; } +{% include "templates/styles/card_style.css" %} {% endblock %} diff --git a/payments/templates/pages/payment-success.html b/payments/templates/pages/payment-success.html index 3572584..8d87645 100644 --- a/payments/templates/pages/payment-success.html +++ b/payments/templates/pages/payment-success.html @@ -16,5 +16,6 @@ .hero-and-content { background-color: #f5f7fa; } +{% include "templates/styles/card_style.css" %} {% endblock %} From 2ace04e47205f56150a87600ef5e7c3ac562ec3f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 10 Jul 2018 18:36:58 +0530 Subject: [PATCH 11/35] [style] make page-card self contained --- payments/templates/pages/payment-failed.html | 1 + payments/templates/pages/payment-success.html | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/payments/templates/pages/payment-failed.html b/payments/templates/pages/payment-failed.html index 570785f..8f416b5 100644 --- a/payments/templates/pages/payment-failed.html +++ b/payments/templates/pages/payment-failed.html @@ -16,6 +16,7 @@ .hero-and-content { background-color: #f5f7fa; } +{% include "templates/styles/card_style.css" %} {% endblock %} diff --git a/payments/templates/pages/payment-success.html b/payments/templates/pages/payment-success.html index 8d87645..a1b5092 100644 --- a/payments/templates/pages/payment-success.html +++ b/payments/templates/pages/payment-success.html @@ -13,9 +13,6 @@ {{ _("Continue") }} {% endblock %} From 94898a01bdd1006cc15de913b94dcb71dd041714 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Thu, 19 Jul 2018 09:20:35 +0200 Subject: [PATCH 12/35] Allow custom confirmation page (#5835) --- .../doctype/braintree_settings/braintree_settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py index c2a00cd..768f58c 100644 --- a/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py +++ b/payments/payment_gateways/doctype/braintree_settings/braintree_settings.py @@ -102,6 +102,9 @@ class BraintreeSettings(Document): 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()) From 6a4d5e35fdc20bcd0b814426253c8d9d7cea5f3e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 30 Aug 2018 10:03:46 +0530 Subject: [PATCH 13/35] Support for razorpay subscription and addons --- .../razorpay_settings/razorpay_settings.py | 68 ++++++++++++++++++- .../templates/includes/razorpay_checkout.js | 1 + payments/templates/pages/razorpay_checkout.py | 8 ++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 4d6c5d5..2120e8c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -82,6 +82,59 @@ 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 before_get_payment_url(self, **kwargs): + if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): + return + + settings = self.get_settings(kwargs) + if kwargs.get('subscription_id') and kwargs.get('addons'): + return self.setup_addon(settings, **kwargs) + else: + return self.setup_subscription(settings, **kwargs) + + def setup_addon(self, settings, **kwargs): + url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) + + for addon in kwargs.get("addons"): + try: + resp = make_post_request( + url, + auth=(settings.api_key, settings.api_secret), + data=json.dumps(addon), + headers={ + "content-type": "application/json" + } + ) + + if not resp.get('id'): + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + + return {} + + def setup_subscription(self, settings, **kwargs): + try: + resp = make_post_request( + "https://api.razorpay.com/v1/subscriptions", + auth=(settings.api_key, settings.api_secret), + data=kwargs.get('subscription_details') + ) + + if resp.get('status') == 'created': + kwargs['subscription_details']['subscription_id'] = resp.get('id') + else: + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + + return kwargs + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass + def get_payment_url(self, **kwargs): integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) @@ -119,6 +172,15 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" + elif data.get('subscription_details') and data.get('subscription_details').get('subscription_id'): + if resp.get("status") == "refunded": + self.integration_request.update_status(data, 'Refunded') + self.flags.status_changed_to = "Authorized" + + if resp.get("status") == "captured": + self.integration_request.update_status(data, 'Completed') + self.flags.status_changed_to = "Authorized" + else: frappe.log_error(str(resp), 'Razorpay Payment not authorized') @@ -136,8 +198,10 @@ class RazorpaySettings(Document): 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()) @@ -164,7 +228,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, @@ -201,4 +265,4 @@ 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)) 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/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 1d988e9..6e64d3f 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -27,7 +27,13 @@ def get_context(context): context['token'] = frappe.form_dict['token'] context['amount'] = flt(context['amount']) - except Exception: + if payment_details.get('subscription_details'): + context['subscription_id'] = payment_details.get("subscription_details", {}).get('subscription_id', '') + else: + context['subscription_id'] = '' + + except Exception as e: + print(e) frappe.redirect_to_message(_('Invalid Token'), _('Seems token you are using is invalid!'), http_status_code=400, indicator_color='red') From fd3b828b1756978f0807df3f4eac5051733ca3f4 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 30 Aug 2018 19:25:42 +0530 Subject: [PATCH 14/35] Support for paypal subscription --- .../paypal_settings/paypal_settings.py | 131 ++++++++++++++++-- 1 file changed, 117 insertions(+), 14 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index eb28b99..627d01a 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -28,7 +28,14 @@ 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 + } } # redirect the user to this url @@ -59,7 +66,8 @@ from __future__ import unicode_literals import frappe import json from frappe import _ -from frappe.utils import get_url, call_hook_method, cint +from datetime import datetime +from frappe.utils import get_url, call_hook_method, cint, get_timestamp 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 @@ -124,7 +132,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,21 +143,31 @@ class PayPalSettings(Document): "token": response.get("TOKEN")[0], "correlation_id": response.get("CORRELATIONID")[0] }) - + print(kwargs) self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) - + print(return_url.format(kwargs["token"])) 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", - "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") }) + + if kwargs.get('subscription_details'): + params.update({ + "L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement + "L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] + }) + else: + params.update({ + "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", + "PAYMENTREQUEST_0_AMT": kwargs['amount'], + "PAYMENTREQUEST_0_CURRENCYCODE": kwargs['currency'].upper(), + }) params = urlencode(params) @@ -181,14 +199,16 @@ 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()) @@ -251,5 +271,88 @@ def confirm_payment(token): 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) +@frappe.whitelist(allow_guest=True, xss_safe=True) +def create_recurring_profile(token, payerid): + try: + redirect = True + status_changed_to, redirect_to = None, None + + 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) + + subscription_details = data.get("subscription_details") + 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": "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"), + "CURRENCYCODE": data.get("currency").upper() + }) + + if subscription_details.get("start_date"): + starts_at = subscription_details.get("start_date") + else: + starts_at = frappe.utils.now() + + params.update({ + "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(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"): + frappe.flags.data = data + custom_redirect_to = frappe.get_doc(data.get("reference_doctype"), + 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' + 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}) + + # 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) + + except Exception: + frappe.log_error(frappe.get_traceback()) + +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"): + return get_url( \ + "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.create_recurring_profile?token={0}&payerid={1}".format(token, payerid)) + else: + return get_url( \ + "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.confirm_payment?token={0}".format(token)) From 30e43060ad27e3d4b66c4011649510ad5d06173e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 30 Aug 2018 19:26:04 +0530 Subject: [PATCH 15/35] [fix] razorpay subscription --- .../razorpay_settings/razorpay_settings.py | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 2120e8c..2068f04 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -28,7 +28,14 @@ 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 + } } # Redirect the user to this url @@ -58,7 +65,7 @@ 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.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): @@ -82,46 +89,56 @@ 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 before_get_payment_url(self, **kwargs): - if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): - return - - settings = self.get_settings(kwargs) - if kwargs.get('subscription_id') and kwargs.get('addons'): - return self.setup_addon(settings, **kwargs) - else: - return self.setup_subscription(settings, **kwargs) - - def setup_addon(self, settings, **kwargs): - url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - - for addon in kwargs.get("addons"): - try: - resp = make_post_request( - url, - auth=(settings.api_key, settings.api_secret), - data=json.dumps(addon), - headers={ - "content-type": "application/json" - } - ) - - if not resp.get('id'): - frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - - except: - frappe.log_error(frappe.get_traceback()) - # failed - pass - - return {} + # def before_get_payment_url(self, **kwargs): + # if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): + # return + # + # settings = self.get_settings(kwargs) + # if kwargs.get('subscription_id') and kwargs.get('addons'): + # return self.setup_addon(settings, **kwargs) + # else: + # return self.setup_subscription(settings, **kwargs) + # + # def setup_addon(self, settings, **kwargs): + # url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) + # + # for addon in kwargs.get("addons"): + # try: + # resp = make_post_request( + # url, + # auth=(settings.api_key, settings.api_secret), + # data=json.dumps(addon), + # headers={ + # "content-type": "application/json" + # } + # ) + # + # if not resp.get('id'): + # frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + # + # except: + # frappe.log_error(frappe.get_traceback()) + # # failed + # pass + # + # return {} def setup_subscription(self, settings, **kwargs): + 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"), + } + try: resp = make_post_request( "https://api.razorpay.com/v1/subscriptions", auth=(settings.api_key, settings.api_secret), - data=kwargs.get('subscription_details') + data=subscription_details ) if resp.get('status') == 'created': @@ -129,13 +146,17 @@ class RazorpaySettings(Document): else: frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - return kwargs except: frappe.log_error(frappe.get_traceback()) # failed pass def get_payment_url(self, **kwargs): + settings = self.get_settings(kwargs) + + if kwargs.get('subscription_details'): + self.setup_subscription(settings, **kwargs) + integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) From ea4968faf3cc54d3dd851c265b89c4a6edaedde4 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 5 Sep 2018 11:55:03 +0530 Subject: [PATCH 16/35] setup upfront amount --- .../doctype/paypal_settings/paypal_settings.py | 12 ++++++------ .../doctype/razorpay_settings/razorpay_settings.py | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 627d01a..e783fca 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -34,7 +34,8 @@ Example: "start_date": "2018-08-30", "billing_period": "Month" #(Day, Week, SemiMonth, Month, Year), "billing_frequency": 1, - "customer_notify": 1 + "customer_notify": 1, + "upfront_amount": 1000 } } @@ -296,13 +297,12 @@ def create_recurring_profile(token, payerid): "BILLINGPERIOD": subscription_details.get("billing_period"), "BILLINGFREQUENCY": subscription_details.get("billing_frequency"), "AMT": data.get("amount"), - "CURRENCYCODE": data.get("currency").upper() + "CURRENCYCODE": data.get("currency").upper(), + "INITAMT": subscription_details.get("upfront_amount") }) - if subscription_details.get("start_date"): - starts_at = subscription_details.get("start_date") - else: - starts_at = frappe.utils.now() + starts_at = subscription_details.get("start_date") if subscription_details.get("start_date")\ + else frappe.utils.now() params.update({ "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 2068f04..3ad9559 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -34,7 +34,8 @@ Example: "start_date": "2018-08-30", "billing_period": "Month" #(Day, Week, Month, Year), "billing_frequency": 1, - "customer_notify": 1 + "customer_notify": 1, + "upfront_amount": 1000 } } @@ -132,6 +133,7 @@ class RazorpaySettings(Document): "start_at": cint(start_date), "total_count": kwargs.get('subscription_details').get("billing_frequency"), "customer_notify": kwargs.get('subscription_details').get("customer_notify"), + "upfront_amount": kwargs.get('subscription_details').get("upfront_amount") } try: From 19df9a9da3ace3ef451d182b93ace5ffdf4ea2a4 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 10 Sep 2018 18:50:17 +0530 Subject: [PATCH 17/35] Provision to cancel existing subscription --- .../paypal_settings/paypal_settings.py | 22 +++++++++++++++++++ .../razorpay_settings/razorpay_settings.py | 11 ++++++++++ 2 files changed, 33 insertions(+) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index e783fca..b56a8c6 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -177,6 +177,26 @@ class PayPalSettings(Document): frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) return response + + def cancel_recurring_profile(self, **kwargs): + params, url = self.get_paypal_params_and_url() + + if not kwargs.get('profile_id'): + frappe.throw(_("PayPal Recurring Profile ID is required")) + + params.update({ + "METHOD": "ManageRecurringPaymentsProfileStatus", + "PROFILEID": kwargs['profile_id'], + "ACTION": "Cancel" + }) + + params = urlencode(params) + + response = make_post_request(url, data=params.encode("utf-8")) + if response.get("ACK")[0] != "Success": + frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) + + return response @frappe.whitelist(allow_guest=True, xss_safe=True) def get_express_checkout_details(token): @@ -316,6 +336,8 @@ def create_recurring_profile(token, payerid): }, "Completed") if data.get("reference_doctype") and data.get("reference_docname"): + data['subscription_details']['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", "Completed") diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 3ad9559..920da52 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -258,6 +258,17 @@ class RazorpaySettings(Document): }) return settings + + + def cancel_subscription(self, subscription_id): + settings = self.get_settings() + + try: + resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" + .format(subscription_id), auth=(settings.api_key, + settings.api_secret)) + except Exception: + frappe.log_error(frappe.get_traceback()) def capture_payment(is_sandbox=False, sanbox_response=None): """ From 4d3f3421519d0cd49b603652ed5fe90be6dd07e0 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 18 Sep 2018 17:16:53 +0530 Subject: [PATCH 18/35] setup addons on razorpay --- .../razorpay_settings/razorpay_settings.py | 95 +++++++++++-------- payments/templates/pages/razorpay_checkout.py | 7 +- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 920da52..580e139 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -90,39 +90,42 @@ 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 before_get_payment_url(self, **kwargs): - # if not kwargs.get('subscription_details') and not kwargs.get('subscription_id'): - # return - # - # settings = self.get_settings(kwargs) - # if kwargs.get('subscription_id') and kwargs.get('addons'): - # return self.setup_addon(settings, **kwargs) - # else: - # return self.setup_subscription(settings, **kwargs) - # - # def setup_addon(self, settings, **kwargs): - # url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - # - # for addon in kwargs.get("addons"): - # try: - # resp = make_post_request( - # url, - # auth=(settings.api_key, settings.api_secret), - # data=json.dumps(addon), - # headers={ - # "content-type": "application/json" - # } - # ) - # - # if not resp.get('id'): - # frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - # - # except: - # frappe.log_error(frappe.get_traceback()) - # # failed - # pass - # - # return {} + def setup_addon(self, settings, **kwargs): + """ + Addon template: + { + "item": { + "name": row.upgrade_type, + "amount": row.amount, + "currency": currency, + "description": "add-on description" + }, + "quantity": 1 (The total amount is calculated as item.amount * quantity) + } + """ + + url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) + + for addon in kwargs.get("addons"): + try: + addon['item']['amount'] *= 100 #convert amount to paisa + + resp = make_post_request( + url, + auth=(settings.api_key, settings.api_secret), + data=json.dumps(addon), + headers={ + "content-type": "application/json" + } + ) + + if not resp.get('id'): + frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') + + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass def setup_subscription(self, settings, **kwargs): start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ @@ -144,7 +147,8 @@ class RazorpaySettings(Document): ) if resp.get('status') == 'created': - kwargs['subscription_details']['subscription_id'] = resp.get('id') + kwargs['subscription_id'] = resp.get('id') + return kwargs else: frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') @@ -153,12 +157,20 @@ class RazorpaySettings(Document): # failed pass - def get_payment_url(self, **kwargs): - settings = self.get_settings(kwargs) - + def prepare_subscription_details(self, settings, **kwargs): if kwargs.get('subscription_details'): - self.setup_subscription(settings, **kwargs) + kwargs = self.setup_subscription(settings, **kwargs) + + if kwargs.get("subscription_id") and kwargs.get("addons"): + self.setup_addon(settings, **kwargs) + kwargs['subscription_id'] = None + return kwargs + + def get_payment_url(self, **kwargs): + settings = self.get_settings(kwargs) + kwargs = self.prepare_subscription_details(settings, **kwargs) + integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) @@ -195,14 +207,15 @@ class RazorpaySettings(Document): self.integration_request.update_status(data, 'Authorized') self.flags.status_changed_to = "Authorized" - elif data.get('subscription_details') and data.get('subscription_details').get('subscription_id'): + elif data.get('subscription_id'): if resp.get("status") == "refunded": - self.integration_request.update_status(data, 'Refunded') + # razorpay refunds the amount after authorizing the card details + self.integration_request.update_status(data, 'Completed') self.flags.status_changed_to = "Authorized" if resp.get("status") == "captured": self.integration_request.update_status(data, 'Completed') - self.flags.status_changed_to = "Authorized" + self.flags.status_changed_to = "Completed" else: frappe.log_error(str(resp), 'Razorpay Payment not authorized') diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 6e64d3f..3885224 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -27,13 +27,10 @@ def get_context(context): context['token'] = frappe.form_dict['token'] context['amount'] = flt(context['amount']) - if payment_details.get('subscription_details'): - context['subscription_id'] = payment_details.get("subscription_details", {}).get('subscription_id', '') - else: - context['subscription_id'] = '' + context['subscription_id'] = payment_details['subscription_id'] \ + if payment_details.get('subscription_id') else '' except Exception as e: - print(e) frappe.redirect_to_message(_('Invalid Token'), _('Seems token you are using is invalid!'), http_status_code=400, indicator_color='red') From 3a7b0f8f5bec3418a0603de6253acf7b8a84e231 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Sep 2018 16:36:03 +0530 Subject: [PATCH 19/35] finalize subscription razorpay --- .../razorpay_settings/razorpay_settings.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 580e139..9922a9c 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -105,11 +105,10 @@ class RazorpaySettings(Document): """ url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - - for addon in kwargs.get("addons"): - try: + + try: + for addon in kwargs.get("addons"): addon['item']['amount'] *= 100 #convert amount to paisa - resp = make_post_request( url, auth=(settings.api_key, settings.api_secret), @@ -121,11 +120,10 @@ class RazorpaySettings(Document): if not resp.get('id'): frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') - - except: - frappe.log_error(frappe.get_traceback()) - # failed - pass + except: + frappe.log_error(frappe.get_traceback()) + # failed + pass def setup_subscription(self, settings, **kwargs): start_date = get_timestamp(kwargs.get('subscription_details').get("start_date")) \ @@ -170,7 +168,7 @@ class RazorpaySettings(Document): def get_payment_url(self, **kwargs): settings = self.get_settings(kwargs) kwargs = self.prepare_subscription_details(settings, **kwargs) - + integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) @@ -209,11 +207,18 @@ class RazorpaySettings(Document): 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 = "Authorized" + 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" From 7f116b7bdca3e00983c5970ac317b71c0b4238bb Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 21 Sep 2018 18:11:25 +0530 Subject: [PATCH 20/35] finalize recurring payments for paypal --- .../paypal_settings/paypal_settings.py | 167 ++++++++---------- 1 file changed, 78 insertions(+), 89 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index b56a8c6..b10a24f 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -68,11 +68,14 @@ import frappe import json from frappe import _ from datetime import datetime -from frappe.utils import get_url, call_hook_method, cint, get_timestamp +from frappe.utils import get_url, call_hook_method, cint, get_timestamp, cstr, nowdate, date_diff 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 + +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", "TWD", "NZD", "NOK", "PHP", "PLN", "GBP", "RUB", "SGD", "SEK", "CHF", "THB", "TRY", "USD"] @@ -144,9 +147,8 @@ class PayPalSettings(Document): "token": response.get("TOKEN")[0], "correlation_id": response.get("CORRELATIONID")[0] }) - print(kwargs) self.integration_request = create_request_log(kwargs, "Remote", "PayPal", response.get("TOKEN")[0]) - print(return_url.format(kwargs["token"])) + return return_url.format(kwargs["token"]) def execute_set_express_checkout(self, **kwargs): @@ -154,49 +156,61 @@ class PayPalSettings(Document): params.update({ "METHOD": "SetExpressCheckout", - "returnUrl": get_url("/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.get_express_checkout_details"), - "cancelUrl": get_url("/payment-cancel") + "returnUrl": get_url("{0}.get_express_checkout_details".format(api_path)), + "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'): - params.update({ - "L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement - "L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] - }) - else: - params.update({ - "PAYMENTREQUEST_0_PAYMENTACTION": "SALE", - "PAYMENTREQUEST_0_AMT": kwargs['amount'], - "PAYMENTREQUEST_0_CURRENCYCODE": kwargs['currency'].upper(), - }) + 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 cancel_recurring_profile(self, **kwargs): - params, url = self.get_paypal_params_and_url() - if not kwargs.get('profile_id'): - frappe.throw(_("PayPal Recurring Profile ID is required")) + def configure_recurring_payments(self, params, kwargs): + # removing the params as we have to setup rucurring payments + for param in ('PAYMENTREQUEST_0_PAYMENTACTION', 'PAYMENTREQUEST_0_AMT', + 'PAYMENTREQUEST_0_CURRENCYCODE'): + del params[param] params.update({ - "METHOD": "ManageRecurringPaymentsProfileStatus", - "PROFILEID": kwargs['profile_id'], - "ACTION": "Cancel" + "L_BILLINGTYPE0": "RecurringPayments", #The type of billing agreement + "L_BILLINGAGREEMENTDESCRIPTION0": kwargs['description'] }) - params = urlencode(params) +def get_paypal_and_transaction_details(token): + doc = frappe.get_doc("PayPal Settings") + doc.setup_sandbox_env(token) + params, url = doc.get_paypal_params_and_url() - response = make_post_request(url, data=params.encode("utf-8")) - if response.get("ACK")[0] != "Success": - frappe.throw(_("Looks like something is wrong with this site's Paypal configuration.")) + integration_request = frappe.get_doc("Integration Request", token) + data = json.loads(integration_request.data) - return response + return data, params, url + +def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): + redirect_to = data.get('redirect_to') or None + redirect_message = data.get('redirect_message') or None + + if custom_redirect_to: + redirect_to = custom_redirect_to + + if redirect_to: + redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + if redirect_message: + redirect_url += '&' + urlencode({'redirect_message': redirect_message}) + + # this is done so that functions called via hooks can update flags.redirect_to + if redirect: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url(redirect_url) @frappe.whitelist(allow_guest=True, xss_safe=True) def get_express_checkout_details(token): @@ -225,8 +239,6 @@ def get_express_checkout_details(token): "payerid": response.get("PAYERID")[0], "payer_email": response.get("EMAIL")[0] }, "Authorized", doc=doc) - - frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_redirect_uri(doc, token, response.get("PAYERID")[0]) @@ -237,19 +249,8 @@ 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 - - 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 + data, params, url = get_paypal_and_transaction_details(token) - params, url = doc.get_paypal_params_and_url() params.update({ "METHOD": "DoExpressCheckoutPayment", "PAYERID": data.get("payerid"), @@ -272,22 +273,11 @@ 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' 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}) - - # 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) + setup_redirect(data, redirect_url, custom_redirect_to) except Exception: frappe.log_error(frappe.get_traceback()) @@ -295,20 +285,15 @@ def confirm_payment(token): @frappe.whitelist(allow_guest=True, xss_safe=True) def create_recurring_profile(token, payerid): try: - redirect = True - status_changed_to, redirect_to = None, None - - 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) + data, params, url = get_paypal_and_transaction_details(token) + addons = data.get("addons") subscription_details = data.get("subscription_details") - redirect_to = data.get('redirect_to') or None - redirect_message = data.get('redirect_message') or None + setup_subscription_amount(data, addons) + + if data['subscription_id'] and addons: + manage_recurring_payment_profile_status(data['subscription_id'], 'Cancel', params, url) - params, url = doc.get_paypal_params_and_url() params.update({ "METHOD": "CreateRecurringPaymentsProfile", "PAYERID": payerid, @@ -316,13 +301,13 @@ def create_recurring_profile(token, payerid): "DESC": data.get("description"), "BILLINGPERIOD": subscription_details.get("billing_period"), "BILLINGFREQUENCY": subscription_details.get("billing_frequency"), - "AMT": data.get("amount"), + "AMT": data.get("amount") if data.get("subscription_amount") == data.get("amount") else data.get("subscription_amount"), "CURRENCYCODE": data.get("currency").upper(), "INITAMT": subscription_details.get("upfront_amount") }) - starts_at = subscription_details.get("start_date") if subscription_details.get("start_date")\ - else frappe.utils.now() + starts_at = subscription_details.get("start_date") or nowdate() + status_changed_to = 'Completed' if subscription_details.get("upfront_amount") else 'Verified' params.update({ "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() @@ -336,33 +321,26 @@ def create_recurring_profile(token, payerid): }, "Completed") if data.get("reference_doctype") and data.get("reference_docname"): - data['subscription_details']['subscription_id'] = response.get("PROFILEID")[0] + 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", "Completed") + data.get("reference_docname")).run_method("on_payment_authorized", status_changed_to) frappe.db.commit() - if custom_redirect_to: - redirect_to = custom_redirect_to - redirect_url = '/integrations/payment-success' 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}) - - # 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) + setup_redirect(data, redirect_url, custom_redirect_to) except Exception: frappe.log_error(frappe.get_traceback()) +def setup_subscription_amount(data, addons): + for addon in addons: + data['subscription_amount'] += addon['item']['amount'] + def update_integration_request_status(token, data, status, error=False, doc=None): if not doc: doc = frappe.get_doc("Integration Request", token) @@ -372,9 +350,20 @@ def update_integration_request_status(token, data, status, error=False, doc=None def get_redirect_uri(doc, token, payerid): data = json.loads(doc.data) - if data.get("subscription_details"): - return get_url( \ - "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.create_recurring_profile?token={0}&payerid={1}".format(token, payerid)) + 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( \ - "/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings.confirm_payment?token={0}".format(token)) + 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")) + From b258e61230e269c5c41a6a3d475452d24a8709d1 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 25 Sep 2018 11:52:56 +0530 Subject: [PATCH 21/35] fix for immediate subscriptions --- .../paypal_settings/paypal_settings.py | 5 --- .../razorpay_settings/razorpay_settings.py | 38 +++++++++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index b10a24f..1c60a2d 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -289,7 +289,6 @@ def create_recurring_profile(token, payerid): addons = data.get("addons") subscription_details = data.get("subscription_details") - setup_subscription_amount(data, addons) if data['subscription_id'] and addons: manage_recurring_payment_profile_status(data['subscription_id'], 'Cancel', params, url) @@ -337,10 +336,6 @@ def create_recurring_profile(token, payerid): except Exception: frappe.log_error(frappe.get_traceback()) -def setup_subscription_amount(data, addons): - for addon in addons: - data['subscription_amount'] += addon['item']['amount'] - def update_integration_request_status(token, data, status, error=False, doc=None): if not doc: doc = frappe.get_doc("Integration Request", token) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 9922a9c..398a7e6 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -67,7 +67,8 @@ 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, get_timestamp -from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway +from frappe.integrations.utils import (make_get_request, make_post_request, create_request_log, + create_payment_gateway) class RazorpaySettings(Document): supported_currencies = ["INR"] @@ -103,12 +104,13 @@ class RazorpaySettings(Document): "quantity": 1 (The total amount is calculated as item.amount * quantity) } """ - url = "https://api.razorpay.com/v1/subscriptions/{0}/addons".format(kwargs.get('subscription_id')) - + try: + if not frappe.conf.converted_rupee_to_paisa: + convert_rupee_to_paisa(**kwargs) + for addon in kwargs.get("addons"): - addon['item']['amount'] *= 100 #convert amount to paisa resp = make_post_request( url, auth=(settings.api_key, settings.api_secret), @@ -133,19 +135,28 @@ class RazorpaySettings(Document): "plan_id": kwargs.get('subscription_details').get("plan_id"), "start_at": cint(start_date), "total_count": kwargs.get('subscription_details').get("billing_frequency"), - "customer_notify": kwargs.get('subscription_details').get("customer_notify"), - "upfront_amount": kwargs.get('subscription_details').get("upfront_amount") + "customer_notify": kwargs.get('subscription_details').get("customer_notify") } + if kwargs.get('addons'): + convert_rupee_to_paisa(**kwargs) + subscription_details.update({ + "addons": kwargs.get('addons') + }) + try: resp = make_post_request( "https://api.razorpay.com/v1/subscriptions", auth=(settings.api_key, settings.api_secret), - data=subscription_details + data=json.dumps(subscription_details), + headers={ + "content-type": "application/json" + } ) if resp.get('status') == 'created': kwargs['subscription_id'] = resp.get('id') + frappe.flags.status = 'created' return kwargs else: frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') @@ -156,11 +167,10 @@ class RazorpaySettings(Document): pass def prepare_subscription_details(self, settings, **kwargs): - if kwargs.get('subscription_details'): + if not kwargs.get("subscription_id"): kwargs = self.setup_subscription(settings, **kwargs) - if kwargs.get("subscription_id") and kwargs.get("addons"): - self.setup_addon(settings, **kwargs) + if frappe.flags.status !='created': kwargs['subscription_id'] = None return kwargs @@ -235,7 +245,7 @@ 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: @@ -318,3 +328,9 @@ def capture_payment(is_sandbox=False, sanbox_response=None): doc.status = "Failed" doc.error = frappe.get_traceback() 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 \ No newline at end of file From 0d0d19d1dc3ede0ca9c1b64334344d4532dfb1a3 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 26 Sep 2018 19:19:47 +0530 Subject: [PATCH 22/35] [fix] call setup subscription and addons explicitly --- .../doctype/paypal_settings/paypal_settings.py | 15 ++++++++++----- .../razorpay_settings/razorpay_settings.py | 3 --- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 1c60a2d..58d2c9d 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -68,7 +68,7 @@ import frappe import json from frappe import _ from datetime import datetime -from frappe.utils import get_url, call_hook_method, cint, get_timestamp, cstr, nowdate, date_diff +from frappe.utils import get_url, call_hook_method, cint, get_timestamp, cstr, now, date_diff, get_datetime 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 @@ -249,6 +249,7 @@ def get_express_checkout_details(token): @frappe.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({ @@ -285,12 +286,15 @@ def confirm_payment(token): @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['subscription_id'] and addons: + updating = True manage_recurring_payment_profile_status(data['subscription_id'], 'Cancel', params, url) params.update({ @@ -302,14 +306,15 @@ def create_recurring_profile(token, payerid): "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": subscription_details.get("upfront_amount") + "INITAMT": data.get("upfront_amount") }) - starts_at = subscription_details.get("start_date") or nowdate() - status_changed_to = 'Completed' if subscription_details.get("upfront_amount") else 'Verified' + starts_at = get_datetime(subscription_details.get("start_date")) or frappe.utils.now_datetime() + status_changed_to = 'Completed' if data.get("starting_immediately") or updating else 'Verified' + #"PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() params.update({ - "PROFILESTARTDATE": datetime.utcfromtimestamp(get_timestamp(starts_at)).isoformat() + "PROFILESTARTDATE": starts_at.isoformat() }) response = make_post_request(url, data=params) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 398a7e6..064d509 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -176,9 +176,6 @@ class RazorpaySettings(Document): return kwargs def get_payment_url(self, **kwargs): - settings = self.get_settings(kwargs) - kwargs = self.prepare_subscription_details(settings, **kwargs) - integration_request = create_request_log(kwargs, "Host", "Razorpay") return get_url("./integrations/razorpay_checkout?token={0}".format(integration_request.name)) From e49ef3e919cf592df8db394fd4f1e6509dd8eecb Mon Sep 17 00:00:00 2001 From: Saurabh Date: Sat, 29 Sep 2018 22:22:29 +0530 Subject: [PATCH 23/35] [fix] minor-fixes --- payments/templates/pages/razorpay_checkout.py | 1 - 1 file changed, 1 deletion(-) diff --git a/payments/templates/pages/razorpay_checkout.py b/payments/templates/pages/razorpay_checkout.py index 3885224..c45279c 100644 --- a/payments/templates/pages/razorpay_checkout.py +++ b/payments/templates/pages/razorpay_checkout.py @@ -26,7 +26,6 @@ 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 '' From 547863149c7e95ed63ffd39556af8545f9198d5c Mon Sep 17 00:00:00 2001 From: Saurabh Date: Sun, 30 Sep 2018 09:29:43 +0530 Subject: [PATCH 24/35] check for subscription id --- .../payment_gateways/doctype/paypal_settings/paypal_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 58d2c9d..c59a240 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -293,7 +293,7 @@ def create_recurring_profile(token, payerid): addons = data.get("addons") subscription_details = data.get("subscription_details") - if data['subscription_id'] and addons: + if data.get('subscription_id') and addons: updating = True manage_recurring_payment_profile_status(data['subscription_id'], 'Cancel', params, url) From 969303dc92205a310bb841b33ad7fa49d5948623 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 11 Oct 2018 17:49:19 +0530 Subject: [PATCH 25/35] [fix] if redirect uri exists then automatically redirect the user --- payments/templates/pages/payment-success.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/payments/templates/pages/payment-success.html b/payments/templates/pages/payment-success.html index a1b5092..ce382cd 100644 --- a/payments/templates/pages/payment-success.html +++ b/payments/templates/pages/payment-success.html @@ -15,4 +15,13 @@ + {% endblock %} From 8c2434dc1f72092b61d99f030210a2ca6c743275 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 15 Oct 2018 05:57:08 +0200 Subject: [PATCH 26/35] Improve checkout UX (#6248) --- payments/templates/includes/braintree_checkout.js | 8 ++++++++ payments/templates/pages/braintree_checkout.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/payments/templates/includes/braintree_checkout.js b/payments/templates/includes/braintree_checkout.js index 96bc9c3..6e6ec4c 100644 --- a/payments/templates/includes/braintree_checkout.js +++ b/payments/templates/includes/braintree_checkout.js @@ -42,6 +42,14 @@ $(document).ready(function() { }) }); }); + + instance.on('paymentMethodRequestable', function (event) { + button.removeAttribute('disabled'); + }); + + instance.on('noPaymentMethodRequestable', function () { + button.setAttribute('disabled', true); + }); }); }) diff --git a/payments/templates/pages/braintree_checkout.html b/payments/templates/pages/braintree_checkout.html index a9fec0e..bdc7b46 100644 --- a/payments/templates/pages/braintree_checkout.html +++ b/payments/templates/pages/braintree_checkout.html @@ -26,7 +26,7 @@ - + From e49643feb5b6fcfdef848f01b958392d23d53cd6 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 2 Oct 2018 22:11:43 +0530 Subject: [PATCH 27/35] provision to handel paypal and razoapay subscription charge notifications --- .../paypal_settings/paypal_settings.py | 20 ++++++++++++++ .../razorpay_settings/razorpay_settings.py | 26 ++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index c59a240..80aadfd 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -363,7 +363,27 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): }) 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(): + data = frappe.local.form_dict + data.update({ + "payment_gateway": "PayPal" + }) + + doc = frappe.get_doc({ + "data": frappe.local.form_dict, + "doctype": "Integration Request", + "status": "Subscription Notification" + }).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}) + +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.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 064d509..a1029aa 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -119,7 +119,6 @@ class RazorpaySettings(Document): "content-type": "application/json" } ) - if not resp.get('id'): frappe.log_error(str(resp), 'Razorpay Failed while creating subscription') except: @@ -329,5 +328,26 @@ def capture_payment(is_sandbox=False, sanbox_response=None): def convert_rupee_to_paisa(**kwargs): for addon in kwargs.get('addons'): addon['item']['amount'] *= 100 - - frappe.conf.converted_rupee_to_paisa = True \ No newline at end of file + + frappe.conf.converted_rupee_to_paisa = True + + +@frappe.whitelist(allow_guest=True) +def razorpay_subscription_callback(): + data = frappe.local.form_dict + data.update({ + "payment_gateway": "Razorpay" + }) + + doc = frappe.get_doc({ + "data": json.dumps(frappe.local.form_dict), + "doctype": "Integration Request", + "status": "Subscription Notification" + }).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}) + +def handle_subscription_notification(doctype, docname): + call_hook_method("handle_subscription_notification", doctype=doctype, docname=docname) \ No newline at end of file From 0e2cf29f1636568c98fa4c66f041e2658286571e Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 5 Oct 2018 17:16:28 +0530 Subject: [PATCH 28/35] [fix] add validations on payment notification callback --- .../paypal_settings/paypal_settings.py | 55 +++++++++++++----- .../razorpay_settings/razorpay_settings.py | 57 ++++++++++++++----- 2 files changed, 84 insertions(+), 28 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 80aadfd..9fa3164 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -68,10 +68,10 @@ import frappe import json from frappe import _ from datetime import datetime -from frappe.utils import get_url, call_hook_method, cint, get_timestamp, cstr, now, date_diff, get_datetime 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_timestamp, cstr, now, date_diff, get_datetime api_path = '/api/method/frappe.integrations.doctype.paypal_settings.paypal_settings' @@ -363,27 +363,56 @@ def manage_recurring_payment_profile_status(profile_id, action, args, url): }) 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(): - data = frappe.local.form_dict - data.update({ - "payment_gateway": "PayPal" + 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", + "status": "Subscription Notification" + }).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") }) - doc = frappe.get_doc({ - "data": frappe.local.form_dict, - "doctype": "Integration Request", - "status": "Subscription Notification" - }).insert(ignore_permissions=True) - frappe.db.commit() + params = urlencode(params) + res = make_post_request(url=url, data=params.encode("utf-8")) - 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}) + 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.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index a1029aa..0eb41e5 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -331,23 +331,50 @@ def convert_rupee_to_paisa(**kwargs): frappe.conf.converted_rupee_to_paisa = True - @frappe.whitelist(allow_guest=True) def razorpay_subscription_callback(): - data = frappe.local.form_dict - data.update({ - "payment_gateway": "Razorpay" - }) - - doc = frappe.get_doc({ - "data": json.dumps(frappe.local.form_dict), - "doctype": "Integration Request", - "status": "Subscription Notification" - }).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}) + try: + data = frappe.local.form_dict + + validate_payment_callback() + + data.update({ + "payment_gateway": "Razorpay" + }) + + doc = frappe.get_doc({ + "data": json.dumps(frappe.local.form_dict), + "doctype": "Integration Request", + "status": "Subscription Notification" + }).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 From 47228bdae18cd28a0b09481a24d1e3668b90d55c Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 8 Oct 2018 13:49:12 +0530 Subject: [PATCH 29/35] [fix] pass request type as Subscription Notification --- .../doctype/paypal_settings/paypal_settings.py | 3 ++- .../doctype/razorpay_settings/razorpay_settings.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 9fa3164..3d4be38 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -381,7 +381,8 @@ def ipn_handler(): doc = frappe.get_doc({ "data": json.dumps(frappe.local.form_dict), "doctype": "Integration Request", - "status": "Subscription Notification" + "integration_type": "Subscription Notification", + "status": "Queued" }).insert(ignore_permissions=True) frappe.db.commit() diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 0eb41e5..3cca99e 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -345,7 +345,8 @@ def razorpay_subscription_callback(): doc = frappe.get_doc({ "data": json.dumps(frappe.local.form_dict), "doctype": "Integration Request", - "status": "Subscription Notification" + "integration_type": "Subscription Notification", + "status": "Queued" }).insert(ignore_permissions=True) frappe.db.commit() From 69fb2e7768857be6fb2c71cd45622e116ce30128 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 8 Oct 2018 18:24:07 +0530 Subject: [PATCH 30/35] [fix] pass form dict to validate function --- .../doctype/razorpay_settings/razorpay_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 3cca99e..1848e4d 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -336,7 +336,7 @@ def razorpay_subscription_callback(): try: data = frappe.local.form_dict - validate_payment_callback() + validate_payment_callback(data) data.update({ "payment_gateway": "Razorpay" From dba5a1e540fc32cd21a5acba1feae4d31f559b31 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 10 Oct 2018 17:28:19 +0530 Subject: [PATCH 31/35] [fix] do not cache payment success page --- payments/templates/pages/payment_success.py | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/templates/pages/payment_success.py b/payments/templates/pages/payment_success.py index 655767e..4ece359 100644 --- a/payments/templates/pages/payment_success.py +++ b/payments/templates/pages/payment_success.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +no_cache = True def get_context(context): token = frappe.local.form_dict.token From 8e138648a505d8509f2cffed3d6be934c93b8cf7 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 17 Oct 2018 19:50:54 +0530 Subject: [PATCH 32/35] [fix] razorpay subscription cancellation --- .../doctype/razorpay_settings/razorpay_settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index 1848e4d..bca9291 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -282,13 +282,12 @@ class RazorpaySettings(Document): }) return settings - - + def cancel_subscription(self, subscription_id): - settings = self.get_settings() - + settings = self.get_settings({}) + try: - resp = make_get_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" + resp = make_post_request("https://api.razorpay.com/v1/subscriptions/{0}/cancel" .format(subscription_id), auth=(settings.api_key, settings.api_secret)) except Exception: From 0e5bfb54a75e24ff18a197cef38bf9442530a695 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 29 Nov 2018 09:07:38 +0530 Subject: [PATCH 33/35] Customize payment success message (#6460) * Provision to set custom payment success message * typo fix * Fix formatting --- .../doctype/paypal_settings/paypal_settings.py | 6 +++--- .../razorpay_settings/razorpay_settings.py | 4 ++-- payments/templates/pages/payment-success.html | 15 +++++++++++---- payments/templates/pages/payment_success.py | 8 +++++++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py index 3d4be38..83a1cb0 100644 --- a/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py +++ b/payments/payment_gateways/doctype/paypal_settings/paypal_settings.py @@ -203,7 +203,7 @@ def setup_redirect(data, redirect_url, custom_redirect_to=None, redirect=True): redirect_to = custom_redirect_to if redirect_to: - redirect_url += '?' + urlencode({'redirect_to': redirect_to}) + redirect_url += '&' + urlencode({'redirect_to': redirect_to}) if redirect_message: redirect_url += '&' + urlencode({'redirect_message': redirect_message}) @@ -274,7 +274,7 @@ def confirm_payment(token): data.get("reference_docname")).run_method("on_payment_authorized", "Completed") frappe.db.commit() - 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" @@ -332,7 +332,7 @@ def create_recurring_profile(token, payerid): data.get("reference_docname")).run_method("on_payment_authorized", status_changed_to) frappe.db.commit() - 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" diff --git a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py index bca9291..cb66ed7 100644 --- a/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py +++ b/payments/payment_gateways/doctype/razorpay_settings/razorpay_settings.py @@ -255,12 +255,12 @@ class RazorpaySettings(Document): 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}) diff --git a/payments/templates/pages/payment-success.html b/payments/templates/pages/payment-success.html index ce382cd..45bc303 100644 --- a/payments/templates/pages/payment-success.html +++ b/payments/templates/pages/payment-success.html @@ -8,9 +8,16 @@ {{ _("Success") }} -

{{ _("Your payment was successfully accepted") }}

- +

{{ payment_message or _("Your payment was successfully accepted") }}

+ {% if not payment_message %} + + {% endif %}