Per Pretix 3.10
This commit is contained in:
commit
5bd64741d9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
*.pyc
|
32
COPYING
Normal file
32
COPYING
Normal 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
33
stripe/__init__.py
Normal 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
40
stripe/forms.py
Normal 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,
|
||||
)
|
0
stripe/management/__init__.py
Normal file
0
stripe/management/__init__.py
Normal file
0
stripe/management/commands/__init__.py
Normal file
0
stripe/management/commands/__init__.py
Normal file
35
stripe/management/commands/stripe_connect_fill_countries.py
Normal file
35
stripe/management/commands/stripe_connect_fill_countries.py
Normal 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')
|
26
stripe/migrations/0001_initial.py
Normal file
26
stripe/migrations/0001_initial.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
22
stripe/migrations/0002_referencedstripeobject_payment.py
Normal file
22
stripe/migrations/0002_referencedstripeobject_payment.py
Normal 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'),
|
||||
),
|
||||
]
|
21
stripe/migrations/0003_registeredapplepaydomain.py
Normal file
21
stripe/migrations/0003_registeredapplepaydomain.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
0
stripe/migrations/__init__.py
Normal file
0
stripe/migrations/__init__.py
Normal file
12
stripe/models.py
Normal file
12
stripe/models.py
Normal 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
1369
stripe/payment.py
Normal file
File diff suppressed because it is too large
Load Diff
178
stripe/signals.py
Normal file
178
stripe/signals.py
Normal 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 []
|
81
stripe/static/pretixplugins/stripe/pretix-stripe.css
Normal file
81
stripe/static/pretixplugins/stripe/pretix-stripe.css
Normal 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;
|
||||
}
|
229
stripe/static/pretixplugins/stripe/pretix-stripe.js
Normal file
229
stripe/static/pretixplugins/stripe/pretix-stripe.js
Normal 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
51
stripe/tasks.py
Normal 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
|
||||
)
|
9
stripe/templates/pretixplugins/stripe/action_double.html
Normal file
9
stripe/templates/pretixplugins/stripe/action_double.html
Normal 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>
|
10
stripe/templates/pretixplugins/stripe/action_overpaid.html
Normal file
10
stripe/templates/pretixplugins/stripe/action_overpaid.html
Normal 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>
|
9
stripe/templates/pretixplugins/stripe/action_refund.html
Normal file
9
stripe/templates/pretixplugins/stripe/action_refund.html
Normal 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
@ -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 %}
|
@ -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>
|
@ -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>
|
@ -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>
|
69
stripe/templates/pretixplugins/stripe/control.html
Normal file
69
stripe/templates/pretixplugins/stripe/control.html
Normal 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 %}
|
24
stripe/templates/pretixplugins/stripe/organizer_stripe.html
Normal file
24
stripe/templates/pretixplugins/stripe/organizer_stripe.html
Normal 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 %}
|
39
stripe/templates/pretixplugins/stripe/pending.html
Normal file
39
stripe/templates/pretixplugins/stripe/pending.html
Normal 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 %}
|
19
stripe/templates/pretixplugins/stripe/presale_head.html
Normal file
19
stripe/templates/pretixplugins/stripe/presale_head.html
Normal 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>
|
32
stripe/templates/pretixplugins/stripe/redirect.html
Normal file
32
stripe/templates/pretixplugins/stripe/redirect.html
Normal 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>
|
41
stripe/templates/pretixplugins/stripe/sca.html
Normal file
41
stripe/templates/pretixplugins/stripe/sca.html
Normal 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 %}
|
20
stripe/templates/pretixplugins/stripe/sca_return.html
Normal file
20
stripe/templates/pretixplugins/stripe/sca_return.html
Normal 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
38
stripe/urls.py
Normal 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
0
stripe/utils.py
Normal file
602
stripe/views.py
Normal file
602
stripe/views.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user