diff --git a/src/pretix/base/migrations/0292_giftcard_customer.py b/src/pretix/base/migrations/0292_giftcard_customer.py new file mode 100644 index 000000000..5ee6b7dd4 --- /dev/null +++ b/src/pretix/base/migrations/0292_giftcard_customer.py @@ -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'), + ), + ] diff --git a/src/pretix/base/models/customers.py b/src/pretix/base/models/customers.py index 5d94657a4..942ae5918 100644 --- a/src/pretix/base/models/customers.py +++ b/src/pretix/base/models/customers.py @@ -19,6 +19,8 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +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( diff --git a/src/pretix/base/models/giftcards.py b/src/pretix/base/models/giftcards.py index d93eba36b..409bf4a1c 100644 --- a/src/pretix/base/models/giftcards.py +++ b/src/pretix/base/models/giftcards.py @@ -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, ) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 548739182..72f127770 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -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, diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 01be0e254..bfe817d0f 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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={}) diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 8e614a12e..c8f63914f 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -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 = { diff --git a/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html b/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html index ca579a36b..23403ac1e 100644 --- a/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html +++ b/src/pretix/control/templates/pretixcontrol/giftcards/checkout.html @@ -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 %} +

+ {% trans "Information" %} + {% trans "The following gift cards are available in your customer account:" %} +

+
+ {% csrf_token %} + +
+{% endif %} {% bootstrap_form form layout='checkout' %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/customer.html b/src/pretix/control/templates/pretixcontrol/organizers/customer.html index b5954d068..8a86c8f68 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/customer.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/customer.html @@ -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 @@ {% include "pretixcontrol/pagination.html" %} +
+
+

+ {% trans "Gift cards" %} +

+
+ + + + + + + + {% for gc in gift_cards %} + + + + + + + + + {% endfor %} + +
{% trans "Gift card code" %} + {% trans "Creation date" %} + {% trans "Expiry date" %} + {% trans "Last transaction" %} + {% trans "Current value" %} +
+ + {{ gc.secret }} + {% if gc.testmode %} + {% trans "TEST MODE" %} + {% endif %} + {% if gc.expired %} + {% trans "Expired" %} + {% endif %} + {{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }}{% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}{% if gc.last_tx %}{{ gc.last_tx|date:"SHORT_DATETIME_FORMAT" }}{% endif %} +

{{ gc.value|money:gc.currency }}

+
+ + + +
+
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html index 271780abe..8a035e490 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html @@ -54,6 +54,14 @@ {{ card.owner_ticket.order.code }}-{{ card.owner_ticket.positionid }} {% endif %} + {% if card.customer %} +
{% trans "Customer account" %}
+
+ + {{ card.customer.identifier }} – {{ card.customer.email }} + +
+ {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcard_create.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcard_create.html index 86bac3b05..5c21b8fc7 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/giftcard_create.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcard_create.html @@ -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 %}
+
+ + {% endfor %} + + + {% endif %} {% if current_payments %}

{% trans "You already selected the following payment methods:" %}

@@ -77,6 +105,15 @@ aria-controls="payment_{{ p.provider.identifier }}" data-wallets="{{ p.provider.walletqueries|join:"|" }}" /> {{ p.provider.public_name }} + {% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %} + + {% blocktrans trimmed count count=p.provider.customer_gift_cards|length %} + ({{ count }} available) + {% plural %} + ({{ count }} available) + {% endblocktrans %} + + {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html index c32e568c5..324d697a6 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html @@ -40,6 +40,15 @@ {% if selected == p.provider.identifier %}checked="checked"{% endif %} data-wallets="{{ p.provider.walletqueries|join:"|" }}"/> {{ p.provider.public_name }} + {% if p.provider.identifier == 'giftcard' and p.provider.customer_gift_cards %} + + {% blocktrans trimmed count count=p.provider.customer_gift_cards|length %} + ({{ count }} available) + {% plural %} + ({{ count }} available) + {% endblocktrans %} + + {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/organizers/customer_giftcards.html b/src/pretix/presale/templates/pretixpresale/organizers/customer_giftcards.html new file mode 100644 index 000000000..691ffed59 --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/organizers/customer_giftcards.html @@ -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 %} +
+
+

+ {% icon "gift" %} + {% trans "Gift cards" %} ({{ page_obj.paginator.count }}) +

+
+
+ {% if gift_cards %} +
+ {% for gc in gift_cards %} +
+
+

+ {% icon "gift" %} {{ gc }} +

+ {% if gc.issuance %} +

+ {% icon "calendar" %} + {% blocktrans trimmed with date=gc.issuance|date:"SHORT_DATE_FORMAT" %} + Issued on {{ date }} + {% endblocktrans %} +

+ {% endif %} +

+ + {% 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 %} + +

+ {% if gc.testmode %} +

+ + {% textbubble "warning" %} + {% trans "TEST MODE" %} + {% endtextbubble %} + +

+ {% endif %} +
+
+ {% blocktrans trimmed with value=gc.value|money:gc.currency %} + Remaining value: + {% endblocktrans %} +

{{ value }}

+
+
+ {% endfor %} +
+ {% else %} +

+ {% trans "You don’t 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." %} +

+ {% endif %} +
+
+ {% include "pretixcontrol/pagination.html" %} +{% endblock %} diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index 2fbfac589..2fe4fc398 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -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\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\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'), diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index 45c6216ba..fa0dd884b 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -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' diff --git a/src/pretix/static/pretixpresale/js/ui/main.js b/src/pretix/static/pretixpresale/js/ui/main.js index 0a5c71552..c83eb3409 100644 --- a/src/pretix/static/pretixpresale/js/ui/main.js +++ b/src/pretix/static/pretixpresale/js/ui/main.js @@ -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) { diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index baf197b64..2241e4300 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -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() diff --git a/src/tests/presale/test_order_change.py b/src/tests/presale/test_order_change.py index 3ebc21e97..0bcafeb4e 100644 --- a/src/tests/presale/test_order_change.py +++ b/src/tests/presale/test_order_change.py @@ -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() diff --git a/src/tests/presale/test_orders.py b/src/tests/presale/test_orders.py index 583177df5..be4d7d033 100644 --- a/src/tests/presale/test_orders.py +++ b/src/tests/presale/test_orders.py @@ -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")