Explorar el Código

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 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
Charles-Henri Decultot hace 7 años
committed by Rushabh Mehta
padre
commit
e7cf9d737c
Se han modificado 6 ficheros con 445 adiciones y 88 borrados
  1. +161
    -1
      payments/payment_gateways/doctype/stripe_settings/stripe_settings.json
  2. +21
    -22
      payments/payment_gateways/doctype/stripe_settings/stripe_settings.py
  3. +81
    -44
      payments/templates/includes/stripe_checkout.js
  4. +113
    -0
      payments/templates/pages/stripe_checkout.css
  5. +45
    -15
      payments/templates/pages/stripe_checkout.html
  6. +24
    -6
      payments/templates/pages/stripe_checkout.py

+ 161
- 1
payments/payment_gateways/doctype/stripe_settings/stripe_settings.json Ver fichero

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


+ 21
- 22
payments/payment_gateways/doctype/stripe_settings/stripe_settings.py Ver fichero

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



+ 81
- 44
payments/templates/includes/stripe_checkout.js Ver fichero

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

+ 113
- 0
payments/templates/pages/stripe_checkout.css Ver fichero

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

+ 45
- 15
payments/templates/pages/stripe_checkout.html Ver fichero

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

+ 24
- 6
payments/templates/pages/stripe_checkout.py Ver fichero

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

Cargando…
Cancelar
Guardar