From 3750c27b05339ac68bbc1969ae57248535cac02b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 3 Feb 2022 15:37:09 +0100
Subject: [PATCH 4/5] api: remove user bookings on subscription date changes
 (#61065)

---
 chrono/api/views.py            |  42 +++--
 tests/api/test_subscription.py | 303 +++++++++++++++++++++++++++++++++
 2 files changed, 333 insertions(+), 12 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index f8bbb17..c360db9 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1967,6 +1967,20 @@ class SubscriptionAPI(APIView):
         response.update({'err': 0})
         return Response(response)
 
+    def delete_out_of_period_bookings(self, date_start, date_end):
+        booking_qs = Booking.objects.filter(
+            # remove user bookings for this agenda
+            event__agenda=self.subscription.agenda,
+            user_external_id=self.subscription.user_external_id,
+            # in the requested period
+            event__start_datetime__gt=date_start,
+            event__start_datetime__lt=date_end + datetime.timedelta(days=1),
+        ).filter(
+            # but only in the future
+            event__start_datetime__gt=now(),
+        )
+        booking_qs.delete()
+
     def patch(self, request, *args, **kwargs):
         serializer = self.serializer_class(self.subscription, data=request.data, partial=True)
         old_date_start = self.subscription.date_start
@@ -2006,21 +2020,25 @@ class SubscriptionAPI(APIView):
             self.subscription.extra_data.update(extra_data)
             self.subscription.save()
 
+        if old_date_start > self.subscription.date_end or old_date_end < self.subscription.date_start:
+            # new period does not overlaps the old one, delete all bookings in the old period
+            self.delete_out_of_period_bookings(old_date_start, old_date_end)
+        else:
+            if old_date_start < self.subscription.date_start:
+                # date start has been postponed, remove all bookings from old start to new start
+                self.delete_out_of_period_bookings(
+                    old_date_start, self.subscription.date_start - datetime.timedelta(days=1)
+                )
+            if old_date_end > self.subscription.date_end:
+                # date end has been brought forward, remove all bookings from new end to old end
+                self.delete_out_of_period_bookings(
+                    self.subscription.date_end + datetime.timedelta(days=1), old_date_end
+                )
+
         return self.get(request, *args, **kwargs)
 
     def delete(self, request, *args, **kwargs):
-        booking_qs = Booking.objects.filter(
-            # remove user bookings for this agenda
-            event__agenda=self.subscription.agenda,
-            user_external_id=self.subscription.user_external_id,
-            # in the period of the deleted subscription
-            event__start_datetime__gt=self.subscription.date_start,
-            event__start_datetime__lt=self.subscription.date_end + datetime.timedelta(days=1),
-        ).filter(
-            # but only in the future
-            event__start_datetime__gt=now(),
-        )
-        booking_qs.delete()
+        self.delete_out_of_period_bookings(self.subscription.date_start, self.subscription.date_end)
         self.subscription.delete()
         response = {'err': 0}
         return Response(response)
diff --git a/tests/api/test_subscription.py b/tests/api/test_subscription.py
index 888b82c..aa07e7a 100644
--- a/tests/api/test_subscription.py
+++ b/tests/api/test_subscription.py
@@ -729,3 +729,306 @@ def test_api_patch_subscription_check_dates(app, user):
         '/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params, status=400
     )
     assert resp.json['err_desc'] == 'another subscription overlapping this period already exists'
+
+
+@pytest.mark.parametrize(
+    'date_now, event_date, user_id, in_waiting_list, cancelled, deleted',
+    [
+        # event in the future, but no booking for the user
+        ('2021-09-04 09:59', (2021, 9, 4, 12, 0), 'yyy', False, False, False),
+        # event in the future
+        ('2021-09-04 09:59', (2021, 9, 4, 12, 0), 'xxx', False, False, True),
+        ('2021-09-04 09:59', (2021, 9, 4, 12, 0), 'xxx', True, False, True),
+        ('2021-09-04 09:59', (2021, 9, 4, 12, 0), 'xxx', False, True, True),
+        # event in the past
+        ('2021-09-04 10:00', (2021, 9, 4, 12, 0), 'xxx', False, False, False),
+        # event in the future, before the period
+        ('2021-08-01 10:00', (2021, 8, 14, 12, 0), 'xxx', False, False, False),
+        ('2021-08-01 10:00', (2021, 8, 31, 12, 0), 'xxx', False, False, False),
+        # event in the future, after the period
+        ('2021-08-01 10:00', (2021, 10, 1, 12, 0), 'xxx', False, False, False),
+        ('2021-08-01 10:00', (2021, 10, 16, 12, 0), 'xxx', False, False, False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings(
+    app, user, freezer, date_now, event_date, user_id, in_waiting_list, cancelled, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+    Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='zzz',  # another user
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    other_agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    Subscription.objects.create(
+        agenda=other_agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=10, day=1),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to(date_now)
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id=user_id,
+        in_waiting_list=in_waiting_list,
+        cancellation_datetime=(now() if cancelled else None),
+    )
+
+    # date_start is postponed, date_end is brought forward
+    params = {
+        'date_start': '2021-09-05',
+        'date_end': '2021-09-25',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
+
+
+@pytest.mark.parametrize(
+    'event_date, deleted',
+    [
+        # just before old first day
+        ((2021, 8, 31, 12, 00), False),
+        # old first day
+        ((2021, 9, 1, 12, 00), True),
+        # old last day
+        ((2021, 9, 30, 12, 00), True),
+        # just after old last day
+        ((2021, 10, 1, 12, 00), False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings_forwards_no_overlaps(
+    app, user, freezer, event_date, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to('2021-08-01 10:00')
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id='xxx',
+    )
+    # subscription moved forwards, no overlaps
+    params = {
+        'date_start': '2021-11-01',
+        'date_end': '2021-11-30',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
+
+
+@pytest.mark.parametrize(
+    'event_date, deleted',
+    [
+        # just before old first day
+        ((2021, 8, 31, 12, 00), False),
+        # old first day
+        ((2021, 9, 1, 12, 00), True),
+        # old last day
+        ((2021, 9, 30, 12, 00), True),
+        # just after old last day
+        ((2021, 10, 1, 12, 00), False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings_backwards_no_overlaps(
+    app, user, freezer, event_date, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to('2021-06-01 10:00')
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id='xxx',
+    )
+    # subscription moved backwards, no overlaps
+    params = {
+        'date_start': '2021-07-01',
+        'date_end': '2021-07-31',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
+
+
+@pytest.mark.parametrize(
+    'event_date, deleted',
+    [
+        # just before old first day
+        ((2021, 8, 31, 12, 00), False),
+        # old first day
+        ((2021, 9, 1, 12, 00), True),
+        # just before new first day
+        ((2021, 9, 4, 12, 00), True),
+        # new first day
+        ((2021, 9, 5, 12, 00), False),
+        # new last day
+        ((2021, 9, 25, 12, 00), False),
+        # just after new last day
+        ((2021, 9, 26, 12, 00), True),
+        # old last day
+        ((2021, 9, 30, 12, 00), True),
+        # just after old last day
+        ((2021, 10, 1, 12, 00), False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings_shorter_period(
+    app, user, freezer, event_date, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to('2021-08-01 10:00')
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id='xxx',
+    )
+    # date_start is postponed, date_end is brought forward
+    params = {
+        'date_start': '2021-09-05',
+        'date_end': '2021-09-25',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
+
+
+@pytest.mark.parametrize(
+    'event_date, deleted',
+    [
+        # just before old first day
+        ((2021, 8, 31, 12, 00), False),
+        # old first day
+        ((2021, 9, 1, 12, 00), True),
+        # just before new first day
+        ((2021, 9, 14, 12, 00), True),
+        # new first day
+        ((2021, 9, 15, 12, 00), False),
+        # old last day
+        ((2021, 9, 30, 12, 00), False),
+        # just after old last day
+        ((2021, 10, 1, 12, 00), False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings_forwards_with_overlaps(
+    app, user, freezer, event_date, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to('2021-08-01 10:00')
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id='xxx',
+    )
+    # subscription moved forwards, with overlaps
+    params = {
+        'date_start': '2021-09-15',
+        'date_end': '2021-10-15',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
+
+
+@pytest.mark.parametrize(
+    'event_date, deleted',
+    [
+        # just before old first day
+        ((2021, 8, 31, 12, 00), False),
+        # old first day
+        ((2021, 9, 1, 12, 00), False),
+        # new last day
+        ((2021, 9, 15, 12, 00), False),
+        # just after new last day
+        ((2021, 9, 16, 12, 00), True),
+        # old last day
+        ((2021, 9, 30, 12, 00), True),
+        # just after old last day
+        ((2021, 10, 1, 12, 00), False),
+    ],
+)
+def test_api_patch_subscription_date_changes_delete_bookings_backwards_with_overlaps(
+    app, user, freezer, event_date, deleted
+):
+    agenda = Agenda.objects.create(label='Foo bar', kind='events')
+    subscription = Subscription.objects.create(
+        agenda=agenda,
+        user_external_id='xxx',
+        date_start=datetime.date(year=2021, month=9, day=1),
+        date_end=datetime.date(year=2021, month=9, day=30),
+    )
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    freezer.move_to('2021-08-01 10:00')
+    event = Event.objects.create(
+        agenda=agenda, start_datetime=make_aware(datetime.datetime(*event_date)), places=10
+    )
+    booking = Booking.objects.create(
+        event=event,
+        user_external_id='xxx',
+    )
+    # subscription moved backwards, with overlaps
+    params = {
+        'date_start': '2021-08-15',
+        'date_end': '2021-09-15',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    assert Booking.objects.filter(pk=booking.pk).exists() is not deleted
-- 
2.34.1

