From d2e9eed632da8c204443f4c61d7d2eb06f223be5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 27 Feb 2020 14:11:10 +0100
Subject: [PATCH 2/2] toulouse_axel: endpoint for booking (40118)

---
 passerelle/contrib/toulouse_axel/models.py    | 74 +++++++++++++++++
 .../xsd/Dui/Q_ReservationAnnuelle.xsd         | 75 +++++++++++++++++
 .../xsd/Dui/R_ReservationAnnuelle.xsd         | 21 +++++
 tests/test_toulouse_axel.py                   | 83 +++++++++++++++++++
 4 files changed, 253 insertions(+)
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationAnnuelle.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationAnnuelle.xsd

diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index a1ce2e40..cb6e2a85 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -213,6 +213,7 @@ ref_facture_pdf = Operation('RefFacturePDF', prefix='')
 list_dui_factures = Operation('ListeDuiFacturesPayeesRecettees', request_root_element='LISTFACTURE')
 enfants_activites = Operation('EnfantsActivites', request_root_element='DUI')
 reservation_periode = Operation('ReservationPeriode')
+reservation_annuelle = Operation('ReservationAnnuelle')
 
 
 class ToulouseAxel(BaseResource):
@@ -330,6 +331,23 @@ class ToulouseAxel(BaseResource):
 
     UPDATE_FAMILY_SCHEMA['unflatten'] = True
 
+    BOOKING_SCHEMA = copy.deepcopy(
+        reservation_annuelle.request_schema['properties']['PORTAIL']['properties']['DUI'])
+    BOOKING_SCHEMA['properties'].pop('IDDUI')
+    BOOKING_SCHEMA['properties'].pop('DATEDEMANDE')
+    BOOKING_SCHEMA['required'].remove('IDDUI')
+    BOOKING_SCHEMA['required'].remove('DATEDEMANDE')
+    activite_properties = BOOKING_SCHEMA['properties']['ENFANT']['items']['properties']['ACTIVITE']['items']['properties']
+    activite_properties.pop('ANNEEREFERENCE')
+    BOOKING_SCHEMA['properties']['ENFANT']['items']['properties']['ACTIVITE']['items']['required'].remove('ANNEEREFERENCE')
+    activite_properties['PERIODE']['items']['properties'].pop('SEMAINETYPE')
+    activite_properties['PERIODE']['items']['required'].remove('SEMAINETYPE')
+    for day in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']:
+        bool_type = copy.deepcopy(utils.boolean_type)
+        activite_properties['PERIODE']['items']['properties'][day] = bool_type
+        activite_properties['PERIODE']['items']['required'].append(day)
+    BOOKING_SCHEMA['unflatten'] = True
+
     @endpoint(
         description=_('Lock a resource'),
         perm='can_access',
@@ -1093,6 +1111,62 @@ class ToulouseAxel(BaseResource):
 
         return {'data': child_activities}
 
+    @endpoint(
+        description=_("CLAE/Cantine booking"),
+        perm='can_access',
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+        },
+        post={
+            'request_body': {
+                'schema': {
+                    'application/json': BOOKING_SCHEMA,
+                }
+            }
+        })
+    def clae_booking(self, request, NameID, post_data):
+        link = self.get_link(NameID)
+
+        # prepare data
+        post_data['IDDUI'] = link.dui
+        post_data['DATEDEMANDE'] = datetime.date.today().strftime('%Y-%m-%d')
+        # compute reference years for each activity of each child
+        for child in post_data['ENFANT']:
+            # at least one child (cf definition)
+            for activity in child.get('ACTIVITE', []):
+                # at least one periode (cf definition)
+                start_date = datetime.datetime.strptime(
+                    activity['PERIODE'][0]['DATEDEBUT'],
+                    utils.json_date_format)
+                reference_year = utils.get_reference_year_from_date(start_date)
+                activity['ANNEEREFERENCE'] = str(reference_year)
+                for periode in activity['PERIODE']:
+                    periode['SEMAINETYPE'] = '{}{}{}{}{}'.format(
+                        '1' if utils.encode_bool(periode['monday']) == 'OUI' else 0,
+                        '1' if utils.encode_bool(periode['tuesday']) == 'OUI' else 0,
+                        '1' if utils.encode_bool(periode['wednesday']) == 'OUI' else 0,
+                        '1' if utils.encode_bool(periode['thursday']) == 'OUI' else 0,
+                        '1' if utils.encode_bool(periode['friday']) == 'OUI' else 0)
+                    for day in ['monday', 'tuesday', 'wednesday', 'thursday', 'friday']:
+                        periode.pop(day)
+
+        try:
+            result = reservation_annuelle(self, {'PORTAIL': {'DUI': post_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/xsd/Dui/Q_ReservationAnnuelle.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationAnnuelle.xsd
new file mode 100644
index 00000000..b3c01d25
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationAnnuelle.xsd
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:all="urn:AllAxelTypes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" >
+	
+	<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes"  />
+	
+	<xsd:simpleType name="semainetype">
+		<xsd:restriction base="xsd:string">
+			<xsd:pattern value="[0-1][0-1][0-1][0-1][0-1]"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+		
+	<xsd:complexType name="periode">
+		<xsd:sequence>
+			<xsd:element ref="SEMAINETYPE" />
+			<xsd:element ref="DATEDEBUT" />
+			<xsd:element ref="DATEDFIN" />
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="activite">
+		<xsd:sequence>
+			<xsd:element ref="ANNEEREFERENCE" />
+			<xsd:element ref="IDACTIVITE" />
+			<xsd:element ref="PERIODE"  minOccurs="1" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="ENFANTType">
+		<xsd:sequence>
+			<xsd:element ref="IDPERSONNE" />
+			<xsd:element ref="REGIME" />
+			<xsd:element ref="ACTIVITE" minOccurs="0" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<xsd:element ref="DATEDEMANDE" />
+			<xsd:element ref="ENFANT"  minOccurs="1" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="PORTAILType">
+		<xsd:sequence>
+				<xsd:element ref="DUI"/>
+		</xsd:sequence>	
+	</xsd:complexType>
+	
+	<xsd:simpleType name="regime">
+		<xsd:restriction base="xsd:string">
+			<xsd:maxLength value="4" />
+		</xsd:restriction>
+	</xsd:simpleType>
+	
+	<xsd:element name="IDDUI" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="DATEDEMANDE" type="all:DATEREQUIREDType"/>
+	<xsd:element name="ENFANT" type="ENFANTType"/>
+	
+	<xsd:element name="IDPERSONNE" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="REGIME" type="regime" />
+	
+	<xsd:element name="ANNEEREFERENCE" type="all:ANNEEType"/>
+	<xsd:element name="IDACTIVITE" type="all:IDREQUIREDType"/>
+	<xsd:element name="SEMAINETYPE" type="semainetype"/>
+	<xsd:element name="DATEDEBUT" type="all:DATEREQUIREDType"/>
+	<xsd:element name="DATEDFIN" type="all:DATEREQUIREDType"/>
+			
+	<xsd:element name="ACTIVITE" type="activite"/>
+	<xsd:element name="PERIODE" type="periode"/>
+		
+	<xsd:element name="DUI" type="DUIType"/>
+	
+	<xsd:element name="PORTAIL" type="PORTAILType"/>
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationAnnuelle.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationAnnuelle.xsd
new file mode 100644
index 00000000..6e7d3fed
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationAnnuelle.xsd
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+
+	<xsd:redefine schemaLocation="../R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="ReservationAnnuelle" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+						<xsd:element name="PORTAIL" type="xsd:string" minOccurs="0"/>
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>		
+</xsd:schema>
\ No newline at end of file
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index 5f5e5ba0..9812e3f2 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -136,6 +136,34 @@ def flat_update_params():
     return json.loads(content)
 
 
+@pytest.fixture
+def booking_params():
+    return {
+        "ENFANT": [
+            {
+                "IDPERSONNE": "4242",
+                "REGIME": "",
+                "ACTIVITE": [
+                    {
+                        "IDACTIVITE": "A19P1M1",
+                        "PERIODE": [
+                            {
+                                "DATEDEBUT": "2020-01-01",
+                                "DATEDFIN": "2020-07-31",
+                                "monday": "1",
+                                "tuesday": "1",
+                                "wednesday": "0",
+                                "thursday": "1",
+                                "friday": "1"
+                            }
+                        ]
+                    }
+                ]
+            }
+        ]
+    }
+
+
 def test_lock(app, resource):
     resp = app.get('/toulouse-axel/test/lock?key=&locker=', status=400)
     assert resp.json['err_desc'] == "key is empty"
@@ -2176,3 +2204,58 @@ def test_are_children_registered(resource, activities, expected):
     with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_children_activities') as get_children_activities:
         get_children_activities.return_value = activities
         assert resource.are_children_registered(dui='XXX', reference_year=get_reference_year_from_date(today)) == expected
+
+
+def test_clae_booking_endpoint_axel_error(app, resource, booking_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.toulouse_axel.models.reservation_annuelle') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+def test_clae_booking_endpoint_no_result(app, resource, booking_params):
+    resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
+    assert resp.json['err_desc'] == "Person not found"
+    assert resp.json['err'] == 'not-found'
+
+
+def test_clae_booking_endpoint(app, resource, booking_params):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    content = "<PORTAIL/>"
+    with mock_getdata(content, 'ReservationAnnuelle'):
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=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.models.reservation_annuelle') as operation:
+        operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['SEMAINETYPE'] == '11011'
+
+    new_booking_params = copy.deepcopy(booking_params)
+    new_booking_params['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['monday'] = '0'
+    with mock.patch('passerelle.contrib.toulouse_axel.models.reservation_annuelle') as operation:
+        operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['SEMAINETYPE'] == '01011'
+
+    new_booking_params['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['monday'] = 'OUI'
+    with mock.patch('passerelle.contrib.toulouse_axel.models.reservation_annuelle') as operation:
+        operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['SEMAINETYPE'] == '11011'
+
+    new_booking_params['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['monday'] = False
+    with mock.patch('passerelle.contrib.toulouse_axel.models.reservation_annuelle') as operation:
+        operation.return_value = OperationResult(json_response={}, xml_request='', xml_response='')
+        resp = app.post_json('/toulouse-axel/test/clae_booking?NameID=yyy', params=new_booking_params)
+    payload = operation.call_args_list[0][0][1]['PORTAIL']['DUI']
+    assert payload['ENFANT'][0]['ACTIVITE'][0]['PERIODE'][0]['SEMAINETYPE'] == '01011'
-- 
2.20.1

