Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions app/eventyay/api/views/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,65 @@ def mark_canceled(self, request, **kwargs):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return self.retrieve(request, [], **kwargs)

@action(detail=False, methods=['POST'], url_path='create_onsite')
def create_onsite(self, request, **kwargs):
"""
Create a new order for on-site sales (box office).

This endpoint is restricted to staff with can_change_orders permission
(enforced via OrderViewSet.write_permission).

Orders are created as paid with manual payment provider, assuming payment
(cash/card) has been completed at the counter. This matches the behavior
of existing order import and box-office workflows.
"""
data = request.data.copy()

# Force box-office semantics - do not allow client overrides
# Payment is assumed to be completed at the counter (cash/manual)
data['payment_provider'] = 'manual'
data['status'] = 'p' # Paid
data['sales_channel'] = 'box_office' # Always box office
data['send_email'] = False # Never send email for on-site orders

serializer = OrderCreateSerializer(data=data, context=self.get_serializer_context())
serializer.is_valid(raise_exception=True)

with transaction.atomic():
try:
self.perform_create(serializer)
except TaxRule.SaleNotAllowed:
raise ValidationError(_('One of the selected products is not available in the selected country.'))

order = serializer.instance
if not order.pk:
raise ValidationError(_('Order creation failed.'))

serializer_output = OrderSerializer(order, context=serializer.context)

order.log_action(
'eventyay.event.order.placed',
user=request.user if request.user.is_authenticated else None,
auth=request.auth,
data={'source': 'onsite_api'},
)

with language(order.locale, self.request.event.settings.region):
order_placed.send(self.request.event, order=order)
if order.status == Order.STATUS_PAID:
order_paid.send(self.request.event, order=order)

return Response(
{
'order_id': order.pk,
'code': order.code,
'status': order.status,
'total': str(order.total),
'order': serializer_output.data,
},
status=status.HTTP_201_CREATED,
)

@action(detail=True, methods=['POST'])
def reactivate(self, request, **kwargs):
order = self.get_object()
Expand Down
129 changes: 129 additions & 0 deletions src/tests/api/test_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -4918,3 +4918,132 @@ def test_position_update_question_handling(token_client, organizer, event, order
answ = op.answers.get()
assert answ.file
assert answ.answer.startswith('file://')


@pytest.mark.django_db
def test_order_create_onsite(token_client, organizer, event, item, quota, question):
res = {
'email': '[email protected]',
'locale': 'en',
'positions': [
{
'item': item.pk,
'variation': None,
'price': '23.00',
'attendee_name_parts': {'full_name': 'John Doe'},
'attendee_email': '[email protected]',
'answers': [{'question': question.pk, 'answer': 'M', 'options': []}],
'subevent': None,
}
],
}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/create_onsite/'.format(organizer.slug, event.slug),
format='json',
data=res,
)
assert resp.status_code == 201
assert resp.data['status'] == 'p'
assert resp.data['code']
assert resp.data['order_id']
assert resp.data['total'] == '23.00'

with scopes_disabled():
o = Order.objects.get(pk=resp.data['order_id'])
assert o.status == Order.STATUS_PAID
assert o.email == '[email protected]'
assert o.total == Decimal('23.00')
assert o.positions.count() == 1
assert o.sales_channel == 'box_office'

payment = o.payments.filter(provider='manual', state=OrderPayment.PAYMENT_STATE_CONFIRMED).first()
assert payment is not None
assert payment.amount == o.total


@pytest.mark.django_db
def test_order_create_onsite_without_permission(client, organizer, event, item, quota, question):
res = {
'email': '[email protected]',
'locale': 'en',
'positions': [
{
'item': item.pk,
'variation': None,
'price': '23.00',
'attendee_name_parts': {'full_name': 'John Doe'},
'subevent': None,
}
],
}
resp = client.post(
'/api/v1/organizers/{}/events/{}/orders/create_onsite/'.format(organizer.slug, event.slug),
format='json',
data=res,
)
assert resp.status_code == 401


@pytest.mark.django_db
def test_order_create_onsite_authenticated_without_can_change_orders(user_client, organizer, event, item, quota, question, team):
team.can_change_orders = False
team.save()

res = {
'email': '[email protected]',
'locale': 'en',
'positions': [
{
'item': item.pk,
'variation': None,
'price': '23.00',
'attendee_name_parts': {'full_name': 'John Doe'},
'subevent': None,
}
],
}
resp = user_client.post(
'/api/v1/organizers/{}/events/{}/orders/create_onsite/'.format(organizer.slug, event.slug),
format='json',
data=res,
)
assert resp.status_code == 403


@pytest.mark.django_db
def test_order_create_onsite_authenticated_with_can_change_orders(user_client, organizer, event, item, quota, question, team):
team.can_change_orders = True
team.save()

res = {
'email': '[email protected]',
'locale': 'en',
'positions': [
{
'item': item.pk,
'variation': None,
'price': '23.00',
'attendee_name_parts': {'full_name': 'John Doe'},
'attendee_email': '[email protected]',
'answers': [{'question': question.pk, 'answer': 'M', 'options': []}],
'subevent': None,
}
],
}
resp = user_client.post(
'/api/v1/organizers/{}/events/{}/orders/create_onsite/'.format(organizer.slug, event.slug),
format='json',
data=res,
)
assert resp.status_code == 201
assert resp.data['status'] == 'p'
assert resp.data['code']
assert resp.data['order_id']

with scopes_disabled():
o = Order.objects.get(pk=resp.data['order_id'])
assert o.status == Order.STATUS_PAID
assert o.sales_channel == 'box_office'

payment = o.payments.filter(provider='manual', state=OrderPayment.PAYMENT_STATE_CONFIRMED).first()
assert payment is not None