Fix #599 -- Add API to create orders (#911)

* [WIP] Fix #599 -- Add API to create orders

* Add more validation logic

* Add docs and some validation

* Fix test on MySQl

* Validation is fun, let's do more of it!

* Fix live_issues
This commit is contained in:
Raphael Michel
2018-05-16 12:14:31 +02:00
committed by GitHub
parent 359a5d01e6
commit 35e8dcf2bc
9 changed files with 1510 additions and 43 deletions

View File

@@ -100,6 +100,7 @@ last_modified datetime Last modificati
.. versionchanged:: 1.16
The attributes ``order.last_modified`` as well as the corresponding filters to the resource have been added.
An endpoint for order creation has been added.
.. _order-position-resource:
@@ -112,7 +113,7 @@ Order position resource
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the order position
code string Order code of the order the position belongs to
order string Order code of the order the position belongs to
positionid integer Number of the position within the order
item integer ID of the purchased item
variation integer ID of the purchased variation (or ``null``)
@@ -425,6 +426,179 @@ Order endpoints
:statuscode 409: The file is not yet ready and will now be prepared. Retry the request after waiting for a few
seconds.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/
Creates a new order.
.. warning:: This endpoint is considered **experimental**. It might change at any time without prior notice.
.. warning::
This endpoint is intended for advanced users. It is not designed to be used to build your own shop frontend,
it's rather intended to import attendees from external sources etc.
There is a lot that it does not or can not do, and you will need to be careful using it.
It allows to bypass many of the restrictions imposed when creating an order through the
regular shop.
Specifically, this endpoint currently
* does not validate if products are only to be sold in a specific time frame
* does not validate if the event's ticket sales are already over or haven't started
* does not validate the number of items per order or the number of times an item can be included in an order
* does not validate any requirements related to add-on products
* does not check or calculate prices but believes any prices you send
* does not support the redemption of vouchers
* does not prevent you from buying items that can only be bought with a voucher
* does not calculate fees
* does not allow to pass data to plugins and will therefore cause issues with some plugins like the shipping
module
* does not send order confirmations via email
* does not support reverse charge taxation
* does not support file upload questions
You can supply the following fields of the resource:
* ``code`` (optional)
* ``status`` (optional) Defaults to pending for non-free orders and paid for free orders. You can only set this to
``"n"`` for pending or ``"p"`` for paid. If you create a paid order, the ``order_paid`` signal will **not** be
sent out to plugins and no email will be sent. If you want that behavior, create an unpaid order and then call
the ``mark_paid`` API method.
* ``email``
* ``locale``
* ``payment_provider`` The identifier of the payment provider set for this order. This needs to be an existing
payment provider. You should use ``"free"`` for free orders.
* ``payment_info`` (optional) You can pass a nested JSON object that will be set as the internal ``payment_info``
value of the order. How this value is handled is up to the payment provider and you should only use this if you
know the specific payment provider in detail. Please keep in mind that the payment provider will not be called
to do anything about this (i.e. if you pass a bank account to a debit provider, *no* charge will be created),
this is just informative in case you *handled the payment already*.
* ``comment`` (optional)
* ``checkin_attention`` (optional)
* ``invoice_address`` (optional)
* ``company``
* ``is_business``
* ``name``
* ``street``
* ``zipcode``
* ``city``
* ``country``
* ``internal_reference``
* ``vat_id``
* ``positions``
* ``positionid`` (optional, see below)
* ``item``
* ``variation``
* ``price``
* ``attendee_name``
* ``attendee_email``
* ``secret`` (optional)
* ``addon_to`` (optional, see below)
* ``subevent``
* ``answers``
* ``question``
* ``answer``
* ``options``
* ``fees``
* ``fee_type``
* ``value``
* ``description``
* ``internal_type``
* ``tax_rule``
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
IDs in the ``addon_to`` field of another position. Note that all add_ons for a specific position need to come
immediately after the position itself.
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/orders/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content: application/json
{
"email": "dummy@example.org",
"locale": "en",
"fees": [
{
"fee_type": "payment",
"value": "0.25",
"description": "",
"internal_type": "",
"tax_rule": 2
}
],
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": False,
"company": "Sample company",
"name": "John Doe",
"street": "Sesam Street 12",
"zipcode": "12345",
"city": "Sample City",
"country": "UK",
"internal_reference": "",
"vat_id": ""
},
"positions": [
{
"positionid": 1,
"item": 1,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [
{
"question": 1,
"answer": "23",
"options": []
}
],
"subevent": None
}
],
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
(Full order resource, see above.)
:param organizer: The ``slug`` field of the organizer of the event to create an item for
:param event: The ``slug`` field of the event to create an item for
:statuscode 201: no error
:statuscode 400: The item could not be created due to invalid submitted data or lack of quota.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this
order.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/orders/(code)/mark_paid/
Marks a pending or expired order as successfully paid.

View File

@@ -38,6 +38,7 @@ gunicorn
hardcoded
hostname
idempotency
incrementing
inofficial
invalidations
iterable

View File

@@ -1,16 +1,25 @@
import json
from collections import Counter
from decimal import Decimal
from django_countries.fields import Country
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
QuestionAnswer,
Question, QuestionAnswer, Quota,
)
from pretix.base.models.orders import OrderFee
from pretix.base.signals import register_ticket_outputs
class CompatibleCountryField(serializers.Field):
def to_internal_value(self, data):
return {self.field_name: Country(data)}
def to_representation(self, instance: InvoiceAddress):
if instance.country:
return str(instance.country)
@@ -25,6 +34,13 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
model = InvoiceAddress
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
'vat_id_validated', 'internal_reference')
read_only_fields = ('last_modified', 'vat_id_validated')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for v in self.fields.values():
v.required = False
v.allow_blank = True
class AnswerQuestionIdentifierField(serializers.Field):
@@ -134,6 +150,279 @@ class OrderSerializer(I18nAwareModelSerializer):
'checkin_attention', 'last_modified')
class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
raise ValidationError(
'File uploads are currently not supported via the API.'
)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rule')
def validate_tax_rule(self, tr):
if tr and tr.event != self.context['event']:
raise ValidationError(
'The specified tax rate does not belong to this event.'
)
return tr
class OrderPositionCreateSerializer(I18nAwareModelSerializer):
answers = AnswerCreateSerializer(many=True, required=False)
addon_to = serializers.IntegerField(required=False, allow_null=True)
secret = serializers.CharField(required=False)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
'secret', 'addon_to', 'subevent', 'answers')
def validate_secret(self, secret):
if secret and OrderPosition.objects.filter(order__event=self.context['event'], secret=secret).exists():
raise ValidationError(
'You cannot assign a position secret that already exists.'
)
return secret
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
'The specified item does not belong to this event.'
)
if not item.active:
raise ValidationError(
'The specified item is not active.'
)
return item
def validate_subevent(self, subevent):
if self.context['event'].has_subevents:
if not subevent:
raise ValidationError(
'You need to set a subevent.'
)
if subevent.event != self.context['event']:
raise ValidationError(
'The specified subevent does not belong to this event.'
)
elif subevent:
raise ValidationError(
'You cannot set a subevent for this event.'
)
return subevent
def validate(self, data):
if data.get('item'):
if data.get('item').has_variations:
if not data.get('variation'):
raise ValidationError('You should specify a variation for this item.')
else:
if data.get('variation').item != data.get('item'):
raise ValidationError(
'The specified variation does not belong to the specified item.'
)
elif data.get('variation'):
raise ValidationError(
'You cannot specify a variation for this item.'
)
return data
class CompatibleJSONField(serializers.JSONField):
def to_internal_value(self, data):
try:
return json.dumps(data)
except (TypeError, ValueError):
self.fail('invalid')
def to_representation(self, value):
if value:
return json.load(value)
return value
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=False)
fees = OrderFeeCreateSerializer(many=True, required=False)
status = serializers.ChoiceField(choices=(
('n', Order.STATUS_PENDING),
('p', Order.STATUS_PAID),
), default='n', required=False)
code = serializers.CharField(
required=False,
max_length=16,
min_length=5
)
comment = serializers.CharField(required=False, allow_blank=True)
payment_provider = serializers.CharField(required=True)
payment_info = CompatibleJSONField(required=False)
class Meta:
model = Order
fields = ('code', 'status', 'email', 'locale', 'payment_provider', 'fees', 'comment',
'invoice_address', 'positions', 'checkin_attention', 'payment_info')
def validate_payment_provider(self, pp):
if pp not in self.context['event'].get_payment_providers():
raise ValidationError('The given payment provider is not known.')
return pp
def validate_code(self, code):
if code and Order.objects.filter(event__organizer=self.context['event'].organizer, code=code).exists():
raise ValidationError(
'This order code is already in use.'
)
if any(c not in 'ABCDEFGHJKLMNPQRSTUVWXYZ1234567890' for c in code):
raise ValidationError(
'This order code contains invalid characters.'
)
return code
def validate_positions(self, data):
if not data:
raise ValidationError(
'An order cannot be empty.'
)
if any([p.get('positionid') for p in data]):
if not all([p.get('positionid') for p in data]):
raise ValidationError(
'If you set position IDs manually, you need to do so for all positions.'
)
last_non_add_on = None
last_posid = 0
for p in data:
if p['positionid'] != last_posid + 1:
raise ValidationError("Position IDs need to be consecutive.")
if p.get('addon_to') and p['addon_to'] != last_non_add_on:
raise ValidationError("If you set addon_to, you need to make sure that the referenced "
"position ID exists and is transmitted directly before its add-ons.")
if not p.get('addon_to'):
last_non_add_on = p['positionid']
last_posid = p['positionid']
elif any([p.get('addon_to') for p in data]):
raise ValidationError("If you set addon_to, you need to specify position IDs manually.")
return data
def create(self, validated_data):
fees_data = validated_data.pop('fees') if 'fees' in validated_data else []
positions_data = validated_data.pop('positions') if 'positions' in validated_data else []
if 'invoice_address' in validated_data:
ia = InvoiceAddress(**validated_data.pop('invoice_address'))
else:
ia = None
with self.context['event'].lock():
quotadiff = Counter()
for pos_data in positions_data:
new_quotas = (pos_data.get('variation').quotas.filter(subevent=pos_data.get('subevent'))
if pos_data.get('variation')
else pos_data.get('item').quotas.filter(subevent=pos_data.get('subevent')))
quotadiff.update(new_quotas)
for quota, diff in quotadiff.items():
avail = quota.availability()
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < diff):
raise ValidationError(
'There is not enough quota available on quota "{}" to perform the operation.'.format(
quota.name
)
)
order = Order(event=self.context['event'], **validated_data)
order.set_expires(subevents=[p['subevent'] for p in positions_data])
order.total = sum([p['price'] for p in positions_data]) + sum([f['value'] for f in fees_data], Decimal('0.00'))
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
order.payment_provider = 'free'
order.status = Order.STATUS_PAID
elif order.payment_provider == "free" and order.total != Decimal('0.00'):
raise ValidationError('You cannot use the "free" payment provider for non-free orders.')
order.save()
if ia:
ia.order = order
ia.save()
pos_map = {}
for pos_data in positions_data:
answers_data = pos_data.pop('answers')
addon_to = pos_data.pop('addon_to')
pos = OrderPosition(**pos_data)
pos.order = order
pos._calculate_tax()
if addon_to:
pos.addon_to = pos_map[addon_to]
pos.save()
pos_map[pos.positionid] = pos
for answ_data in answers_data:
options = answ_data.pop('options')
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
for fee_data in fees_data:
f = OrderFee(**fee_data)
f.order = order
f._calculate_tax()
f.save()
return order
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
class Meta:
model = InvoiceLine

View File

@@ -2,6 +2,7 @@ import datetime
import django_filters
import pytz
from django.db import transaction
from django.db.models import Q
from django.db.models.functions import Concat
from django.http import FileResponse
@@ -13,15 +14,18 @@ from rest_framework.exceptions import (
APIException, NotFound, PermissionDenied, ValidationError,
)
from rest_framework.filters import OrderingFilter
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from pretix.api.serializers.order import (
InvoiceSerializer, OrderPositionSerializer, OrderSerializer,
InvoiceSerializer, OrderCreateSerializer, OrderPositionSerializer,
OrderSerializer,
)
from pretix.base.models import Invoice, Order, OrderPosition, Quota
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, regenerate_invoice,
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
@@ -31,7 +35,7 @@ from pretix.base.services.orders import (
from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.signals import register_ticket_outputs
from pretix.base.signals import order_placed, register_ticket_outputs
class OrderFilter(FilterSet):
@@ -45,7 +49,7 @@ class OrderFilter(FilterSet):
fields = ['code', 'status', 'email', 'locale']
class OrderViewSet(viewsets.ReadOnlyModelViewSet):
class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderSerializer
queryset = Order.objects.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -56,6 +60,11 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'can_view_orders'
write_permission = 'can_change_orders'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
@@ -226,6 +235,34 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
def create(self, request, *args, **kwargs):
serializer = OrderCreateSerializer(data=request.data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)
with transaction.atomic():
self.perform_create(serializer)
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
order.log_action(
'pretix.event.order.placed',
user=request.user if request.user.is_authenticated else None,
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
)
order_placed.send(self.request.event, order=order)
gen_invoice = invoice_qualified(order) and (
(order.event.settings.get('invoice_generate') == 'True') or
(order.event.settings.get('invoice_generate') == 'paid' and order.status == Order.STATUS_PAID)
) and not order.invoices.last()
if gen_invoice:
generate_invoice(order, trigger_pdf=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
class OrderPositionFilter(FilterSet):
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')

View File

@@ -545,7 +545,7 @@ class Event(EventMixin, LoggedModel):
def has_payment_provider(self):
result = False
for provider in self.get_payment_providers().values():
if provider.is_enabled and provider.identifier != 'free':
if provider.is_enabled and provider.identifier not in ('free', 'boxoffice'):
result = True
break
return result

View File

@@ -2,7 +2,7 @@ import copy
import json
import os
import string
from datetime import datetime, time
from datetime import datetime, time, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Union
@@ -218,11 +218,42 @@ class Order(LoggedModel):
self.assign_code()
if not self.datetime:
self.datetime = now()
if not self.expires:
self.set_expires()
super().save(**kwargs)
def touch(self):
self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None):
now_dt = now_dt or now()
tz = pytz.timezone(self.event.settings.timezone)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
self.expires = exp_by_date
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents and subevents:
term_last = min([
term_last.datetime(se).date()
for se in subevents
])
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
@cached_property
def tax_total(self):
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + (self.fees.aggregate(s=Sum('tax_value'))['s'] or 0)

View File

@@ -658,6 +658,39 @@ class FreeOrderProvider(BasePaymentProvider):
return False
class BoxOfficeProvider(BasePaymentProvider):
is_implicit = True
is_enabled = True
identifier = "boxoffice"
verbose_name = _("Box office")
def payment_perform(self, request: HttpRequest, order: Order):
from pretix.base.services.orders import mark_order_paid
try:
mark_order_paid(order, 'boxoffice', send_mail=False)
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
@property
def settings_form_fields(self) -> dict:
return {}
def order_control_refund_render(self, order: Order) -> str:
return ''
def order_control_refund_perform(self, request: HttpRequest, order: Order) -> Union[bool, str]:
from pretix.base.services.orders import mark_order_refunded
mark_order_refunded(order, user=request.user)
messages.success(request, _('The order has been marked as refunded.'))
def is_allowed(self, request: HttpRequest) -> bool:
return False
def order_change_allowed(self, order: Order) -> bool:
return False
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):
return FreeOrderProvider
return [FreeOrderProvider, BoxOfficeProvider]

View File

@@ -13,7 +13,7 @@ from django.db import transaction
from django.db.models import F, Max, Q, Sum
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.timezone import make_aware, now
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from pretix.base.i18n import (
@@ -31,7 +31,6 @@ from pretix.base.models.orders import (
from pretix.base.models.organizer import TeamAPIToken
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.async import ProfiledTask
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
@@ -448,50 +447,22 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: InvoiceAddress=None,
meta_info: dict=None):
from datetime import time
fees = _get_fees(positions, payment_provider, address, meta_info, event)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
tz = pytz.timezone(event.settings.timezone)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents:
term_last = min([
term_last.datetime(se).date()
for se in event.subevents.filter(id__in=[p.subevent_id for p in positions])
])
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
expires = term_last
with transaction.atomic():
order = Order.objects.create(
order = Order(
status=Order.STATUS_PENDING,
event=event,
email=email,
datetime=now_dt,
expires=expires,
locale=locale,
total=total,
payment_provider=payment_provider.identifier,
meta_info=json.dumps(meta_info or {}),
)
order.set_expires(now_dt, event.subevents.filter(id__in=[p.subevent_id for p in positions]))
order.save()
if address:
if address.order is not None:

View File

@@ -1,5 +1,6 @@
import copy
import datetime
import json
from decimal import Decimal
from unittest import mock
@@ -9,7 +10,7 @@ from django.utils.timezone import now
from django_countries.fields import Country
from pytz import UTC
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Question
from pretix.base.models.orders import OrderFee
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice,
@@ -21,6 +22,11 @@ def item(event):
return event.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def item2(event2):
return event2.items.create(name="Budget Ticket", default_price=23)
@pytest.fixture
def taxrule(event):
return event.tax_rules.create(rate=Decimal('19.00'))
@@ -34,6 +40,13 @@ def question(event, item):
return q
@pytest.fixture
def question2(event2, item2):
q = event2.questions.create(question="T-Shirt size", type="S", identifier="ABC")
q.items.add(item2)
return q
@pytest.fixture
def quota(event, item):
q = event.quotas.create(name="Budget Quota", size=200)
@@ -752,3 +765,921 @@ def test_order_extend_expired_quota_left(token_client, organizer, event, order,
order.refresh_from_db()
assert order.status == Order.STATUS_PENDING
assert order.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate[:10] + " 23:59:59"
ORDER_CREATE_PAYLOAD = {
"email": "dummy@dummy.test",
"locale": "en",
"fees": [
{
"fee_type": "payment",
"value": "0.25",
"description": "",
"internal_type": "",
"tax_rule": None
}
],
"payment_provider": "banktransfer",
"invoice_address": {
"is_business": False,
"company": "Sample company",
"name": "Fo",
"street": "Bar",
"zipcode": "",
"city": "Sample City",
"country": "NZ",
"internal_reference": "",
"vat_id": ""
},
"positions": [
{
"positionid": 1,
"item": 1,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [
{
"question": 1,
"answer": "S",
"options": []
}
],
"subevent": None
}
],
}
@pytest.mark.django_db
def test_order_create(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
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.email == "dummy@dummy.test"
assert o.locale == "en"
assert o.total == Decimal('23.25')
assert o.status == Order.STATUS_PENDING
assert o.payment_provider == "banktransfer"
fee = o.fees.first()
assert fee.fee_type == "payment"
assert fee.value == Decimal('0.25')
ia = o.invoice_address
assert ia.company == "Sample company"
assert o.positions.count() == 1
pos = o.positions.first()
assert pos.item == item
assert pos.price == Decimal("23.00")
answ = pos.answers.first()
assert answ.question == question
assert answ.answer == "S"
@pytest.mark.django_db
def test_order_create_invoice_address_optional(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
del res['invoice_address']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
with pytest.raises(InvoiceAddress.DoesNotExist):
o.invoice_address
@pytest.mark.django_db
def test_order_create_code_optional(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
res['code'] = 'ABCDE'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.code == "ABCDE"
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.data == {'code': ['This order code is already in use.']}
res['code'] = 'ABaDE'
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.data == {'code': ['This order code contains invalid characters.']}
@pytest.mark.django_db
def test_order_email_optional(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
del res['email']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert not o.email
@pytest.mark.django_db
def test_order_create_payment_info_optional(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
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert not o.payment_info == "{}"
res['payment_info'] = {
'foo': {
'bar': [1, 2],
'test': False
}
}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert json.loads(o.payment_info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_position_secret_optional(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
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.positions.first().secret
res['positions'][0]['secret'] = "aaa"
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.positions.first().secret == "aaa"
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.data == {'positions': [{'secret': ['You cannot assign a position secret that already exists.']}]}
@pytest.mark.django_db
def test_order_create_tax_rules(token_client, organizer, event, item, quota, question, taxrule):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['fees'][0]['tax_rule'] = taxrule.pk
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
item.tax_rule = taxrule
item.save()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
fee = o.fees.first()
assert fee.fee_type == "payment"
assert fee.value == Decimal('0.25')
assert fee.tax_rate == Decimal('19.00')
assert fee.tax_rule == taxrule
ia = o.invoice_address
assert ia.company == "Sample company"
pos = o.positions.first()
assert pos.item == item
assert pos.tax_rate == Decimal('19.00')
assert pos.tax_value == Decimal('3.67')
assert pos.tax_rule == taxrule
@pytest.mark.django_db
def test_order_create_fee_type_validation(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['fees'][0]['fee_type'] = 'unknown'
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'fees': [{'fee_type': ['"unknown" is not a valid choice.']}]}
@pytest.mark.django_db
def test_order_create_tax_rule_wrong_event(token_client, organizer, event, item, quota, question, taxrule2):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['fees'][0]['tax_rule'] = taxrule2.pk
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'fees': [{'tax_rule': ['The specified tax rate does not belong to this event.']}]}
@pytest.mark.django_db
def test_order_create_subevent_not_allowed(token_client, organizer, event, item, quota, question, subevent2):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['subevent'] = subevent2.pk
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.data == {'positions': [{'subevent': ['You cannot set a subevent for this event.']}]}
@pytest.mark.django_db
def test_order_create_empty(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'] = []
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.data == {'positions': ['An order cannot be empty.']}
@pytest.mark.django_db
def test_order_create_subevent_validation(token_client, organizer, event, item, subevent, subevent2, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'positions': [{'subevent': ['You need to set a subevent.']}]}
res['positions'][0]['subevent'] = subevent2.pk
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.data == {'positions': [{'subevent': ['The specified subevent does not belong to this event.']}]}
@pytest.mark.django_db
def test_order_create_item_validation(token_client, organizer, event, item, item2, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
item.active = False
item.save()
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'positions': [{'item': ['The specified item is not active.']}]}
item.active = True
item.save()
res['positions'][0]['item'] = item2.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'positions': [{'item': ['The specified item does not belong to this event.']}]}
var2 = item2.variations.create(value="A")
res['positions'][0]['item'] = item.pk
res['positions'][0]['variation'] = var2.pk
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.data == {'positions': [{'non_field_errors': ['You cannot specify a variation for this item.']}]}
var1 = item.variations.create(value="A")
res['positions'][0]['item'] = item.pk
res['positions'][0]['variation'] = var1.pk
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
res['positions'][0]['variation'] = var2.pk
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.data == {'positions': [{'non_field_errors': ['The specified variation does not belong to the specified item.']}]}
res['positions'][0]['variation'] = None
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.data == {'positions': [{'non_field_errors': ['You should specify a variation for this item.']}]}
@pytest.mark.django_db
def test_order_create_positionids_addons(token_client, organizer, event, item, quota):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'] = [
{
"positionid": 1,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [],
"subevent": None
},
{
"positionid": 2,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": 1,
"answers": [],
"subevent": None
}
]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos1 = o.positions.first()
pos2 = o.positions.last()
assert pos2.addon_to == pos1
@pytest.mark.django_db
def test_order_create_positionid_validation(token_client, organizer, event, item, quota):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'] = [
{
"positionid": 1,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [],
"subevent": None
},
{
"positionid": 2,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": 2,
"answers": [],
"subevent": None
}
]
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.data == {'positions': ['If you set addon_to, you need to make sure that the '
'referenced position ID exists and is transmitted directly '
'before its add-ons.']}
res['positions'] = [
{
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [],
"subevent": None
},
{
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": 2,
"answers": [],
"subevent": None
}
]
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.data == {'positions': ['If you set addon_to, you need to specify position IDs manually.']}
res['positions'] = [
{
"positionid": 1,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"answers": [],
"subevent": None
},
{
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"answers": [],
"subevent": None
}
]
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.data == {'positions': ['If you set position IDs manually, you need to do so for all positions.']}
res['positions'] = [
{
"positionid": 1,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"answers": [],
"subevent": None
},
{
"positionid": 3,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"answers": [],
"subevent": None
}
]
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.data == {'positions': ['Position IDs need to be consecutive.']}
@pytest.mark.django_db
def test_order_create_answer_validation(token_client, organizer, event, item, quota, question, question2):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question2.pk
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.data == {'positions': [{'answers': [{'question': ['The specified question does not belong to this event.']}]}]}
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['answers'][0]['options'] = [question.options.first().pk]
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.data == {'positions': [{'answers': [{'non_field_errors': ['You should not specify options if the question is not of a choice type.']}]}]}
question.type = Question.TYPE_CHOICE
question.save()
res['positions'][0]['answers'][0]['options'] = []
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.data == {'positions': [{'answers': [{'non_field_errors': ['You need to specify options if the question is of a choice type.']}]}]}
question.options.create(answer="L")
res['positions'][0]['answers'][0]['options'] = [
question.options.first().pk,
question.options.last().pk,
]
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.data == {'positions': [{'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}]}
question.type = Question.TYPE_FILE
question.save()
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.data == {'positions': [{'answers': [{'non_field_errors': ['File uploads are currently not supported via the API.']}]}]}
question.type = Question.TYPE_CHOICE_MULTIPLE
question.save()
res['positions'][0]['answers'][0]['options'] = [
question.options.first().pk,
question.options.last().pk,
]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.question == question
assert answ.answer == "XL, L"
question.type = Question.TYPE_NUMBER
question.save()
res['positions'][0]['answers'][0]['options'] = []
res['positions'][0]['answers'][0]['answer'] = '3.45'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "3.45"
question.type = Question.TYPE_NUMBER
question.save()
res['positions'][0]['answers'][0]['options'] = []
res['positions'][0]['answers'][0]['answer'] = 'foo'
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.data == {'positions': [{'answers': [{'non_field_errors': ['A valid number is required.']}]}]}
question.type = Question.TYPE_BOOLEAN
question.save()
res['positions'][0]['answers'][0]['options'] = []
res['positions'][0]['answers'][0]['answer'] = 'True'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "True"
question.type = Question.TYPE_BOOLEAN
question.save()
res['positions'][0]['answers'][0]['answer'] = '0'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "False"
question.type = Question.TYPE_BOOLEAN
question.save()
res['positions'][0]['answers'][0]['answer'] = 'bla'
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.data == {'positions': [{'answers': [{'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}]}]}
question.type = Question.TYPE_DATE
question.save()
res['positions'][0]['answers'][0]['answer'] = '2018-05-14'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "2018-05-14"
question.type = Question.TYPE_DATE
question.save()
res['positions'][0]['answers'][0]['answer'] = 'bla'
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.data == {'positions': [{'answers': [{'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].']}]}]}
question.type = Question.TYPE_DATETIME
question.save()
res['positions'][0]['answers'][0]['answer'] = '2018-05-14T13:00:00Z'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "2018-05-14 13:00:00+00:00"
question.type = Question.TYPE_DATETIME
question.save()
res['positions'][0]['answers'][0]['answer'] = 'bla'
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.data == {'positions': [{'answers': [{'non_field_errors': [
'Datetime has wrong format. Use one of these formats instead: '
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}]}]}
question.type = Question.TYPE_TIME
question.save()
res['positions'][0]['answers'][0]['answer'] = '13:00:00'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.answer == "13:00:00"
question.type = Question.TYPE_TIME
question.save()
res['positions'][0]['answers'][0]['answer'] = 'bla'
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.data == {'positions': [{'answers': [{'non_field_errors': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}]}]}
@pytest.mark.django_db
def test_order_create_quota_validation(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
quota.size = 0
quota.save()
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.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
quota.size = 1
quota.save()
res['positions'] = [
{
"positionid": 1,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": None,
"answers": [],
"subevent": None
},
{
"positionid": 2,
"item": item.pk,
"variation": None,
"price": "23.00",
"attendee_name": "Peter",
"attendee_email": None,
"addon_to": 1,
"answers": [],
"subevent": None
}
]
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.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
@pytest.mark.django_db
def test_order_create_free(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['fees'] = []
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['price'] = '0.00'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.total == Decimal('0.00')
assert o.status == Order.STATUS_PAID
assert o.payment_provider == "free"
@pytest.mark.django_db
def test_order_create_require_payment_provider(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
del res['payment_provider']
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'payment_provider': ['This field is required.']}
@pytest.mark.django_db
def test_order_create_invalid_payment_provider(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['payment_provider'] = 'foo'
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'payment_provider': ['The given payment provider is not known.']}
@pytest.mark.django_db
def test_order_create_invalid_free_order(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['payment_provider'] = 'free'
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == ['You cannot use the "free" payment provider for non-free orders.']
@pytest.mark.django_db
def test_order_create_invalid_status(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['status'] = 'e'
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
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.data == {'status': ['"e" is not a valid choice.']}
@pytest.mark.django_db
def test_order_create_paid_generate_invoice(token_client, organizer, event, item, quota, question):
event.settings.invoice_generate = 'paid'
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['status'] = 'p'
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
o = Order.objects.get(code=resp.data['code'])
assert o.invoices.count() == 1