From df49bc9f520903809afa3e47f5eae8d1e5bf4ad4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 7 Feb 2022 13:53:04 +0100
Subject: [PATCH 3/3] api: cancel booking in agendas events fillslots - instead
 of delete (#61066)

---
 chrono/api/views.py        |   6 +-
 tests/api/test_fillslot.py | 205 +++++++++++++++++++++++++++++--------
 2 files changed, 168 insertions(+), 43 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index 160f7c2..46bcd6e 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1918,7 +1918,11 @@ class MultipleAgendasEventsFillslots(EventsFillslots):
         return events
 
     def get_already_booked_events(self, user_external_id):
-        return Event.objects.filter(agenda__in=self.agendas, booking__user_external_id=user_external_id)
+        return Event.objects.filter(
+            agenda__in=self.agendas,
+            booking__user_external_id=user_external_id,
+            booking__cancellation_datetime__isnull=True,
+        )
 
     def get_agendas_by_ids(self):
         return {a.pk: a for a in self.agendas}
diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py
index 1fd99c1..1356c04 100644
--- a/tests/api/test_fillslot.py
+++ b/tests/api/test_fillslot.py
@@ -3560,7 +3560,7 @@ def test_api_events_fillslots_preserve_past_bookings(app, user, freezer):
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
     assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
 
 
 @pytest.mark.freeze_time('2021-09-06 12:00')
@@ -3588,16 +3588,16 @@ def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 0
-    assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
     booking = event.booking_set.get()
     # except if we want to bypass delays
     params = {'user_external_id': 'user_id', 'slots': 'event-2', 'bypass_delays': True}
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert event.booking_set.count() == 0
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
     booking.save()  # reset
 
     # cancel all bookings in delays
@@ -3606,14 +3606,14 @@ def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
     assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
     # bypass delays
     params = {'user_external_id': 'user_id', 'slots': '', 'bypass_delays': True}
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert event.booking_set.count() == 0
-    assert second_event.booking_set.count() == 0
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
     booking.save()  # reset
 
     # book only second event while first event is out of delay with multiple agendas API
@@ -3628,8 +3628,8 @@ def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer
     resp = app.post_json('/api/agendas/events/fillslots/?agendas=foo-bar', params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert event.booking_set.count() == 0
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
     booking.save()  # reset
 
     # book only first event while second event is out of delay
@@ -3638,38 +3638,38 @@ def test_api_events_fillslots_preserve_out_of_delays_bookings(app, user, freezer
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 0
-    assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
     # bypass_delays has no effect on maximal_booking_delay
     params = {'user_external_id': 'user_id', 'slots': 'event', 'bypass_delays': True}
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 0
-    assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
 
     # cancel all bookings in delays
     params = {'user_external_id': 'user_id', 'slots': ''}
     resp = app.post_json(fillslots_url, params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert event.booking_set.count() == 0
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
 
     # book only first event while second event is out of delay with multiple agendas API
     params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event'}
     resp = app.post_json('/api/agendas/events/fillslots/?agendas=foo-bar', params=params)
     assert resp.json['booking_count'] == 1
     assert resp.json['cancelled_booking_count'] == 0
-    assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
     # bypass_delays has no effect on maximal_booking_delay
     params = {'user_external_id': 'user_id', 'slots': 'foo-bar@event', 'bypass_delays': True}
     resp = app.post_json('/api/agendas/events/fillslots/?agendas=foo-bar', params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 0
-    assert event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
 
 
 @pytest.mark.freeze_time('2021-09-06 12:00')
@@ -3703,8 +3703,8 @@ def test_api_events_fillslots_multiple_agendas(app, user):
     assert len(resp.json['booked_events']) == 2
     assert resp.json['booked_events'][0]['id'] == 'first-agenda@event'
     assert resp.json['booked_events'][1]['id'] == 'second-agenda@event'
-    assert first_event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 1
+    assert first_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
 
     # booking modification
     params = {'user_external_id': 'user_id', 'slots': 'first-agenda@event'}
@@ -3712,8 +3712,8 @@ def test_api_events_fillslots_multiple_agendas(app, user):
     assert resp.json['booking_count'] == 0
     assert len(resp.json['booked_events']) == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert first_event.booking_set.count() == 1
-    assert second_event.booking_set.count() == 0
+    assert first_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 0
 
     params = {'user_external_id': 'user_id_2', 'slots': event_slugs}
     resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
@@ -3721,8 +3721,8 @@ def test_api_events_fillslots_multiple_agendas(app, user):
     assert len(resp.json['booked_events']) == 2
     assert resp.json['booked_events'][0]['id'] == 'first-agenda@event'
     assert resp.json['booked_events'][1]['id'] == 'second-agenda@event'
-    assert first_event.booking_set.count() == 2
-    assert second_event.booking_set.count() == 1
+    assert first_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 2
+    assert second_event.booking_set.filter(cancellation_datetime__isnull=True).count() == 1
 
     params = {'user_external_id': 'user_id_3', 'slots': event_slugs}
     resp = app.post_json('/api/agendas/events/fillslots/?agendas=%s' % agenda_slugs, params=params)
@@ -3771,6 +3771,72 @@ def test_api_events_fillslots_multiple_agendas(app, user):
     assert resp.json['errors']['slots'] == ['Missing agenda slug in slot @event']
 
 
+@pytest.mark.freeze_time('2021-09-06 12:00')
+def test_api_events_fillslots_multiple_agendas_with_cancelled(app, user):
+    agenda_1 = Agenda.objects.create(label='Agenda 1', kind='events')
+    Desk.objects.create(agenda=agenda_1, slug='_exceptions_holder')
+    event_1 = Event.objects.create(
+        label='Event 1',
+        start_datetime=now() + datetime.timedelta(days=1),
+        places=2,
+        agenda=agenda_1,
+    )
+    agenda_2 = Agenda.objects.create(label='Agenda 2', kind='events')
+    Desk.objects.create(agenda=agenda_2, slug='_exceptions_holder')
+    event_2 = Event.objects.create(
+        label='Event 2',
+        start_datetime=now() + datetime.timedelta(days=2),
+        places=2,
+        agenda=agenda_2,
+    )
+    Event.objects.create(
+        label='Event 3',
+        start_datetime=now() + datetime.timedelta(days=3),
+        places=2,
+        agenda=agenda_2,
+    )
+
+    # create cancelled booking for the user
+    booking_1 = Booking.objects.create(event=event_1, user_external_id='user_id')
+    booking_1.cancel()
+    assert booking_1.cancellation_datetime is not None
+    # and non cancelled booking for the user
+    booking_2 = Booking.objects.create(event=event_2, user_external_id='user_id')
+    assert booking_2.cancellation_datetime is None
+    # and bookings for another user
+    Booking.objects.create(event=event_1, user_external_id='user_id_foobar')
+    other_booking = Booking.objects.create(event=event_2, user_external_id='user_id_foobar')
+    other_booking.cancel()
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    fillslots_url = '/api/agendas/events/fillslots/?agendas=%s,%s' % (agenda_1.slug, agenda_2.slug)
+
+    params = {'user_external_id': 'user_id', 'slots': 'agenda-1@event-1,agenda-2@event-2,agenda-2@event-3'}
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['booking_count'] == 2
+    assert len(resp.json['booked_events']) == 2
+    assert resp.json['cancelled_booking_count'] == 0
+    assert resp.json['booked_events'][0]['id'] == 'agenda-1@event-1'
+    assert resp.json['booked_events'][1]['id'] == 'agenda-2@event-3'
+    assert Booking.objects.filter(user_external_id='user_id').count() == 3
+    assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 3
+
+    assert Booking.objects.filter(pk=booking_1.pk).exists() is False  # cancelled booking deleted
+    booking_2.refresh_from_db()
+    assert booking_2.cancellation_datetime is None
+
+    params = {'user_external_id': 'user_id', 'slots': 'agenda-2@event-3'}
+    resp = app.post_json(fillslots_url, params=params)
+    assert resp.json['booking_count'] == 0
+    assert resp.json['cancelled_booking_count'] == 2
+    assert Booking.objects.filter(user_external_id='user_id').count() == 3
+    assert Booking.objects.filter(user_external_id='user_id', cancellation_datetime__isnull=True).count() == 1
+
+    assert Booking.objects.filter(pk=booking_1.pk).exists() is False  # cancelled booking deleted
+    booking_2.refresh_from_db()
+    assert booking_2.cancellation_datetime is not None
+
+
 def test_api_events_fillslots_multiple_agendas_check_delays(app, user):
     agenda = Agenda.objects.create(
         label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7
@@ -3858,18 +3924,38 @@ def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
     params = {'user_external_id': 'xxx', 'slots': 'first-agenda@event,second-agenda@event'}
     resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
     assert resp.json['booking_count'] == 2
-    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
-    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
-    assert Booking.objects.count() == 2
+    assert (
+        Event.objects.get(agenda=first_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert (
+        Event.objects.get(agenda=second_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 2
 
     # update bookings for category-a
     params = {'user_external_id': 'xxx', 'slots': 'second-agenda@event'}
     resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-a', params=params)
     assert resp.json['booking_count'] == 0
     assert resp.json['cancelled_booking_count'] == 1
-    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
-    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
-    assert Booking.objects.count() == 1
+    assert (
+        Event.objects.get(agenda=first_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 0
+    )
+    assert (
+        Event.objects.get(agenda=second_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 1
 
     # try to book event from agenda with no subscription TODO messages
     params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
@@ -3890,10 +3976,25 @@ def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
     params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event'}
     resp = app.post_json('/api/agendas/events/fillslots/?subscribed=category-b', params=params)
     assert resp.json['booking_count'] == 1
-    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 0
-    assert Event.objects.get(agenda=second_agenda, slug='event').booking_set.count() == 1
-    assert Event.objects.get(agenda=third_agenda, slug='event').booking_set.count() == 1
-    assert Booking.objects.count() == 2
+    assert (
+        Event.objects.get(agenda=first_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 0
+    )
+    assert (
+        Event.objects.get(agenda=second_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert (
+        Event.objects.get(agenda=third_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 2
 
     # add subscription to first agenda (disjoint) spanning event-2
     for agenda in (first_agenda, second_agenda):
@@ -3908,9 +4009,19 @@ def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
     resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
     assert resp.json['booking_count'] == 2
     assert resp.json['cancelled_booking_count'] == 2
-    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 1
-    assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 1
-    assert Booking.objects.count() == 2
+    assert (
+        Event.objects.get(agenda=first_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert (
+        Event.objects.get(agenda=second_agenda, slug='event-2')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 1
+    )
+    assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 2
 
     # other user
     for agenda in (first_agenda, second_agenda):
@@ -3924,9 +4035,19 @@ def test_api_events_fillslots_multiple_agendas_subscribed(app, user):
     resp = app.post_json('/api/agendas/events/fillslots/?subscribed=all', params=params)
     assert resp.json['booking_count'] == 2
     assert resp.json['cancelled_booking_count'] == 0
-    assert Event.objects.get(agenda=first_agenda, slug='event').booking_set.count() == 2
-    assert Event.objects.get(agenda=second_agenda, slug='event-2').booking_set.count() == 2
-    assert Booking.objects.count() == 4
+    assert (
+        Event.objects.get(agenda=first_agenda, slug='event')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 2
+    )
+    assert (
+        Event.objects.get(agenda=second_agenda, slug='event-2')
+        .booking_set.filter(cancellation_datetime__isnull=True)
+        .count()
+        == 2
+    )
+    assert Booking.objects.filter(cancellation_datetime__isnull=True).count() == 4
 
     # try to book event outside subscription date range
     params = {'user_external_id': 'xxx', 'slots': 'third-agenda@event-2'}
-- 
2.34.1

