forked from CGM_Public/pretix_original
handle gift card payment via create order api endpoint (Z#23224691) (#5968)
* adds safeguard to prevent empty giftcard transactions on giftcards of value 0.00 * implement giftcard payment via order create * styling * let create_transactions() handle all the mailing * docs * provide more context for failed transactions * documentation lectoring * reject duplicate gift card secrets * make payment_provider and use_gift_cards exclusive * handle unknown gift cards * Apply suggestion from @pajowu Co-authored-by: pajowu <engelhardt@pretix.eu> * Update src/pretix/control/templates/pretixcontrol/giftcards/payment.html Co-authored-by: pajowu <engelhardt@pretix.eu> --------- Co-authored-by: pajowu <engelhardt@pretix.eu>
This commit is contained in:
committed by
GitHub
parent
894128deab
commit
c39f1bfcc2
@@ -117,6 +117,8 @@ cancellation_date datetime Time of order c
|
|||||||
reliable for orders that have been cancelled,
|
reliable for orders that have been cancelled,
|
||||||
reactivated and cancelled again.
|
reactivated and cancelled again.
|
||||||
plugin_data object Additional data added by plugins.
|
plugin_data object Additional data added by plugins.
|
||||||
|
use_gift_cards list of strings List of unique gift card secrets that are used to pay
|
||||||
|
for this order.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +158,10 @@ plugin_data object Additional data
|
|||||||
|
|
||||||
The ``tax_rounding_mode`` attribute has been added.
|
The ``tax_rounding_mode`` attribute has been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2026.03
|
||||||
|
|
||||||
|
The ``use_gift_cards `` attribute has been added.
|
||||||
|
|
||||||
.. _order-position-resource:
|
.. _order-position-resource:
|
||||||
|
|
||||||
Order position resource
|
Order position resource
|
||||||
@@ -987,8 +993,6 @@ Creating orders
|
|||||||
|
|
||||||
* does not support file upload questions
|
* does not support file upload questions
|
||||||
|
|
||||||
* does not support redeeming gift cards
|
|
||||||
|
|
||||||
* does not support or validate memberships
|
* does not support or validate memberships
|
||||||
|
|
||||||
|
|
||||||
@@ -1095,6 +1099,14 @@ Creating orders
|
|||||||
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
whether these emails are enabled for certain sales channels. If set to ``null``, behavior will be controlled by pretix'
|
||||||
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
settings based on the sales channels (added in pretix 4.7). Defaults to ``false``.
|
||||||
Used to be ``send_mail`` before pretix 3.14.
|
Used to be ``send_mail`` before pretix 3.14.
|
||||||
|
* ``use_gift_cards`` (optional) The provided gift cards will be used to pay for this order. They will be debited and
|
||||||
|
all the necessary payment records for these transactions will be created. The gift cards will be used in sequence to
|
||||||
|
pay for the order. Processing of the gift cards stops as soon as the order is payed for. All gift card transactions
|
||||||
|
are listed under ``payments`` in the response.
|
||||||
|
This option can only be used with orders that are in the pending state.
|
||||||
|
The ``use_gift_cards`` attribute can not be combined with ``payment_info`` and ``payment_provider`` fields. If the
|
||||||
|
order isn't completely paid after its creation with ``use_gift_cards``, then a subsequent request to the payment
|
||||||
|
endpoint is needed.
|
||||||
|
|
||||||
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
If you want to use add-on products, you need to set the ``positionid`` fields of all positions manually
|
||||||
to incrementing integers starting with ``1``. Then, you can reference one of these
|
to incrementing integers starting with ``1``. Then, you can reference one of these
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from pretix.base.decimal import round_decimal
|
|||||||
from pretix.base.i18n import language
|
from pretix.base.i18n import language
|
||||||
from pretix.base.invoicing.transmission import get_transmission_types
|
from pretix.base.invoicing.transmission import get_transmission_types
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, Checkin, Customer, Device, Invoice, InvoiceAddress,
|
CachedFile, Checkin, Customer, Device, GiftCard, Invoice, InvoiceAddress,
|
||||||
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
InvoiceLine, Item, ItemVariation, Order, OrderPosition, Question,
|
||||||
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
QuestionAnswer, ReusableMedium, SalesChannel, Seat, SubEvent, TaxRule,
|
||||||
Voucher,
|
Voucher,
|
||||||
@@ -62,6 +62,7 @@ from pretix.base.models.orders import (
|
|||||||
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
BlockedTicketSecret, CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||||
PrintLog, RevokedTicketSecret, Transaction,
|
PrintLog, RevokedTicketSecret, Transaction,
|
||||||
)
|
)
|
||||||
|
from pretix.base.payment import GiftCardPayment, PaymentException
|
||||||
from pretix.base.pdf import get_images, get_variables
|
from pretix.base.pdf import get_images, get_variables
|
||||||
from pretix.base.services.cart import error_messages
|
from pretix.base.services.cart import error_messages
|
||||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
from pretix.base.services.locking import LOCK_TRUST_WINDOW, lock_objects
|
||||||
@@ -1200,6 +1201,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
)
|
)
|
||||||
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
tax_rounding_mode = serializers.ChoiceField(choices=ROUNDING_MODES, allow_null=True, required=False,)
|
||||||
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
locale = serializers.ChoiceField(choices=[], required=False, allow_null=True)
|
||||||
|
use_gift_cards = serializers.ListField(child=serializers.CharField(required=False), required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1215,7 +1217,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||||
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode')
|
'require_approval', 'valid_if_pending', 'expires', 'api_meta', 'tax_rounding_mode', 'use_gift_cards')
|
||||||
|
|
||||||
def validate_payment_provider(self, pp):
|
def validate_payment_provider(self, pp):
|
||||||
if pp is None:
|
if pp is None:
|
||||||
@@ -1310,6 +1312,14 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
payment_date = validated_data.pop('payment_date', now())
|
payment_date = validated_data.pop('payment_date', now())
|
||||||
force = validated_data.pop('force', False)
|
force = validated_data.pop('force', False)
|
||||||
simulate = validated_data.pop('simulate', False)
|
simulate = validated_data.pop('simulate', False)
|
||||||
|
gift_card_secrets = validated_data.pop('use_gift_cards') if 'use_gift_cards' in validated_data else []
|
||||||
|
|
||||||
|
if (payment_provider is not None or payment_info != '{}') and len(gift_card_secrets) > 0:
|
||||||
|
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is not compatible with payment_provider or payment_info']})
|
||||||
|
if validated_data.get('status') != Order.STATUS_PENDING and len(gift_card_secrets) > 0:
|
||||||
|
raise ValidationError({"use_gift_cards": ['The attribute use_gift_cards is only supported for orders that are created as pending']})
|
||||||
|
if len(set(gift_card_secrets)) != len(gift_card_secrets):
|
||||||
|
raise ValidationError({"use_gift_cards": ['Multiple copies of the same gift card secret are not allowed']})
|
||||||
|
|
||||||
if not validated_data.get("sales_channel"):
|
if not validated_data.get("sales_channel"):
|
||||||
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
validated_data["sales_channel"] = self.context['event'].organizer.sales_channels.get(identifier="web")
|
||||||
@@ -1794,6 +1804,45 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
|||||||
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
if order.total != Decimal('0.00') and order.event.currency == "XXX":
|
||||||
raise ValidationError('Paid products not supported without a valid currency.')
|
raise ValidationError('Paid products not supported without a valid currency.')
|
||||||
|
|
||||||
|
for gift_card_secret in gift_card_secrets:
|
||||||
|
try:
|
||||||
|
if order.status != Order.STATUS_PAID:
|
||||||
|
gift_card_payment_provider = GiftCardPayment(event=order.event)
|
||||||
|
|
||||||
|
gc = order.event.organizer.accepted_gift_cards.get(
|
||||||
|
secret=gift_card_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
payment = order.payments.create(
|
||||||
|
amount=min(order.pending_sum, gc.value),
|
||||||
|
provider=gift_card_payment_provider.identifier,
|
||||||
|
info_data={
|
||||||
|
'gift_card': gc.pk,
|
||||||
|
'gift_card_secret': gc.secret,
|
||||||
|
'retry': True
|
||||||
|
},
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||||
|
)
|
||||||
|
gift_card_payment_provider.execute_payment(request=None, payment=payment, is_early_special_case=True)
|
||||||
|
|
||||||
|
if order.pending_sum <= Decimal('0.00'):
|
||||||
|
order.status = Order.STATUS_PAID
|
||||||
|
|
||||||
|
except PaymentException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except GiftCard.DoesNotExist as e:
|
||||||
|
payment = order.payments.create(
|
||||||
|
amount=order.pending_sum,
|
||||||
|
provider=GiftCardPayment.identifier,
|
||||||
|
info_data={
|
||||||
|
'gift_card_secret': gift_card_secret,
|
||||||
|
},
|
||||||
|
state=OrderPayment.PAYMENT_STATE_CREATED
|
||||||
|
)
|
||||||
|
payment.fail(info={**payment.info_data, 'error': str(e)},
|
||||||
|
send_mail=False)
|
||||||
|
|
||||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID and not validated_data.get('require_approval'):
|
||||||
order.status = Order.STATUS_PAID
|
order.status = Order.STATUS_PAID
|
||||||
order.save()
|
order.save()
|
||||||
|
|||||||
@@ -1526,16 +1526,26 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def payment_control_render(self, request, payment) -> str:
|
def payment_control_render(self, request, payment) -> str:
|
||||||
from .models import GiftCard
|
from .models import GiftCard
|
||||||
|
|
||||||
if 'gift_card' in payment.info_data:
|
if any(key in payment.info_data for key in ('gift_card', 'error')):
|
||||||
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
|
||||||
template = get_template('pretixcontrol/giftcards/payment.html')
|
template = get_template('pretixcontrol/giftcards/payment.html')
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'request': request,
|
'request': request,
|
||||||
'event': self.event,
|
'event': self.event,
|
||||||
'gc': gc,
|
**({'error': payment.info_data[
|
||||||
|
'error']} if 'error' in payment.info_data else {}),
|
||||||
|
**({'gift_card_secret': payment.info_data[
|
||||||
|
'gift_card_secret']} if 'gift_card_secret' in payment.info_data else {})
|
||||||
}
|
}
|
||||||
return template.render(ctx)
|
|
||||||
|
try:
|
||||||
|
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
||||||
|
ctx = {
|
||||||
|
'gc': gc,
|
||||||
|
}
|
||||||
|
except GiftCard.DoesNotExist:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
return template.render(ctx)
|
||||||
|
|
||||||
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||||
d = payment.info_data
|
d = payment.info_data
|
||||||
@@ -1550,12 +1560,16 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
try:
|
try:
|
||||||
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
gc = GiftCard.objects.get(pk=payment.info_data.get('gift_card'))
|
||||||
except GiftCard.DoesNotExist:
|
except GiftCard.DoesNotExist:
|
||||||
return {}
|
return {
|
||||||
|
**({'error': payment.info_data[
|
||||||
|
'error']} if 'error' in payment.info_data else {})
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
'gift_card': {
|
'gift_card': {
|
||||||
'id': gc.pk,
|
'id': gc.pk,
|
||||||
'secret': gc.secret,
|
'secret': gc.secret,
|
||||||
'organizer': gc.issuer.slug
|
'organizer': gc.issuer.slug,
|
||||||
|
** ({'error': payment.info_data['error']} if 'error' in payment.info_data else {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1627,6 +1641,8 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
raise PaymentException(_("This gift card does not support this currency."))
|
raise PaymentException(_("This gift card does not support this currency."))
|
||||||
if not gc.accepted_by(self.event.organizer):
|
if not gc.accepted_by(self.event.organizer):
|
||||||
raise PaymentException(_("This gift card is not accepted by this event organizer."))
|
raise PaymentException(_("This gift card is not accepted by this event organizer."))
|
||||||
|
if gc.value <= Decimal("0.00"):
|
||||||
|
raise PaymentException(_("All credit on this gift card has been used."))
|
||||||
if payment.amount > gc.value:
|
if payment.amount > gc.value:
|
||||||
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
|
raise PaymentException(_("This gift card was used in the meantime. Please try again."))
|
||||||
if gc.testmode and not payment.order.testmode:
|
if gc.testmode and not payment.order.testmode:
|
||||||
@@ -1656,7 +1672,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except PaymentException as e:
|
except PaymentException as e:
|
||||||
payment.fail(info={'error': str(e)})
|
payment.fail(info={**payment.info_data, 'error': str(e)}, send_mail=not is_early_special_case)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def payment_is_valid_session(self, request: HttpRequest) -> bool:
|
def payment_is_valid_session(self, request: HttpRequest) -> bool:
|
||||||
|
|||||||
@@ -3,16 +3,26 @@
|
|||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>{% trans "Gift card code" %}</dt>
|
<dt>{% trans "Gift card code" %}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
{% if gc %}
|
||||||
{{ gc.secret }}
|
<a href="{% url "control:organizer.giftcard" organizer=gc.issuer.slug giftcard=gc.pk %}">
|
||||||
</a>
|
{{ gc.secret }}
|
||||||
{% if gc.issuer != request.organizer %}
|
</a>
|
||||||
<span class="text-muted">
|
{% if gc.issuer.slug != request.organizer %}
|
||||||
<br>
|
<span class="text-muted">
|
||||||
<span class="fa fa-group"></span> {{ gc.issuer }}
|
<br>
|
||||||
</span>
|
<span class="fa fa-group"></span> {{ gc.issuer }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% elif gift_card_secret %}
|
||||||
|
{{ gift_card_secret }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>{% trans "Issuer" %}</dt>
|
{% if gc %}
|
||||||
<dd>{{ gc.issuer }}</dd>
|
<dt>{% trans "Issuer" %}</dt>
|
||||||
|
<dd>{{ gc.issuer }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
{% if error %}
|
||||||
|
<dt>{% trans "Error" %}</dt>
|
||||||
|
<dd>{{ error }}</dd>
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ from django_scopes import scopes_disabled
|
|||||||
from tests.const import SAMPLE_PNG
|
from tests.const import SAMPLE_PNG
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
InvoiceAddress, Item, Order, OrderPosition, Organizer, Question,
|
GiftCard, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
|
||||||
SeatingPlan,
|
Organizer, Question, SeatingPlan,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
|
from pretix.base.models.orders import CartPosition, OrderFee, QuestionAnswer
|
||||||
|
|
||||||
@@ -3371,3 +3371,251 @@ def test_order_create_rounding_default_pretixpos_fallback(device, device_client,
|
|||||||
assert resp.data["total"] == "500.00"
|
assert resp.data["total"] == "500.00"
|
||||||
assert resp.data["positions"][0]["price"] == "100.00"
|
assert resp.data["positions"][0]["price"] == "100.00"
|
||||||
assert resp.data["positions"][-1]["price"] == "100.00"
|
assert resp.data["positions"][-1]["price"] == "100.00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"order_status,status_code",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
Order.STATUS_PENDING, 201
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Order.STATUS_PAID, 400
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_use_gift_cards_only_pending(token_client, organizer, event, item, quota, question, order_status, status_code):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
res['status'] = order_status
|
||||||
|
del res['payment_provider']
|
||||||
|
res['use_gift_cards'] = [gc.secret]
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == status_code
|
||||||
|
if status_code != 201:
|
||||||
|
assert resp.data == {'use_gift_cards': ['The attribute use_gift_cards is only supported for orders that are created as pending']}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"send_mail,mail_amount",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
False, 0
|
||||||
|
),
|
||||||
|
(
|
||||||
|
True, 2
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_order_create_use_gift_card(token_client, organizer, event, item, quota, question, send_mail, mail_amount):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if send_mail:
|
||||||
|
res['send_email'] = True
|
||||||
|
|
||||||
|
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||||
|
del res['payment_provider']
|
||||||
|
res['use_gift_cards'] = [gc.secret]
|
||||||
|
|
||||||
|
djmail.outbox = []
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
o = Order.objects.get(code=resp.data['code'])
|
||||||
|
assert o.status == Order.STATUS_PAID
|
||||||
|
|
||||||
|
assert gc.transactions.count() == 2
|
||||||
|
assert -gc.transactions.last().value == o.total
|
||||||
|
|
||||||
|
assert len(djmail.outbox) == mail_amount
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_use_multiple_gift_cards(token_client, organizer, event, item, quota, question):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
del res['payment_provider']
|
||||||
|
|
||||||
|
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
gc_empty = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
|
||||||
|
gc_wrong_currency = GiftCard.objects.create(issuer=organizer, currency='USD')
|
||||||
|
gc_wrong_currency.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
res['use_gift_cards'] = [gc_one_eur.secret, gc_empty.secret, gc_wrong_currency.secret, gc_enough_eur.secret]
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
o = Order.objects.get(code=resp.data['code'])
|
||||||
|
# order has a payment entry per giftcard
|
||||||
|
assert o.status == Order.STATUS_PAID
|
||||||
|
assert o.payments.count() == 4
|
||||||
|
|
||||||
|
assert gc_one_eur.transactions.count() == 2 # +1€ charge and -1€ payment
|
||||||
|
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||||
|
assert Decimal(-1.00) == gc_one_eur.transactions.last().value
|
||||||
|
|
||||||
|
assert gc_empty.transactions.count() == 0 # no charge and no payment transaction
|
||||||
|
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||||
|
|
||||||
|
assert gc_wrong_currency.transactions.count() == 1 # charge transaction
|
||||||
|
assert o.payments.all()[2].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||||
|
|
||||||
|
assert gc_enough_eur.transactions.count() == 2 # +100€ charge and -remainder € payment
|
||||||
|
assert o.payments.all()[3].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||||
|
assert -(o.total - Decimal(1.00)) == gc_enough_eur.transactions.last().value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_use_gift_card_exclusive_with_payment_provider(token_client, organizer, event, item, quota, question):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
gc_value = Decimal("1.00")
|
||||||
|
gc = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc.transactions.create(value=gc_value, acceptor=organizer).save()
|
||||||
|
|
||||||
|
res['use_gift_cards'] = [gc.secret]
|
||||||
|
|
||||||
|
res_with_payment_provider = copy.deepcopy(res)
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res_with_payment_provider
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
|
||||||
|
|
||||||
|
res_with_payment_info = copy.deepcopy(res)
|
||||||
|
res_with_payment_info['payment_info'] = {"a": "b"}
|
||||||
|
del res_with_payment_info['payment_provider']
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res_with_payment_info
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.json() == {"use_gift_cards": ["The attribute use_gift_cards is not compatible with payment_provider or payment_info"]}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_use_gift_card_repeated(token_client, organizer, event, item, quota, question):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
del res['payment_provider']
|
||||||
|
|
||||||
|
gc_one_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc_one_eur.transactions.create(value=Decimal("1.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc_enough_eur.transactions.create(value=Decimal("100.00"), acceptor=organizer).save()
|
||||||
|
|
||||||
|
res['use_gift_cards'] = [gc_one_eur.secret, gc_one_eur.secret, gc_enough_eur.secret]
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.json() == {'use_gift_cards': ['Multiple copies of the same gift card secret are not allowed']}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_order_create_use_gift_card_invalid_secret(token_client, organizer, event, item, quota, question):
|
||||||
|
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||||
|
res['positions'][0]['item'] = item.pk
|
||||||
|
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||||
|
with scopes_disabled():
|
||||||
|
customer = organizer.customers.create()
|
||||||
|
|
||||||
|
res['customer'] = customer.identifier
|
||||||
|
res['api_meta'] = {
|
||||||
|
'test': 1
|
||||||
|
}
|
||||||
|
del res['payment_provider']
|
||||||
|
|
||||||
|
gc_enough_eur = GiftCard.objects.create(issuer=organizer, currency='EUR')
|
||||||
|
gc_enough_eur.transactions.create(value=Decimal("100.00"),
|
||||||
|
acceptor=organizer).save()
|
||||||
|
|
||||||
|
res['use_gift_cards'] = ["INVALID", gc_enough_eur.secret]
|
||||||
|
|
||||||
|
resp = token_client.post(
|
||||||
|
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||||
|
organizer.slug, event.slug
|
||||||
|
), format='json', data=res
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
with scopes_disabled():
|
||||||
|
o = Order.objects.get(code=resp.data['code'])
|
||||||
|
assert o.status == Order.STATUS_PAID
|
||||||
|
assert o.payments.count() == 2
|
||||||
|
assert o.payments.all()[0].state == OrderPayment.PAYMENT_STATE_FAILED
|
||||||
|
assert o.payments.all()[1].state == OrderPayment.PAYMENT_STATE_CONFIRMED
|
||||||
|
|||||||
Reference in New Issue
Block a user