From 7d8d2eefc53e27210a5aba6040e48f3590949ff6 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sat, 23 Jul 2022 19:10:41 +0200
Subject: [PATCH 4/4] toulouse_smart: allow to redo a failed intervention
 (#62012)

---
 passerelle/contrib/toulouse_smart/models.py | 43 +++++++++++----------
 tests/test_toulouse_smart.py                | 21 ++++++++--
 2 files changed, 39 insertions(+), 25 deletions(-)

diff --git a/passerelle/contrib/toulouse_smart/models.py b/passerelle/contrib/toulouse_smart/models.py
index dd1c87b2..89e2632a 100644
--- a/passerelle/contrib/toulouse_smart/models.py
+++ b/passerelle/contrib/toulouse_smart/models.py
@@ -190,29 +190,29 @@ class ToulouseSmartResource(BaseResource, HTTPResource):
                 except ValueError:
                     raise APIError(
                         "cannot cast '%s' field to %s : '%s'" % (varname, cast[prop['type']], block[varname]),
                         http_status=400,
                     )
             elif prop['required']:
                 raise APIError("'%s' field is required on '%s' block" % (varname, slug), http_status=400)
 
-        if self.wcs_requests.filter(
-            wcs_form_api_url=post_data['form_api_url'], wcs_form_step=post_data.get('form_step', 'initial')
-        ):
-            raise APIError(
-                "'%s' intervention already created at step '%s'"
-                % (post_data['external_number'], post_data.get('form_step', 'initial')),
-                http_status=400,
-            )
-        wcs_request = self.wcs_requests.create(
+        wcs_request, created = self.wcs_requests.get_or_create(
             wcs_form_api_url=post_data['form_api_url'],
-            wcs_form_number=post_data['external_number'],
             wcs_form_step=post_data.get('form_step', 'initial'),
+            defaults={'wcs_form_number': post_data['external_number']},
         )
+        wcs_request = self.wcs_requests.select_for_update().get(pk=wcs_request.pk)
+        if not created:
+            if wcs_request.status != 'failed':
+                return {'data': wcs_request.reply}
+            wcs_request.tries = 0
+            wcs_request.status = 'registered'
+            wcs_request.result = None
+
         endpoint_url = {}
         for endpoint_name in 'update-intervention', 'add-media':
             endpoint_url[endpoint_name] = request.build_absolute_uri(
                 reverse(
                     'generic-endpoint',
                     kwargs={'connector': 'toulouse-smart', 'endpoint': endpoint_name, 'slug': self.slug},
                 )
             )
@@ -250,27 +250,28 @@ class ToulouseSmartResource(BaseResource, HTTPResource):
             self.add_job(
                 'create_intervention_job',
                 pk=str(wcs_request.pk),
                 natural_id='wcs-request-%s' % wcs_request.pk,
             )
         return {'data': wcs_request.reply}
 
     def create_intervention_job(self, *args, **kwargs):
-        wcs_request = self.wcs_requests.get(pk=kwargs['pk'])
-        after_timestamp = None
-        if not wcs_request.push():
-            if wcs_request.tries < 5:
-                if wcs_request.tries == 3:
-                    after_timestamp = datetime.timedelta(hours=1)
-                if wcs_request.tries == 4:
-                    after_timestamp = datetime.timedelta(days=1)
-            else:
-                wcs_request.status = 'failed'
-                wcs_request.save()
+        wcs_request = self.wcs_requests.select_for_update().get(pk=kwargs['pk'])
+        with atomic():
+            after_timestamp = None
+            if not wcs_request.push():
+                if wcs_request.tries < 5:
+                    if wcs_request.tries == 3:
+                        after_timestamp = datetime.timedelta(hours=1)
+                    if wcs_request.tries == 4:
+                        after_timestamp = datetime.timedelta(days=1)
+                else:
+                    wcs_request.status = 'failed'
+                    wcs_request.save()
         payload = {'creation_response': wcs_request.reply}
         smart_request = wcs_request.smart_requests.create(payload=payload)
         self.add_job(
             'update_intervention_job',
             id=smart_request.id,
             natural_id='smart-request-%s' % smart_request.id,
         )
         if wcs_request.status == 'registered':
diff --git a/tests/test_toulouse_smart.py b/tests/test_toulouse_smart.py
index 25c27748..4494219b 100644
--- a/tests/test_toulouse_smart.py
+++ b/tests/test_toulouse_smart.py
@@ -537,23 +537,27 @@ def test_create_intervention_missing_field(app, smart):
     assert "field is required on 'coin' block" in resp.json['err_desc']
 
 
 @mock_response(
     ['/v1/type-intervention', None, INTERVENTION_TYPES],
     ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
 )
 @mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
-def test_create_intervention_twice_error(mocked_uuid4, app, smart):
+def test_create_intervention_twice(mocked_uuid4, app, smart):
     resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
     assert not resp.json['err']
+    assert resp.json['data']['status'] == 'registered'
+    assert smart.wcs_requests.count() == 1
 
-    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD, status=400)
-    assert resp.json['err']
-    assert 'already created' in resp.json['err_desc']
+    # re-create intervention after it success: no error is returned, but no new request is sent
+    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
+    assert not resp.json['err']
+    assert resp.json['data']['status'] == 'registered'
+    assert smart.wcs_requests.count() == 1
 
 
 @mock_response(
     ['/v1/type-intervention', None, INTERVENTION_TYPES],
     ['/v1/intervention', CREATE_INTERVENTION_QUERY, None, 500],
 )
 @mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
 def test_create_intervention_transport_error(mocked_uuid, app, freezer, smart):
@@ -677,16 +681,25 @@ def test_create_intervention_client_error(mocked_uuid, app, freezer, smart):
     assert wcs_request.status == 'failed'
     job = Job.objects.get(method_name='create_intervention_job')
     assert job.status == 'failed'
     assert '400 Client Error' in job.status_details['error_summary']
     assert wcs_request.smart_requests.count() == 4
     smart_request = wcs_request.smart_requests.latest('id')
     assert smart_request.payload['creation_response']['status'] == 'failed'
 
+    # re-create intervention after it fails: a new request is sent
+    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
+    assert not resp.json['err']
+    assert resp.json['data']['status'] == 'registered'
+    wcs_request = smart.wcs_requests.get(uuid=UUID)
+    assert '400 Client Error' in wcs_request.result
+    assert wcs_request.tries == 1
+    assert wcs_request.status == 'registered'
+
 
 @mock.patch('passerelle.utils.RequestSession.request')
 @mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
 def test_create_intervention_timeout(mocked_uuid, mocked_get, app, freezer, smart):
     from tests.utils import FakedResponse
 
     mocked_get.side_effect = [
         FakedResponse(
-- 
2.30.2

