From 4fc091409463b11bd6a38433e36a9ea07b40cf04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 16 Apr 2020 16:01:33 +0200
Subject: [PATCH 2/2] toulouse-axel: post annual bookings (#41405)

---
 passerelle/contrib/toulouse_axel/models.py  |  85 ++++++++
 passerelle/contrib/toulouse_axel/schemas.py |  67 +++++++
 tests/test_toulouse_axel.py                 | 203 ++++++++++++++++++++
 3 files changed, 355 insertions(+)

diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index f431350d..e4541083 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -1157,6 +1157,91 @@ class ToulouseAxel(BaseResource):
             }
         }
 
+    @endpoint(
+        description=_("CLAE/Cantine annual booking"),
+        perm='can_access',
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+        },
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': schemas.ANNUAL_BOOKING_SCHEMA,
+                }
+            }
+        })
+    def clae_booking_annual(self, request, NameID, post_data):
+        link = self.get_link(NameID)
+
+        # build dates of the period
+        today = datetime.date.today()
+        reference_year = utils.get_reference_year_from_date(today)
+        start_date = today + datetime.timedelta(days=8)
+        end_date = datetime.date(reference_year+1, 7, 31)
+
+        # get known activities for this child, to have the ids
+        child_activities_info = self.get_child_activities(link.dui, reference_year, post_data['child_id'])
+        child_known_activities_by_type = {a['TYPEACTIVITE']: a for a in child_activities_info.get('ACTIVITE', [])}
+
+        # build activity list to post
+        activities_by_type = {}
+        for activity_type in ['MAT', 'MIDI', 'SOIR', 'GARD']:
+            if post_data['booking_list_%s' % activity_type] is None:
+                # exclude if None (not updated)
+                continue
+            if activity_type not in child_known_activities_by_type:
+                # exclude activity types not registered for the child
+                continue
+            week_pattern = ''
+            activity_id = child_known_activities_by_type[activity_type]['IDACTIVITE']
+            # cross days of the week to find bookings
+            for i, day in enumerate(['monday', 'tuesday', 'wednesday', 'thursday', 'friday']):
+                key = '{}:{}:{}:{}'.format(post_data['child_id'], activity_type, activity_id, day)
+                week_pattern += key in post_data['booking_list_%s' % activity_type] and '1' or '0'
+            activities_by_type[activity_type] = {
+                'IDACTIVITE': activity_id,
+                'ANNEEREFERENCE': str(reference_year),
+                'PERIODE': [{
+                    'DATEDEBUT': start_date.strftime(utils.json_date_format),
+                    'DATEDFIN': end_date.strftime(utils.json_date_format),
+                    'SEMAINETYPE': week_pattern,
+                }],
+            }
+
+        # build data
+        data = {
+            'IDDUI': link.dui,
+            'DATEDEMANDE': today.strftime(utils.json_date_format),
+            'ENFANT': [
+                {
+                    'IDPERSONNE': post_data['child_id'],
+                    'REGIME': post_data.get('regime') or child_activities_info['REGIME'],
+                }
+            ]
+        }
+        for activity_type in ['MAT', 'MIDI', 'SOIR', 'GARD']:
+            if activity_type in activities_by_type:
+                if 'ACTIVITE' not in data['ENFANT'][0]:
+                    data['ENFANT'][0]['ACTIVITE'] = []
+                data['ENFANT'][0]['ACTIVITE'].append(activities_by_type[activity_type])
+
+        try:
+            result = schemas.reservation_annuelle(self, {'PORTAIL': {'DUI': data}})
+        except AxelError as e:
+            raise APIError(
+                'Axel error: %s' % e,
+                err_code='error',
+                data={'xml_request': e.xml_request,
+                      'xml_response': e.xml_response})
+
+        return {
+            'updated': True,
+            'data': {
+                'xml_request': result.xml_request,
+                'xml_response': result.xml_response,
+            }
+        }
+
 
 class Link(models.Model):
     resource = models.ForeignKey(ToulouseAxel, on_delete=models.CASCADE)
diff --git a/passerelle/contrib/toulouse_axel/schemas.py b/passerelle/contrib/toulouse_axel/schemas.py
index 53823a1b..232d1fc6 100644
--- a/passerelle/contrib/toulouse_axel/schemas.py
+++ b/passerelle/contrib/toulouse_axel/schemas.py
@@ -363,3 +363,70 @@ BOOKING_SCHEMA = {
     'required': ['booking_start_date', 'booking_end_date', 'booking_list_MAT', 'booking_list_MIDI', 'booking_list_SOIR', 'booking_list_GARD',
                  'child_id', 'regime']
 }
+
+
+ANNUAL_BOOKING_SCHEMA = {
+    'type': 'object',
+    'properties': {
+        'booking_list_MAT': {
+            'oneOf': [
+                {'type': 'null'},
+                {
+                    'type': 'array',
+                    'items': {
+                        'type': 'string',
+                        'pattern': '[A-Za-z0-9]+:MAT:[A-Za-z0-9]+:(monday|tuesday|wednesday|thursday|friday)',
+                    }
+                }
+            ]
+        },
+        'booking_list_MIDI': {
+            'oneOf': [
+                {'type': 'null'},
+                {
+                    'type': 'array',
+                    'items': {
+                        'type': 'string',
+                        'pattern': '[A-Za-z0-9]+:MIDI:[A-Za-z0-9]+:(monday|tuesday|wednesday|thursday|friday)',
+                    }
+                }
+            ]
+        },
+        'booking_list_SOIR': {
+            'oneOf': [
+                {'type': 'null'},
+                {
+                    'type': 'array',
+                    'items': {
+                        'type': 'string',
+                        'pattern': '[A-Za-z0-9]+:SOIR:[A-Za-z0-9]+:(monday|tuesday|wednesday|thursday|friday)',
+                    }
+                }
+            ]
+        },
+        'booking_list_GARD': {
+            'oneOf': [
+                {'type': 'null'},
+                {
+                    'type': 'array',
+                    'items': {
+                        'type': 'string',
+                        'pattern': '[A-Za-z0-9]+:GARD:[A-Za-z0-9]+:(monday|tuesday|wednesday|thursday|friday)',
+                    }
+                }
+            ]
+        },
+        'child_id': {
+            'type': 'string',
+            'minLength': 1,
+            'maxLength': 8,
+        },
+        'regime': {
+            'oneOf': [
+                {'type': 'null'},
+                {'type': 'string', 'enum': ['', 'SV', 'AV']}
+            ]
+        }
+    },
+    'required': ['booking_list_MAT', 'booking_list_MIDI', 'booking_list_SOIR', 'booking_list_GARD', 'child_id']
+}
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index 18f9a6b7..2720721a 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -154,6 +154,18 @@ def booking_params():
     }
 
 
+@pytest.fixture
+def annual_booking_params():
+    return {
+        'booking_list_MAT': ['3535:MAT:A19P1M1:monday', '3535:MAT:A19P1M1:tuesday', '3535:MAT:A19P1M1:thursday', '3535:MAT:A19P1M1:friday'],
+        'booking_list_MIDI': ['3535:MIDI:A19P1M2:monday', '3535:MIDI:A19P1M2:tuesday', '3535:MIDI:A19P1M2:thursday', '3535:MIDI:A19P1M2:friday'],
+        'booking_list_SOIR': ['3535:SOIR:A19P1M3:monday'],
+        'booking_list_GARD': ['3535:GARD:A19P1M4:wednesday'],
+        'child_id': '3535',
+        'regime': 'AV',
+    }
+
+
 def test_lock(app, resource):
     resp = app.get('/toulouse-axel/test/lock?key=&locker=', status=400)
     assert resp.json['err_desc'] == "key is empty"
@@ -3301,3 +3313,194 @@ def test_clae_booking_endpoint(app, resource, booking_params, child_activities_d
         assert activity['PERIODE'][4]['DATEDEBUT'] == '2020-04-27'
         assert activity['PERIODE'][4]['DATEDFIN'] == '2020-04-30'
         assert activity['PERIODE'][4]['SEMAINETYPE'] == '00010'
+
+
+@freezegun.freeze_time('2019-09-01')
+def test_clae_booking_annual_endpoint_axel_error(app, resource, annual_booking_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.toulouse_axel.schemas.enfants_activites') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/child_activities.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    with mock_getdata(content, 'EnfantsActivites'):
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+def test_clae_booking_annual_endpoint_no_result(app, resource, annual_booking_params):
+    resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+    assert resp.json['err_desc'] == "Person not found"
+    assert resp.json['err'] == 'not-found'
+
+
+@freezegun.freeze_time('2019-09-01')
+def test_clae_booking_annual_endpoint(app, resource, annual_booking_params, child_activities_data):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    activities = child_activities_data['ENFANT'][0]
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+        content = "<PORTAIL/>"
+        with mock_getdata(content, 'ReservationAnnuelle'):
+            resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+        assert resp.json['err'] == 0
+        assert resp.json['updated'] is True
+        assert 'data' in resp.json
+        assert 'xml_request' in resp.json['data']
+        assert 'xml_response' in resp.json['data']
+
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert payload == {
+            'DATEDEMANDE': '2019-09-01',
+            'ENFANT': [{
+                'IDPERSONNE': '3535',
+                'REGIME': 'AV',
+                'ACTIVITE': [{
+                    'ANNEEREFERENCE': '2019',
+                    'IDACTIVITE': 'A19P1M1',
+                    'PERIODE': [
+                        {
+                            'DATEDEBUT': '2019-09-09',
+                            'DATEDFIN': '2020-07-31',
+                            'SEMAINETYPE': '11011'
+                        }
+                    ]
+                }, {
+                    'ANNEEREFERENCE': '2019',
+                    'IDACTIVITE': 'A19P1M2',
+                    'PERIODE': [
+                        {
+                            'DATEDEBUT': '2019-09-09',
+                            'DATEDFIN': '2020-07-31',
+                            'SEMAINETYPE': '11011'
+                        }
+                    ]
+                }, {
+                    'ANNEEREFERENCE': '2019',
+                    'IDACTIVITE': 'A19P1M3',
+                    'PERIODE': [
+                        {
+                            'DATEDEBUT': '2019-09-09',
+                            'DATEDFIN': '2020-07-31',
+                            'SEMAINETYPE': '10000'
+                        }
+                    ]
+                }, {
+                    'ANNEEREFERENCE': '2019',
+                    'IDACTIVITE': 'A19P1M4',
+                    'PERIODE': [
+                        {
+                            'DATEDEBUT': '2019-09-09',
+                            'DATEDFIN': '2020-07-31',
+                            'SEMAINETYPE': '00100'
+                        }
+                    ]
+                }]
+            }],
+            'IDDUI': 'XXX',
+        }
+
+        # empty list
+        new_booking_params = copy.deepcopy(annual_booking_params)
+        new_booking_params['booking_list_MAT'] = ['42:MAT:A19P1M1:monday']  # wrong child, ignored
+        new_booking_params['booking_list_MIDI'] = []
+        new_booking_params['booking_list_SOIR'] = []
+        new_booking_params['booking_list_GARD'] = []
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=new_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert len(payload['ENFANT']) == 1
+        assert payload['ENFANT'][0]['IDPERSONNE'] == '3535'
+        assert len(payload['ENFANT'][0]['ACTIVITE']) == 4
+        activity1 = payload['ENFANT'][0]['ACTIVITE'][0]
+        assert activity1['IDACTIVITE'] == 'A19P1M1'
+        assert len(activity1['PERIODE']) == 1
+        assert activity1['PERIODE'][0]['DATEDEBUT'] == '2019-09-09'
+        assert activity1['PERIODE'][0]['DATEDFIN'] == '2020-07-31'
+        assert activity1['PERIODE'][0]['SEMAINETYPE'] == '00000'
+        activity2 = payload['ENFANT'][0]['ACTIVITE'][1]
+        assert activity2['IDACTIVITE'] == 'A19P1M2'
+        assert len(activity2['PERIODE']) == 1
+        assert activity2['PERIODE'][0]['DATEDEBUT'] == '2019-09-09'
+        assert activity2['PERIODE'][0]['DATEDFIN'] == '2020-07-31'
+        assert activity2['PERIODE'][0]['SEMAINETYPE'] == '00000'
+        activity3 = payload['ENFANT'][0]['ACTIVITE'][2]
+        assert activity3['IDACTIVITE'] == 'A19P1M3'
+        assert len(activity3['PERIODE']) == 1
+        assert activity3['PERIODE'][0]['DATEDEBUT'] == '2019-09-09'
+        assert activity3['PERIODE'][0]['DATEDFIN'] == '2020-07-31'
+        assert activity3['PERIODE'][0]['SEMAINETYPE'] == '00000'
+        activity4 = payload['ENFANT'][0]['ACTIVITE'][3]
+        assert activity4['IDACTIVITE'] == 'A19P1M4'
+        assert len(activity4['PERIODE']) == 1
+        assert activity4['PERIODE'][0]['DATEDEBUT'] == '2019-09-09'
+        assert activity4['PERIODE'][0]['DATEDFIN'] == '2020-07-31'
+        assert activity4['PERIODE'][0]['SEMAINETYPE'] == '00000'
+
+        # null list
+        new_booking_params = copy.deepcopy(annual_booking_params)
+        new_booking_params['booking_list_MAT'] = None
+        new_booking_params['booking_list_MIDI'] = None
+        new_booking_params['booking_list_SOIR'] = None
+        new_booking_params['booking_list_GARD'] = None
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            resp = app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=new_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert 'ACTIVITE' not in payload['ENFANT'][0]
+
+
+def test_clae_booking_annual_reference_year(app, resource, annual_booking_params, child_activities_data):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    activities = child_activities_data['ENFANT'][0]
+    with freezegun.freeze_time('2020-04-16'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+            with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+                operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+                app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['DATEDEBUT'] == '2020-04-24'
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['DATEDFIN'] == '2020-07-31'
+    with freezegun.freeze_time('2020-08-01'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+            with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+                operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+                app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['DATEDEBUT'] == '2020-08-09'
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['DATEDFIN'] == '2021-07-31'
+
+
+@freezegun.freeze_time('2019-09-01')
+def test_clae_booking_annual_regime(app, resource, annual_booking_params, child_activities_data):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    activities = child_activities_data['ENFANT'][0]
+    with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+        annual_booking_params['regime'] = None
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert payload['ENFANT'][0]['REGIME'] == 'SV'
+        annual_booking_params['regime'] = ''
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert payload['ENFANT'][0]['REGIME'] == 'SV'
+        del annual_booking_params['regime']
+        with mock.patch('passerelle.contrib.toulouse_axel.schemas.reservation_annuelle') as operation:
+            operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+            app.post_json('/toulouse-axel/test/clae_booking_annual?NameID=yyy', params=annual_booking_params)
+        payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+        assert payload['ENFANT'][0]['REGIME'] == 'SV'
-- 
2.20.1

