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 -%} +