Fix #407 -- Integrate more Stripe payment methods

This commit is contained in:
Raphael Michel
2017-07-12 16:42:44 +02:00
parent 1c6858653a
commit 48095d38be
16 changed files with 743 additions and 164 deletions

View File

@@ -463,6 +463,9 @@ class SettingsSandbox:
self._type = typestr self._type = typestr
self._key = key self._key = key
def get_prefix(self):
return '%s_%s_' % (self._type, self._key)
def _convert_key(self, key: str) -> str: def _convert_key(self, key: str) -> str:
return '%s_%s_%s' % (self._type, self._key, key) return '%s_%s_%s' % (self._type, self._key, key)

View File

@@ -186,12 +186,12 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
for provider in self.request.event.get_payment_providers().values(): for provider in self.request.event.get_payment_providers().values():
provider.form = ProviderForm( provider.form = ProviderForm(
obj=self.request.event, obj=self.request.event,
settingspref='payment_%s_' % provider.identifier, settingspref=provider.settings.get_prefix(),
data=(self.request.POST if self.request.method == 'POST' else None) data=(self.request.POST if self.request.method == 'POST' else None)
) )
provider.form.fields = OrderedDict( provider.form.fields = OrderedDict(
[ [
('payment_%s_%s' % (provider.identifier, k), v) ('%s%s' % (provider.settings.get_prefix(), k), v)
for k, v in provider.settings_form_fields.items() for k, v in provider.settings_form_fields.items()
] ]
) )

View File

@@ -7,20 +7,34 @@ import stripe
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from pretix.base.models import Quota, RequiredAction from pretix.base.models import Event, Quota, RequiredAction
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import mark_order_paid, mark_order_refunded from pretix.base.services.orders import mark_order_paid, mark_order_refunded
from pretix.base.settings import SettingsSandbox
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.plugins.stripe') logger = logging.getLogger('pretix.plugins.stripe')
class Stripe(BasePaymentProvider): class StripeSettingsHolder(BasePaymentProvider):
identifier = 'stripe' identifier = 'stripe_settings'
verbose_name = _('Credit Card via Stripe') verbose_name = _('Stripe')
is_enabled = False
def __init__(self, event: Event):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'stripe', event)
def settings_content_render(self, request):
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a <a href="https://dashboard.stripe.com/account/webhooks">Stripe Webhook</a> to '
'the following endpoint in order to automatically cancel orders when charges are refunded externally '
'and to process asynchronous payment methods like SOFORT.'),
build_absolute_uri(self.event, 'plugins:stripe:webhook')
)
@property @property
def settings_form_fields(self): def settings_form_fields(self):
@@ -44,56 +58,87 @@ class Stripe(BasePaymentProvider):
choices=( choices=(
('pretix', _('Simple (pretix design)')), ('pretix', _('Simple (pretix design)')),
('checkout', _('Stripe Checkout')), ('checkout', _('Stripe Checkout')),
) ),
)) help_text=_('Only relevant for credit card payments.')
)),
('method_cc',
forms.BooleanField(
label=_('Credit card payments'),
required=False,
)),
('method_giropay',
forms.BooleanField(
label=_('giropay'),
disabled=self.event.currency != 'EUR',
help_text=_('Needs to be enabled in your Stripe account first.'),
required=False,
)),
('method_ideal',
forms.BooleanField(
label=_('iDEAL'),
disabled=self.event.currency != 'EUR',
help_text=_('Needs to be enabled in your Stripe account first.'),
required=False,
)),
('method_alipay',
forms.BooleanField(
label=_('Alipay'),
disabled=self.event.currency not in ('EUR', 'AUD', 'CAD', 'GBP', 'HKD', 'JPY', 'NZD', 'SGD', 'USD'),
help_text=_('Needs to be enabled in your Stripe account first.'),
required=False,
)),
('method_bancontact',
forms.BooleanField(
label=_('Bancontact'),
disabled=self.event.currency != 'EUR',
help_text=_('Needs to be enabled in your Stripe account first.'),
required=False,
)),
('method_sofort',
forms.BooleanField(
label=_('SOFORT'),
disabled=self.event.currency != 'EUR',
help_text=_('Needs to be enabled in your Stripe account first. Note that, despite the name, '
'payments are not immediately confirmed but might take some time.'),
required=False,
)),
] ]
) )
def settings_content_render(self, request):
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a <a href="https://dashboard.stripe.com/account/webhooks">Stripe Webhook</a> to '
'the following endpoint in order to automatically cancel orders when charges are refunded externally.'),
build_absolute_uri(self.event, 'plugins:stripe:webhook')
)
def payment_is_valid_session(self, request): class StripeMethod(BasePaymentProvider):
return request.session.get('payment_stripe_token', '') != '' identifier = ''
method = ''
def __init__(self, event: Event):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'stripe', event)
@property
def settings_form_fields(self):
return {}
@property
def is_enabled(self) -> bool:
return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method),
as_type=bool)
def order_prepare(self, request, order): def order_prepare(self, request, order):
return self.checkout_prepare(request, None) return self.checkout_prepare(request, None)
def checkout_prepare(self, request, cart):
token = request.POST.get('stripe_token', '')
request.session['payment_stripe_token'] = token
request.session['payment_stripe_brand'] = request.POST.get('stripe_card_brand', '')
request.session['payment_stripe_last4'] = request.POST.get('stripe_card_last4', '')
if token == '':
messages.error(request, _('You may need to enable JavaScript for Stripe payments.'))
return False
return True
def payment_form_render(self, request) -> str:
ui = self.settings.get('ui', default='pretix')
if ui == 'checkout':
template = get_template('pretixplugins/stripe/checkout_payment_form_stripe_checkout.html')
else:
template = get_template('pretixplugins/stripe/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
def _init_api(self): def _init_api(self):
stripe.api_version = '2015-04-07' stripe.api_version = '2017-06-05'
stripe.api_key = self.settings.get('secret_key') stripe.api_key = self.settings.get('secret_key')
def checkout_confirm_render(self, request) -> str: def checkout_confirm_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_confirm.html') template = get_template('pretixplugins/stripe/checkout_payment_confirm.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings} ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self}
return template.render(ctx) return template.render(ctx)
def order_can_retry(self, order): def order_can_retry(self, order):
return self._is_still_available(order=order) return self._is_still_available(order=order)
def _charge_source(self, source, order): def _charge_source(self, request, source, order):
try: try:
charge = stripe.Charge.create( charge = stripe.Charge.create(
amount=int(order.total * 100), amount=int(order.total * 100),
@@ -139,7 +184,7 @@ class Stripe(BasePaymentProvider):
else: else:
if charge.status == 'succeeded' and charge.paid: if charge.status == 'succeeded' and charge.paid:
try: try:
mark_order_paid(order, 'stripe', str(charge)) mark_order_paid(order, self.identifier, str(charge))
except Quota.QuotaExceededException as e: except Quota.QuotaExceededException as e:
RequiredAction.objects.create( RequiredAction.objects.create(
event=self.event, action_type='pretix.plugins.stripe.overpaid', data=json.dumps({ event=self.event, action_type='pretix.plugins.stripe.overpaid', data=json.dumps({
@@ -151,56 +196,32 @@ class Stripe(BasePaymentProvider):
except SendMailException: except SendMailException:
raise PaymentException(_('There was an error sending the confirmation mail.')) raise PaymentException(_('There was an error sending the confirmation mail.'))
elif charge.status == 'pending':
messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the '
'payment completed.'))
order.payment_info = str(charge)
order.save(update_fields=['payment_info'])
return
else: else:
logger.info('Charge failed: %s' % str(charge)) logger.info('Charge failed: %s' % str(charge))
order.payment_info = str(charge) order.payment_info = str(charge)
order.save(update_fields=['payment_info']) order.save(update_fields=['payment_info'])
raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message) raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message)
def payment_perform(self, request, order) -> str:
self._init_api()
if request.session['payment_stripe_token'].startswith('src_'):
src = stripe.Source.retrieve(request.session['payment_stripe_token'])
if src.type == 'card' and src.card and src.card.three_d_secure == 'required':
request.session['payment_stripe_order_secret'] = order.secret
source = stripe.Source.create(
type='three_d_secure',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
three_d_secure={
'card': src.id
},
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
if source.status == "pending":
order.payment_info = str(source)
order.save(update_fields=['payment_info'])
return source.redirect.url
try:
self._charge_source(request.session['payment_stripe_token'], order)
finally:
del request.session['payment_stripe_token']
def order_pending_render(self, request, order) -> str: def order_pending_render(self, request, order) -> str:
if order.payment_info: if order.payment_info:
payment_info = json.loads(order.payment_info) payment_info = json.loads(order.payment_info)
else: else:
payment_info = None payment_info = None
template = get_template('pretixplugins/stripe/pending.html') template = get_template('pretixplugins/stripe/pending.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings, ctx = {
'order': order, 'payment_info': payment_info} 'request': request,
'event': self.event,
'settings': self.settings,
'provider': self,
'order': order,
'payment_info': payment_info,
}
return template.render(ctx) return template.render(ctx)
def order_control_render(self, request, order) -> str: def order_control_render(self, request, order) -> str:
@@ -211,8 +232,15 @@ class Stripe(BasePaymentProvider):
else: else:
payment_info = None payment_info = None
template = get_template('pretixplugins/stripe/control.html') template = get_template('pretixplugins/stripe/control.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings, ctx = {
'payment_info': payment_info, 'order': order} 'request': request,
'event': self.event,
'settings': self.settings,
'payment_info': payment_info,
'order': order,
'method': self.method,
'provider': self,
}
return template.render(ctx) return template.render(ctx)
def order_control_refund_render(self, order) -> str: def order_control_refund_render(self, order) -> str:
@@ -255,3 +283,409 @@ class Stripe(BasePaymentProvider):
order = mark_order_refunded(order, user=request.user) order = mark_order_refunded(order, user=request.user)
order.payment_info = str(ch) order.payment_info = str(ch)
order.save() order.save()
def payment_perform(self, request, order) -> str:
self._init_api()
try:
source = self._create_source(request, order)
except stripe.error.StripeError as e:
if e.json_body:
err = e.json_body['error']
logger.exception('Stripe error: %s' % str(err))
else:
err = {'message': str(e)}
logger.exception('Stripe error: %s' % str(e))
order.payment_info = json.dumps({
'error': True,
'message': err['message'],
})
order.save(update_fields=['payment_info'])
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
'with us if this problem persists.'))
order.payment_info = str(source)
order.save(update_fields=['payment_info'])
request.session['payment_stripe_order_secret'] = order.secret
return source.redirect.url
class StripeCC(StripeMethod):
identifier = 'stripe'
verbose_name = _('Credit card via Stripe')
public_name = _('Credit card')
method = 'cc'
def payment_form_render(self, request) -> str:
ui = self.settings.get('ui', default='pretix')
if ui == 'checkout':
template = get_template('pretixplugins/stripe/checkout_payment_form_stripe_checkout.html')
else:
template = get_template('pretixplugins/stripe/checkout_payment_form.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
}
return template.render(ctx)
def payment_is_valid_session(self, request):
return request.session.get('payment_stripe_token', '') != ''
def checkout_prepare(self, request, cart):
token = request.POST.get('stripe_token', '')
request.session['payment_stripe_token'] = token
request.session['payment_stripe_brand'] = request.POST.get('stripe_card_brand', '')
request.session['payment_stripe_last4'] = request.POST.get('stripe_card_last4', '')
if token == '':
messages.error(request, _('You may need to enable JavaScript for Stripe payments.'))
return False
return True
def payment_perform(self, request, order) -> str:
self._init_api()
if request.session['payment_stripe_token'].startswith('src_'):
try:
src = stripe.Source.retrieve(request.session['payment_stripe_token'])
if src.type == 'card' and src.card and src.card.three_d_secure == 'required':
request.session['payment_stripe_order_secret'] = order.secret
source = stripe.Source.create(
type='three_d_secure',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
three_d_secure={
'card': src.id
},
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
if source.status == "pending":
order.payment_info = str(source)
order.save(update_fields=['payment_info'])
return source.redirect.url
except stripe.error.StripeError as e:
if e.json_body:
err = e.json_body['error']
logger.exception('Stripe error: %s' % str(err))
else:
err = {'message': str(e)}
logger.exception('Stripe error: %s' % str(e))
order.payment_info = json.dumps({
'error': True,
'message': err['message'],
})
order.save(update_fields=['payment_info'])
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
'with us if this problem persists.'))
try:
self._charge_source(request, request.session['payment_stripe_token'], order)
finally:
del request.session['payment_stripe_token']
class StripeGiropay(StripeMethod):
identifier = 'stripe_giropay'
verbose_name = _('giropay via Stripe')
public_name = _('giropay')
method = 'giropay'
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_giropay.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
'form': self.payment_form(request)
}
return template.render(ctx)
@property
def payment_form_fields(self):
return OrderedDict([
('account', forms.CharField(label=_('Account holder'))),
])
def _create_source(self, request, order):
try:
source = stripe.Source.create(
type='giropay',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
owner={
'name': request.session.get('payment_stripe_giropay_account') or ugettext('unknown name')
},
giropay={
'statement_descriptor': ugettext('{event}-{code}').format(
event=self.event.slug.upper(),
code=order.code
)[:35]
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
return source
finally:
if 'payment_stripe_giropay_account' in request.session:
del request.session['payment_stripe_giropay_account']
def payment_is_valid_session(self, request):
return (
request.session.get('payment_stripe_giropay_account', '') != ''
)
def checkout_prepare(self, request, cart):
form = self.payment_form(request)
if form.is_valid():
request.session['payment_stripe_giropay_account'] = form.cleaned_data['account']
return True
return False
class StripeIdeal(StripeMethod):
identifier = 'stripe_ideal'
verbose_name = _('iDEAL via Stripe')
public_name = _('iDEAL')
method = 'ideal'
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
}
return template.render(ctx)
def _create_source(self, request, order):
source = stripe.Source.create(
type='ideal',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
ideal={
'statement_descriptor': ugettext('{event}-{code}').format(
event=self.event.slug.upper(),
code=order.code
)
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
return source
def payment_is_valid_session(self, request):
return True
def checkout_prepare(self, request, cart):
return True
class StripeAlipay(StripeMethod):
identifier = 'stripe_alipay'
verbose_name = _('Alipay via Stripe')
public_name = _('Alipay')
method = 'alipay'
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
}
return template.render(ctx)
def _create_source(self, request, order):
source = stripe.Source.create(
type='alipay',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
return source
def payment_is_valid_session(self, request):
return True
def checkout_prepare(self, request, cart):
return True
class StripeBancontact(StripeMethod):
identifier = 'stripe_bancontact'
verbose_name = _('Bancontact via Stripe')
public_name = _('Bancontact')
method = 'bancontact'
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_bancontact.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
'form': self.payment_form(request)
}
return template.render(ctx)
@property
def payment_form_fields(self):
return OrderedDict([
('account', forms.CharField(label=_('Account holder'), min_length=3)),
])
def _create_source(self, request, order):
try:
source = stripe.Source.create(
type='bancontact',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
owner={
'name': request.session.get('payment_stripe_bancontact_account') or ugettext('unknown name')
},
bancontact={
'statement_descriptor': ugettext('{event}-{code}').format(
event=self.event.slug.upper(),
code=order.code
)[:35]
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
return source
finally:
if 'payment_stripe_bancontact_account' in request.session:
del request.session['payment_stripe_bancontact_account']
def payment_is_valid_session(self, request):
return (
request.session.get('payment_stripe_bancontact_account', '') != ''
)
def checkout_prepare(self, request, cart):
form = self.payment_form(request)
if form.is_valid():
request.session['payment_stripe_bancontact_account'] = form.cleaned_data['account']
return True
return False
class StripeSofort(StripeMethod):
identifier = 'stripe_sofort'
verbose_name = _('SOFORT via Stripe')
public_name = _('SOFORT')
method = 'sofort'
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_sofort.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
'form': self.payment_form(request)
}
return template.render(ctx)
@property
def payment_form_fields(self):
return OrderedDict([
('bank_country', forms.ChoiceField(label=_('Country of your bank'), choices=(
('de', _('Germany')),
('at', _('Austria')),
('be', _('Belgium')),
('nl', _('Netherlands')),
('es', _('Spain'))
))),
])
def _create_source(self, request, order):
source = stripe.Source.create(
type='sofort',
amount=int(order.total * 100),
currency=self.event.currency.lower(),
metadata={
'order': str(order.id),
'event': self.event.id,
'code': order.code
},
sofort={
'country': request.session.get('payment_stripe_sofort_bank_country'),
'statement_descriptor': ugettext('{event}-{code}').format(
event=self.event.slug.upper(),
code=order.code
)[:35]
},
redirect={
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
'order': order.code,
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
})
},
)
return source
def payment_is_valid_session(self, request):
return (
request.session.get('payment_stripe_sofort_bank_country', '') != ''
)
def checkout_prepare(self, request, cart):
form = self.payment_form(request)
if form.is_valid():
request.session['payment_stripe_sofort_bank_country'] = form.cleaned_data['bank_country']
return True
return False
def order_can_retry(self, order):
try:
d = json.loads(order.payment_info)
except ValueError:
return self._is_still_available(order=order)
return not (
d.get('object') == 'charge' and d.get('status') == 'pending'
)

View File

@@ -5,6 +5,7 @@ from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.settings import settings_hierarkey
from pretix.base.signals import ( from pretix.base.signals import (
logentry_display, register_payment_providers, requiredaction_display, logentry_display, register_payment_providers, requiredaction_display,
) )
@@ -13,18 +14,21 @@ from pretix.presale.signals import html_head
@receiver(register_payment_providers, dispatch_uid="payment_stripe") @receiver(register_payment_providers, dispatch_uid="payment_stripe")
def register_payment_provider(sender, **kwargs): def register_payment_provider(sender, **kwargs):
from .payment import Stripe from .payment import (
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort
)
return Stripe return [StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact, StripeSofort]
@receiver(html_head, dispatch_uid="payment_stripe_html_head") @receiver(html_head, dispatch_uid="payment_stripe_html_head")
def html_head_presale(sender, request=None, **kwargs): def html_head_presale(sender, request=None, **kwargs):
from .payment import Stripe from .payment import StripeSettingsHolder
provider = Stripe(sender) provider = StripeSettingsHolder(sender)
url = resolve(request.path_info) url = resolve(request.path_info)
if provider.is_enabled and ("checkout" in url.url_name or "order.pay" in url.url_name): 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') template = get_template('pretixplugins/stripe/presale_head.html')
ctx = {'event': sender, 'settings': provider.settings} ctx = {'event': sender, 'settings': provider.settings}
return template.render(ctx) return template.render(ctx)
@@ -81,3 +85,6 @@ def pretixcontrol_action_display(sender, action, request, **kwargs):
ctx = {'data': data, 'event': sender, 'action': action} ctx = {'data': data, 'event': sender, 'action': action}
return template.render(ctx, request) return template.render(ctx, request)
settings_hierarkey.add_default('payment_stripe_method_cc', True, bool)

View File

@@ -6,7 +6,7 @@ var pretixstripe = {
elements: null, elements: null,
card: null, card: null,
'request': function () { 'cc_request': function () {
waitingDialog.show(gettext("Contacting Stripe …")); waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide(); $(".stripe-errors").hide();
@@ -35,42 +35,91 @@ var pretixstripe = {
success: function () { success: function () {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html())); pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
pretixstripe.elements = pretixstripe.stripe.elements(); pretixstripe.elements = pretixstripe.stripe.elements();
pretixstripe.card = pretixstripe.elements.create('card', { if ($("#stripe-card").length) {
'style': { pretixstripe.card = pretixstripe.elements.create('card', {
'base': { 'style': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif', 'base': {
'fontSize': '14px', 'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'color': '#555555', 'fontSize': '14px',
'lineHeight': '1.42857', 'color': '#555555',
'border': '1px solid #ccc', 'lineHeight': '1.42857',
'::placeholder': { 'border': '1px solid #ccc',
color: 'rgba(0,0,0,0.4)', '::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
}, },
}, },
'invalid': { classes: {
'color': 'red', focus: 'is-focused',
}, invalid: 'has-error',
}, }
classes: { });
focus: 'is-focused', pretixstripe.card.mount("#stripe-card");
invalid: 'has-error', }
}
});
pretixstripe.card.mount("#stripe-card");
} }
} }
); );
} },
'load_checkout': function () {
$.ajax(
{
url: 'https://checkout.stripe.com/checkout.js',
dataType: 'script',
success: function () {
pretixstripe.checkout_handler = StripeCheckout.configure({
key: $.trim($("#stripe_pubkey").html()),
locale: 'auto',
token: function (token) {
var $form = $("#stripe-checkout").parents("form");
$("#stripe_token").val(token.id);
$("#stripe_card_brand").val(token.card.brand);
$("#stripe_card_last4").val(token.card.last4);
$("#stripe_card_brand_display").text(token.card.brand);
$("#stripe_card_last4_display").text(token.card.last4);
$($form.get(0)).submit();
},
shippingAddress: false,
allowRememberMe: false,
billingAddress: false
});
}
}
);
},
'show_checkout': function () {
var amount = Math.round(
parseFloat(
$("#stripe-checkout").parents("[data-total]").attr("data-total").replace(",", ".")
) * 100
);
pretixstripe.checkout_handler.open({
name: $("#organizer_name").val(),
description: $("#event_name").val(),
currency: $("#stripe_currency").val(),
email: $("#stripe_email").val(),
amount: amount
});
},
'checkout_handler': null
}; };
$(function () { $(function () {
if (!$("#stripe-card").length) // Not on the checkout page if (!$(".stripe-container").length) // Not on the checkout page
return; return;
if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) { if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) {
if ($("#stripe-checkout").length) {
pretixstripe.load_checkout();
}
pretixstripe.load(); pretixstripe.load();
} else { } else {
$("input[name=payment]").change(function () { $("input[name=payment]").change(function () {
if ($(this).val() == 'stripe') { if ($(this).val() === 'stripe') {
if ($("#stripe-checkout").length) {
pretixstripe.load_checkout();
}
pretixstripe.load(); pretixstripe.load();
} }
}) })
@@ -79,9 +128,12 @@ $(function () {
$("#stripe_other_card").click( $("#stripe_other_card").click(
function (e) { function (e) {
$("#stripe_token").val(""); $("#stripe_token").val("");
$("#stripe-current-card").slideUp(); if ($("#stripe-checkout").length) {
$("#stripe-card").slideDown(); pretixstripe.show_checkout();
pretixstripe.start(); } else {
$("#stripe-current-card").slideUp();
$("#stripe-card").slideDown();
}
e.preventDefault(); e.preventDefault();
return false; return false;
} }
@@ -91,13 +143,23 @@ $(function () {
$("#stripe-card").hide(); $("#stripe-card").hide();
} }
$("#stripe-card").parents("form").submit( $('.stripe-container').closest("form").submit(
function () { function () {
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment]").length === 0) if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment]").length === 0)
&& $("#stripe_token").val() == "") { && $("#stripe_token").val() == "") {
pretixstripe.request(); console.log("foo");
if ($("#stripe-checkout").length) {
pretixstripe.show_checkout();
} else {
pretixstripe.cc_request();
}
return false; return false;
} }
} }
); );
$(window).on('popstate', function () {
if (pretixstripe.checkout_handler) {
pretixstripe.checkout_handler.close();
}
});
}); });

View File

@@ -1,12 +1,29 @@
{% load i18n %} {% load i18n %}
<p>{% blocktrans trimmed %} {% if provider.method == "cc" %}
The total amount will be withdrawn from your credit card. <p>{% blocktrans trimmed %}
{% endblocktrans %}</p> The total amount will be withdrawn from your credit card.
<dl class="dl-horizontal"> {% endblocktrans %}</p>
<dt>{% trans "Card type" %}</dt> <dl class="dl-horizontal">
<dd>{{ request.session.payment_stripe_brand }}</dd> <dt>{% trans "Card type" %}</dt>
<dt>{% trans "Card number" %}</dt> <dd>{{ request.session.payment_stripe_brand }}</dd>
<dd>**** **** **** {{ request.session.payment_stripe_last4 }}</dd> <dt>{% trans "Card number" %}</dt>
</dl> <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

@@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="form-horizontal"> <div class="form-horizontal stripe-container">
<div class="stripe-errors sr-only"> <div class="stripe-errors sr-only">
</div> </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,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,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

@@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<div class="form-horizontal" id="stripe-checkout"> <div class="form-horizontal stripe-container" id="stripe-checkout">
<noscript> <noscript>
<div class="alert alert-warning"> <div class="alert alert-warning">
{% trans "For a credit card payment, please turn on JavaScript." %} {% trans "For a credit card payment, please turn on JavaScript." %}

View File

@@ -2,28 +2,48 @@
{% if payment_info %} {% if payment_info %}
{% if order.status == "p" %} {% if order.status == "p" %}
<p>{% blocktrans trimmed %} <p>{% blocktrans trimmed with method=provider.verbose_name %}
This order has been paid via Stripe. This order has been paid with {{ method }}.
{% endblocktrans %}</p> {% endblocktrans %}</p>
{% elif order.status == "r" %} {% elif order.status == "r" %}
<p>{% blocktrans trimmed %} <p>{% blocktrans trimmed with method=provider.verbose_name %}
This order has been planned to be paid via Stripe and has been marked as refunded. This order has been planned to be paid with {{ method }} and has been marked as refunded.
{% endblocktrans %}</p> {% endblocktrans %}</p>
{% else %} {% else %}
<p>{% blocktrans trimmed %} <p>{% blocktrans trimmed with method=provider.verbose_name %}
This order has been planned to be paid via Stripe, but the payment has not yet been completed. This order has been planned to be paid with {{ method }}, but the payment has not yet been completed.
{% endblocktrans %}</p> {% endblocktrans %}</p>
{% endif %} {% endif %}
{% if order.status == "p" %} {% if order.status == "p" %}
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>{% trans "Charge ID" %}</dt> <dt>{% trans "Charge ID" %}</dt>
<dd>{{ payment_info.id }}</dd> <dd>{{ payment_info.id }}</dd>
<dt>{% trans "Card type" %}</dt> {% if payment_info.source.type == "card" or payment_info.source.type == "three_d_secure" %}
<dd>{{ payment_info.source.brand }}</dd> <dt>{% trans "Card type" %}</dt>
<dt>{% trans "Card number" %}</dt> <dd>{{ payment_info.source.brand }}</dd>
<dd>**** **** **** {{ payment_info.source.last4 }}</dd> <dt>{% trans "Card number" %}</dt>
<dt>{% trans "Payer name" %}</dt> <dd>**** **** **** {{ payment_info.source.last4 }}</dd>
<dd>{{ payment_info.source.name }}</dd> <dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.name }}</dd>
{% 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 %}
<dt>{% trans "Total value" %}</dt> <dt>{% trans "Total value" %}</dt>
<dd>{{ payment_info.amount|floatformat:2 }}</dd> <dd>{{ payment_info.amount|floatformat:2 }}</dd>
<dt>{% trans "Currency" %}</dt> <dt>{% trans "Currency" %}</dt>

View File

@@ -1,12 +1,19 @@
{% load i18n %} {% load i18n %}
<p>{% blocktrans trimmed %} {% if provider.method == "sofort" %}
The credit card transaction could not be completed for the following reason: <p>{% blocktrans trimmed %}
{% endblocktrans %} We're waiting for an answer from the payment provider regarding your payment. Please contact us if this
<br /> takes more than a few days.
{% if payment_info and payment_info.error %} {% endblocktrans %}</p>
{{ payment_info.message }} {% else %}
{% else %} <p>{% blocktrans trimmed %}
{% trans "Unknown reason" %} The payment transaction could not be completed for the following reason:
{% endif %} {% endblocktrans %}
</p> <br/>
{% if payment_info and payment_info.error %}
{{ payment_info.message }}
{% else %}
{% trans "Unknown reason" %}
{% endif %}
</p>
{% endif %}

View File

@@ -2,14 +2,8 @@
{% load compress %} {% load compress %}
{% load i18n %} {% load i18n %}
{% if settings.ui == "checkout" %} {% compress js %}
{% compress js %} <script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe-checkout.js" %}"></script> {% endcompress %}
{% endcompress %}
{% else %}
{% compress js %}
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
{% endcompress %}
{% endif %}
<script type="text/plain" id="stripe_pubkey">{{ settings.publishable_key }}</script> <script type="text/plain" id="stripe_pubkey">{{ settings.publishable_key }}</script>

View File

@@ -20,7 +20,7 @@ from pretix.base.payment import PaymentException
from pretix.base.services.orders import mark_order_paid, mark_order_refunded from pretix.base.services.orders import mark_order_paid, mark_order_refunded
from pretix.control.permissions import event_permission_required from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.stripe.payment import Stripe from pretix.plugins.stripe.payment import StripeCC
from pretix.presale.utils import event_view from pretix.presale.utils import event_view
logger = logging.getLogger('pretix.plugins.stripe') logger = logging.getLogger('pretix.plugins.stripe')
@@ -48,7 +48,7 @@ def webhook(request, *args, **kwargs):
def charge_webhook(request, event_json, charge_id): def charge_webhook(request, event_json, charge_id):
prov = Stripe(request.event) prov = StripeCC(request.event)
prov._init_api() prov._init_api()
try: try:
charge = stripe.Charge.retrieve(charge_id) charge = stripe.Charge.retrieve(charge_id)
@@ -68,6 +68,10 @@ def charge_webhook(request, event_json, charge_id):
except Order.DoesNotExist: except Order.DoesNotExist:
return HttpResponse('Order not found', status=200) return HttpResponse('Order not found', status=200)
if order.payment_provider != prov.identifier:
prov = request.event.get_payment_providers()[order.payment_provider]
prov._init_api()
order.log_action('pretix.plugins.stripe.event', data=event_json) order.log_action('pretix.plugins.stripe.event', data=event_json)
is_refund = charge['refunds']['total_count'] or charge['dispute'] is_refund = charge['refunds']['total_count'] or charge['dispute']
@@ -97,7 +101,7 @@ def charge_webhook(request, event_json, charge_id):
def source_webhook(request, event_json, source_id): def source_webhook(request, event_json, source_id):
prov = Stripe(request.event) prov = StripeCC(request.event)
prov._init_api() prov._init_api()
try: try:
src = stripe.Source.retrieve(source_id) src = stripe.Source.retrieve(source_id)
@@ -118,12 +122,16 @@ def source_webhook(request, event_json, source_id):
except Order.DoesNotExist: except Order.DoesNotExist:
return HttpResponse('Order not found', status=200) return HttpResponse('Order not found', status=200)
if order.payment_provider != prov.identifier:
prov = request.event.get_payment_providers()[order.payment_provider]
prov._init_api()
order.log_action('pretix.plugins.stripe.event', data=event_json) order.log_action('pretix.plugins.stripe.event', data=event_json)
go = (event_json['type'] == 'source.chargeable' and order.status == Order.STATUS_PENDING and go = (event_json['type'] == 'source.chargeable' and order.status == Order.STATUS_PENDING and
src.status == 'chargeable') src.status == 'chargeable')
if go: if go:
try: try:
prov._charge_source(source_id, order) prov._charge_source(request, source_id, order)
except PaymentException: except PaymentException:
logger.exception('Webhook error') logger.exception('Webhook error')
@@ -178,7 +186,7 @@ class StripeOrderView:
@method_decorator(event_view, name='dispatch') @method_decorator(event_view, name='dispatch')
class ReturnView(StripeOrderView, View): class ReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
prov = Stripe(request.event) prov = self.pprov
prov._init_api() prov._init_api()
src = stripe.Source.retrieve(request.GET.get('source')) src = stripe.Source.retrieve(request.GET.get('source'))
if src.client_secret != request.GET.get('client_secret'): if src.client_secret != request.GET.get('client_secret'):
@@ -194,12 +202,13 @@ class ReturnView(StripeOrderView, View):
if src.status == 'chargeable': if src.status == 'chargeable':
try: try:
prov._charge_source(src.id, self.order) prov._charge_source(request, src.id, self.order)
except PaymentException as e: except PaymentException as e:
messages.error(request, str(e)) messages.error(request, str(e))
return self._redirect_to_order() return self._redirect_to_order()
finally: finally:
del request.session['payment_stripe_token'] if 'payment_stripe_token' in request.session:
del request.session['payment_stripe_token']
else: else:
messages.error(self.request, _('We had trouble authorizing your card payment. Please try again and ' messages.error(self.request, _('We had trouble authorizing your card payment. Please try again and '
'get in touch with us if this problem persists.')) 'get in touch with us if this problem persists.'))