* Stripe Payments Improvements
* Automatically create a new stripe customer
* Charge arguments correction
* Move Stripe to ERPNext
* Revert "Move Stripe to ERPNext"
This reverts commit f71f995299
.
* 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
pull/2/head
@@ -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", | |||
@@ -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' | |||
@@ -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); | |||
}) | |||
} | |||
}); |
@@ -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; | |||
} |
@@ -2,27 +2,57 @@ | |||
{% block title %} Payment {% endblock %} | |||
{%- block header -%}{% endblock %} | |||
{%- block header -%} | |||
{% endblock %} | |||
{% block script %} | |||
<script src="https://checkout.stripe.com/checkout.js"></script> | |||
<script src="https://js.stripe.com/v3/"></script> | |||
<script>{% include "templates/includes/integrations/stripe_checkout.js" %}</script> | |||
{% endblock %} | |||
{%- block page_content -%} | |||
<p class='lead text-center centered'> | |||
<span class='stripe-loading'>{{ _("Loading Payment System") }}</span> | |||
<span class='stripe-confirming hidden'>{{ _("Confirming Payment") }}</span> | |||
</p> | |||
{% endblock %} | |||
<div class="row stripe" style="min-height: 400px; padding-bottom: 50px; margin-top:100px;"> | |||
<div class="col-sm-8 col-sm-offset-2"> | |||
{% if image %} | |||
<img src={{image}}> | |||
{% endif %} | |||
<h2 class="text-center">{{description}}</h2> | |||
<form id="payment-form"> | |||
<div class="form-row"> | |||
<div class="group"> | |||
<div> | |||
<label> | |||
<span>{{ _("Name") }}</span> | |||
<input id="cardholder-name" name="cardholder-name" class="field" placeholder="{{ _('John Doe') }}" value="{{payer_name}}"/> | |||
</label> | |||
</div> | |||
</div> | |||
<div class="group"> | |||
<div> | |||
<label> | |||
<span>{{ _("Email") }}</span> | |||
<input id="cardholder-email" name="cardholder-email" class="field" placeholder="{{ _('john@doe.com') }}" value="{{payer_email}}"/> | |||
</label> | |||
</div> | |||
</div> | |||
<div class="group"> | |||
<label> | |||
<span>{{ _("Card Details") }}</span> | |||
<div id="card-element" name="card-element" class="field"></div> | |||
<div id="card-errors" role="alert"></div> | |||
</label> | |||
</div> | |||
</div> | |||
<button type="submit" class="submit" id="submit">{{_('Pay')}} {{amount}}</button> | |||
<div class="outcome text-center"> | |||
<div class="error" hidden>{{ _("An error occured during the payment process. Please contact us.") }}</div> | |||
<div class="success" hidden>{{ _("Your payment has been successfully registered.") }}</div> | |||
</div> | |||
</form> | |||
</div> | |||
</div> | |||
{% block style %} | |||
<style> | |||
header, footer { | |||
display: none; | |||
} | |||
</style> | |||
{% endblock %} | |||
{% endblock %} |
@@ -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 |