Selaa lähdekoodia

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
pull/2/head
Charles-Henri Decultot 7 vuotta sitten
committed by Rushabh Mehta
vanhempi
commit
800cd733a6
11 muutettua tiedostoa jossa 684 lisäystä ja 2 poistoa
  1. +0
    -0
      payments/payment_gateways/doctype/braintree_settings/__init__.py
  2. +6
    -0
      payments/payment_gateways/doctype/braintree_settings/braintree_settings.js
  3. +273
    -0
      payments/payment_gateways/doctype/braintree_settings/braintree_settings.json
  4. +136
    -0
      payments/payment_gateways/doctype/braintree_settings/braintree_settings.py
  5. +23
    -0
      payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js
  6. +9
    -0
      payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py
  7. +65
    -2
      payments/payments/doctype/payment_gateway/payment_gateway.json
  8. +23
    -0
      payments/payments/doctype/payment_gateway/test_payment_gateway.js
  9. +47
    -0
      payments/templates/includes/braintree_checkout.js
  10. +54
    -0
      payments/templates/pages/braintree_checkout.html
  11. +48
    -0
      payments/templates/pages/braintree_checkout.py

+ 0
- 0
payments/payment_gateways/doctype/braintree_settings/__init__.py Näytä tiedosto


+ 6
- 0
payments/payment_gateways/doctype/braintree_settings/braintree_settings.js Näytä tiedosto

@@ -0,0 +1,6 @@
// Copyright (c) 2018, Frappe Technologies and contributors
// For license information, please see license.txt

frappe.ui.form.on('Braintree Settings', {

});

+ 273
- 0
payments/payment_gateways/doctype/braintree_settings/braintree_settings.json Näytä tiedosto

@@ -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
}

+ 136
- 0
payments/payment_gateways/doctype/braintree_settings/braintree_settings.py Näytä tiedosto

@@ -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()

+ 23
- 0
payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.js Näytä tiedosto

@@ -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()
]);

});

+ 9
- 0
payments/payment_gateways/doctype/braintree_settings/test_braintree_settings.py Näytä tiedosto

@@ -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

+ 65
- 2
payments/payments/doctype/payment_gateway/payment_gateway.json Näytä tiedosto

@@ -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",


+ 23
- 0
payments/payments/doctype/payment_gateway/test_payment_gateway.js Näytä tiedosto

@@ -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()
]);

});

+ 47
- 0
payments/templates/includes/braintree_checkout.js Näytä tiedosto

@@ -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
}
}
})
});
});
});

})

+ 54
- 0
payments/templates/pages/braintree_checkout.html Näytä tiedosto

@@ -0,0 +1,54 @@
{% extends "templates/web.html" %}

{% block title %} Payment {% endblock %}

{%- block header -%}{% endblock %}

{% block script %}
<script src="https://js.braintreegateway.com/web/dropin/1.9.3/js/dropin.min.js"></script>
<script>{% include "templates/includes/integrations/braintree_checkout.js" %}</script>
{% endblock %}

{%- block page_content -%}
<div class="wrapper">
<div class="checkout container">

<header>
<div>
<img class="center" src="{{ header_img }}"></img>
</div>
</header>

<form id="payment-form">
<section>
<div class="bt-drop-in-wrapper">
<div id="bt-dropin"></div>
</div>
</section>

<button class="btn btn-primary" type="submit" id="submit-button"><span>{{ _("Pay") }} {{ amount }} {{ currency }}</span></button>
</form>

</div>
</div>

<style>
.checkout {
max-width: 60%;
}

.center {
margin:auto;
display: block;
}

#payment-form {
margin-top: 40px;
}

#submit-button {
float: right;
}

</style>
{% endblock %}

+ 48
- 0
payments/templates/pages/braintree_checkout.py Näytä tiedosto

@@ -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

Ladataan…
Peruuta
Tallenna