From c97afdac51e322797104abf007179f1ee7ee5cff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 5 Mar 2020 14:02:52 +0100
Subject: [PATCH 2/2] api: endpoint to resize a booking (#40039)

---
 chrono/api/urls.py  |   1 +
 chrono/api/views.py | 101 ++++++++++++++++++++++++++++++++++++++
 tests/test_api.py   | 117 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 219 insertions(+)

diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index 119210a..8cbd115 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -49,5 +49,6 @@ urlpatterns = [
     url(r'^booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, name='api-cancel-booking'),
     url(r'^booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, name='api-accept-booking'),
     url(r'^booking/(?P<booking_pk>\w+)/suspend/$', views.suspend_booking, name='api-suspend-booking'),
+    url(r'^booking/(?P<booking_pk>\w+)/resize/$', views.resize_booking, name='api-resize-booking'),
     url(r'^booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'),
 ]
diff --git a/chrono/api/views.py b/chrono/api/views.py
index f52f780..3584778 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -842,6 +842,107 @@ class SuspendBooking(APIView):
 suspend_booking = SuspendBooking.as_view()
 
 
+class ResizeSerializer(serializers.Serializer):
+    count = serializers.IntegerField(min_value=1)
+
+
+class ResizeBooking(APIView):
+    '''
+    Resize a booking.
+
+    It will return error codes if the booking was cancelled before (code 1)
+    if the booking is not primary (code 2)
+    if the event is sold out (code 3) or
+    if the booking is on multi events (code 4).
+    '''
+
+    permission_classes = (permissions.IsAuthenticated,)
+    serializer_class = ResizeSerializer
+
+    def post(self, request, booking_pk=None, format=None):
+        serializer = self.serializer_class(data=request.data)
+        if not serializer.is_valid():
+            return Response(
+                {
+                    'err': 1,
+                    'err_class': 'invalid payload',
+                    'err_desc': _('invalid payload'),
+                    'errors': serializer.errors,
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+        payload = serializer.validated_data
+
+        booking = get_object_or_404(Booking, pk=booking_pk)
+        event = booking.event
+        if booking.cancellation_datetime:
+            response = {
+                'err': 1,
+                'err_class': 'booking is cancelled',
+                'err_desc': _('booking is cancelled'),
+            }
+            return Response(response)
+        if booking.primary_booking is not None:
+            response = {
+                'err': 2,
+                'err_class': 'secondary booking',
+                'err_desc': _('secondary booking'),
+            }
+            return Response(response)
+        event_ids = set([event.pk])
+        secondary_bookings = booking.secondary_booking_set.all().order_by('-creation_datetime')
+        for secondary in secondary_bookings:
+            event_ids.add(secondary.event_id)
+        if len(event_ids) > 1:
+            response = {
+                'err': 4,
+                'err_class': 'can not resize multi event booking',
+                'err_desc': _('can not resize multi event booking'),
+            }
+            return Response(response)
+
+        count = payload['count']
+        booked_places = event.waiting_list_places and event.waiting_list or event.booked_places
+        if booked_places < count:
+            if (event.waiting_list and count > event.waiting_list_places or
+                    not event.waiting_list and count > event.places):
+                response = {
+                    'err': 3,
+                    'err_class': 'sold out',
+                    'err_desc': _('sold out'),
+                }
+                return Response(response)
+
+        with transaction.atomic():
+            if booked_places > count:
+                # decrease places
+                for secondary in secondary_bookings[:booked_places - count]:
+                    secondary.delete()
+            elif booked_places < count:
+                # increase places
+                bulk_bookings = []
+                for i in range(0, count - booked_places):
+                    bulk_bookings.append(
+                        Booking(
+                            primary_booking=booking,
+                            event_id=event.pk,
+                            in_waiting_list=bool(event.waiting_list_places and event.waiting_list),
+                            label=booking.label,
+                            user_name=booking.user_name,
+                            backoffice_url=booking.backoffice_url,
+                            user_display_label=booking.user_display_label,
+                            extra_data=booking.extra_data
+                        )
+                    )
+                Booking.objects.bulk_create(bulk_bookings)
+
+        response = {'err': 0, 'booking_id': booking.pk}
+        return Response(response)
+
+
+resize_booking = ResizeBooking.as_view()
+
+
 class SlotStatus(APIView):
     permission_classes = (permissions.IsAuthenticated,)
 
diff --git a/tests/test_api.py b/tests/test_api.py
index 534d535..817eebe 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1404,6 +1404,123 @@ def test_suspend_booking(app, some_data, user):
     assert primary.in_waiting_list is False
 
 
+def test_resize_booking(app, some_data, user):
+    agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
+    event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]
+    event.places = 5
+    event.waiting_list_places = 5
+    event.save()
+    event2 = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[1]
+
+    # create a booking not on the waiting list
+    primary = Booking.objects.create(event=event, in_waiting_list=False)
+    secondary = Booking.objects.create(event=event, in_waiting_list=False, primary_booking=primary)
+
+    assert Booking.objects.filter(in_waiting_list=False).count() == 2
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    resp = app.post('/api/booking/%s/resize/' % secondary.pk, params={'count': 42})
+    assert resp.json['err'] == 2
+    assert resp.json['reason'] == 'secondary booking'  # legacy
+    assert resp.json['err_class'] == 'secondary booking'
+    assert resp.json['err_desc'] == 'secondary booking'
+
+    # resize a booking that doesn't exist
+    resp = app.post('/api/booking/0/resize/', params={'count': 42}, status=404)
+
+    # decrease a booking to 0
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 0}, status=400)
+    assert resp.json['err'] == 1
+
+    # decrease a booking not in waiting list
+    assert Booking.objects.filter(in_waiting_list=False).count() == 2
+    assert Booking.objects.filter().count() == 2
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 1})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=False).count() == 1
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 1
+
+    # no changes
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 1})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=False).count() == 1
+    assert Booking.objects.filter().count() == 1
+
+    # increase a booking not in waiting list
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 2})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=False).count() == 2
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 2
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 6})
+    assert resp.json['err'] == 3
+    assert resp.json['reason'] == 'sold out'  # legacy
+    assert resp.json['err_class'] == 'sold out'
+    assert resp.json['err_desc'] == 'sold out'
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 5})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=False).count() == 5
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 5
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 6})
+    assert resp.json['err'] == 3
+    assert resp.json['reason'] == 'sold out'  # legacy
+    assert resp.json['err_class'] == 'sold out'
+    assert resp.json['err_desc'] == 'sold out'
+
+    # decrease a booking in waiting list
+    primary.suspend()
+    assert Booking.objects.filter(in_waiting_list=True).count() == 5
+    assert Booking.objects.filter().count() == 5
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 1})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=True).count() == 1
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 1
+
+    # no changes
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 1})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=True).count() == 1
+    assert Booking.objects.filter().count() == 1
+
+    # increase a booking in waiting list
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 2})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=True).count() == 2
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 2
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 6})
+    assert resp.json['err'] == 3
+    assert resp.json['reason'] == 'sold out'  # legacy
+    assert resp.json['err_class'] == 'sold out'
+    assert resp.json['err_desc'] == 'sold out'
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 5})
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(in_waiting_list=True).count() == 5
+    assert Booking.objects.filter(primary_booking__isnull=True).count() == 1
+    assert Booking.objects.filter().count() == 5
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 6})
+    assert resp.json['err'] == 3
+    assert resp.json['reason'] == 'sold out'  # legacy
+    assert resp.json['err_class'] == 'sold out'
+    assert resp.json['err_desc'] == 'sold out'
+
+    # resize a booking that is on multi events
+    secondary = Booking.objects.create(event=event2, in_waiting_list=False, primary_booking=primary)
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 42})
+    assert resp.json['err'] == 4
+    assert resp.json['reason'] == 'can not resize multi event booking'  # legacy
+    assert resp.json['err_class'] == 'can not resize multi event booking'
+    assert resp.json['err_desc'] == 'can not resize multi event booking'
+    # resize a booking that was cancelled before
+    primary.cancel()
+    resp = app.post('/api/booking/%s/resize/' % primary.pk, params={'count': 42})
+    assert resp.json['err'] == 1
+
+
 def test_multiple_booking_api(app, some_data, user):
     agenda = Agenda.objects.filter(label=u'Foo bar')[0]
     event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0]
-- 
2.20.1

