forked from CGM_Public/pretix_original
Fix #571 -- Partial payments and refunds
This commit is contained in:
@@ -3,18 +3,20 @@ import textwrap
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.fields import I18nFormField, I18nTextarea
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models import OrderPayment
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
|
||||
|
||||
class BankTransfer(BasePaymentProvider):
|
||||
identifier = 'banktransfer'
|
||||
verbose_name = _('Bank transfer')
|
||||
abort_pending_allowed = True
|
||||
|
||||
@staticmethod
|
||||
def form_field(**kwargs):
|
||||
@@ -65,6 +67,9 @@ class BankTransfer(BasePaymentProvider):
|
||||
def checkout_prepare(self, request, total):
|
||||
return True
|
||||
|
||||
def payment_prepare(self, request: HttpRequest, payment: OrderPayment):
|
||||
return True
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
return True
|
||||
|
||||
@@ -81,12 +86,12 @@ class BankTransfer(BasePaymentProvider):
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_pending_render(self, request, order) -> str:
|
||||
def payment_pending_render(self, request: HttpRequest, payment: OrderPayment):
|
||||
template = get_template('pretixplugins/banktransfer/pending.html')
|
||||
ctx = {
|
||||
'event': self.event,
|
||||
'order': order,
|
||||
'code': self._code(order),
|
||||
'code': self._code(payment.order),
|
||||
'order': payment.order,
|
||||
'details': self.settings.get('bank_details', as_type=LazyI18nString),
|
||||
}
|
||||
return template.render(ctx)
|
||||
@@ -102,18 +107,18 @@ class BankTransfer(BasePaymentProvider):
|
||||
'payment_info': payment_info, 'order': order}
|
||||
return template.render(ctx)
|
||||
|
||||
def _code(self, order: Order):
|
||||
def _code(self, order):
|
||||
if self.settings.get('omit_hyphen', as_type=bool):
|
||||
return self.event.slug.upper() + order.code
|
||||
else:
|
||||
return order.full_code
|
||||
|
||||
def shred_payment_info(self, order: Order):
|
||||
if not order.payment_info:
|
||||
def shred_payment_info(self, obj):
|
||||
if not obj.info_data:
|
||||
return
|
||||
d = json.loads(order.payment_info)
|
||||
d = obj.info_data
|
||||
d['reference'] = '█'
|
||||
d['payer'] = '█'
|
||||
d['_shredded'] = True
|
||||
order.payment_info = json.dumps(d)
|
||||
order.save(update_fields=['payment_info'])
|
||||
obj.info = json.dumps(d)
|
||||
obj.save(update_fields=['info'])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from decimal import Decimal
|
||||
@@ -10,11 +9,10 @@ from django.db.models import Q
|
||||
from django.utils.translation import ugettext_noop
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Event, Order, Organizer, Quota
|
||||
from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota
|
||||
from pretix.base.services.async import TransactionAwareTask
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid
|
||||
from pretix.celery_app import app
|
||||
|
||||
from .models import BankImportJob, BankTransaction
|
||||
@@ -50,7 +48,7 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
|
||||
trans.save()
|
||||
return
|
||||
|
||||
if trans.order.status == Order.STATUS_PAID:
|
||||
if trans.order.status == Order.STATUS_PAID and trans.order.pending_sum <= Decimal('0.00'):
|
||||
trans.state = BankTransaction.STATE_DUPLICATE
|
||||
elif trans.order.status == Order.STATUS_REFUNDED:
|
||||
trans.state = BankTransaction.STATE_ERROR
|
||||
@@ -58,17 +56,23 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
|
||||
elif trans.order.status == Order.STATUS_CANCELED:
|
||||
trans.state = BankTransaction.STATE_ERROR
|
||||
trans.message = ugettext_noop('The order has already been canceled.')
|
||||
elif trans.amount != trans.order.total:
|
||||
trans.state = BankTransaction.STATE_INVALID
|
||||
trans.message = ugettext_noop('The transaction amount is incorrect.')
|
||||
else:
|
||||
p = trans.order.payments.get_or_create(
|
||||
amount=trans.amount,
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
defaults={
|
||||
'state': OrderPayment.PAYMENT_STATE_CREATED,
|
||||
}
|
||||
)[0]
|
||||
p.info_data = {
|
||||
'reference': trans.reference,
|
||||
'date': trans.date,
|
||||
'payer': trans.payer,
|
||||
'trans_id': trans.pk
|
||||
}
|
||||
try:
|
||||
mark_order_paid(trans.order, provider='banktransfer', info=json.dumps({
|
||||
'reference': trans.reference,
|
||||
'date': trans.date,
|
||||
'payer': trans.payer,
|
||||
'trans_id': trans.pk
|
||||
}))
|
||||
p.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
trans.state = BankTransaction.STATE_ERROR
|
||||
trans.message = str(e)
|
||||
@@ -77,6 +81,10 @@ def _handle_transaction(trans: BankTransaction, code: str, event: Event=None, or
|
||||
trans.message = ugettext_noop('Problem sending email.')
|
||||
else:
|
||||
trans.state = BankTransaction.STATE_VALID
|
||||
trans.order.payments.filter(
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
trans.save()
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info and order.status == "p" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been paid via bank transfer.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif order.status == "p" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been marked as paid via bank transfer manually.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif order.status == "r" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been paid via bank transfer and marked as refunded.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid via bank transfer, but no payment has been received yet.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Payer" %}</dt>
|
||||
|
||||
@@ -14,9 +14,8 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DetailView, ListView, View
|
||||
|
||||
from pretix.base.models import Order, Quota
|
||||
from pretix.base.models import Order, OrderPayment, Quota
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.permissions import (
|
||||
@@ -66,15 +65,22 @@ class ActionView(View):
|
||||
'message': _('The order has already been canceled.')
|
||||
})
|
||||
|
||||
p = trans.order.payments.get_or_create(
|
||||
amount=trans.amount,
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
defaults={
|
||||
'state': OrderPayment.PAYMENT_STATE_CREATED,
|
||||
}
|
||||
)[0]
|
||||
p.info_data = {
|
||||
'reference': trans.reference,
|
||||
'date': trans.date,
|
||||
'payer': trans.payer,
|
||||
'trans_id': trans.pk
|
||||
}
|
||||
try:
|
||||
mark_order_paid(trans.order, provider='banktransfer', info=json.dumps({
|
||||
'reference': trans.reference,
|
||||
'date': trans.date,
|
||||
'payer': trans.payer,
|
||||
'trans_id': trans.pk
|
||||
}), count_waitinglist=False)
|
||||
trans.state = BankTransaction.STATE_VALID
|
||||
trans.save()
|
||||
p.confirm(user=self.request.user)
|
||||
except Quota.QuotaExceededException as e:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
@@ -88,8 +94,12 @@ class ActionView(View):
|
||||
else:
|
||||
trans.state = BankTransaction.STATE_VALID
|
||||
trans.save()
|
||||
trans.order.payments.filter(
|
||||
provider='banktransfer',
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING),
|
||||
).update(state=OrderPayment.PAYMENT_STATE_CANCELED)
|
||||
return JsonResponse({
|
||||
'status': 'ok'
|
||||
'status': 'ok',
|
||||
})
|
||||
|
||||
def _assign(self, trans, code):
|
||||
|
||||
@@ -14,8 +14,5 @@ class ManualPaymentApp(AppConfig):
|
||||
version = version
|
||||
description = _("This plugin adds a customizable payment method for manual processing.")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
|
||||
default_app_config = 'pretix.plugins.manualpayment.ManualPaymentApp'
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.fields import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.forms import PlaceholderValidator
|
||||
from pretix.base.payment import BasePaymentProvider
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
|
||||
|
||||
class ManualPayment(BasePaymentProvider):
|
||||
identifier = 'manual'
|
||||
verbose_name = _('Manual payment')
|
||||
|
||||
@property
|
||||
def public_name(self):
|
||||
return str(self.settings.get('public_name', as_type=LazyI18nString))
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
d = OrderedDict(
|
||||
[
|
||||
('public_name', I18nFormField(
|
||||
label=_('Payment method name'),
|
||||
widget=I18nTextInput,
|
||||
)),
|
||||
('checkout_description', I18nFormField(
|
||||
label=_('Payment process description during checkout'),
|
||||
help_text=_('This text will be shown during checkout when the user selects this payment method. '
|
||||
'It should give a short explanation on this payment method.'),
|
||||
widget=I18nTextarea,
|
||||
)),
|
||||
('email_instructions', I18nFormField(
|
||||
label=_('Payment process description in order confirmation emails'),
|
||||
help_text=_('This text will be included for the {payment_info} placeholder in order confirmation '
|
||||
'mails. It should instruct the user on how to proceed with the payment. You can use'
|
||||
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
||||
)),
|
||||
('pending_description', I18nFormField(
|
||||
label=_('Payment process description for pending orders'),
|
||||
help_text=_('This text will be shown on the order confirmation page for pending orders. '
|
||||
'It should instruct the user on how to proceed with the payment. You can use'
|
||||
'the placeholders {order}, {total}, {currency} and {total_with_currency}'),
|
||||
widget=I18nTextarea,
|
||||
validators=[PlaceholderValidator(['{order}', '{total}', '{currency}', '{total_with_currency}'])],
|
||||
)),
|
||||
] + list(super().settings_form_fields.items())
|
||||
)
|
||||
d.move_to_end('_enabled', last=False)
|
||||
return d
|
||||
|
||||
def payment_form_render(self, request) -> str:
|
||||
return rich_text(
|
||||
str(self.settings.get('checkout_description', as_type=LazyI18nString))
|
||||
)
|
||||
|
||||
def checkout_prepare(self, request, total):
|
||||
return True
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
return True
|
||||
|
||||
def checkout_confirm_render(self, request):
|
||||
return self.payment_form_render(request)
|
||||
|
||||
def format_map(self, order):
|
||||
return {
|
||||
'order': order.code,
|
||||
'total': order.total,
|
||||
'currency': self.event.currency,
|
||||
'total_with_currency': money_filter(order.total, self.event.currency)
|
||||
}
|
||||
|
||||
def order_pending_mail_render(self, order) -> str:
|
||||
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order))
|
||||
return msg
|
||||
|
||||
def order_pending_render(self, request, order) -> str:
|
||||
return rich_text(
|
||||
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(order))
|
||||
)
|
||||
|
||||
def order_control_render(self, request, order) -> str:
|
||||
template = get_template('pretixplugins/manualpayment/control.html')
|
||||
ctx = {'request': request, 'event': self.event,
|
||||
'order': order}
|
||||
return template.render(ctx)
|
||||
@@ -1,10 +0,0 @@
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_payment_providers
|
||||
|
||||
from .payment import ManualPayment
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_manual")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
return ManualPayment
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if order.status == "p" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been paid manually.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid manually, but is not marked as paid.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-07-23 09:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0097_auto_20180722_0804'),
|
||||
('paypal', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='referencedpaypalobject',
|
||||
name='payment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.OrderPayment'),
|
||||
),
|
||||
]
|
||||
@@ -4,3 +4,4 @@ from django.db import models
|
||||
class ReferencedPayPalObject(models.Model):
|
||||
reference = models.CharField(max_length=190, db_index=True, unique=True)
|
||||
order = models.ForeignKey('pretixbase.Order')
|
||||
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True)
|
||||
|
||||
@@ -7,14 +7,14 @@ import paypalrestsdk
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext as __, ugettext_lazy as _
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Order, Quota, RequiredAction
|
||||
from pretix.base.models import OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
@@ -22,18 +22,6 @@ from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
|
||||
class RefundForm(forms.Form):
|
||||
auto_refund = forms.ChoiceField(
|
||||
initial='auto',
|
||||
label=_('Refund automatically?'),
|
||||
choices=(
|
||||
('auto', _('Automatically refund charge with PayPal')),
|
||||
('manual', _('Do not send refund instruction to PayPal, only mark as refunded in pretix'))
|
||||
),
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
|
||||
class Paypal(BasePaymentProvider):
|
||||
identifier = 'paypal'
|
||||
verbose_name = _('PayPal')
|
||||
@@ -162,6 +150,10 @@ class Paypal(BasePaymentProvider):
|
||||
'XPF': 0,
|
||||
}))
|
||||
|
||||
@property
|
||||
def abort_pending_allowed(self):
|
||||
return False
|
||||
|
||||
def _create_payment(self, request, payment):
|
||||
try:
|
||||
if payment.create():
|
||||
@@ -196,36 +188,23 @@ class Paypal(BasePaymentProvider):
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
|
||||
return template.render(ctx)
|
||||
|
||||
def payment_perform(self, request, order) -> str:
|
||||
"""
|
||||
Will be called if the user submitted his order successfully to initiate the
|
||||
payment process.
|
||||
|
||||
It should return a custom redirct URL, if you need special behavior, or None to
|
||||
continue with default behavior.
|
||||
|
||||
On errors, it should use Django's message framework to display an error message
|
||||
to the user (or the normal form validation error messages).
|
||||
|
||||
:param order: The order object
|
||||
"""
|
||||
if (request.session.get('payment_paypal_id', '') == ''
|
||||
or request.session.get('payment_paypal_payer', '') == ''):
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
self.init_api()
|
||||
payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
|
||||
ReferencedPayPalObject.objects.get_or_create(order=order, reference=payment.id)
|
||||
if str(payment.transactions[0].amount.total) != str(order.total) or payment.transactions[0].amount.currency != \
|
||||
self.event.currency:
|
||||
logger.error('Value mismatch: Order %s vs payment %s' % (order.id, str(payment)))
|
||||
pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
|
||||
if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
|
||||
!= self.event.currency:
|
||||
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
return self._execute_payment(payment, request, order)
|
||||
return self._execute_payment(pp_payment, request, payment)
|
||||
|
||||
def _execute_payment(self, payment, request, order):
|
||||
def _execute_payment(self, payment, request, payment_obj):
|
||||
if payment.state == 'created':
|
||||
payment.replace([
|
||||
{
|
||||
@@ -234,10 +213,11 @@ class Paypal(BasePaymentProvider):
|
||||
"value": {
|
||||
"items": [
|
||||
{
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(),
|
||||
code=payment_obj.order.code),
|
||||
"quantity": 1,
|
||||
"price": self.format_price(order.total),
|
||||
"currency": order.event.currency
|
||||
"price": self.format_price(payment_obj.amount),
|
||||
"currency": payment_obj.order.event.currency
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -247,122 +227,92 @@ class Paypal(BasePaymentProvider):
|
||||
"path": "/transactions/0/description",
|
||||
"value": __('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=order.code
|
||||
order=payment_obj.order.code
|
||||
)
|
||||
}
|
||||
])
|
||||
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
|
||||
|
||||
order.refresh_from_db()
|
||||
payment_obj.refresh_from_db()
|
||||
if payment.state == 'pending':
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
|
||||
'payment completed.'))
|
||||
order.payment_info = json.dumps(payment.to_dict())
|
||||
order.save()
|
||||
payment_obj.info = json.dumps(payment.to_dict())
|
||||
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment_obj.save()
|
||||
return
|
||||
|
||||
if payment.state != 'approved':
|
||||
payment_obj.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment_obj.save()
|
||||
logger.error('Invalid state: %s' % str(payment))
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
if order.status == Order.STATUS_PAID:
|
||||
if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
mark_order_paid(order, 'paypal', json.dumps(payment.to_dict()))
|
||||
payment_obj.info = json.dumps(payment.to_dict())
|
||||
payment_obj.save(update_fields=['info'])
|
||||
payment_obj.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
RequiredAction.objects.create(
|
||||
event=request.event, action_type='pretix.plugins.paypal.overpaid', data=json.dumps({
|
||||
'order': order.code,
|
||||
'payment': payment.id
|
||||
})
|
||||
)
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
messages.warning(request, _('There was an error sending the confirmation mail.'))
|
||||
return None
|
||||
|
||||
def order_pending_render(self, request, order) -> str:
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
retry = True
|
||||
try:
|
||||
if order.payment_info and json.loads(order.payment_info)['state'] == 'pending':
|
||||
if payment.info and payment.info_data['state'] == 'pending':
|
||||
retry = False
|
||||
except KeyError:
|
||||
pass
|
||||
template = get_template('pretixplugins/paypal/pending.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||
'retry': retry, 'order': order}
|
||||
'retry': retry, 'order': payment.order}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_control_render(self, request, order) -> str:
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
else:
|
||||
payment_info = None
|
||||
def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
|
||||
template = get_template('pretixplugins/paypal/control.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||
'payment_info': payment_info, 'order': order}
|
||||
'payment_info': payment.info_data, 'order': payment.order}
|
||||
return template.render(ctx)
|
||||
|
||||
def _refund_form(self, request):
|
||||
return RefundForm(data=request.POST if request.method == "POST" else None)
|
||||
def payment_partial_refund_supported(self, payment: OrderPayment):
|
||||
return True
|
||||
|
||||
def order_control_refund_render(self, order, request) -> str:
|
||||
template = get_template('pretixplugins/paypal/control_refund.html')
|
||||
ctx = {
|
||||
'request': request,
|
||||
'form': self._refund_form(request),
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_control_refund_perform(self, request, order) -> "bool|str":
|
||||
f = self._refund_form(request)
|
||||
if not f.is_valid():
|
||||
messages.error(request, _('Your input was invalid, please try again.'))
|
||||
return
|
||||
elif f.cleaned_data.get('auto_refund') == 'manual':
|
||||
order = mark_order_refunded(order, user=request.user)
|
||||
order.payment_manual = True
|
||||
order.save()
|
||||
return
|
||||
def payment_refund_supported(self, payment: OrderPayment):
|
||||
return True
|
||||
|
||||
def execute_refund(self, refund: OrderRefund):
|
||||
self.init_api()
|
||||
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
else:
|
||||
payment_info = None
|
||||
|
||||
if not payment_info:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.warning(request, _('We were unable to transfer the money back automatically. '
|
||||
'Please get in touch with the customer and transfer it back manually.'))
|
||||
return
|
||||
|
||||
for res in payment_info['transactions'][0]['related_resources']:
|
||||
sale = None
|
||||
for res in refund.payment.info_data['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale':
|
||||
sale = paypalrestsdk.Sale.find(v['id'])
|
||||
break
|
||||
|
||||
refund = sale.refund({})
|
||||
if not refund.success():
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.warning(request, _('We were unable to transfer the money back automatically. '
|
||||
'Please get in touch with the customer and transfer it back manually.'))
|
||||
pp_refund = sale.refund({
|
||||
"amount": {
|
||||
"total": self.format_price(refund.amount),
|
||||
"currency": refund.order.event.currency
|
||||
}
|
||||
})
|
||||
if not pp_refund.success():
|
||||
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(refund.error))
|
||||
else:
|
||||
sale = paypalrestsdk.Payment.find(payment_info['id'])
|
||||
order = mark_order_refunded(order, user=request.user)
|
||||
order.payment_info = json.dumps(sale.to_dict())
|
||||
order.save()
|
||||
sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
|
||||
refund.payment.info = json.dumps(sale.to_dict())
|
||||
refund.info = json.dumps(pp_refund.to_dict())
|
||||
refund.done()
|
||||
|
||||
def order_can_retry(self, order):
|
||||
return self._is_still_available(order=order)
|
||||
|
||||
def order_prepare(self, request, order):
|
||||
def payment_prepare(self, request, payment_obj):
|
||||
self.init_api()
|
||||
payment = paypalrestsdk.Payment({
|
||||
'intent': 'sale',
|
||||
@@ -378,43 +328,58 @@ class Paypal(BasePaymentProvider):
|
||||
"item_list": {
|
||||
"items": [
|
||||
{
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(), code=order.code),
|
||||
"name": __('Order {slug}-{code}').format(slug=self.event.slug.upper(),
|
||||
code=payment_obj.order.code),
|
||||
"quantity": 1,
|
||||
"price": self.format_price(order.total),
|
||||
"currency": order.event.currency
|
||||
"price": self.format_price(payment_obj.amount),
|
||||
"currency": payment_obj.order.event.currency
|
||||
}
|
||||
]
|
||||
},
|
||||
"amount": {
|
||||
"currency": request.event.currency,
|
||||
"total": self.format_price(order.total)
|
||||
"total": self.format_price(payment_obj.amount)
|
||||
},
|
||||
"description": __('Order {order} for {event}').format(
|
||||
event=request.event.name,
|
||||
order=order.code
|
||||
order=payment_obj.order.code
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
request.session['payment_paypal_order'] = order.pk
|
||||
request.session['payment_paypal_order'] = payment_obj.order.pk
|
||||
request.session['payment_paypal_payment'] = payment_obj.pk
|
||||
return self._create_payment(request, payment)
|
||||
|
||||
def shred_payment_info(self, order: Order):
|
||||
d = json.loads(order.payment_info)
|
||||
new = {
|
||||
'id': d.get('id'),
|
||||
'payer': {
|
||||
'payer_info': {
|
||||
'email': '█'
|
||||
def shred_payment_info(self, obj):
|
||||
if obj.info:
|
||||
d = json.loads(obj.info)
|
||||
new = {
|
||||
'id': d.get('id'),
|
||||
'payer': {
|
||||
'payer_info': {
|
||||
'email': '█'
|
||||
}
|
||||
},
|
||||
'update_time': d.get('update_time'),
|
||||
'transactions': [
|
||||
{
|
||||
'amount': t.get('amount')
|
||||
} for t in d.get('transactions', [])
|
||||
],
|
||||
'_shredded': True
|
||||
}
|
||||
obj.info = json.dumps(new)
|
||||
obj.save(update_fields=['info'])
|
||||
|
||||
for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'resource' in d:
|
||||
d['resource'] = {
|
||||
'id': d['resource'].get('id'),
|
||||
'sale_id': d['resource'].get('sale_id'),
|
||||
'parent_payment': d['resource'].get('parent_payment'),
|
||||
}
|
||||
},
|
||||
'update_time': d.get('update_time'),
|
||||
'transactions': [
|
||||
{
|
||||
'amount': t.get('amount')
|
||||
} for t in d.get('transactions', [])
|
||||
],
|
||||
'_shredded': True
|
||||
}
|
||||
order.payment_info = json.dumps(new)
|
||||
order.save(update_fields=['payment_info'])
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
@@ -4,10 +4,8 @@ from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.shredder import BaseDataShredder
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_data_shredders, register_payment_providers,
|
||||
requiredaction_display,
|
||||
logentry_display, register_payment_providers, requiredaction_display,
|
||||
)
|
||||
|
||||
|
||||
@@ -55,32 +53,3 @@ def pretixcontrol_action_display(sender, action, request, **kwargs):
|
||||
|
||||
ctx = {'data': data, 'event': sender, 'action': action}
|
||||
return template.render(ctx, request)
|
||||
|
||||
|
||||
class PaymentLogsShredder(BaseDataShredder):
|
||||
verbose_name = _('PayPal payment history')
|
||||
identifier = 'paypal_logs'
|
||||
description = _('This will remove payment-related history information. No download will be offered.')
|
||||
|
||||
def generate_files(self):
|
||||
pass
|
||||
|
||||
def shred_data(self):
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'resource' in d:
|
||||
d['resource'] = {
|
||||
'id': d['resource'].get('id'),
|
||||
'sale_id': d['resource'].get('sale_id'),
|
||||
'parent_payment': d['resource'].get('parent_payment'),
|
||||
}
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
@receiver(register_data_shredders, dispatch_uid="paypal_shredders")
|
||||
def register_shredder(sender, **kwargs):
|
||||
return [
|
||||
PaymentLogsShredder,
|
||||
]
|
||||
|
||||
@@ -7,14 +7,3 @@
|
||||
Do you want to mark the matching order ({{ order }}) as refunded?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-inline" method="post" action="{% url "plugins:paypal:refund" event=event.slug organizer=event.organizer.slug id=action.id %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.requiredaction.discard" event=event.slug organizer=event.organizer.slug id=action.id %}"
|
||||
class="btn btn-default">
|
||||
{% trans "No" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-default btn-danger">
|
||||
{% trans "Yes, mark order as refunded" %}
|
||||
</button>
|
||||
{% trans "This action cannot be undone." %}
|
||||
</form>
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
{% if order.status == "p" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been paid via PayPal.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif order.status == "r" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid via PayPal and has been marked as refunded.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid via PayPal, but the payment has not yet been completed.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Payment ID" %}</dt>
|
||||
<dd>{{ payment_info.id }}</dd>
|
||||
@@ -26,8 +13,4 @@
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid via PayPal, but the payment has not yet been completed.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
{% bootstrap_form form %}
|
||||
@@ -2,7 +2,7 @@ from django.conf.urls import include, url
|
||||
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from .views import abort, redirect_view, refund, success, webhook
|
||||
from .views import abort, redirect_view, success, webhook
|
||||
|
||||
event_patterns = [
|
||||
url(r'^paypal/', include([
|
||||
@@ -19,7 +19,5 @@ event_patterns = [
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/refund/(?P<id>\d+)/',
|
||||
refund, name='refund'),
|
||||
url(r'^_paypal/webhook/$', webhook, name='webhook'),
|
||||
]
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
import paypalrestsdk
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
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 pretix.base.models import Order, Quota, RequiredAction
|
||||
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.plugins.paypal.payment import Paypal
|
||||
@@ -50,16 +48,16 @@ def success(request, *args, **kwargs):
|
||||
if 'cart_namespace' in kwargs:
|
||||
urlkwargs['cart_namespace'] = kwargs['cart_namespace']
|
||||
|
||||
if request.session.get('payment_paypal_order'):
|
||||
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
||||
if request.session.get('payment_paypal_payment'):
|
||||
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||
else:
|
||||
order = None
|
||||
payment = None
|
||||
|
||||
if pid == request.session.get('payment_paypal_id', None):
|
||||
if order:
|
||||
if payment:
|
||||
prov = Paypal(request.event)
|
||||
try:
|
||||
resp = prov.payment_perform(request, order)
|
||||
resp = prov.execute_payment(request, payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
urlkwargs['step'] = 'payment'
|
||||
@@ -72,11 +70,11 @@ def success(request, *args, **kwargs):
|
||||
urlkwargs['step'] = 'payment'
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||
|
||||
if order:
|
||||
if payment:
|
||||
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
|
||||
'order': payment.order.code,
|
||||
'secret': payment.order.secret
|
||||
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
urlkwargs['step'] = 'confirm'
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
||||
@@ -85,16 +83,16 @@ def success(request, *args, **kwargs):
|
||||
def abort(request, *args, **kwargs):
|
||||
messages.error(request, _('It looks like you canceled the PayPal payment'))
|
||||
|
||||
if request.session.get('payment_paypal_order'):
|
||||
order = Order.objects.get(pk=request.session.get('payment_paypal_order'))
|
||||
if request.session.get('payment_paypal_payment'):
|
||||
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
|
||||
else:
|
||||
order = None
|
||||
payment = None
|
||||
|
||||
if order:
|
||||
if payment:
|
||||
return redirect(eventreverse(request.event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}) + ('?paid=yes' if order.status == Order.STATUS_PAID else ''))
|
||||
'order': payment.order.code,
|
||||
'secret': payment.order.secret
|
||||
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
|
||||
else:
|
||||
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
|
||||
|
||||
@@ -124,6 +122,7 @@ def webhook(request, *args, **kwargs):
|
||||
)
|
||||
event = rso.order.event
|
||||
except ReferencedPayPalObject.DoesNotExist:
|
||||
rso = None
|
||||
if hasattr(request, 'event'):
|
||||
event = request.event
|
||||
else:
|
||||
@@ -138,74 +137,67 @@ def webhook(request, *args, **kwargs):
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Sale not found', status=500)
|
||||
|
||||
orders = Order.objects.filter(event=event, payment_provider='paypal',
|
||||
payment_info__icontains=sale['id'])
|
||||
order = None
|
||||
for o in orders:
|
||||
payment_info = json.loads(o.payment_info)
|
||||
for res in payment_info['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale' and v['id'] == sale['id']:
|
||||
order = o
|
||||
break
|
||||
if rso and rso.payment:
|
||||
payment = rso.payment
|
||||
else:
|
||||
payments = OrderPayment.objects.filter(order__event=event, provider='paypal',
|
||||
info__icontains=sale['id'])
|
||||
payment = None
|
||||
for p in payments:
|
||||
payment_info = p.info_data
|
||||
for res in payment_info['transactions'][0]['related_resources']:
|
||||
for k, v in res.items():
|
||||
if k == 'sale' and v['id'] == sale['id']:
|
||||
payment = p
|
||||
break
|
||||
|
||||
if not order:
|
||||
return HttpResponse('Order not found', status=200)
|
||||
if not payment:
|
||||
return HttpResponse('Payment not found', status=200)
|
||||
|
||||
order.log_action('pretix.plugins.paypal.event', data=event_json)
|
||||
payment.order.log_action('pretix.plugins.paypal.event', data=event_json)
|
||||
|
||||
if order.status == Order.STATUS_PAID and sale['state'] in ('partially_refunded', 'refunded'):
|
||||
RequiredAction.objects.create(
|
||||
event=event, action_type='pretix.plugins.paypal.refund', data=json.dumps({
|
||||
'order': order.code,
|
||||
'sale': sale['id']
|
||||
})
|
||||
)
|
||||
elif order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and sale['state'] == 'completed' and \
|
||||
order.payment_provider != "paypal":
|
||||
RequiredAction.objects.create(
|
||||
event=event, action_type='pretix.plugins.paypal.double', data=json.dumps({
|
||||
'order': order.code,
|
||||
'payment': sale['parent_payment']
|
||||
})
|
||||
)
|
||||
elif order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and sale['state'] == 'completed':
|
||||
try:
|
||||
mark_order_paid(order, user=None)
|
||||
except Quota.QuotaExceededException:
|
||||
if not RequiredAction.objects.filter(event=event, action_type='pretix.plugins.paypal.overpaid',
|
||||
data__icontains=order.code).exists():
|
||||
RequiredAction.objects.create(
|
||||
event=event, action_type='pretix.plugins.paypal.overpaid', data=json.dumps({
|
||||
'order': order.code,
|
||||
'payment': sale['parent_payment']
|
||||
})
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
|
||||
if event_json['resource_type'] == 'refund':
|
||||
try:
|
||||
refund = paypalrestsdk.Refund.find(event_json['resource']['id'])
|
||||
except:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Refund not found', status=500)
|
||||
|
||||
known_refunds = {r.info_data.get('id'): r for r in payment.refunds.all()}
|
||||
if refund['id'] not in known_refunds:
|
||||
payment.create_external_refund(
|
||||
amount=abs(Decimal(refund['amount']['total'])),
|
||||
info=json.dumps(refund.to_dict() if not isinstance(refund, dict) else refund)
|
||||
)
|
||||
elif known_refunds.get(refund['id']).state in (
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
|
||||
known_refunds.get(refund['id']).done()
|
||||
|
||||
if 'total_refunded_amount' in refund:
|
||||
known_sum = payment.refunds.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||
total_refunded_amount = Decimal(refund['total_refunded_amount']['value'])
|
||||
if known_sum < total_refunded_amount:
|
||||
payment.create_external_refund(
|
||||
amount=total_refunded_amount - known_sum
|
||||
)
|
||||
elif sale['state'] == 'refunded':
|
||||
known_sum = payment.refunds.filter(
|
||||
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
|
||||
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
|
||||
|
||||
if known_sum < payment.amount:
|
||||
payment.create_external_refund(
|
||||
amount=payment.amount - known_sum
|
||||
)
|
||||
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED) and sale['state'] == 'completed':
|
||||
try:
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@event_permission_required('can_change_orders')
|
||||
@require_POST
|
||||
def refund(request, **kwargs):
|
||||
with transaction.atomic():
|
||||
action = get_object_or_404(RequiredAction, event=request.event, pk=kwargs.get('id'),
|
||||
action_type='pretix.plugins.paypal.refund', done=False)
|
||||
data = json.loads(action.data)
|
||||
action.done = True
|
||||
action.user = request.user
|
||||
action.save()
|
||||
order = get_object_or_404(Order, event=request.event, code=data['order'])
|
||||
if order.status != Order.STATUS_PAID:
|
||||
messages.error(request, _('The order cannot be marked as refunded as it is not marked as paid!'))
|
||||
else:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.success(
|
||||
request, _('The order has been marked as refunded and the issue has been marked as resolved!')
|
||||
)
|
||||
|
||||
return redirect(reverse('control:event.order', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'code': data['order']
|
||||
}))
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="obd_chart" class="chart"></div>
|
||||
<p class="help-block">
|
||||
<small>
|
||||
{% blocktrans trimmed %}
|
||||
Orders paid in multiple payments are shown with the date of their last payment.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
@@ -34,6 +41,14 @@
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="help-block">
|
||||
<small>
|
||||
{% blocktrans trimmed %}
|
||||
Only fully paid orders are counted.
|
||||
Orders paid in multiple payments are shown with the date of their last payment.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
||||
@@ -3,11 +3,13 @@ import json
|
||||
|
||||
import dateutil.parser
|
||||
import dateutil.rrule
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, DateTimeField, Max, OuterRef, Subquery
|
||||
from django.utils import timezone
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.base.models import Item, Order, OrderPosition, SubEvent
|
||||
from pretix.base.models import (
|
||||
Item, Order, OrderPayment, OrderPosition, SubEvent,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import ChartContainingView
|
||||
from pretix.plugins.statistics.signals import clear_cache
|
||||
@@ -35,10 +37,29 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
|
||||
cache = self.request.event.cache
|
||||
ckey = str(subevent.pk) if subevent else 'all'
|
||||
|
||||
p_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('pk'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
payment_date__isnull=False
|
||||
).order_by().values('order').annotate(
|
||||
m=Max('payment_date')
|
||||
).values(
|
||||
'm'
|
||||
)
|
||||
op_date = OrderPayment.objects.filter(
|
||||
order=OuterRef('order'),
|
||||
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED),
|
||||
payment_date__isnull=False
|
||||
).order_by().values('order').annotate(
|
||||
m=Max('payment_date')
|
||||
).values(
|
||||
'm'
|
||||
)
|
||||
|
||||
# Orders by day
|
||||
ctx['obd_data'] = cache.get('statistics_obd_data' + ckey)
|
||||
if not ctx['obd_data']:
|
||||
oqs = Order.objects
|
||||
oqs = Order.objects.annotate(payment_date=Subquery(p_date, output_field=DateTimeField()))
|
||||
if subevent:
|
||||
oqs = oqs.filter(positions__subevent_id=subevent).distinct()
|
||||
|
||||
@@ -106,16 +127,20 @@ class IndexView(EventPermissionRequiredMixin, ChartContainingView, TemplateView)
|
||||
if not ctx['rev_data']:
|
||||
rev_by_day = {}
|
||||
if subevent:
|
||||
for o in OrderPosition.objects.filter(order__event=self.request.event,
|
||||
subevent=subevent,
|
||||
order__status=Order.STATUS_PAID,
|
||||
order__payment_date__isnull=False).values('order__payment_date', 'price'):
|
||||
for o in OrderPosition.objects.annotate(
|
||||
payment_date=Subquery(op_date, output_field=DateTimeField())
|
||||
).filter(order__event=self.request.event,
|
||||
subevent=subevent,
|
||||
order__status=Order.STATUS_PAID,
|
||||
order__payment_date__isnull=False).values('order__payment_date', 'price'):
|
||||
day = o['order__payment_date'].astimezone(tz).date()
|
||||
rev_by_day[day] = rev_by_day.get(day, 0) + o['price']
|
||||
else:
|
||||
for o in Order.objects.filter(event=self.request.event,
|
||||
status=Order.STATUS_PAID,
|
||||
payment_date__isnull=False).values('payment_date', 'total'):
|
||||
for o in Order.objects.annotate(
|
||||
payment_date=Subquery(p_date, output_field=DateTimeField())
|
||||
).filter(event=self.request.event,
|
||||
status=Order.STATUS_PAID,
|
||||
payment_date__isnull=False).values('payment_date', 'total'):
|
||||
day = o['payment_date'].astimezone(tz).date()
|
||||
rev_by_day[day] = rev_by_day.get(day, 0) + o['total']
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -4,3 +4,4 @@ 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')
|
||||
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True)
|
||||
|
||||
@@ -9,6 +9,7 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -16,10 +17,10 @@ from django.utils.http import urlquote
|
||||
from django.utils.translation import pgettext, ugettext, ugettext_lazy as _
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.base.models import Event, Order, Quota, RequiredAction
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
@@ -29,18 +30,6 @@ from pretix.plugins.stripe.models import ReferencedStripeObject
|
||||
logger = logging.getLogger('pretix.plugins.stripe')
|
||||
|
||||
|
||||
class RefundForm(forms.Form):
|
||||
auto_refund = forms.ChoiceField(
|
||||
initial='auto',
|
||||
label=_('Refund automatically?'),
|
||||
choices=(
|
||||
('auto', _('Automatically refund charge with Stripe')),
|
||||
('manual', _('Do not send refund instruction to Stripe, only mark as refunded in pretix'))
|
||||
),
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
|
||||
class StripeSettingsHolder(BasePaymentProvider):
|
||||
identifier = 'stripe_settings'
|
||||
verbose_name = _('Stripe')
|
||||
@@ -233,12 +222,22 @@ class StripeMethod(BasePaymentProvider):
|
||||
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 payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
return True
|
||||
|
||||
def payment_partial_refund_supported(self, payment: OrderPayment) -> bool:
|
||||
return True
|
||||
|
||||
def payment_prepare(self, request, payment):
|
||||
return self.checkout_prepare(request, None)
|
||||
|
||||
def _get_amount(self, order):
|
||||
def _amount_to_decimal(self, cents):
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return int(order.total * 10 ** places)
|
||||
return round_decimal(float(cents) / (10 ** places), self.event.currency)
|
||||
|
||||
def _get_amount(self, payment):
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return int(payment.amount * 10 ** places)
|
||||
|
||||
@property
|
||||
def api_kwargs(self):
|
||||
@@ -268,29 +267,29 @@ class StripeMethod(BasePaymentProvider):
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_can_retry(self, order):
|
||||
return self._is_still_available(order=order)
|
||||
def payment_can_retry(self, payment):
|
||||
return self._is_still_available(order=payment.order)
|
||||
|
||||
def _charge_source(self, request, source, order):
|
||||
def _charge_source(self, request, source, payment):
|
||||
try:
|
||||
params = {}
|
||||
if not source.startswith('src_'):
|
||||
params['statement_descriptor'] = ugettext('{event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=order.code
|
||||
code=payment.order.code
|
||||
)[:22]
|
||||
params.update(self.api_kwargs)
|
||||
charge = stripe.Charge.create(
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
source=source,
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.order.code
|
||||
},
|
||||
# TODO: Is this sufficient?
|
||||
idempotency_key=str(self.event.id) + order.code + source,
|
||||
idempotency_key=str(self.event.id) + payment.order.code + source,
|
||||
**params
|
||||
)
|
||||
except stripe.error.CardError as e:
|
||||
@@ -301,11 +300,12 @@ class StripeMethod(BasePaymentProvider):
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
logger.info('Stripe card error: %s' % str(err))
|
||||
order.payment_info = json.dumps({
|
||||
payment.info_data = {
|
||||
'error': True,
|
||||
'message': err['message'],
|
||||
})
|
||||
order.save(update_fields=['payment_info'])
|
||||
}
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
raise PaymentException(_('Stripe reported an error with your card: %s') % err['message'])
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
@@ -315,25 +315,24 @@ class StripeMethod(BasePaymentProvider):
|
||||
else:
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
order.payment_info = json.dumps({
|
||||
payment.info_data = {
|
||||
'error': True,
|
||||
'message': err['message'],
|
||||
})
|
||||
order.save(update_fields=['payment_info'])
|
||||
}
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
|
||||
'with us if this problem persists.'))
|
||||
else:
|
||||
ReferencedStripeObject.objects.get_or_create(order=order, reference=charge.id)
|
||||
ReferencedStripeObject.objects.get_or_create(
|
||||
reference=charge.id,
|
||||
defaults={'order': payment.order, 'payment': payment}
|
||||
)
|
||||
if charge.status == 'succeeded' and charge.paid:
|
||||
try:
|
||||
mark_order_paid(order, self.identifier, str(charge))
|
||||
payment.info = str(charge)
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
RequiredAction.objects.create(
|
||||
event=self.event, action_type='pretix.plugins.stripe.overpaid', data=json.dumps({
|
||||
'order': order.code,
|
||||
'charge': charge.id
|
||||
})
|
||||
)
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
@@ -342,18 +341,20 @@ class StripeMethod(BasePaymentProvider):
|
||||
if request:
|
||||
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'])
|
||||
payment.info = str(charge)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return
|
||||
else:
|
||||
logger.info('Charge failed: %s' % str(charge))
|
||||
order.payment_info = str(charge)
|
||||
order.save(update_fields=['payment_info'])
|
||||
payment.info = str(charge)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message)
|
||||
|
||||
def order_pending_render(self, request, order) -> str:
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
if payment.info:
|
||||
payment_info = json.loads(payment.info)
|
||||
else:
|
||||
payment_info = None
|
||||
template = get_template('pretixplugins/stripe/pending.html')
|
||||
@@ -362,14 +363,15 @@ class StripeMethod(BasePaymentProvider):
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'provider': self,
|
||||
'order': order,
|
||||
'order': payment.order,
|
||||
'payment': payment,
|
||||
'payment_info': payment_info,
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_control_render(self, request, order) -> str:
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
def payment_control_render(self, request, payment) -> str:
|
||||
if payment.info:
|
||||
payment_info = json.loads(payment.info)
|
||||
if 'amount' in payment_info:
|
||||
payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
else:
|
||||
@@ -380,50 +382,25 @@ class StripeMethod(BasePaymentProvider):
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'payment_info': payment_info,
|
||||
'order': order,
|
||||
'payment': payment,
|
||||
'method': self.method,
|
||||
'provider': self,
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def _refund_form(self, request):
|
||||
return RefundForm(data=request.POST if request.method == "POST" else None)
|
||||
|
||||
def order_control_refund_render(self, order, request) -> str:
|
||||
template = get_template('pretixplugins/stripe/control_refund.html')
|
||||
ctx = {
|
||||
'request': request,
|
||||
'form': self._refund_form(request),
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def order_control_refund_perform(self, request, order) -> "bool|str":
|
||||
def execute_refund(self, refund: OrderRefund):
|
||||
self._init_api()
|
||||
|
||||
f = self._refund_form(request)
|
||||
if not f.is_valid():
|
||||
messages.error(request, _('Your input was invalid, please try again.'))
|
||||
return
|
||||
elif f.cleaned_data.get('auto_refund') == 'manual':
|
||||
order = mark_order_refunded(order, user=request.user)
|
||||
order.payment_manual = True
|
||||
order.save()
|
||||
return
|
||||
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
else:
|
||||
payment_info = None
|
||||
payment_info = refund.payment.info_data
|
||||
|
||||
if not payment_info:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.warning(request, _('We were unable to transfer the money back automatically. '
|
||||
'Please get in touch with the customer and transfer it back manually.'))
|
||||
return
|
||||
raise PaymentException(_('No payment information found.'))
|
||||
|
||||
try:
|
||||
ch = stripe.Charge.retrieve(payment_info['id'], **self.api_kwargs)
|
||||
ch.refunds.create()
|
||||
ch.refunds.create(
|
||||
amount=self._get_amount(refund),
|
||||
)
|
||||
ch.refresh()
|
||||
except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \
|
||||
as e:
|
||||
@@ -433,22 +410,18 @@ class StripeMethod(BasePaymentProvider):
|
||||
else:
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
messages.error(request, _('We had trouble communicating with Stripe. Please try again and contact '
|
||||
'support if the problem persists.'))
|
||||
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and contact '
|
||||
'support if the problem persists.'))
|
||||
except stripe.error.StripeError as err:
|
||||
logger.error('Stripe error: %s' % str(err))
|
||||
except stripe.error.StripeError:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.warning(request, _('We were unable to transfer the money back automatically. '
|
||||
'Please get in touch with the customer and transfer it back manually.'))
|
||||
raise PaymentException(_('Stripe returned an error'))
|
||||
else:
|
||||
order = mark_order_refunded(order, user=request.user)
|
||||
order.payment_info = str(ch)
|
||||
order.save()
|
||||
refund.done()
|
||||
|
||||
def payment_perform(self, request, order) -> str:
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
self._init_api()
|
||||
try:
|
||||
source = self._create_source(request, order)
|
||||
source = self._create_source(request, payment)
|
||||
except stripe.error.StripeError as e:
|
||||
if e.json_body:
|
||||
err = e.json_body['error']
|
||||
@@ -456,18 +429,23 @@ class StripeMethod(BasePaymentProvider):
|
||||
else:
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
order.payment_info = json.dumps({
|
||||
payment.info_data = {
|
||||
'error': True,
|
||||
'message': err['message'],
|
||||
})
|
||||
order.save(update_fields=['payment_info'])
|
||||
}
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
|
||||
'with us if this problem persists.'))
|
||||
|
||||
ReferencedStripeObject.objects.get_or_create(order=order, reference=source.id)
|
||||
order.payment_info = str(source)
|
||||
order.save(update_fields=['payment_info'])
|
||||
request.session['payment_stripe_order_secret'] = order.secret
|
||||
ReferencedStripeObject.objects.get_or_create(
|
||||
reference=source.id,
|
||||
defaults={'order': payment.order, 'payment': payment}
|
||||
)
|
||||
payment.info = str(source)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
request.session['payment_stripe_order_secret'] = payment.order.secret
|
||||
return self.redirect(request, source.redirect.url)
|
||||
|
||||
def redirect(self, request, url):
|
||||
@@ -480,10 +458,10 @@ class StripeMethod(BasePaymentProvider):
|
||||
else:
|
||||
return str(url)
|
||||
|
||||
def shred_payment_info(self, order: Order):
|
||||
if not order.payment_info:
|
||||
def shred_payment_info(self, obj: OrderPayment):
|
||||
if not obj.info:
|
||||
return
|
||||
d = json.loads(order.payment_info)
|
||||
d = json.loads(obj.info)
|
||||
new = {}
|
||||
if 'source' in d:
|
||||
new['source'] = {
|
||||
@@ -500,13 +478,30 @@ class StripeMethod(BasePaymentProvider):
|
||||
'last4': d['source'].get('card', {}).get('last4'),
|
||||
}
|
||||
}
|
||||
if 'amount' in d:
|
||||
new['amount'] = d['amount']
|
||||
if 'currency' in d:
|
||||
new['currency'] = d['currency']
|
||||
if 'status' in d:
|
||||
new['status'] = d['status']
|
||||
if 'id' in d:
|
||||
new['id'] = d['id']
|
||||
new['_shredded'] = True
|
||||
order.payment_info = json.dumps(new)
|
||||
order.save(update_fields=['payment_info'])
|
||||
|
||||
new['_shredded'] = True
|
||||
obj.info = json.dumps(new)
|
||||
obj.save(update_fields=['info'])
|
||||
|
||||
for le in obj.order.all_logentries().filter(
|
||||
action_type="pretix.plugins.stripe.event"
|
||||
).exclude(data="", shredded=True):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for k, v in list(d['data']['object'].items()):
|
||||
if v not in ('reason', 'status', 'failure_message', 'object', 'id'):
|
||||
d['data']['object'][k] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
|
||||
|
||||
class StripeCC(StripeMethod):
|
||||
@@ -549,41 +544,47 @@ class StripeCC(StripeMethod):
|
||||
else:
|
||||
return card.three_d_secure == 'required'
|
||||
|
||||
def payment_perform(self, request, order) -> str:
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
self._init_api()
|
||||
|
||||
if request.session['payment_stripe_token'].startswith('src_'):
|
||||
try:
|
||||
src = stripe.Source.retrieve(request.session['payment_stripe_token'], **self.api_kwargs)
|
||||
if src.type == 'card' and src.card and self._use_3ds(src.card):
|
||||
request.session['payment_stripe_order_secret'] = order.secret
|
||||
request.session['payment_stripe_order_secret'] = payment.order.secret
|
||||
source = stripe.Source.create(
|
||||
type='three_d_secure',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
three_d_secure={
|
||||
'card': src.id
|
||||
},
|
||||
statement_descriptor=ugettext('{event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=order.code
|
||||
code=payment.order.code
|
||||
)[:22],
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.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(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
)
|
||||
ReferencedStripeObject.objects.get_or_create(
|
||||
reference=source.id,
|
||||
defaults={'order': payment.order, 'payment': payment}
|
||||
)
|
||||
if source.status == "pending":
|
||||
order.payment_info = str(source)
|
||||
order.save(update_fields=['payment_info'])
|
||||
payment.info = str(source)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return self.redirect(request, source.redirect.url)
|
||||
except stripe.error.StripeError as e:
|
||||
if e.json_body:
|
||||
@@ -592,16 +593,17 @@ class StripeCC(StripeMethod):
|
||||
else:
|
||||
err = {'message': str(e)}
|
||||
logger.exception('Stripe error: %s' % str(e))
|
||||
order.payment_info = json.dumps({
|
||||
payment.info_data = {
|
||||
'error': True,
|
||||
'message': err['message'],
|
||||
})
|
||||
order.save(update_fields=['payment_info'])
|
||||
}
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
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)
|
||||
self._charge_source(request, request.session['payment_stripe_token'], payment)
|
||||
finally:
|
||||
del request.session['payment_stripe_token']
|
||||
|
||||
@@ -628,16 +630,16 @@ class StripeGiropay(StripeMethod):
|
||||
('account', forms.CharField(label=_('Account holder'))),
|
||||
])
|
||||
|
||||
def _create_source(self, request, order):
|
||||
def _create_source(self, request, payment):
|
||||
try:
|
||||
source = stripe.Source.create(
|
||||
type='giropay',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.order.code
|
||||
},
|
||||
owner={
|
||||
'name': request.session.get('payment_stripe_giropay_account') or ugettext('unknown name')
|
||||
@@ -645,13 +647,14 @@ class StripeGiropay(StripeMethod):
|
||||
giropay={
|
||||
'statement_descriptor': ugettext('{event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=order.code
|
||||
code=payment.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(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
@@ -689,26 +692,27 @@ class StripeIdeal(StripeMethod):
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def _create_source(self, request, order):
|
||||
def _create_source(self, request, payment):
|
||||
source = stripe.Source.create(
|
||||
type='ideal',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.order.code
|
||||
},
|
||||
ideal={
|
||||
'statement_descriptor': ugettext('{event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=order.code
|
||||
code=payment.order.code
|
||||
)[:22]
|
||||
},
|
||||
redirect={
|
||||
'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
|
||||
'order': order.code,
|
||||
'hash': hashlib.sha1(order.secret.lower().encode()).hexdigest(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
@@ -737,20 +741,21 @@ class StripeAlipay(StripeMethod):
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def _create_source(self, request, order):
|
||||
def _create_source(self, request, payment):
|
||||
source = stripe.Source.create(
|
||||
type='alipay',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.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(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
@@ -786,16 +791,16 @@ class StripeBancontact(StripeMethod):
|
||||
('account', forms.CharField(label=_('Account holder'), min_length=3)),
|
||||
])
|
||||
|
||||
def _create_source(self, request, order):
|
||||
def _create_source(self, request, payment):
|
||||
try:
|
||||
source = stripe.Source.create(
|
||||
type='bancontact',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.order.code
|
||||
},
|
||||
owner={
|
||||
'name': request.session.get('payment_stripe_bancontact_account') or ugettext('unknown name')
|
||||
@@ -803,13 +808,14 @@ class StripeBancontact(StripeMethod):
|
||||
bancontact={
|
||||
'statement_descriptor': ugettext('{event}-{code}').format(
|
||||
event=self.event.slug.upper(),
|
||||
code=order.code
|
||||
code=payment.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(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
@@ -860,27 +866,28 @@ class StripeSofort(StripeMethod):
|
||||
))),
|
||||
])
|
||||
|
||||
def _create_source(self, request, order):
|
||||
def _create_source(self, request, payment):
|
||||
source = stripe.Source.create(
|
||||
type='sofort',
|
||||
amount=self._get_amount(order),
|
||||
amount=self._get_amount(payment),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
'order': str(payment.order.id),
|
||||
'event': self.event.id,
|
||||
'code': order.code
|
||||
'code': payment.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
|
||||
code=payment.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(),
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
},
|
||||
**self.api_kwargs
|
||||
@@ -899,12 +906,5 @@ class StripeSofort(StripeMethod):
|
||||
return True
|
||||
return False
|
||||
|
||||
def order_can_retry(self, order):
|
||||
try:
|
||||
if order.payment_info:
|
||||
d = json.loads(order.payment_info)
|
||||
if d.get('object') == 'charge' and d.get('status') == 'pending':
|
||||
return False
|
||||
except ValueError:
|
||||
pass
|
||||
return self._is_still_available(order=order)
|
||||
def payment_can_retry(self, payment):
|
||||
return payment.state != OrderPayment.PAYMENT_STATE_PENDING and self._is_still_available(order=payment.order)
|
||||
|
||||
@@ -8,10 +8,9 @@ from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.settings import settings_hierarkey
|
||||
from pretix.base.shredder import BaseDataShredder
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_data_shredders, register_global_settings,
|
||||
register_payment_providers, requiredaction_display,
|
||||
logentry_display, register_global_settings, register_payment_providers,
|
||||
requiredaction_display,
|
||||
)
|
||||
from pretix.plugins.stripe.forms import StripeKeyValidator
|
||||
from pretix.presale.signals import html_head
|
||||
@@ -74,24 +73,6 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
return _('Stripe reported an event: {}').format(text)
|
||||
|
||||
|
||||
@receiver(signal=requiredaction_display, dispatch_uid="stripe_requiredaction_display")
|
||||
def pretixcontrol_action_display(sender, action, request, **kwargs):
|
||||
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)
|
||||
|
||||
|
||||
settings_hierarkey.add_default('payment_stripe_method_cc', True, bool)
|
||||
settings_hierarkey.add_default('payment_stripe_cc_3ds_mode', 'recommended', str)
|
||||
|
||||
@@ -137,28 +118,20 @@ def register_global_settings(sender, **kwargs):
|
||||
])
|
||||
|
||||
|
||||
class PaymentLogsShredder(BaseDataShredder):
|
||||
verbose_name = _('Stripe payment history')
|
||||
identifier = 'stripe_logs'
|
||||
description = _('This will remove payment-related history information. No download will be offered.')
|
||||
@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
|
||||
|
||||
def generate_files(self):
|
||||
pass
|
||||
data = json.loads(action.data)
|
||||
|
||||
def shred_data(self):
|
||||
for le in self.event.logentry_set.filter(action_type="pretix.plugins.stripe.event").exclude(data=""):
|
||||
d = le.parsed_data
|
||||
if 'data' in d:
|
||||
for k, v in list(d['data']['object'].items()):
|
||||
if v not in ('reason', 'status', 'failure_message', 'object', 'id'):
|
||||
d['data']['object'][k] = '█'
|
||||
le.data = json.dumps(d)
|
||||
le.shredded = True
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
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')
|
||||
|
||||
|
||||
@receiver(register_data_shredders, dispatch_uid="stripe_shredders")
|
||||
def register_shredder(sender, **kwargs):
|
||||
return [
|
||||
PaymentLogsShredder,
|
||||
]
|
||||
ctx = {'data': data, 'event': sender, 'action': action}
|
||||
return template.render(ctx, request)
|
||||
|
||||
@@ -7,14 +7,3 @@
|
||||
Do you want to refund mark the matching order ({{ order }}) as refunded?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-inline" method="post" action="{% url "plugins:stripe:refund" event=event.slug organizer=event.organizer.slug id=action.id %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.requiredaction.discard" event=event.slug organizer=event.organizer.slug id=action.id %}"
|
||||
class="btn btn-default">
|
||||
{% trans "No" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-default btn-danger">
|
||||
{% trans "Yes, mark order as refunded" %}
|
||||
</button>
|
||||
{% trans "This action cannot be undone." %}
|
||||
</form>
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
{% if order.status == "p" %}
|
||||
<p>{% blocktrans trimmed with method=provider.verbose_name %}
|
||||
This order has been paid with {{ method }}.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif order.status == "r" %}
|
||||
<p>{% blocktrans trimmed with method=provider.verbose_name %}
|
||||
This order has been planned to be paid with {{ method }} and has been marked as refunded.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed with method=provider.verbose_name %}
|
||||
This order has been planned to be paid with {{ method }}, but the payment has not yet been completed.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% if "source" in payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Charge ID" %}</dt>
|
||||
<dd>{{ payment_info.id }}</dd>
|
||||
{% if payment_info.source.type == "card" or payment_info.source.type == "three_d_secure" %}
|
||||
{% if payment_info.source.card %}
|
||||
<dt>{% trans "Card type" %}</dt>
|
||||
<dd>{{ payment_info.source.brand }}</dd>
|
||||
<dd>{{ payment_info.source.card.brand }}</dd>
|
||||
<dt>{% trans "Card number" %}</dt>
|
||||
<dd>**** **** **** {{ payment_info.source.last4 }}</dd>
|
||||
<dt>{% trans "Payer name" %}</dt>
|
||||
<dd>{{ payment_info.source.name }}</dd>
|
||||
<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>
|
||||
@@ -58,8 +47,4 @@
|
||||
<dd>{{ payment_info.message }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
This order has been planned to be paid via Stripe, but the payment has not yet been completed.
|
||||
{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
{% bootstrap_form form %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if provider.method == "sofort" %}
|
||||
{% 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.
|
||||
|
||||
@@ -3,20 +3,18 @@ from django.conf.urls import include, url
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
ReturnView, oauth_disconnect, oauth_return, redirect_view, refund, webhook,
|
||||
ReturnView, 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>[^/]+)/$', ReturnView.as_view(), name='return'),
|
||||
url(r'^return/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/stripe/refund/(?P<id>\d+)/',
|
||||
refund, name='refund'),
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/stripe/disconnect/',
|
||||
oauth_disconnect, name='oauth.disconnect'),
|
||||
url(r'^_stripe/webhook/$', webhook, name='webhook'),
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
def refresh_order(order):
|
||||
if not order.payment_provider.startswith('stripe_'):
|
||||
raise ValueError("Not a stripe payment")
|
||||
|
||||
prov = order.event.get_payment_providers()[order.payment_provider]
|
||||
prov._init_api()
|
||||
|
||||
@@ -18,10 +18,9 @@ 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 pretix.base.models import Event, Order, Quota, RequiredAction
|
||||
from pretix.base.models import Event, Order, OrderPayment, Quota
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.locking import LockTimeoutException
|
||||
from pretix.base.services.orders import mark_order_paid, mark_order_refunded
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
@@ -105,8 +104,9 @@ def oauth_return(request, *args, **kwargs):
|
||||
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.'))
|
||||
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']
|
||||
@@ -156,19 +156,30 @@ def webhook(request, *args, **kwargs):
|
||||
|
||||
try:
|
||||
rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get(reference=objid)
|
||||
return func(rso.order.event, event_json, objid)
|
||||
return func(rso.order.event, event_json, objid, rso)
|
||||
except ReferencedStripeObject.DoesNotExist:
|
||||
if hasattr(request, 'event'):
|
||||
return func(request.event, event_json, objid)
|
||||
return func(request.event, event_json, objid, None)
|
||||
else:
|
||||
return HttpResponse("Unable to detect event", status=200)
|
||||
|
||||
|
||||
def charge_webhook(event, event_json, charge_id):
|
||||
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, **prov.api_kwargs)
|
||||
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)
|
||||
@@ -180,55 +191,71 @@ def charge_webhook(event, event_json, charge_id):
|
||||
if int(metadata['event']) != event.pk:
|
||||
return HttpResponse('Not interested in this event', status=200)
|
||||
|
||||
try:
|
||||
order = event.orders.get(id=metadata['order'], payment_provider__startswith='stripe')
|
||||
except Order.DoesNotExist:
|
||||
return HttpResponse('Order not found', 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
|
||||
|
||||
if order.payment_provider != prov.identifier:
|
||||
prov = event.get_payment_providers()[order.payment_provider]
|
||||
if not payment:
|
||||
payment = order.payments.filter(
|
||||
info__icontains=charge['id'],
|
||||
provider__startswith='stripe',
|
||||
amount=prov._amount_to_decimal(charge['amount']),
|
||||
).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 order.status == Order.STATUS_PAID and is_refund:
|
||||
RequiredAction.objects.create(
|
||||
event=event, action_type='pretix.plugins.stripe.refund', data=json.dumps({
|
||||
'order': order.code,
|
||||
'charge': charge_id
|
||||
})
|
||||
)
|
||||
elif order.status == Order.STATUS_PAID and not order.payment_provider.startswith('stripe') and charge['status'] == 'succeeded' and not is_refund:
|
||||
RequiredAction.objects.create(
|
||||
event=event,
|
||||
action_type='pretix.plugins.stripe.double',
|
||||
data=json.dumps({
|
||||
'order': order.code,
|
||||
'charge': charge.id
|
||||
})
|
||||
)
|
||||
elif order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and charge['status'] == 'succeeded' and not is_refund:
|
||||
try:
|
||||
mark_order_paid(order, user=None)
|
||||
except LockTimeoutException:
|
||||
return HttpResponse("Lock timeout, please try again.", status=503)
|
||||
except Quota.QuotaExceededException:
|
||||
if not RequiredAction.objects.filter(event=event, action_type='pretix.plugins.stripe.overpaid',
|
||||
data__icontains=order.code).exists():
|
||||
RequiredAction.objects.create(
|
||||
event=event,
|
||||
action_type='pretix.plugins.stripe.overpaid',
|
||||
data=json.dumps({
|
||||
'order': order.code,
|
||||
'charge': charge.id
|
||||
})
|
||||
if is_refund:
|
||||
known_refunds = [r.info_data.get('id') for r in payment.refunds.all()]
|
||||
for r in charge['refunds']['data']:
|
||||
if r['id'] not in known_refunds:
|
||||
payment.create_external_refund(
|
||||
amount=prov._amount_to_decimal(r['amount']),
|
||||
info=str(r)
|
||||
)
|
||||
if charge['dispute']:
|
||||
if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds:
|
||||
payment.create_external_refund(
|
||||
amount=prov._amount_to_decimal(charge['dispute']['amount']),
|
||||
info=str(charge['dispute'])
|
||||
)
|
||||
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
|
||||
if charge['status'] == 'succeeded':
|
||||
try:
|
||||
payment.confirm()
|
||||
except LockTimeoutException:
|
||||
return HttpResponse("Lock timeout, please try again.", status=503)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
elif charge['status'] == 'failed':
|
||||
payment.info = str(charge)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
def source_webhook(event, event_json, source_id):
|
||||
def source_webhook(event, event_json, source_id, rso):
|
||||
prov = StripeCC(event)
|
||||
prov._init_api()
|
||||
try:
|
||||
@@ -245,24 +272,52 @@ def source_webhook(event, event_json, source_id):
|
||||
return HttpResponse('Not interested in this event', status=200)
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
order = event.orders.get(id=metadata['order'], payment_provider__startswith='stripe')
|
||||
except Order.DoesNotExist:
|
||||
return HttpResponse('Order not found', 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
|
||||
|
||||
if order.payment_provider != prov.identifier:
|
||||
prov = event.get_payment_providers()[order.payment_provider]
|
||||
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 order.status == Order.STATUS_PENDING and
|
||||
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, order)
|
||||
prov._charge_source(None, source_id, payment)
|
||||
except PaymentException:
|
||||
logger.exception('Webhook error')
|
||||
|
||||
elif src.status == 'failed':
|
||||
payment.info = str(src)
|
||||
payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
payment.save()
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@@ -285,32 +340,6 @@ def oauth_disconnect(request, **kwargs):
|
||||
}))
|
||||
|
||||
|
||||
@event_permission_required('can_change_orders')
|
||||
@require_POST
|
||||
def refund(request, **kwargs):
|
||||
with transaction.atomic():
|
||||
action = get_object_or_404(RequiredAction, event=request.event, pk=kwargs.get('id'),
|
||||
action_type='pretix.plugins.stripe.refund', done=False)
|
||||
data = json.loads(action.data)
|
||||
action.done = True
|
||||
action.user = request.user
|
||||
action.save()
|
||||
order = get_object_or_404(Order, event=request.event, code=data['order'])
|
||||
if order.status != Order.STATUS_PAID:
|
||||
messages.error(request, _('The order cannot be marked as refunded as it is not marked as paid!'))
|
||||
else:
|
||||
mark_order_refunded(order, user=request.user)
|
||||
messages.success(
|
||||
request, _('The order has been marked as refunded and the issue has been marked as resolved!')
|
||||
)
|
||||
|
||||
return redirect(reverse('control:event.order', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'code': data['order']
|
||||
}))
|
||||
|
||||
|
||||
class StripeOrderView:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
@@ -325,9 +354,15 @@ class StripeOrderView:
|
||||
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.order.payment_provider]
|
||||
return self.request.event.get_payment_providers()[self.payment.provider]
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
@@ -343,14 +378,15 @@ class ReturnView(StripeOrderView, View):
|
||||
|
||||
with transaction.atomic():
|
||||
self.order.refresh_from_db()
|
||||
if self.order.status == Order.STATUS_PAID:
|
||||
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.order)
|
||||
prov._charge_source(request, src.id, self.payment)
|
||||
except PaymentException as e:
|
||||
messages.error(request, str(e))
|
||||
return self._redirect_to_order()
|
||||
@@ -358,6 +394,9 @@ class ReturnView(StripeOrderView, View):
|
||||
if 'payment_stripe_token' in request.session:
|
||||
del request.session['payment_stripe_token']
|
||||
else:
|
||||
self.payment.state = OrderPayment.PAYMENT_STATE_FAILED
|
||||
self.payment.info = str(src)
|
||||
self.payment.save()
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user