forked from CGM_Public/pretix_original
Allow customers to choose to receive their refund as a gift card (#1626)
* Minor text adjustments * Allow users to receive their cancellation as a gift card
This commit is contained in:
@@ -612,6 +612,7 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -1210,6 +1210,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
)
|
||||
refund.info_data = {
|
||||
'gift_card': gc.pk,
|
||||
'gift_card_code': gc.secret,
|
||||
'transaction_id': trans.pk,
|
||||
}
|
||||
refund.done()
|
||||
|
||||
@@ -1888,7 +1888,8 @@ def perform_order(self, event: Event, payment_provider: str, positions: List[str
|
||||
raise OrderError(str(error_messages['busy']))
|
||||
|
||||
|
||||
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER):
|
||||
def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=OrderRefund.REFUND_SOURCE_BUYER,
|
||||
refund_as_giftcard=False):
|
||||
notify_admin = False
|
||||
error = False
|
||||
if isinstance(order, int):
|
||||
@@ -1897,9 +1898,49 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
if refund_amount <= Decimal('0.00'):
|
||||
return
|
||||
|
||||
proposals = order.propose_auto_refunds(refund_amount)
|
||||
can_auto_refund_sum = sum(proposals.values())
|
||||
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
|
||||
if refund_as_giftcard:
|
||||
proposals = {}
|
||||
can_auto_refund = True
|
||||
can_auto_refund_sum = refund_amount
|
||||
with transaction.atomic():
|
||||
giftcard = order.event.organizer.issued_gift_cards.create(
|
||||
currency=order.event.currency,
|
||||
testmode=order.testmode
|
||||
)
|
||||
giftcard.log_action('pretix.giftcards.created', data={})
|
||||
r = order.refunds.create(
|
||||
order=order,
|
||||
payment=None,
|
||||
source=source,
|
||||
state=OrderRefund.REFUND_STATE_CREATED,
|
||||
execution_date=now(),
|
||||
amount=can_auto_refund_sum,
|
||||
provider='giftcard',
|
||||
info=json.dumps({
|
||||
'gift_card': giftcard.pk
|
||||
})
|
||||
)
|
||||
try:
|
||||
r.payment_provider.execute_refund(r)
|
||||
except PaymentException as e:
|
||||
with transaction.atomic():
|
||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||
r.save()
|
||||
order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': r.local_id,
|
||||
'provider': r.provider,
|
||||
'error': str(e)
|
||||
})
|
||||
error = True
|
||||
notify_admin = True
|
||||
else:
|
||||
if r.state != OrderRefund.REFUND_STATE_DONE:
|
||||
notify_admin = True
|
||||
|
||||
else:
|
||||
proposals = order.propose_auto_refunds(refund_amount)
|
||||
can_auto_refund_sum = sum(proposals.values())
|
||||
can_auto_refund = (allow_partial and can_auto_refund_sum) or can_auto_refund_sum == refund_amount
|
||||
if can_auto_refund:
|
||||
for p, value in proposals.items():
|
||||
with transaction.atomic():
|
||||
@@ -1961,13 +2002,13 @@ def _try_auto_refund(order, manual_refund=False, allow_partial=False, source=Ord
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
@scopes_disabled()
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False):
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False):
|
||||
try:
|
||||
try:
|
||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||
cancellation_fee)
|
||||
if try_auto_refund:
|
||||
_try_auto_refund(order)
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard)
|
||||
return ret
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
|
||||
@@ -917,6 +917,29 @@ DEFAULTS = {
|
||||
help_text=_("With this option enabled, your customers can choose to get a smaller refund to support you.")
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_refund_as_giftcard': {
|
||||
'default': 'off',
|
||||
'type': str,
|
||||
'serializer_class': serializers.ChoiceField,
|
||||
'serializer_kwargs': dict(
|
||||
choices=[
|
||||
('off', _('All refunds are issued to the original payment method')),
|
||||
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||
('force', _('All refunds are issued as gift cards')),
|
||||
],
|
||||
),
|
||||
'form_class': forms.ChoiceField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Refund method'),
|
||||
choices=[
|
||||
('off', _('All refunds are issued to the original payment method')),
|
||||
('option', _('Customers can choose between a gift card and a refund to their payment method')),
|
||||
('force', _('All refunds are issued as gift cards')),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
# When adding a new ordering, remember to also define it in the event model
|
||||
)
|
||||
},
|
||||
'cancel_allow_user_paid_until': {
|
||||
'default': None,
|
||||
'type': RelativeDateWrapper,
|
||||
|
||||
@@ -566,6 +566,7 @@ class CancelSettingsForm(SettingsForm):
|
||||
'cancel_allow_user_paid_keep_fees',
|
||||
'cancel_allow_user_paid_keep_percentage',
|
||||
'cancel_allow_user_paid_adjust_fees',
|
||||
'cancel_allow_user_paid_refund_as_giftcard',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
{% bootstrap_field form.cancel_allow_user_paid_keep_fees layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_until layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_adjust_fees layout="control" %}
|
||||
{% bootstrap_field form.cancel_allow_user_paid_refund_as_giftcard layout="control" %}
|
||||
{% if not gets_notification %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -102,10 +102,17 @@
|
||||
A refund of {{ amount }} will be sent out to you soon, please be patient.
|
||||
{% endblocktrans %}
|
||||
{% elif r.state == "done" %}
|
||||
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
|
||||
A refund of {{ amount }} has been sent to you. Depending on the payment method, please allow for up to 14 days until it shows up
|
||||
on your statement.
|
||||
{% endblocktrans %}
|
||||
{% if r.provider == "giftcard" and "gift_card_code" in r.info_data %}
|
||||
{% blocktrans trimmed with amount=r.amount|money:request.event.currency code=r.info_data.gift_card_code %}
|
||||
We've issued your refund of {{ amount }} as a gift card. On your next purchase with
|
||||
us, you can use the gift card code <strong>{{ code }}</strong> during payment.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed with amount=r.amount|money:request.event.currency %}
|
||||
A refund of {{ amount }} has been sent to you. Depending on the payment method, please allow for up to 14 days until it shows up
|
||||
on your statement.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not forloop.last %}<br />{% endif %}
|
||||
{% endfor %}
|
||||
@@ -271,19 +278,40 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if order.status == "p" and order.total != 0 %}
|
||||
{% if order.user_cancel_fee %}
|
||||
{% if order.user_cancel_fee >= order.total %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can cancel this order, but you will not receive a refund.
|
||||
{% endblocktrans %}
|
||||
{% trans "This will invalidate all tickets in this order." %}
|
||||
</p>
|
||||
{% elif order.user_cancel_fee %}
|
||||
<p>
|
||||
{% blocktrans trimmed with fee=order.user_cancel_fee|money:request.event.currency %}
|
||||
You can cancel this order. In this case, a cancellation fee of <strong>{{ fee }}</strong>
|
||||
will be kept and you will receive a refund of the remainder to your original payment method.
|
||||
will be kept and you will receive a refund of the remainder.
|
||||
{% endblocktrans %}
|
||||
{% if request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "force" %}
|
||||
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
|
||||
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "option" %}
|
||||
{% trans "The refund can be issued to your original payment method or as a gift card." %}
|
||||
{% else %}
|
||||
{% trans "The refund will be issued to your original payment method." %}
|
||||
{% endif %}
|
||||
{% trans "This will invalidate all tickets in this order." %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can cancel this order and receive a full refund to your original payment method.
|
||||
You can cancel this order and receive a full refund.
|
||||
{% endblocktrans %}
|
||||
{% if request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "force" %}
|
||||
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
|
||||
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "option" %}
|
||||
{% trans "The refund can be issued to your original payment method or as a gift card." %}
|
||||
{% else %}
|
||||
{% trans "The refund will be issued to your original payment method." %}
|
||||
{% endif %}
|
||||
{% trans "This will invalidate all tickets in this order." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
Cancel order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h2>
|
||||
<form method="post" action="{% eventurl request.event "presale:event.order.cancel.do" secret=order.secret order=order.code %}" data-asynctask>
|
||||
<form method="post"
|
||||
action="{% eventurl request.event "presale:event.order.cancel.do" secret=order.secret order=order.code %}"
|
||||
data-asynctask
|
||||
class="">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Do you really want to cancel this order? You cannot revert this action.
|
||||
If you cancel this order, all tickets will be invalidated and you can no longer use them. You cannot
|
||||
revert this action.
|
||||
{% endblocktrans %}
|
||||
{% trans "This will invalidate all tickets in this order." %}
|
||||
</p>
|
||||
|
||||
{% if request.event.settings.cancel_allow_user_paid_adjust_fees %}
|
||||
@@ -57,25 +60,62 @@
|
||||
{% endif %}
|
||||
|
||||
{% if refund_amount %}
|
||||
{% if can_auto_refund %}
|
||||
<p>
|
||||
<strong>
|
||||
{% if request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "force" %}
|
||||
<strong>
|
||||
{% trans "The refund will be issued in form of a gift card that you can use for further purchases." %}
|
||||
</strong>
|
||||
{% elif request.event.settings.cancel_allow_user_paid_refund_as_giftcard == "option" %}
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="giftcard" value="true" checked>
|
||||
<strong>{% trans "I want the refund as a gift card for later purchases" %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="giftcard" value="false" checked>
|
||||
<strong>{% trans "I want the refund to be sent to my original payment method" %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
{% if can_auto_refund %}
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
The refund amount will automatically be sent back to your original payment method. Depending
|
||||
on the payment method, please allow for up to two weeks before this appears on your
|
||||
statement.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</p>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
With the payment method you used, the refund amount <strong>can not be sent back to you
|
||||
automatically</strong>. Instead, the event organizer will need to initiate the transfer
|
||||
manually. Please be patient as this might take a bit longer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
With the payment method you used, the refund amount <strong>can not be sent back to you
|
||||
automatically</strong>. Instead, the event organizer will need to initiate the transfer
|
||||
manually. Please be patient as this might take a bit longer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if can_auto_refund %}
|
||||
<p>
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
The refund amount will automatically be sent back to your original payment method. Depending
|
||||
on the payment method, please allow for up to two weeks before this appears on your
|
||||
statement.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
With the payment method you used, the refund amount <strong>can not be sent back to you
|
||||
automatically</strong>. Instead, the event organizer will need to initiate the transfer
|
||||
manually. Please be patient as this might take a bit longer.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
@@ -759,7 +759,13 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
|
||||
else:
|
||||
messages.error(request, _('You chose an invalid cancellation fee.'))
|
||||
return redirect(self.get_order_url())
|
||||
return self.do(self.order.pk, cancellation_fee=fee, try_auto_refund=True)
|
||||
giftcard = (
|
||||
self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard == 'force' or (
|
||||
self.request.event.settings.cancel_allow_user_paid_refund_as_giftcard == 'option' and
|
||||
self.request.POST.get('giftcard') == 'true'
|
||||
)
|
||||
)
|
||||
return self.do(self.order.pk, cancellation_fee=fee, try_auto_refund=True, refund_as_giftcard=giftcard)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
@@ -399,6 +399,68 @@ class OrdersTest(BaseOrdersTest):
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_CANCELED
|
||||
|
||||
def test_orders_cancel_paid_fee_autorefund_gift_card_optional(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
with scopes_disabled():
|
||||
self.order.payments.create(provider='testdummy_partialrefund', amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED)
|
||||
self.event.settings.cancel_allow_user_paid = True
|
||||
self.event.settings.cancel_allow_user_paid_keep = Decimal('3.00')
|
||||
self.event.settings.cancel_allow_user_paid_refund_as_giftcard = 'option'
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert 'manually' not in response.rendered_content
|
||||
assert "gift card" in response.rendered_content
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/cancel/do' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'giftcard': 'true'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response,
|
||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret),
|
||||
target_status_code=200)
|
||||
assert "gift card" in response.rendered_content
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
assert self.order.total == Decimal('3.00')
|
||||
with scopes_disabled():
|
||||
r = self.order.refunds.get()
|
||||
assert r.provider == "giftcard"
|
||||
assert r.amount == Decimal('20.00')
|
||||
|
||||
def test_orders_cancel_paid_fee_autorefund_gift_card_force(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
with scopes_disabled():
|
||||
self.order.payments.create(provider='testdummy_partialrefund', amount=self.order.total, state=OrderPayment.PAYMENT_STATE_CONFIRMED)
|
||||
self.event.settings.cancel_allow_user_paid = True
|
||||
self.event.settings.cancel_allow_user_paid_keep = Decimal('3.00')
|
||||
self.event.settings.cancel_allow_user_paid_refund_as_giftcard = 'force'
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert 'manually' not in response.rendered_content
|
||||
assert "gift card" in response.rendered_content
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/cancel/do' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
|
||||
'giftcard': 'false'
|
||||
}, follow=True)
|
||||
self.assertRedirects(response,
|
||||
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
|
||||
self.order.secret),
|
||||
target_status_code=200)
|
||||
assert "gift card" in response.rendered_content
|
||||
self.order.refresh_from_db()
|
||||
assert self.order.status == Order.STATUS_PAID
|
||||
assert self.order.total == Decimal('3.00')
|
||||
with scopes_disabled():
|
||||
r = self.order.refunds.get()
|
||||
assert r.provider == "giftcard"
|
||||
assert r.amount == Decimal('20.00')
|
||||
|
||||
def test_orders_cancel_paid_fee_autorefund(self):
|
||||
self.order.status = Order.STATUS_PAID
|
||||
self.order.save()
|
||||
|
||||
Reference in New Issue
Block a user