Per Pretix 3.10

This commit is contained in:
RunasSudo 2020-09-04 17:56:10 +10:00
commit 5bd64741d9
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
35 changed files with 3160 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
*.pyc

32
COPYING Normal file
View File

@ -0,0 +1,32 @@
Copyright 2014-2016 Raphael Michel
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This project includes the work of others, namely:
* Django, (c) Django Software Foundation and contributors, BSD License
* Font Awesome, (c) Dave Gandy, SIL Open Font License and MIT License
* Bootstrap, (c) Twitter, Inc., MIT License
* jQuery, (c) jQuery Foundation and contributors, MIT License
* django-formset-js, (c) Ionata Web Solutions, BSD License
* CleanerVersion, (c) Jean-Christophe Zulian, Brian King, Andrea Marcacci, Manuel Jeckelmann, Apache License
* django-bootstrap3, (c) Dylan Verheul, Apache License
* pytz, (c) Stuart Bishop, MIT License
* python-dateutil, (c) Yaron de Leeuw, BSD License
* startbootstrap-sb-admin-2, (c) Iron Summit Media Strategies, LLC, Apache License
* metismenu, (c) Osman Nuri Okumus, MIT License
* easy-thumbnails, (c) Chris Beaven and contributors
* reportlab, (c) ReportLab Europe Ltd, BSD License
* django-compressor, (c) Jannis Leidel and contributors, MIT License
* static3, (c) Roman Mohr and contributors, LGPL License
* Lightbox, (c) Lokesh Dhakar, MIT License

33
stripe/__init__.py Normal file
View File

@ -0,0 +1,33 @@
from django.apps import AppConfig
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix import __version__ as version
class StripeApp(AppConfig):
name = 'pretix.plugins.stripe'
verbose_name = _("Stripe")
class PretixPluginMeta:
name = _("Stripe")
author = _("the pretix team")
version = version
category = 'PAYMENT'
description = _("This plugin allows you to receive credit card payments " +
"via Stripe")
def ready(self):
from . import signals, tasks # NOQA
@cached_property
def compatibility_errors(self):
errs = []
try:
import stripe # NOQA
except ImportError:
errs.append("Python package 'stripe' is not installed.")
return errs
default_app_config = 'pretix.plugins.stripe.StripeApp'

40
stripe/forms.py Normal file
View File

@ -0,0 +1,40 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import SettingsForm
class StripeKeyValidator:
def __init__(self, prefix):
assert len(prefix) > 0
if isinstance(prefix, list):
self._prefixes = prefix
else:
self._prefixes = [prefix]
assert isinstance(prefix, str)
def __call__(self, value):
if not any(value.startswith(p) for p in self._prefixes):
raise forms.ValidationError(
_('The provided key "%(value)s" does not look valid. It should start with "%(prefix)s".'),
code='invalid-stripe-key',
params={
'value': value,
'prefix': self._prefixes[0],
},
)
class OrganizerStripeSettingsForm(SettingsForm):
payment_stripe_connect_app_fee_percent = forms.DecimalField(
label=_('Stripe Connect: App fee (percent)'),
required=False,
)
payment_stripe_connect_app_fee_max = forms.DecimalField(
label=_('Stripe Connect: App fee (max)'),
required=False,
)
payment_stripe_connect_app_fee_min = forms.DecimalField(
label=_('Stripe Connect: App fee (min)'),
required=False,
)

View File

View File

View File

@ -0,0 +1,35 @@
import stripe
from django.core.management.base import BaseCommand
from django_scopes import scopes_disabled
from pretix.base.models import Event
from pretix.base.settings import GlobalSettingsObject
class Command(BaseCommand):
help = "Detect country for Stripe Connect accounts connected with pretix 2.0 (required for payment request buttons)"
@scopes_disabled()
def handle(self, *args, **options):
cache = {}
gs = GlobalSettingsObject()
api_key = gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key
if not api_key:
self.stderr.write(self.style.ERROR("Stripe Connect is not set up!"))
return
for e in Event.objects.filter(plugins__icontains="pretix.plugins.stripe"):
uid = e.settings.payment_stripe_connect_user_id
if uid and not e.settings.payment_stripe_merchant_country:
if uid in cache:
e.settings.payment_stripe_merchant_country = cache[uid]
else:
try:
account = stripe.Account.retrieve(
uid,
api_key=api_key
)
except Exception as e:
print(e)
else:
e.settings.payment_stripe_merchant_country = cache[uid] = account.get('country')

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-23 09:37
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('pretixbase', '0070_auto_20170719_0910'),
]
operations = [
migrations.CreateModel(
name='ReferencedStripeObject',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reference', models.CharField(db_index=True, max_length=190, unique=True)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')),
],
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.13 on 2018-07-22 08:01
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0096_auto_20180722_0801'),
('stripe', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='referencedstripeobject',
name='payment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.OrderPayment'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 2.1 on 2018-08-12 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stripe', '0002_referencedstripeobject_payment'),
]
operations = [
migrations.CreateModel(
name='RegisteredApplePayDomain',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('domain', models.CharField(max_length=190)),
('account', models.CharField(max_length=190)),
],
),
]

View File

12
stripe/models.py Normal file
View File

@ -0,0 +1,12 @@
from django.db import models
class ReferencedStripeObject(models.Model):
reference = models.CharField(max_length=190, db_index=True, unique=True)
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
class RegisteredApplePayDomain(models.Model):
domain = models.CharField(max_length=190)
account = models.CharField(max_length=190)

1369
stripe/payment.py Normal file

File diff suppressed because it is too large Load Diff

178
stripe/signals.py Normal file
View File

@ -0,0 +1,178 @@
import json
from collections import OrderedDict
from django import forms
from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import resolve, reverse
from django.utils.translation import gettext_lazy as _
from pretix.base.forms import SecretKeySettingsField
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import (
logentry_display, register_global_settings, register_payment_providers,
requiredaction_display,
)
from pretix.control.signals import nav_organizer
from pretix.plugins.stripe.forms import StripeKeyValidator
from pretix.presale.signals import html_head
@receiver(register_payment_providers, dispatch_uid="payment_stripe")
def register_payment_provider(sender, **kwargs):
from .payment import (
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay
)
return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay
]
@receiver(html_head, dispatch_uid="payment_stripe_html_head")
def html_head_presale(sender, request=None, **kwargs):
from .payment import StripeSettingsHolder
provider = StripeSettingsHolder(sender)
url = resolve(request.path_info)
if provider.settings.get('_enabled', as_type=bool) and ("checkout" in url.url_name or "order.pay" in url.url_name):
template = get_template('pretixplugins/stripe/presale_head.html')
ctx = {
'event': sender,
'settings': provider.settings,
'testmode': (
(provider.settings.get('endpoint', 'live') == 'test' or sender.testmode)
and provider.settings.publishable_test_key
)
}
return template.render(ctx)
else:
return ""
@receiver(signal=logentry_display, dispatch_uid="stripe_logentry_display")
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
if logentry.action_type != 'pretix.plugins.stripe.event':
return
data = json.loads(logentry.data)
event_type = data.get('type')
text = None
plains = {
'charge.succeeded': _('Charge succeeded.'),
'charge.refunded': _('Charge refunded.'),
'charge.updated': _('Charge updated.'),
'charge.pending': _('Charge pending'),
'source.chargeable': _('Payment authorized.'),
'source.canceled': _('Payment authorization canceled.'),
'source.failed': _('Payment authorization failed.')
}
if event_type in plains:
text = plains[event_type]
elif event_type == 'charge.failed':
text = _('Charge failed. Reason: {}').format(data['data']['object']['failure_message'])
elif event_type == 'charge.dispute.created':
text = _('Dispute created. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.updated':
text = _('Dispute updated. Reason: {}').format(data['data']['object']['reason'])
elif event_type == 'charge.dispute.closed':
text = _('Dispute closed. Status: {}').format(data['data']['object']['status'])
if text:
return _('Stripe reported an event: {}').format(text)
settings_hierarkey.add_default('payment_stripe_method_cc', True, bool)
settings_hierarkey.add_default('payment_stripe_reseller_moto', False, bool)
@receiver(register_global_settings, dispatch_uid='stripe_global_settings')
def register_global_settings(sender, **kwargs):
return OrderedDict([
('payment_stripe_connect_client_id', forms.CharField(
label=_('Stripe Connect: Client ID'),
required=False,
validators=(
StripeKeyValidator('ca_'),
),
)),
('payment_stripe_connect_secret_key', SecretKeySettingsField(
label=_('Stripe Connect: Secret key'),
required=False,
validators=(
StripeKeyValidator(['sk_live_', 'rk_live_']),
),
)),
('payment_stripe_connect_publishable_key', forms.CharField(
label=_('Stripe Connect: Publishable key'),
required=False,
validators=(
StripeKeyValidator('pk_live_'),
),
)),
('payment_stripe_connect_test_secret_key', SecretKeySettingsField(
label=_('Stripe Connect: Secret key (test)'),
required=False,
validators=(
StripeKeyValidator(['sk_test_', 'rk_test_']),
),
)),
('payment_stripe_connect_test_publishable_key', forms.CharField(
label=_('Stripe Connect: Publishable key (test)'),
required=False,
validators=(
StripeKeyValidator('pk_test_'),
),
)),
('payment_stripe_connect_app_fee_percent', forms.DecimalField(
label=_('Stripe Connect: App fee (percent)'),
required=False,
)),
('payment_stripe_connect_app_fee_max', forms.DecimalField(
label=_('Stripe Connect: App fee (max)'),
required=False,
)),
('payment_stripe_connect_app_fee_min', forms.DecimalField(
label=_('Stripe Connect: App fee (min)'),
required=False,
)),
])
@receiver(signal=requiredaction_display, dispatch_uid="stripe_requiredaction_display")
def pretixcontrol_action_display(sender, action, request, **kwargs):
# DEPRECATED
if not action.action_type.startswith('pretix.plugins.stripe'):
return
data = json.loads(action.data)
if action.action_type == 'pretix.plugins.stripe.refund':
template = get_template('pretixplugins/stripe/action_refund.html')
elif action.action_type == 'pretix.plugins.stripe.overpaid':
template = get_template('pretixplugins/stripe/action_overpaid.html')
elif action.action_type == 'pretix.plugins.stripe.double':
template = get_template('pretixplugins/stripe/action_double.html')
ctx = {'data': data, 'event': sender, 'action': action}
return template.render(ctx, request)
@receiver(nav_organizer, dispatch_uid="stripe_nav_organizer")
def nav_o(sender, request, organizer, **kwargs):
if request.user.has_active_staff_session(request.session.session_key):
url = resolve(request.path_info)
return [{
'label': _('Stripe Connect'),
'url': reverse('plugins:stripe:settings.connect', kwargs={
'organizer': request.organizer.slug
}),
'parent': reverse('control:organizer.edit', kwargs={
'organizer': request.organizer.slug
}),
'active': 'settings.connect' in url.url_name,
}]
return []

View File

@ -0,0 +1,81 @@
.sep {
}
.sepText {
width: 75px;
background: #FFFFFF;
margin: -15px 0 0 -38px;
padding: 5px 0;
position: absolute;
top: 50%;
text-align: center;
}
.hr {
width:2px;
height:64px;
background-color: #DDDDDD;
position:inherit;
top:0px;
left:50%;
z-index:10;
}
#stripe-card {
margin: 15px 0;
}
.embed-responsive-sca {
padding-bottom: 75%;
min-height: 600px;
}
@media only screen and (max-width: 999px) {
.hr {
width: 100%;
height: 2px;
left: 0px;
margin: 15px 0 15px 0;
}
.sepText {
left: 50%;
}
#stripe-elements > div.hidden {
height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
display: block !important;
}
#stripe-elements .stripe-or {
height: 16px;
}
#stripe-elements .stripe-payment-request-button {
height: 40px;
}
#stripe-elements > div {
transition: height 0.3s ease-out, padding-top 0.3s ease-out, padding-bottom 0.3s ease-out;
}
}
@media only screen and (min-width: 999px) {
#stripe-elements {
display: flex;
flex-wrap: wrap;
}
.stripe-card-holder {
flex-grow: 1;
}
#stripe-elements > div.hidden {
width: 0;
padding: 0;
overflow: hidden;
display: block !important;
}
#stripe-elements > div {
transition: width 0.3s ease-out, padding-left 0.3s ease-out, padding-right 0.3s ease-out;
}
}
.vcenter {
margin: auto;
}

View File

@ -0,0 +1,229 @@
/*global $, stripe_pubkey, stripe_loadingmessage, gettext */
'use strict';
var pretixstripe = {
stripe: null,
elements: null,
card: null,
paymentRequest: null,
paymentRequestButton: null,
'cc_request': function () {
waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide();
// ToDo: 'card' --> proper type of payment method
pretixstripe.stripe.createPaymentMethod('card', pretixstripe.card).then(function (result) {
waitingDialog.hide();
if (result.error) {
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>" + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
var $form = $("#stripe_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_payment_method_id").val(result.paymentMethod.id);
$("#stripe_card_brand").val(result.paymentMethod.card.brand);
$("#stripe_card_last4").val(result.paymentMethod.card.last4);
// and submit
$form.get(0).submit();
}
});
},
'load': function () {
if (pretixstripe.stripe !== null) {
return;
}
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
$.ajax(
{
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html())
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
}
pretixstripe.elements = pretixstripe.stripe.elements();
if ($.trim($("#stripe_merchantcountry").html()) !== "") {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $("#stripe_merchantcountry").html(),
currency: $("#stripe_currency").val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($("#stripe_total").val())
},
displayItems: [],
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
});
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success');
var $form = $("#stripe_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_payment_method_id").val(ev.paymentMethod.id);
$("#stripe_card_brand").val(ev.paymentMethod.card.brand);
$("#stripe_card_last4").val(ev.paymentMethod.card.last4);
// and submit
$form.get(0).submit();
});
} catch (e) {
pretixstripe.paymentRequest = null;
}
} else {
pretixstripe.paymentRequest = null;
}
if ($("#stripe-card").length) {
pretixstripe.card = pretixstripe.elements.create('card', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.card.mount("#stripe-card");
}
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
});
pretixstripe.paymentRequest.canMakePayment().then(function(result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass("hidden");
} else {
$('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none';
}
});
}
}
}
);
},
'handleCardAction': function (payment_intent_client_secret) {
$.ajax({
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html())
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
}
pretixstripe.stripe.handleCardAction(
payment_intent_client_secret
).then(function (result) {
waitingDialog.show(gettext("Confirming your payment …"));
location.reload();
});
}
});
},
'handleCardActioniFrame': function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext("Contacting your bank …"));
let iframe = document.createElement('iframe');
iframe.src = payment_intent_next_action_redirect_url;
iframe.className = 'embed-responsive-item';
$('#scacontainer').append(iframe);
$('#scacontainer iframe').load(function () {
waitingDialog.hide();
});
}
};
$(function () {
if ($("#stripe_payment_intent_SCA_status").length) {
window.parent.postMessage('3DS-authentication-complete.' + $.trim($("#order_status").html()), '*');
return;
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handleCardActioniFrame(payment_intent_next_action_redirect_url);
} else if ($("#stripe_payment_intent_client_secret").length) {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleCardAction(payment_intent_client_secret);
}
$(window).on("message onmessage", function(e) {
if (typeof e.originalEvent.data === "string" && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext("Confirming your payment …"));
$('#scacontainer').hide();
$('#continuebutton').removeClass('hidden');
if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes';
} else {
window.location.href = $('#continuebutton').attr('href');
}
}
});
if (!$(".stripe-container").length)
return;
if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) {
pretixstripe.load();
} else {
$("input[name=payment]").change(function () {
if ($(this).val() === 'stripe') {
pretixstripe.load();
}
})
}
$("#stripe_other_card").click(
function (e) {
$("#stripe_payment_method_id").val("");
$("#stripe-current-card").slideUp();
$("#stripe-elements").slideDown();
e.preventDefault();
return false;
}
);
if ($("#stripe-current-card").length) {
$("#stripe-elements").hide();
}
$('.stripe-container').closest("form").submit(
function () {
if ($("input[name=card_new]").length && !$("input[name=card_new]").prop('checked')) {
return null;
}
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
&& $("#stripe_payment_method_id").val() == "") {
pretixstripe.cc_request();
return false;
}
}
);
});

51
stripe/tasks.py Normal file
View File

@ -0,0 +1,51 @@
import logging
from urllib.parse import urlsplit
import stripe
from django.conf import settings
from pretix.base.services.tasks import EventTask
from pretix.celery_app import app
from pretix.multidomain.urlreverse import get_event_domain
from pretix.plugins.stripe.models import RegisteredApplePayDomain
logger = logging.getLogger(__name__)
def get_domain_for_event(event):
domain = get_event_domain(event, fallback=True)
if not domain:
siteurlsplit = urlsplit(settings.SITE_URL)
return siteurlsplit.hostname
return domain
def get_stripe_account_key(prov):
if prov.settings.connect_user_id:
return prov.settings.connect_user_id
else:
return prov.settings.publishable_key
@app.task(base=EventTask, max_retries=5, default_retry_delay=1)
def stripe_verify_domain(event, domain):
from pretix.plugins.stripe.payment import StripeCC
prov = StripeCC(event)
account = get_stripe_account_key(prov)
if RegisteredApplePayDomain.objects.filter(account=account, domain=domain).exists():
return
try:
resp = stripe.ApplePayDomain.create(
domain_name=domain,
**prov.api_kwargs
)
except stripe.error.StripeError:
logger.exception('Could not verify domain with Stripe')
else:
if resp.livemode:
RegisteredApplePayDomain.objects.create(
domain=domain,
account=account
)

View File

@ -0,0 +1,9 @@
{% load i18n %}
<p>
{% url "control:event.order" organizer=event.organizer.slug event=event.slug code=data.order as ourl %}
{% blocktrans trimmed with charge=data.charge stripe_href="href='https://dashboard.stripe.com/payments/"|add:data.charge|add:"' target='_blank'"|safe order="<a href='"|add:ourl|add:"'>"|add:data.order|add:"</a>"|safe %}
The Stripe transaction <a {{ stripe_href }}>{{ charge }}</a> has succeeded, but the order {{ order }} has
already been paid by other means. Please double-check and refund the money via Stripe's interface.
{% endblocktrans %}
</p>

View File

@ -0,0 +1,10 @@
{% load i18n %}
<p>
{% url "control:event.order" organizer=event.organizer.slug event=event.slug code=data.order as ourl %}
{% blocktrans trimmed with charge=data.charge stripe_href="href='https://dashboard.stripe.com/payments/"|add:data.charge|add:"' target='_blank'"|safe order="<a href='"|add:ourl|add:"'>"|add:data.order|add:"</a>"|safe %}
The Stripe transaction <a {{ stripe_href }}>{{ charge }}</a> has succeeded, but the order {{ order }} is
expired and the product was sold out in the meantime. Therefore, the payment could not be accepted. Please
contact the user and refund the money via Stripe's interface.
{% endblocktrans %}
</p>

View File

@ -0,0 +1,9 @@
{% load i18n %}
<p>
{% url "control:event.order" organizer=event.organizer.slug event=event.slug code=data.order as ourl %}
{% blocktrans trimmed with charge=data.charge stripe_href="href='https://dashboard.stripe.com/payments/"|add:data.charge|add:"' target='_blank'"|safe order="<a href='"|add:ourl|add:"'>"|add:data.order|add:"</a>"|safe %}
Stripe reported that the transaction <a {{ stripe_href }}>{{ charge }}</a> has been refunded.
Do you want to refund mark the matching order ({{ order }}) as refunded?
{% endblocktrans %}
</p>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
{% load i18n %}
{% if provider.method == "cc" %}
<p>{% blocktrans trimmed %}
The total amount will be withdrawn from your credit card.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Card type" %}</dt>
<dd>{{ request.session.payment_stripe_brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>**** **** **** {{ request.session.payment_stripe_last4 }}</dd>
</dl>
{% else %}
<p>{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Payment method" %}</dt>
<dd>{{ provider.public_name }}</dd>
{% if provider.method == "giropay" %}
<dt>{% trans "Account holder" %}</dt>
<dd>{{ request.session.payment_stripe_giropay_account }}</dd>
{% elif provider.method == "bancontact" %}
<dt>{% trans "Account holder" %}</dt>
<dd>{{ request.session.payment_stripe_bancontact_account }}</dd>
{% endif %}
</dl>
{% endif %}

View File

@ -0,0 +1,75 @@
{% load i18n %}
<div class="form-horizontal stripe-container">
{% if is_moto %}
<h1>
<span class="label label-info pull-right flip" data-toggle="tooltip_html" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
</h1>
<div class="clearfix"></div>
{% endif %}
<div class="stripe-errors sr-only">
</div>
<noscript>
<div class="alert alert-warning">
{% trans "For a credit card payment, please turn on JavaScript." %}
</div>
</noscript>
{% if request.session.payment_stripe_payment_method_id %}
<div id="stripe-current-card">
<p>{% blocktrans trimmed %}
You already entered a card number that we will use to charge the payment amount.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Card type" %}</dt>
<dd id="stripe_card_brand_display">{{ request.session.payment_stripe_brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>
**** **** ****
<span id="stripe_card_last4_display">{{ request.session.payment_stripe_last4 }}</span>
<button class="btn btn-xs btn-default" id="stripe_other_card" type="button">
{% trans "Use a different card" %}
</button>
</dd>
</dl>
</div>
{% endif %}
<div class="row equal" id="stripe-elements">
<div class="col-md-5 vcenter stripe-card-holder">
<div id="stripe-card" class="form-control">
<span class="fa fa-spinner fa-spin"></span>
<!-- a Stripe Element will be inserted here. -->
</div>
</div>
<div class="col-md-1 hidden stripe-or">
<div class="hr">
<div class="sep">
<div class="sepText">{% trans "OR" %}</div>
</div>
</div>
</div>
<div class="col-md-5 vcenter hidden stripe-payment-request-button-container">
<div id="stripe-payment-request-button">
<span class="fa fa-spinner fa-spin"></span>
<!-- A Stripe Element will be inserted here. -->
</div>
</div>
</div>
<p class="help-block">
{% blocktrans trimmed %}
Your payment will be processed by Stripe, Inc. Your credit card data will be transmitted directly to
Stripe and never touches our servers.
{% endblocktrans %}
<input type="hidden" name="stripe_total" value="{{ total }}" id="stripe_total"/>
<input type="hidden" name="stripe_payment_method_id" value="{{ request.session.payment_stripe_payment_method_id }}" id="stripe_payment_method_id"/>
<input type="hidden" name="stripe_card_last4" value="{{ request.session.payment_stripe_last4 }}"
id="stripe_card_last4"/>
<input type="hidden" name="stripe_card_brand" value="{{ request.session.payment_stripe_brand }}"
id="stripe_card_brand"/>
<input type="hidden" id="stripe_currency" value="{{ event.currency }}"/>
</p>
</div>

View File

@ -0,0 +1,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% bootstrap_form form layout='horizontal' %}
<p>{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
{% endblocktrans %}</p>

View File

@ -0,0 +1,5 @@
{% load i18n %}
<p>{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your payment.
You will then be redirected back here to get your tickets.
{% endblocktrans %}</p>

View File

@ -0,0 +1,69 @@
{% load i18n %}
{% if payment_info %}
<dl class="dl-horizontal">
{% if "id" in payment_info %}
<dt>{% trans "Charge ID" %}</dt>
<dd>{{ payment_info.id }}</dd>
{% endif %}
{% if "source" in payment_info %}
{% if payment_info.source.card %}
<dt>{% trans "Card type" %}</dt>
<dd>{{ payment_info.source.card.brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>**** **** **** {{ payment_info.source.card.last4 }}</dd>
{% if payment_info.source.owner.name %}
<dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.owner.name }}</dd>
{% endif %}
{% endif %}
{% if payment_info.source.type == "giropay" %}
<dt>{% trans "Bank" %}</dt>
<dd>{{ payment_info.source.giropay.bank_name }} ({{ payment_info.source.giropay.bic }})</dd>
<dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
{% endif %}
{% if payment_info.source.type == "bancontact" %}
<dt>{% trans "Bank" %}</dt>
<dd>{{ payment_info.source.bancontact.bank_name }} ({{ payment_info.source.bancontact.bic }})</dd>
<dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
{% endif %}
{% if payment_info.source.type == "ideal" %}
<dt>{% trans "Bank" %}</dt>
<dd>{{ payment_info.source.ideal.bank }} ({{ payment_info.source.ideal.bic }})</dd>
<dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
{% endif %}
{% endif %}
{% if payment_info.charges.data.0 %}
{% if payment_info.charges.data.0.payment_method_details.card %}
<dt>{% trans "Card type" %}</dt>
<dd>{{ payment_info.charges.data.0.payment_method_details.card.brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>
**** **** **** {{ payment_info.charges.data.0.payment_method_details.card.last4 }}
{% if payment_info.charges.data.0.payment_method_details.card.moto %}
<span class="label label-info">{% trans "MOTO" %}</span>
{% endif %}
</dd>
{% endif %}
{% endif %}
{% if "amount" in payment_info %}
<dt>{% trans "Total value" %}</dt>
<dd>{{ payment_info.amount|floatformat:2 }}</dd>
{% endif %}
{% if "currency" in payment_info %}
<dt>{% trans "Currency" %}</dt>
<dd>{{ payment_info.currency|upper }}</dd>
{% endif %}
{% if "status" in payment_info %}
<dt>{% trans "Status" %}</dt>
<dd>{{ payment_info.status }}</dd>
{% endif %}
{% if "message" in payment_info %}
<dt>{% trans "Error message" %}</dt>
<dd>{{ payment_info.message }}</dd>
{% endif %}
</dl>
{% endif %}

View File

@ -0,0 +1,24 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load hierarkey_form %}
{% load formset_tags %}
{% block title %}{% trans "Stripe Connect" %}{% endblock %}
{% block content %}
<h1>
{% trans "Stripe Connect" %}
</h1>
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
{% csrf_token %}
{% url "control:global.settings" as g_url %}
{% propagated request.organizer g_url "payment_stripe_connect_app_fee_percent" "payment_stripe_connect_app_fee_min" "payment_stripe_connect_app_fee_max" %}
{% bootstrap_form form layout="control" %}
{% endpropagated %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% load i18n %}
{% load eventurl %}
{% if payment.state == "pending" %}
<p>{% blocktrans trimmed %}
We're waiting for an answer from the payment provider regarding your payment. Please contact us if this
takes more than a few days.
{% endblocktrans %}</p>
{% elif payment.state == "created" and payment_info.status == "requires_action" %}
<p>{% blocktrans trimmed %}
You need to confirm your payment. Please click the link below to do so or start a new payment.
{% endblocktrans %}
<div class="text-right flip">
<a href="{% eventurl event "plugins:stripe:sca" order=order.code payment=payment.pk hash=payment_hash %}"
class="btn btn-primary">
{% trans "Confirm payment" %}
</a>
</div>
<div class="clearfix"></div>
</p>
{% elif payment.state == "created" and payment.provider == "stripe_wechatpay" %}
<p>{% blocktrans trimmed %}
Please scan the barcode below to complete your WeChat payment.
Once you have completed your payment, you can refresh this page.
{% endblocktrans %}</p>
<div class="text-center">
<script type="text/plain" data-size="150" data-replace-with-qr>{{ payment_info.wechat.qr_code_url }}</script>
</div>
{% else %}
<p>{% blocktrans trimmed %}
The payment transaction could not be completed for the following reason:
{% endblocktrans %}
<br/>
{% if payment_info and payment_info.error %}
{{ payment_info.message }}
{% else %}
{% trans "Unknown reason" %}
{% endif %}
</p>
{% endif %}

View File

@ -0,0 +1,19 @@
{% load static %}
{% load compress %}
{% load i18n %}
{% compress js %}
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
{% endcompress %}
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/stripe/pretix-stripe.css" %}">
{% endcompress %}
{% if testmode %}
<script type="text/plain" id="stripe_pubkey">{{ settings.publishable_test_key }}</script>
{% else %}
<script type="text/plain" id="stripe_pubkey">{{ settings.publishable_key }}</script>
{% endif %}
{% if settings.connect_user_id %}
<script type="text/plain" id="stripe_connectedAccountId">{{ settings.connect_user_id }}</script>
{% endif %}
<script type="text/plain" id="stripe_merchantcountry">{{ settings.merchant_country|default:"" }}</script>

View File

@ -0,0 +1,32 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank">
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% load static %}
{% block title %}{% trans "Pay order" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
{% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %}
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url }}</script>
{% endblock %}
{% block content %}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">
{% blocktrans trimmed with code=order.code %}
Confirm payment: {{ code }}
{% endblocktrans %}
</h3>
</div>
<div class="panel-body embed-responsive embed-responsive-sca" id="scacontainer">
</div>
</div>
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
{% trans "Cancel" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
<a class="btn btn-block btn-primary btn-lg hidden"
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}"
id="continuebutton">
{% trans "Continue" %}
</a>
</div>
<div class="clearfix"></div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "pretixpresale/base.html" %}
{% load i18n %}
{% load static %}
{% load thumb %}
{% load eventurl %}
{% block title %}{% trans "Pay order" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
{% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %}
<script type="text/plain" id="stripe_payment_intent_SCA_status">3DS-authentication-complete</script>
<script type="text/plain" id="order_status">{{ order.status }}</script>
{% endblock %}
{% block page %}
<div class="text-center">
<i class="fa fa-cog big-rotating-icon"></i>
</div>
<h2 class="text-center">
{% trans "Confirming your payment…" %}
</h2>
{% endblock %}

38
stripe/urls.py Normal file
View File

@ -0,0 +1,38 @@
from django.conf.urls import include, url
from pretix.multidomain import event_url
from .views import (
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
applepay_association, oauth_disconnect, oauth_return, redirect_view,
webhook,
)
event_patterns = [
url(r'^stripe/', include([
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
url(r'^redirect/$', redirect_view, name='redirect'),
url(r'^return/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
url(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
url(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
ScaReturnView.as_view(), name='sca.return'),
])),
url(r'^.well-known/apple-developer-merchantid-domain-association$',
applepay_association, name='applepay.association'),
]
organizer_patterns = [
url(r'^.well-known/apple-developer-merchantid-domain-association$',
applepay_association, name='applepay.association'),
]
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/stripe/disconnect/',
oauth_disconnect, name='oauth.disconnect'),
url(r'^control/organizer/(?P<organizer>[^/]+)/stripeconnect/',
OrganizerSettingsFormView.as_view(), name='settings.connect'),
url(r'^_stripe/webhook/$', webhook, name='webhook'),
url(r'^_stripe/oauth_return/$', oauth_return, name='oauth.return'),
url(r'^.well-known/apple-developer-merchantid-domain-association$',
applepay_association, name='applepay.association'),
]

0
stripe/utils.py Normal file
View File

602
stripe/views.py Normal file
View File

@ -0,0 +1,602 @@
import hashlib
import json
import logging
import requests
import stripe
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.views.generic import FormView
from django_scopes import scopes_disabled
from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota
from pretix.base.payment import PaymentException
from pretix.base.services.locking import LockTimeoutException
from pretix.base.settings import GlobalSettingsObject
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, event_permission_required,
)
from pretix.control.views.event import DecoupleMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.payment import StripeCC, StripeSettingsHolder
from pretix.plugins.stripe.tasks import (
get_domain_for_event, stripe_verify_domain,
)
logger = logging.getLogger('pretix.plugins.stripe')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/stripe/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
@scopes_disabled()
def oauth_return(request, *args, **kwargs):
if 'payment_stripe_oauth_event' not in request.session:
messages.error(request, _('An error occurred during connecting with Stripe, please try again.'))
return redirect(reverse('control:index'))
event = get_object_or_404(Event, pk=request.session['payment_stripe_oauth_event'])
if request.GET.get('state') != request.session['payment_stripe_oauth_token']:
messages.error(request, _('An error occurred during connecting with Stripe, please try again.'))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
'provider': 'stripe_settings'
}))
gs = GlobalSettingsObject()
testdata = {}
try:
resp = requests.post('https://connect.stripe.com/oauth/token', data={
'grant_type': 'authorization_code',
'client_secret': (
gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key
),
'code': request.GET.get('code')
})
data = resp.json()
if 'error' not in data:
account = stripe.Account.retrieve(
data['stripe_user_id'],
api_key=gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key
)
except:
logger.exception('Failed to obtain OAuth token')
messages.error(request, _('An error occurred during connecting with Stripe, please try again.'))
else:
if 'error' not in data and data['livemode']:
try:
testresp = requests.post('https://connect.stripe.com/oauth/token', data={
'grant_type': 'refresh_token',
'client_secret': gs.settings.payment_stripe_connect_test_secret_key,
'refresh_token': data['refresh_token']
})
testdata = testresp.json()
except:
logger.exception('Failed to obtain OAuth token')
messages.error(request, _('An error occurred during connecting with Stripe, please try again.'))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
'provider': 'stripe_settings'
}))
if 'error' in data:
messages.error(request, _('Stripe returned an error: {}').format(data['error_description']))
elif data['livemode'] and 'error' in testdata:
messages.error(request, _('Stripe returned an error: {}').format(testdata['error_description']))
else:
messages.success(request,
_('Your Stripe account is now connected to pretix. You can change the settings in '
'detail below.'))
event.settings.payment_stripe_publishable_key = data['stripe_publishable_key']
# event.settings.payment_stripe_connect_access_token = data['access_token'] we don't need it, right?
event.settings.payment_stripe_connect_refresh_token = data['refresh_token']
event.settings.payment_stripe_connect_user_id = data['stripe_user_id']
event.settings.payment_stripe_merchant_country = account.get('country')
if account.get('business_name') or account.get('display_name') or account.get('email'):
event.settings.payment_stripe_connect_user_name = (
account.get('business_name') or account.get('display_name') or account.get('email')
)
if data['livemode']:
event.settings.payment_stripe_publishable_test_key = testdata['stripe_publishable_key']
else:
event.settings.payment_stripe_publishable_test_key = event.settings.payment_stripe_publishable_key
if request.session.get('payment_stripe_oauth_enable', False):
event.settings.payment_stripe__enabled = True
del request.session['payment_stripe_oauth_enable']
stripe_verify_domain.apply_async(args=(event.pk, get_domain_for_event(event)))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
'provider': 'stripe_settings'
}))
@csrf_exempt
@require_POST
@scopes_disabled()
def webhook(request, *args, **kwargs):
event_json = json.loads(request.body.decode('utf-8'))
# We do not check for the event type as we are not interested in the event it self,
# we just use it as a trigger to look the charge up to be absolutely sure.
# Another reason for this is that stripe events are not authenticated, so they could
# come from anywhere.
if event_json['data']['object']['object'] == "charge":
func = charge_webhook
objid = event_json['data']['object']['id']
elif event_json['data']['object']['object'] == "dispute":
func = charge_webhook
objid = event_json['data']['object']['charge']
elif event_json['data']['object']['object'] == "source":
func = source_webhook
objid = event_json['data']['object']['id']
elif event_json['data']['object']['object'] == "payment_intent":
func = paymentintent_webhook
objid = event_json['data']['object']['id']
else:
return HttpResponse("Not interested in this data type", status=200)
try:
rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get(reference=objid)
return func(rso.order.event, event_json, objid, rso)
except ReferencedStripeObject.DoesNotExist:
if event_json['data']['object']['object'] == "charge" and 'payment_intent' in event_json['data']['object']:
# If we receive a charge webhook *before* the payment intent webhook, we don't know the charge ID yet
# and can't match it -- but we know the payment intent ID!
try:
rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get(
reference=event_json['data']['object']['payment_intent']
)
return func(rso.order.event, event_json, objid, rso)
except ReferencedStripeObject.DoesNotExist:
return HttpResponse("Unable to detect event", status=200)
elif hasattr(request, 'event') and func != paymentintent_webhook:
# This is a legacy integration from back when didn't have ReferencedStripeObject. This can't happen for
# payment intents or charges connected with payment intents since they didn't exist back then. Our best
# hope is to go for request.event and see if we can find the order ID.
return func(request.event, event_json, objid, None)
else:
# Okay, this is probably not an event that concerns us, maybe other applications talk to the same stripe
# account
return HttpResponse("Unable to detect event", status=200)
SOURCE_TYPES = {
'sofort': 'stripe_sofort',
'three_d_secure': 'stripe',
'card': 'stripe',
'giropay': 'stripe_giropay',
'ideal': 'stripe_ideal',
'alipay': 'stripe_alipay',
'bancontact': 'stripe_bancontact',
}
def charge_webhook(event, event_json, charge_id, rso):
prov = StripeCC(event)
prov._init_api()
try:
charge = stripe.Charge.retrieve(charge_id, expand=['dispute'], **prov.api_kwargs)
except stripe.error.StripeError:
logger.exception('Stripe error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Charge not found', status=500)
metadata = charge['metadata']
if 'event' not in metadata:
return HttpResponse('Event not given in charge metadata', status=200)
if int(metadata['event']) != event.pk:
return HttpResponse('Not interested in this event', status=200)
if rso and rso.payment:
order = rso.payment.order
payment = rso.payment
elif rso:
order = rso.order
payment = None
else:
try:
order = event.orders.get(id=metadata['order'])
except Order.DoesNotExist:
return HttpResponse('Order not found', status=200)
payment = None
with transaction.atomic():
if not payment:
payment = order.payments.filter(
info__icontains=charge['id'],
provider__startswith='stripe',
amount=prov._amount_to_decimal(charge['amount']),
).select_for_update().last()
if not payment:
payment = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'),
amount=prov._amount_to_decimal(charge['amount']),
info=str(charge),
)
if payment.provider != prov.identifier:
prov = payment.payment_provider
prov._init_api()
order.log_action('pretix.plugins.stripe.event', data=event_json)
is_refund = charge['refunds']['total_count'] or charge['dispute']
if is_refund:
known_refunds = [r.info_data.get('id') for r in payment.refunds.all()]
migrated_refund_amounts = [r.amount for r in payment.refunds.all() if not r.info_data.get('id')]
for r in charge['refunds']['data']:
a = prov._amount_to_decimal(r['amount'])
if r['status'] in ('failed', 'canceled'):
continue
if a in migrated_refund_amounts:
migrated_refund_amounts.remove(a)
continue
if r['id'] not in known_refunds:
payment.create_external_refund(
amount=a,
info=str(r)
)
if charge['dispute']:
if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds:
a = prov._amount_to_decimal(charge['dispute']['amount'])
if a in migrated_refund_amounts:
migrated_refund_amounts.remove(a)
else:
payment.create_external_refund(
amount=a,
info=str(charge['dispute'])
)
elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_CANCELED,
OrderPayment.PAYMENT_STATE_FAILED):
try:
payment.confirm()
except LockTimeoutException:
return HttpResponse("Lock timeout, please try again.", status=503)
except Quota.QuotaExceededException:
pass
elif charge['status'] == 'failed' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
payment.fail(info=str(charge))
return HttpResponse(status=200)
def source_webhook(event, event_json, source_id, rso):
prov = StripeCC(event)
prov._init_api()
try:
src = stripe.Source.retrieve(source_id, **prov.api_kwargs)
except stripe.error.StripeError:
logger.exception('Stripe error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Charge not found', status=500)
metadata = src['metadata']
if 'event' not in metadata:
return HttpResponse('Event not given in charge metadata', status=200)
if int(metadata['event']) != event.pk:
return HttpResponse('Not interested in this event', status=200)
with transaction.atomic():
if rso and rso.payment:
order = rso.payment.order
payment = rso.payment
elif rso:
order = rso.order
payment = None
else:
try:
order = event.orders.get(id=metadata['order'])
except Order.DoesNotExist:
return HttpResponse('Order not found', status=200)
payment = None
if not payment:
payment = order.payments.filter(
info__icontains=src['id'],
provider__startswith='stripe',
amount=prov._amount_to_decimal(src['amount']) if src['amount'] is not None else order.total,
).last()
if not payment:
payment = order.payments.create(
state=OrderPayment.PAYMENT_STATE_CREATED,
provider=SOURCE_TYPES.get(src['type'], 'stripe'),
amount=prov._amount_to_decimal(src['amount']) if src['amount'] is not None else order.total,
info=str(src),
)
if payment.provider != prov.identifier:
prov = payment.payment_provider
prov._init_api()
order.log_action('pretix.plugins.stripe.event', data=event_json)
go = (event_json['type'] == 'source.chargeable' and
payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED) and
src.status == 'chargeable')
if go:
try:
prov._charge_source(None, source_id, payment)
except PaymentException:
logger.exception('Webhook error')
elif src.status == 'failed':
payment.fail(info=str(src))
elif src.status == 'canceled' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
payment.info = str(src)
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
return HttpResponse(status=200)
def paymentintent_webhook(event, event_json, paymentintent_id, rso):
prov = StripeCC(event)
prov._init_api()
try:
paymentintent = stripe.PaymentIntent.retrieve(paymentintent_id, **prov.api_kwargs)
except stripe.error.StripeError:
logger.exception('Stripe error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Charge not found', status=500)
for charge in paymentintent.charges.data:
ReferencedStripeObject.objects.get_or_create(
reference=charge.id,
defaults={'order': rso.payment.order, 'payment': rso.payment}
)
return HttpResponse(status=200)
@event_permission_required('can_change_event_settings')
@require_POST
def oauth_disconnect(request, **kwargs):
del request.event.settings.payment_stripe_publishable_key
del request.event.settings.payment_stripe_publishable_test_key
del request.event.settings.payment_stripe_connect_access_token
del request.event.settings.payment_stripe_connect_refresh_token
del request.event.settings.payment_stripe_connect_user_id
del request.event.settings.payment_stripe_connect_user_name
request.event.settings.payment_stripe__enabled = False
messages.success(request, _('Your Stripe account has been disconnected.'))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
'provider': 'stripe_settings'
}))
@xframe_options_exempt
def applepay_association(request, *args, **kwargs):
r = render(request, 'pretixplugins/stripe/apple-developer-merchantid-domain-association')
r._csp_ignore = True
return r
class StripeOrderView:
def dispatch(self, request, *args, **kwargs):
try:
self.order = request.event.orders.get(code=kwargs['order'])
if hashlib.sha1(self.order.secret.lower().encode()).hexdigest() != kwargs['hash'].lower():
raise Http404('')
except Order.DoesNotExist:
# Do a hash comparison as well to harden timing attacks
if 'abcdefghijklmnopq'.lower() == hashlib.sha1('abcdefghijklmnopq'.encode()).hexdigest():
raise Http404('')
else:
raise Http404('')
return super().dispatch(request, *args, **kwargs)
@cached_property
def payment(self):
return get_object_or_404(self.order.payments,
pk=self.kwargs['payment'],
provider__startswith='stripe')
@cached_property
def pprov(self):
return self.request.event.get_payment_providers()[self.payment.provider]
def _redirect_to_order(self):
if self.request.session.get('payment_stripe_order_secret') != self.order.secret and self.payment.provider != 'stripe_ideal':
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
return redirect(eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
@method_decorator(xframe_options_exempt, 'dispatch')
class ReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs):
prov = self.pprov
prov._init_api()
try:
src = stripe.Source.retrieve(request.GET.get('source'), **prov.api_kwargs)
except stripe.error.InvalidRequestError:
logger.exception('Could not retrieve source')
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
if src.client_secret != request.GET.get('client_secret'):
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
with transaction.atomic():
self.order.refresh_from_db()
self.payment.refresh_from_db()
if self.payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
if 'payment_stripe_token' in request.session:
del request.session['payment_stripe_token']
return self._redirect_to_order()
if src.status == 'chargeable':
try:
prov._charge_source(request, src.id, self.payment)
except PaymentException as e:
messages.error(request, str(e))
return self._redirect_to_order()
finally:
if 'payment_stripe_token' in request.session:
del request.session['payment_stripe_token']
elif src.status == 'consumed':
# Webhook was faster, wow! ;)
if 'payment_stripe_token' in request.session:
del request.session['payment_stripe_token']
return self._redirect_to_order()
elif src.status == 'pending':
self.payment.state = OrderPayment.PAYMENT_STATE_PENDING
self.payment.info = str(src)
self.payment.save()
else: # failed or canceled
self.payment.fail(info=str(src))
messages.error(self.request, _('We had trouble authorizing your card payment. Please try again and '
'get in touch with us if this problem persists.'))
return self._redirect_to_order()
@method_decorator(xframe_options_exempt, 'dispatch')
class ScaView(StripeOrderView, View):
def get(self, request, *args, **kwargs):
prov = self.pprov
prov._init_api()
if self.payment.state in (OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_CANCELED,
OrderPayment.PAYMENT_STATE_FAILED):
return self._redirect_to_order()
payment_info = json.loads(self.payment.info)
if 'id' in payment_info:
try:
intent = stripe.PaymentIntent.retrieve(
payment_info['id'],
**prov.api_kwargs
)
except stripe.error.InvalidRequestError:
logger.exception('Could not retrieve payment intent')
messages.error(self.request, _('Sorry, there was an error in the payment process.'))
return self._redirect_to_order()
else:
messages.error(self.request, _('Sorry, there was an error in the payment process.'))
return self._redirect_to_order()
if intent.status == 'requires_action' and intent.next_action.type in ['use_stripe_sdk', 'redirect_to_url']:
ctx = {
'order': self.order,
'stripe_settings': StripeSettingsHolder(self.order.event).settings,
}
if intent.next_action.type == 'use_stripe_sdk':
ctx['payment_intent_client_secret'] = intent.client_secret
elif intent.next_action.type == 'redirect_to_url':
ctx['payment_intent_next_action_redirect_url'] = intent.next_action.redirect_to_url['url']
r = render(request, 'pretixplugins/stripe/sca.html', ctx)
r._csp_ignore = True
return r
else:
try:
prov._handle_payment_intent(request, self.payment, intent)
except PaymentException as e:
messages.error(request, str(e))
return self._redirect_to_order()
@method_decorator(xframe_options_exempt, 'dispatch')
class ScaReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs):
prov = self.pprov
try:
prov._handle_payment_intent(request, self.payment)
except PaymentException as e:
messages.error(request, str(e))
self.order.refresh_from_db()
return render(request, 'pretixplugins/stripe/sca_return.html', {'order': self.order})
class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, AdministratorPermissionRequiredMixin, FormView):
model = Organizer
permission = 'can_change_organizer_settings'
form_class = OrganizerStripeSettingsForm
template_name = 'pretixplugins/stripe/organizer_stripe.html'
def get_success_url(self):
return reverse('plugins:stripe:settings.connect', kwargs={
'organizer': self.request.organizer.slug,
})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['obj'] = self.request.organizer
return kwargs
@transaction.atomic
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
form.save()
if form.has_changed():
self.request.organizer.log_action(
'pretix.organizer.settings', user=self.request.user, data={
k: form.cleaned_data.get(k) for k in form.changed_data
}
)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
else:
messages.error(self.request, _('We could not save your changes. See below for details.'))
return self.get(request)