From cfc9e12509da23518117d9c65557bc7c81e885a6 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 16:50:21 +0100
Subject: [PATCH 3/3] api: update user bookings extra_data on subscription
 update (#61065)

---
 chrono/api/views.py            | 27 +++++++++---
 tests/api/test_subscription.py | 80 ++++++++++++++++++++++++++++++++++
 2 files changed, 102 insertions(+), 5 deletions(-)

diff --git a/chrono/api/views.py b/chrono/api/views.py
index cc23295..a48a1fc 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -18,11 +18,13 @@ import collections
 import copy
 import datetime
 import itertools
+import json
 import uuid
 
 from django.conf import settings
 from django.db import transaction
 from django.db.models import BooleanField, Count, ExpressionWrapper, F, Prefetch, Q
+from django.db.models.expressions import RawSQL
 from django.db.models.functions import TruncDay
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
@@ -1986,11 +1988,6 @@ class SubscriptionAPI(APIView):
             raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors, err=4)
 
         serializer.save()
-        extra_data = {k: v for k, v in request.data.items() if k not in serializer.validated_data}
-        if extra_data:
-            self.subscription.extra_data = self.subscription.extra_data or {}
-            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
@@ -2007,6 +2004,26 @@ class SubscriptionAPI(APIView):
                     self.subscription.date_end + datetime.timedelta(days=1), old_date_end
                 )
 
+        extra_data = {k: v for k, v in request.data.items() if k not in serializer.validated_data}
+        if extra_data:
+            self.subscription.extra_data = self.subscription.extra_data or {}
+            self.subscription.extra_data.update(extra_data)
+            self.subscription.save()
+            # update bookings inside the new period (other bookings were deleted)
+            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(),
+            ).update(
+                extra_data=RawSQL("COALESCE(extra_data, '{}'::jsonb) || %s::jsonb", (json.dumps(extra_data),))
+            )
+
         return self.get(request, *args, **kwargs)
 
     def delete(self, request, *args, **kwargs):
diff --git a/tests/api/test_subscription.py b/tests/api/test_subscription.py
index cc7e464..6294262 100644
--- a/tests/api/test_subscription.py
+++ b/tests/api/test_subscription.py
@@ -870,3 +870,83 @@ def test_api_patch_subscription_date_changes_delete_bookings_backwards_with_over
     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(
+    'date_now, event_date, user_id, in_waiting_list, cancelled, updated',
+    [
+        # event in the future, but no booking for the user
+        ('2021-09-01 09:59', (2021, 9, 1, 12, 0), 'yyy', False, False, False),
+        # event in the future
+        ('2021-09-01 09:59', (2021, 9, 1, 12, 0), 'xxx', False, False, True),
+        ('2021-09-01 09:59', (2021, 9, 1, 12, 0), 'xxx', True, False, True),
+        ('2021-09-01 09:59', (2021, 9, 1, 12, 0), 'xxx', False, True, True),
+        # event in the past
+        ('2021-09-01 10:00', (2021, 9, 1, 12, 0), 'xxx', False, False, False),
+        # event in the future
+        ('2021-08-01 10:00', (2021, 9, 1, 12, 0), 'xxx', False, False, True),
+        ('2021-08-01 10:00', (2021, 9, 30, 12, 0), 'xxx', False, False, True),
+        # event in the future, before the period
+        ('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),
+    ],
+)
+def test_api_patch_subscription_update_bookings_extra_data(
+    app, user, freezer, date_now, event_date, user_id, in_waiting_list, cancelled, updated
+):
+    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(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),
+    )
+
+    # extra_data is None
+    params = {
+        'foo': 'bar',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    booking.refresh_from_db()
+    if updated:
+        assert booking.extra_data == {'foo': 'bar'}
+    else:
+        assert booking.extra_data is None
+    booking.extra_data = {'some': 'thing'}
+    booking.save()
+
+    # extra_data is not None, update
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    booking.refresh_from_db()
+    if updated:
+        assert booking.extra_data == {'foo': 'bar', 'some': 'thing'}
+    else:
+        assert booking.extra_data == {'some': 'thing'}
+
+    # extra_data is not None, merge
+    params = {
+        'foo': 'bar2',
+    }
+    resp = app.patch('/api/agenda/%s/subscription/%s/' % (agenda.slug, subscription.pk), params=params)
+    assert resp.json['err'] == 0
+    booking.refresh_from_db()
+    if updated:
+        assert booking.extra_data == {'foo': 'bar2', 'some': 'thing'}
+    else:
+        assert booking.extra_data == {'some': 'thing'}
-- 
2.34.1

