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:
Raphael Michel
2020-03-25 11:41:40 +01:00
committed by GitHub
parent a5910016fd
commit 3eafec9d6e
10 changed files with 233 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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