Connect giftcards with customer accounts (#5126)

Connect giftcards with customer accounts, show giftcards during checkout and in account , show giftcard list in backend customer view
This commit is contained in:
Phin Wolkwitz
2025-10-16 13:20:00 +02:00
committed by GitHub
parent 71f2c8093f
commit 8a3da37b45
22 changed files with 490 additions and 6 deletions

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.19 on 2025-05-19 11:06
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0291_alter_logentry_object_id'),
]
operations = [
migrations.AddField(
model_name='giftcard',
name='customer',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='customer_gift_cards', to='pretixbase.customer'),
),
]

View File

@@ -19,6 +19,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
import pycountry
from django.conf import settings
from django.contrib.auth.hashers import (
@@ -27,7 +29,11 @@ from django.contrib.auth.hashers import (
from django.core.validators import RegexValidator, URLValidator
from django.db import models
from django.db.models import F, Q
from django.db.models.aggregates import Sum
from django.db.models.expressions import OuterRef, Subquery
from django.db.models.functions.comparison import Coalesce
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField
@@ -36,6 +42,7 @@ from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.banlist import banned
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.giftcards import GiftCardTransaction
from pretix.base.models.organizer import Organizer
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.helpers.countries import FastCountryField
@@ -288,6 +295,19 @@ class Customer(LoggedModel):
organizer=self.organizer,
)
def usable_gift_cards(self, used_cards=[]):
s = GiftCardTransaction.objects.filter(
card=OuterRef('pk')
).order_by().values('card').annotate(s=Sum('value')).values('s')
qs = self.customer_gift_cards.annotate(
cached_value=Coalesce(Subquery(s), Decimal('0.00')),
)
ne_qs = qs.filter(
Q(expires__isnull=True) | Q(expires__gte=now()),
)
ex_qs = ne_qs.exclude(id__in=used_cards)
return ex_qs.filter(cached_value__gt=0)
class AttendeeProfile(models.Model):
customer = models.ForeignKey(

View File

@@ -80,6 +80,13 @@ class GiftCard(LoggedModel):
null=True, blank=True,
verbose_name=_('Owned by ticket holder')
)
customer = models.ForeignKey(
'Customer',
related_name='customer_gift_cards',
on_delete=models.PROTECT,
null=True, blank=True,
verbose_name=_('Owned by customer account')
)
issuance = models.DateTimeField(
auto_now_add=True,
)

View File

@@ -38,6 +38,7 @@ import json
import logging
from collections import OrderedDict
from decimal import ROUND_HALF_UP, Decimal
from functools import cached_property
from typing import Any, Dict, Union
from zoneinfo import ZoneInfo
@@ -57,8 +58,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import I18nMarkdownTextarea, PlaceholderValidator
from pretix.base.models import (
CartPosition, Event, GiftCard, InvoiceAddress, Order, OrderPayment,
OrderRefund, Quota, TaxRule,
CartPosition, Customer, Event, GiftCard, InvoiceAddress, Order,
OrderPayment, OrderRefund, Quota, TaxRule,
)
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
from pretix.base.settings import SettingsSandbox
@@ -99,6 +100,7 @@ class PaymentProviderForm(Form):
class GiftCardPaymentForm(PaymentProviderForm):
def __init__(self, *args, **kwargs):
self.customer_gift_cards = kwargs.pop('customer_gift_cards') if 'customer_gift_cards' in kwargs else None
self.event = kwargs.pop('event')
self.testmode = kwargs.pop('testmode')
self.positions = kwargs.pop('positions')
@@ -1373,6 +1375,31 @@ class GiftCardPayment(BasePaymentProvider):
payment_form_class = GiftCardPaymentForm
payment_form_template_name = 'pretixcontrol/giftcards/checkout.html'
@cached_property
def customer_gift_cards(self):
if not self.request:
return None
if not self.used_cards:
self.used_cards = []
cs = None
if 'checkout' in self.request.resolver_match.url_name:
cs = cart_session(self.request)
customer = getattr(self.request, "customer", None)
if customer:
return customer.usable_gift_cards(self.used_cards)
elif cs and cs.get('customer_mode', 'guest') == 'login':
try:
customer = self.request.organizer.customers.get(pk=cs["customer"])
return customer.usable_gift_cards(self.used_cards)
except Customer.DoesNotExist:
return None
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order = None) -> str:
form = self.payment_form(request)
template = get_template(self.payment_form_template_name)
ctx = {'request': request, 'form': form, 'customer_gift_cards': form.customer_gift_cards, }
return template.render(ctx)
def payment_form(self, request: HttpRequest) -> Form:
# Unfortunately, in payment_form we do not know if we're in checkout
# or in an existing order. But we need to do the validation logic in the
@@ -1392,8 +1419,12 @@ class GiftCardPayment(BasePaymentProvider):
positions = order.positions.all()
testmode = order.testmode
self.request = request
self.used_cards = used_cards
form = self.payment_form_class(
event=self.event,
customer_gift_cards=self.customer_gift_cards,
used_cards=used_cards,
positions=positions,
testmode=testmode,

View File

@@ -3087,6 +3087,7 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
expires=order.event.organizer.default_gift_card_expiry if giftcard_expires is _unset else giftcard_expires,
conditions=giftcard_conditions,
currency=order.event.currency,
customer=order.customer,
testmode=order.testmode
)
giftcard.log_action('pretix.giftcards.created', data={})

View File

@@ -731,6 +731,21 @@ class GiftCardCreateForm(forms.ModelForm):
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if self.organizer.settings.customer_accounts:
self.fields['customer'].queryset = self.organizer.customers.all()
self.fields['customer'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
'organizer': self.organizer.slug,
}),
}
)
self.fields['customer'].widget.choices = self.fields['customer'].choices
self.fields['customer'].required = False
else:
del self.fields['customer']
def clean_secret(self):
s = self.cleaned_data['secret']
if GiftCard.objects.filter(
@@ -749,9 +764,10 @@ class GiftCardCreateForm(forms.ModelForm):
class Meta:
model = GiftCard
fields = ['secret', 'currency', 'testmode', 'expires', 'conditions']
fields = ['secret', 'currency', 'testmode', 'expires', 'conditions', 'customer']
field_classes = {
'expires': SplitDateTimeField
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
@@ -762,10 +778,11 @@ class GiftCardCreateForm(forms.ModelForm):
class GiftCardUpdateForm(forms.ModelForm):
class Meta:
model = GiftCard
fields = ['expires', 'conditions', 'owner_ticket']
fields = ['expires', 'conditions', 'owner_ticket', 'customer']
field_classes = {
'expires': SplitDateTimeField,
'owner_ticket': SafeOrderPositionChoiceField,
'customer': SafeModelChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
@@ -788,6 +805,21 @@ class GiftCardUpdateForm(forms.ModelForm):
self.fields['owner_ticket'].widget.choices = self.fields['owner_ticket'].choices
self.fields['owner_ticket'].required = False
if organizer.settings.customer_accounts:
self.fields['customer'].queryset = organizer.customers.all()
self.fields['customer'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.customers.select2', kwargs={
'organizer': organizer.slug,
}),
}
)
self.fields['customer'].widget.choices = self.fields['customer'].choices
self.fields['customer'].required = False
else:
del self.fields['customer']
class ReusableMediumUpdateForm(forms.ModelForm):
error_messages = {

View File

@@ -1,7 +1,35 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% load rich_text %}
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
{% if customer_gift_cards %}
<p><strong>
<span class="sr-only">{% trans "Information" %}</span>
{% trans "The following gift cards are available in your customer account:" %}
</strong></p>
<form method="post">
{% csrf_token %}
<ul class="list-group">
{% for c in customer_gift_cards %}
<li class="list-group-item row row-no-gutters">
<div class="col-sm-8 col-md-9" id="gc-code-{{ forloop.counter }}">
{{ c }}
</div>
<div class="col-sm-2 text-right" id="gc-value-{{ forloop.counter }}">
{{ c.value|money:c.currency }}
</div>
<div class="col-sm-2 col-md-1 text-right">
<button name="use_giftcard" class="btn btn-primary btn-xs use_giftcard" data-value="{{ c.secret }}"
title="{% trans "Use gift card" %}"
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}">
{% trans "Apply" %}
</button>
</div>
</li>
{% endfor %}
</ul>
</form>
{% endif %}
{% bootstrap_form form layout='checkout' %}

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load icon %}
{% load money %}
{% load static %}
{% block title %}
@@ -278,6 +279,53 @@
</table>
{% include "pretixcontrol/pagination.html" %}
</div>
<div class="panel panel-default items">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Gift cards" %}
</h3>
</div>
<table class="panel-body table">
<thead>
<tr>
<th>{% trans "Gift card code" %}
<th>{% trans "Creation date" %}
<th>{% trans "Expiry date" %}
<th>{% trans "Last transaction" %}
<th class="text-right">{% trans "Current value" %}
<th></th>
</tr>
</thead>
<tbody>
{% for gc in gift_cards %}
<tr>
<td>
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
<strong>{{ gc.secret }}</strong></a>
{% if gc.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if gc.expired %}
<span class="label label-danger">{% trans "Expired" %}</span>
{% endif %}
</td>
<td>{{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
<td>{% if gc.last_tx %}{{ gc.last_tx|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
<td class="text-right flip">
<p class="text-right">{{ gc.value|money:gc.currency }}</p>
</td>
<td class="text-right">
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
<i class="fa fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-2 col-xs-12">
<div class="panel panel-default">

View File

@@ -54,6 +54,14 @@
{{ card.owner_ticket.order.code }}</a>-{{ card.owner_ticket.positionid }}
</dd>
{% endif %}
{% if card.customer %}
<dt>{% trans "Customer account" %}</dt>
<dd>
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=card.customer.identifier %}">
{{ card.customer.identifier }} {{ card.customer.email }}
</a>
</dd>
{% endif %}
</dl>
</div>
</div>

View File

@@ -12,6 +12,9 @@
{% bootstrap_field form.testmode layout="control" %}
{% bootstrap_field form.expires layout="control" %}
{% bootstrap_field form.conditions layout="control" %}
{% if form.customer %}
{% bootstrap_field form.customer layout="control" %}
{% endif %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -16,6 +16,9 @@
{% bootstrap_field form.expires layout="control" %}
{% bootstrap_field form.owner_ticket layout="control" %}
{% bootstrap_field form.conditions layout="control" %}
{% if form.customer %}
{% bootstrap_field form.customer layout="control" %}
{% endif %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -1221,6 +1221,7 @@ class OrderRefundView(OrderView):
giftcard = self.request.organizer.issued_gift_cards.create(
expires=expires,
currency=self.request.event.currency,
customer=order.customer,
testmode=order.testmode
)
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})

View File

@@ -3085,6 +3085,8 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
.annotate(spending=Sum("total"))
)
ctx["gift_cards"] = self.customer.customer_gift_cards.all()
return ctx

View File

@@ -9,6 +9,34 @@
{% endblock %}
{% block inner %}
<h3 class="sr-only">{% trans "Payment" %}</h3>
{% if customer_gift_cards %}
<p><strong>
<span class="sr-only">{% trans "Information" %}</span>
{% trans "The following gift cards are available in your customer account:" %}
</strong></p>
<form method="post">
{% csrf_token %}
<ul class="list-group">
{% for c in customer_gift_cards %}
<li class="list-group-item row">
<div class="col-xs-9" id="gc-code-{{ forloop.counter }}">
{{ c }}
</div>
<div class="col-xs-2 text-right" id="gc-value-{{ forloop.counter }}">
{{ c.value|money:c.currency }}
</div>
<div class="col-xs-1 text-right">
<button name="use_giftcard" value="{{ c.secret }}" title="{% trans "Use gift card" %}"
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}"
class="btn btn-primary btn-xs">
{% trans "Apply" %}
</button>
</div>
</li>
{% endfor %}
</ul>
</form>
{% endif %}
{% if current_payments %}
<p>{% trans "You already selected the following payment methods:" %}</p>
<form method="post">
@@ -77,6 +105,15 @@
aria-controls="payment_{{ p.provider.identifier }}"
data-wallets="{{ p.provider.walletqueries|join:"|" }}" />
<strong class="accordion-label-text">{{ p.provider.public_name }}</strong>
{% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %}
<small>
{% blocktrans trimmed count count=p.provider.customer_gift_cards|length %}
({{ count }} available)
{% plural %}
({{ count }} available)
{% endblocktrans %}
</small>
{% endif %}
</span>
</label>
</legend>

View File

@@ -40,6 +40,15 @@
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
data-wallets="{{ p.provider.walletqueries|join:"|" }}"/>
<strong class="accordion-label-text">{{ p.provider.public_name }}</strong>
{% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %}
<small>
{% blocktrans trimmed count count=p.provider.customer_gift_cards|length %}
({{ count }} available)
{% plural %}
({{ count }} available)
{% endblocktrans %}
</small>
{% endif %}
</span>
</label>
</legend>

View File

@@ -0,0 +1,83 @@
{% extends "pretixpresale/organizers/customer_base.html" %}
{% load money %}
{% load i18n %}
{% load icon %}
{% load eventurl %}
{% load textbubble %}
{% block title %}{% trans "Gift cards" %}{% endblock %}
{% block inner %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% icon "gift" %}
<strong>{% trans "Gift cards" %}</strong> ({{ page_obj.paginator.count }})
</h3>
</div>
<div class="panel-body">
{% if gift_cards %}
<div class="event-list full-width-list alternating-rows">
{% for gc in gift_cards %}
<article class="row">
<div class="col-xs-6">
<h4>
{% icon "gift" %} {{ gc }}
</h4>
{% if gc.issuance %}
<p class="text-muted">
{% icon "calendar" %}
{% blocktrans trimmed with date=gc.issuance|date:"SHORT_DATE_FORMAT" %}
Issued on {{ date }}
{% endblocktrans %}
</p>
{% endif %}
<p class="text-muted">
<small>
{% if gc.expired %}
{% icon "clock-o" %}
{% if gc.expires %}
{% blocktrans trimmed with date=gc.expires|date:"SHORT_DATETIME_FORMAT" %}
Expired since {{ date }}
{% endblocktrans %}
{% else %}
{% trans "Expired" %}
{% endif %}
{% elif gc.expires %}
{% icon "check" %}
{% blocktrans trimmed with date=gc.expires|date:"SHORT_DATETIME_FORMAT" %}
Valid until {{ date }}
{% endblocktrans %}
{% else %}
{% icon "check" %}
{% trans "Valid" %}
{% endif %}
</small>
</p>
{% if gc.testmode %}
<p>
<small>
{% textbubble "warning" %}
{% trans "TEST MODE" %}
{% endtextbubble %}
</small>
</p>
{% endif %}
</div>
<div class="col-xs-6 text-right">
{% blocktrans trimmed with value=gc.value|money:gc.currency %}
Remaining value:
{% endblocktrans %}
<p class="text-right">{{ value }}</p>
</div>
</article>
{% endfor %}
</div>
{% else %}
<p class="text-center">
{% trans "You dont have any gift cards in your account currently." %}
{% trans "Currently, only gift cards resulting from refunds show up here, any purchased gift cards show up under the orders tab." %}
</p>
{% endif %}
</div>
</div>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -214,6 +214,8 @@ organizer_patterns = [
re_path(r'^account/confirmchange$', pretix.presale.views.customer.ConfirmChangeView.as_view(), name='organizer.customer.change.confirm'),
re_path(r'^account/memberships$', pretix.presale.views.customer.MembershipView.as_view(), name='organizer.customer.memberships'),
re_path(r'^account/memberships/(?P<id>\d+)/$', pretix.presale.views.customer.MembershipUsageView.as_view(), name='organizer.customer.membership'),
re_path(r'^account/giftcards$', pretix.presale.views.customer.GiftcardView.as_view(),
name='organizer.customer.giftcards'),
re_path(r'^account/addresses$', pretix.presale.views.customer.AddressView.as_view(), name='organizer.customer.addresses'),
re_path(r'^account/addresses/(?P<id>\d+)/delete$', pretix.presale.views.customer.AddressDeleteView.as_view(), name='organizer.customer.address.delete'),
re_path(r'^account/profiles$', pretix.presale.views.customer.ProfileView.as_view(), name='organizer.customer.profiles'),

View File

@@ -368,6 +368,12 @@ class CustomerAccountBaseMixin(CustomerRequiredMixin):
'active': url_name.startswith('organizer.customer.membership'),
'icon': 'id-badge',
},
{
'label': _('Gift cards'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.giftcards', kwargs={}),
'active': url_name.startswith('organizer.customer.giftcard'),
'icon': 'gift',
},
{
'label': _('Addresses'),
'url': eventreverse(self.request.organizer, 'presale:organizer.customer.addresses', kwargs={}),
@@ -461,6 +467,15 @@ class MembershipUsageView(CustomerAccountBaseMixin, ListView):
return ctx
class GiftcardView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_giftcards.html'
context_object_name = 'gift_cards'
paginate_by = 20
def get_queryset(self):
return self.request.customer.customer_gift_cards.all()
class AddressView(CustomerAccountBaseMixin, ListView):
template_name = 'pretixpresale/organizers/customer_addresses.html'
context_object_name = 'invoice_addresses'

View File

@@ -222,6 +222,12 @@ var form_handlers = function (el) {
}
});
}
el.find('.use_giftcard').on("click", function () {
var value = $(this).data('value');
$('#id_payment_giftcard-code').val(value)
})
};
function setup_basics(el) {

View File

@@ -5488,3 +5488,22 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
with scopes_disabled():
assert order.positions.first().used_membership == m_correct1
assert order.positions.first().attendee_name == 'John Doe'
def test_giftcard_customer_offered(self):
with scopes_disabled():
gc = self.orga.issued_gift_cards.create(currency="EUR", customer=self.customer)
gc.transactions.create(value=23, acceptor=self.orga)
ugc = self.customer.usable_gift_cards()
assert len(ugc) == 1
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self.client.post('/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug), {
'customer_mode': 'login',
'login-email': 'john@example.org',
'login-password': 'foo',
}, follow=True)
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
assert 'Gift card' in response.content.decode()
assert '(1 available)' in response.content.decode()

View File

@@ -1915,6 +1915,47 @@ class OrderChangeAddonsTest(BaseOrdersTest):
r = self.order.refunds.get()
assert r.provider == 'giftcard'
def test_refund_giftcard_to_customer_account(self):
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password('foo')
customer.save()
self.order.customer = customer
self.event.settings.cancel_allow_user_paid_refund_as_giftcard = 'force'
with scopes_disabled():
gc = customer.usable_gift_cards()
assert len(gc) == 0
OrderPosition.objects.create(
order=self.order,
item=self.workshop1,
variation=None,
price=Decimal("12"),
addon_to=self.ticket_pos,
attendee_name_parts={'full_name': "Peter"},
)
self.order.status = Order.STATUS_PAID
self.order.total += Decimal("12")
self.order.save()
self.order.payments.create(provider='testdummy_partialrefund', amount=self.order.total,
state=OrderPayment.PAYMENT_STATE_CONFIRMED)
response = self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {},
follow=True
)
doc = BeautifulSoup(response.content.decode(), "lxml")
form_data = extract_form_fields(doc.select('.main-box form')[0])
form_data['confirm'] = 'true'
self.client.post(
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
form_data, follow=True
)
with scopes_disabled():
gc = customer.usable_gift_cards()
assert len(gc) == 1
def test_attendee(self):
self.workshop2a.default_price = Decimal('0.00')
self.workshop2a.save()

View File

@@ -702,6 +702,46 @@ class OrdersTest(BaseOrdersTest):
assert r.provider == "giftcard"
assert r.amount == Decimal('20.00')
def test_orders_cancel_autorefund_gift_card_customer(self):
self.order.status = Order.STATUS_PAID
self.order.save()
with scopes_disabled():
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password('foo')
customer.save()
self.order.customer = customer
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_refund_as_giftcard = 'option'
gc = customer.usable_gift_cards()
assert len(gc) == 0
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.content.decode()
assert "gift card" in response.content.decode()
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.content.decode()
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_CANCELED
with scopes_disabled():
r = self.order.refunds.get()
assert r.provider == "giftcard"
assert r.amount == Decimal('23.00')
gc = customer.usable_gift_cards()
assert len(gc) == 1
def test_orders_cancel_unpaid_fee(self):
self.order.status = Order.STATUS_PENDING
self.order.save()
@@ -1583,6 +1623,35 @@ class OrdersTest(BaseOrdersTest):
assert self.order.status == Order.STATUS_PAID
assert gc.value == Decimal('87.00')
def test_change_paymentmethod_customeraccount_giftcard_offered(self):
with scopes_disabled():
self.orga.settings.customer_accounts = True
customer = self.orga.customers.create(email='john@example.org', is_verified=True)
customer.set_password('foo')
customer.save()
self.order.customer = customer
self.order.payments.create(
provider='manual',
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
amount=Decimal('10.00'),
)
gc = self.orga.issued_gift_cards.create(currency="EUR", customer=customer)
gc.transactions.create(value=100, acceptor=self.orga)
ugc = customer.usable_gift_cards()
assert len(ugc) == 1
r = self.client.post('/%s/account/login' % (self.orga.slug), {
'email': 'john@example.org',
'password': 'foo',
})
assert r.status_code == 302
r = self.client.get('/%s/account/' % (self.orga.slug))
assert r.status_code == 200
response = self.client.get(
'/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
)
assert 'Gift card' in response.content.decode()
assert '1 available' in response.content.decode()
def test_answer_download_token(self):
with scopes_disabled():
q = self.event.questions.create(question="Foo", type="F")