API: Allow to simulate orders

This commit is contained in:
Raphael Michel
2020-03-05 12:52:26 +01:00
parent f7fddc05dd
commit ee260c8231
4 changed files with 207 additions and 23 deletions

View File

@@ -25,6 +25,7 @@ from pretix.base.models.orders import (
)
from pretix.base.pdf import get_variables
from pretix.base.services.cart import error_messages
from pretix.base.services.locking import NoLockManager
from pretix.base.services.pricing import get_price
from pretix.base.settings import COUNTRIES_WITH_STATE_IN_ADDRESS
from pretix.base.signals import register_ticket_outputs
@@ -96,6 +97,11 @@ class AnswerQuestionOptionsIdentifierField(serializers.Field):
return [o.identifier for o in instance.options.all()]
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class InlineSeatSerializer(I18nAwareModelSerializer):
class Meta:
@@ -106,6 +112,7 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
options = AnswerQuestionOptionsField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
@@ -585,6 +592,28 @@ class CompatibleJSONField(serializers.JSONField):
return value
class WrappedList:
def __init__(self, data):
self._data = data
def all(self):
return self._data
class WrappedModel:
def __init__(self, model):
self._wrapped = model
def __getattr__(self, item):
return getattr(self._wrapped, item)
def save(self, *args, **kwargs):
raise NotImplementedError
def delete(self, *args, **kwargs):
raise NotImplementedError
class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAddressSerializer(required=False)
positions = OrderPositionCreateSerializer(many=True, required=True)
@@ -605,6 +634,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
force = serializers.BooleanField(default=False, required=False)
payment_date = serializers.DateTimeField(required=False, allow_null=True)
send_mail = serializers.BooleanField(default=False, required=False)
simulate = serializers.BooleanField(default=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -614,7 +644,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
model = Order
fields = ('code', 'status', 'testmode', 'email', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
'force', 'send_mail')
'force', 'send_mail', 'simulate')
def validate_payment_provider(self, pp):
if pp is None:
@@ -706,6 +736,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
payment_info = validated_data.pop('payment_info', '{}')
payment_date = validated_data.pop('payment_date', now())
force = validated_data.pop('force', False)
simulate = validated_data.pop('simulate', False)
self._send_mail = validated_data.pop('send_mail', False)
if 'invoice_address' in validated_data:
@@ -719,7 +750,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
else:
ia = None
with self.context['event'].lock() as now_dt:
lockfn = self.context['event'].lock
if simulate:
lockfn = NoLockManager
with lockfn() as now_dt:
free_seats = set()
seats_seen = set()
consume_carts = validated_data.pop('consume_carts', [])
@@ -869,11 +903,20 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.set_expires(subevents=[p.get('subevent') for p in positions_data])
order.meta_info = "{}"
order.total = Decimal('0.00')
order.save()
if simulate:
order = WrappedModel(order)
order.last_modified = now()
order.code = 'PREVIEW'
else:
order.save()
if ia:
ia.order = order
ia.save()
if not simulate:
ia.order = order
ia.save()
else:
order.invoice_address = ia
ia.last_modified = now()
pos_map = {}
for pos_data in positions_data:
@@ -885,7 +928,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'_legacy': attendee_name
}
pos = OrderPosition(**pos_data)
pos.order = order
if simulate:
pos.order = order._wrapped
else:
pos.order = order
if addon_to:
pos.addon_to = pos_map[addon_to]
@@ -916,19 +962,33 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
invoice_address=ia,
).gross
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
if simulate:
pos = WrappedModel(pos)
pos.id = 0
answers = []
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = WrappedModel(QuestionAnswer(**answ_data))
answ.options = WrappedList(options)
answers.append(answ)
pos.answers = answers
pos.pseudonymization_id = "PREVIEW"
else:
if pos.voucher:
Voucher.objects.filter(pk=pos.voucher.pk).update(redeemed=F('redeemed') + 1)
pos.save()
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
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 cp in delete_cps:
cp.delete()
if not simulate:
for cp in delete_cps:
cp.delete()
order.total = sum([p.price for p in order.positions.all()])
order.total = sum([p.price for p in pos_map.values()])
fees = []
for fee_data in fees_data:
is_percentage = fee_data.pop('_treat_value_as_percentage', False)
if is_percentage:
@@ -960,17 +1020,26 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
fee_data['tax_rule'] = tr
fee_data['value'] = val
f = OrderFee(**fee_data)
f.order = order
f.order = order._wrapped if simulate else order
f._calculate_tax()
f.save()
fees.append(f)
if not simulate:
f.save()
else:
f = OrderFee(**fee_data)
f.order = order
f.order = order._wrapped if simulate else order
f._calculate_tax()
f.save()
fees.append(f)
if not simulate:
f.save()
order.total += sum([f.value for f in order.fees.all()])
order.save(update_fields=['total'])
order.total += sum([f.value for f in fees])
if simulate:
order.fees = fees
order.positions = pos_map.values()
return order # ignore payments
else:
order.save(update_fields=['total'])
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
payment_provider = 'free'

View File

@@ -466,6 +466,9 @@ class OrderViewSet(viewsets.ModelViewSet):
send_mail = serializer._send_mail
order = serializer.instance
serializer = OrderSerializer(order, context=serializer.context)
if not order.pk:
# Simulation
return Response(serializer.data, status=status.HTTP_201_CREATED)
order.log_action(
'pretix.event.order.placed',

View File

@@ -19,7 +19,7 @@ from pretix.base.models import (
InvoiceAddress, Order, OrderPosition, Question, SeatingPlan,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund,
CartPosition, OrderFee, OrderPayment, OrderRefund, QuestionAnswer,
)
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice,
@@ -1539,6 +1539,111 @@ def test_order_create(token_client, organizer, event, item, quota, question):
assert answ.answer == "S"
@pytest.mark.django_db
def test_order_create_simulate(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
question.type = Question.TYPE_CHOICE_MULTIPLE
question.save()
with scopes_disabled():
opt = question.options.create(answer="L")
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['answers'][0]['options'] = [opt.pk]
res['simulate'] = True
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():
assert Order.objects.count() == 0
assert QuestionAnswer.objects.count() == 0
assert OrderPosition.objects.count() == 0
assert OrderFee.objects.count() == 0
assert InvoiceAddress.objects.count() == 0
d = resp.data
del d['last_modified']
del d['secret']
del d['url']
del d['expires']
del d['invoice_address']['last_modified']
del d['positions'][0]['secret']
assert d == {
'code': 'PREVIEW',
'status': 'n',
'testmode': False,
'email': 'dummy@dummy.test',
'locale': 'en',
'datetime': None,
'payment_date': None,
'payment_provider': None,
'fees': [
{
'fee_type': 'payment',
'value': '0.25',
'description': '',
'internal_type': '',
'tax_rate': '0.00',
'tax_value': '0.00',
'tax_rule': None,
'canceled': False
}
],
'total': '23.25',
'comment': '',
'invoice_address': {
'is_business': False,
'company': 'Sample company',
'name': 'Fo',
'name_parts': {'full_name': 'Fo', '_scheme': 'full'},
'street': 'Bar',
'zipcode': '',
'city': 'Sample City',
'country': 'NZ',
'state': '',
'vat_id': '',
'vat_id_validated': False,
'internal_reference': ''
},
'positions': [
{
'id': 0,
'order': '',
'positionid': 1,
'item': 1,
'variation': None,
'price': '23.00',
'attendee_name': 'Peter',
'attendee_name_parts': {'full_name': 'Peter', '_scheme': 'full'},
'attendee_email': None,
'voucher': None,
'tax_rate': '0.00',
'tax_value': '0.00',
'addon_to': None,
'subevent': None,
'checkins': [],
'downloads': [],
'answers': [
{'question': question.pk, 'answer': 'L', 'question_identifier': 'ABC',
'options': [opt.pk],
'option_identifiers': [opt.identifier]}
],
'tax_rule': None,
'pseudonymization_id': 'PREVIEW',
'seat': None,
'canceled': False
}
],
'downloads': [],
'checkin_attention': False,
'payments': [],
'refunds': [],
'require_approval': False,
'sales_channel': 'web',
}
@pytest.mark.django_db
def test_order_create_autocheckin(token_client, organizer, event, item, quota, question, clist_autocheckin):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)