Fix #571 -- Partial payments and refunds

This commit is contained in:
Raphael Michel
2018-06-26 12:09:36 +02:00
parent 8e7af49206
commit 18a378976b
115 changed files with 6026 additions and 1598 deletions

View File

@@ -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'),
),
]

View File

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

View File

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

View File

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

View File

@@ -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>&nbsp;
{% trans "This action cannot be undone." %}
</form>

View File

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

View File

@@ -1,2 +0,0 @@
{% load bootstrap3 %}
{% bootstrap_form form %}

View File

@@ -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'),
]

View File

@@ -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']
}))