From 93e90c2902003aec30942f4291b55b2b7afe3f6a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 27 Jan 2020 11:20:47 +0100
Subject: [PATCH 3/3] toulouse_axel: booking_info endpoint (#39126)

---
 passerelle/contrib/toulouse_axel/models.py    | 104 +++++++
 passerelle/contrib/toulouse_axel/utils.py     |  26 ++
 .../xsd/Dui/Q_EnfantsActivites.xsd            |  24 ++
 .../xsd/Dui/Q_ReservationPeriode.xsd          |  55 ++++
 .../xsd/Dui/R_EnfantsActivites.xsd            |  96 +++++++
 .../xsd/Dui/R_ReservationPeriode.xsd          |  59 ++++
 tests/data/toulouse_axel/booking_info.xml     |  28 ++
 tests/data/toulouse_axel/child_activities.xml |  62 +++++
 tests/test_toulouse_axel.py                   | 259 +++++++++++++++++-
 tests/test_toulouse_axel_utils.py             |  62 +++++
 10 files changed, 774 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/Q_EnfantsActivites.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationPeriode.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/R_EnfantsActivites.xsd
 create mode 100644 passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationPeriode.xsd
 create mode 100644 tests/data/toulouse_axel/booking_info.xml
 create mode 100644 tests/data/toulouse_axel/child_activities.xml
 create mode 100644 tests/test_toulouse_axel_utils.py

diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index 6a086822..4d7a0e4d 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -211,6 +211,8 @@ form_paiement_dui = Operation('FormPaiementDui')
 ref_facture_a_payer = Operation('RefFactureAPayer')
 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')
 
 
 class ToulouseAxel(BaseResource):
@@ -882,6 +884,108 @@ class ToulouseAxel(BaseResource):
                       'xml_response': e.xml_response})
         return {'data': True}
 
+    def get_child_activities(self, dui, reference_year, child_id):
+        try:
+            result = enfants_activites(self, {
+                'DUI': {
+                    'IDDUI': dui,
+                    'ANNEEREFERENCE': str(reference_year),
+                    'TYPESACTIVITES': 'MAT,MIDI,SOIR,GARD',
+                }
+            })
+        except AxelError as e:
+            raise APIError(
+                'Axel error: %s' % e,
+                err_code='error',
+                data={'xml_request': e.xml_request,
+                      'xml_response': e.xml_response})
+
+        child_activities_data = result.json_response['DATA']['PORTAIL']['DUI'].get('ENFANT', [])
+        for child in child_activities_data:
+            if child['IDPERSONNE'] == child_id:
+                return child
+
+        raise APIError('Child not found', err_code='not-found')
+
+    @endpoint(
+        description=_("Get information about CLAE booking"),
+        perm='can_access',
+        parameters={
+            'NameID': {'description': _('Publik ID')},
+            'idpersonne': {'description': _('Child ID')},
+            'booking_date': {'description': _('Booking date')},
+        })
+    def clae_booking_info(self, request, NameID, idpersonne, booking_date):
+        link = self.get_link(NameID)
+        try:
+            booking_date = datetime.datetime.strptime(booking_date, utils.json_date_format)
+        except ValueError:
+            raise APIError('bad date format', err_code='bad-request', http_status=400)
+
+        reference_year = utils.get_reference_year_from_date(booking_date)
+
+        # first get activities information for the child
+        child_activities = self.get_child_activities(
+            dui=link.dui,
+            reference_year=reference_year,
+            child_id=idpersonne)
+
+        # then get booking of the requested week for the child
+        activity_ids = [act['IDACTIVITE'] for act in child_activities.get('ACTIVITE', [])]
+        start_date, end_date = utils.get_week_dates_from_date(booking_date)
+        activity_data = []
+        for activity_id in activity_ids:
+            activity_data.append({
+                'IDACTIVITE': activity_id,
+                'ANNEEREFERENCE': str(reference_year),
+                'DATEDEBUT': start_date.strftime(utils.xml_date_format),
+                'DATEDFIN': end_date.strftime(utils.xml_date_format),
+            })
+        try:
+            data = reservation_periode(self, {'PORTAIL': {
+                'DUI': {
+                    'IDDUI': link.dui,
+                    'ENFANT': {
+                        'IDPERSONNE': idpersonne,
+                        'ACTIVITE': activity_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})
+
+        child_booking = None
+        for child in data.json_response['DATA']['PORTAIL']['DUI'].get('ENFANT', []):
+            if child['IDPERSONNE'] == idpersonne:
+                child_booking = child
+                break
+        if child_booking is None:
+            # should not happen
+            raise APIError('Child not found', err_code='not-found')
+
+        # build the response payload: add booking to activities information
+        booking_days = {}
+        for booking in child_booking.get('ACTIVITE', []):
+            booking_days[booking['IDACTIVITE']] = {
+                'raw_value': booking['JOUR'],
+                'days': {
+                    'monday': utils.get_booking(booking['JOUR'][0]),
+                    'tuesday': utils.get_booking(booking['JOUR'][1]),
+                    'wednesday': utils.get_booking(booking['JOUR'][2]),
+                    'thursday': utils.get_booking(booking['JOUR'][3]),
+                    'friday': utils.get_booking(booking['JOUR'][4]),
+                }
+            }
+
+        for activity in child_activities.get('ACTIVITE', []):
+            activity['booking'] = booking_days.get(activity['IDACTIVITE'], {})
+
+        return {'data': child_activities}
+
 
 class Link(models.Model):
     resource = models.ForeignKey(ToulouseAxel, on_delete=models.CASCADE)
diff --git a/passerelle/contrib/toulouse_axel/utils.py b/passerelle/contrib/toulouse_axel/utils.py
index c856eff0..0b1dd4d5 100644
--- a/passerelle/contrib/toulouse_axel/utils.py
+++ b/passerelle/contrib/toulouse_axel/utils.py
@@ -122,3 +122,29 @@ def normalize_invoice(invoice, dui, historical=False, vendor_base=None):
         pay_limit_date = datetime.datetime.strptime(invoice['DATEECHEANCE'], '%Y-%m-%d').date()
         data['online_payment'] = data['amount'] > 0 and pay_limit_date >= datetime.date.today()
     return data
+
+
+def get_reference_year_from_date(booking_date):
+    if booking_date.month <= 7:
+        # between january and july, reference year is the year just before
+        # (september)
+        return booking_date.year - 1
+    return booking_date.year
+
+
+def get_week_dates_from_date(booking_date):
+    day = booking_date.weekday()
+    # return monday and friday of the week
+    return (
+        booking_date - datetime.timedelta(days=day),
+        booking_date + datetime.timedelta(days=4 - day)
+    )
+
+
+def get_booking(value):
+    # 0: no registration
+    # 1: registration
+    # 2: not applicable (example: GARDERIE is applicable only on wednesday)
+    if value == '2':
+        return None
+    return value == '1'
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/Q_EnfantsActivites.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_EnfantsActivites.xsd
new file mode 100644
index 00000000..a62fe721
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_EnfantsActivites.xsd
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+	xmlns:all="urn:AllAxelTypes">
+
+	<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes" />
+
+
+	<xsd:element name="IDDUI" type="all:IDENTREQUIREDType" />
+	<xsd:element name="ANNEEREFERENCE" type="all:ANNEEType" />
+	<xsd:element name="TYPESACTIVITES" type="xsd:string" />
+
+
+	<xsd:element name="DUI">
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element ref="IDDUI" minOccurs="1" maxOccurs="1" />
+				<xsd:element ref="ANNEEREFERENCE" minOccurs="1"
+					maxOccurs="1" />
+				<xsd:element ref="TYPESACTIVITES" minOccurs="1"
+					maxOccurs="1" />
+			</xsd:sequence>
+		</xsd:complexType>
+	</xsd:element>
+</xsd:schema>
\ No newline at end of file
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationPeriode.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationPeriode.xsd
new file mode 100644
index 00000000..d29dc5b0
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/Q_ReservationPeriode.xsd
@@ -0,0 +1,55 @@
+<?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:complexType name="activite">
+		<xsd:sequence>
+			<xsd:element ref="ANNEEREFERENCE" />
+			<xsd:element ref="IDACTIVITE" />
+			<xsd:element ref="DATEDEBUT" />
+			<xsd:element ref="DATEDFIN" />
+			<xsd:element ref="FILTRESD" minOccurs="0"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="enfant">
+		<xsd:sequence>
+			<xsd:element ref="IDPERSONNE" />
+			<xsd:element ref="ACTIVITE" minOccurs="1" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<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="filtresd">
+		<xsd:union memberTypes="all:ONType all:empty-string" />
+	</xsd:simpleType>
+	
+	<xsd:element name="IDDUI" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="ENFANT" type="enfant"/>
+	
+	<xsd:element name="IDPERSONNE" type="all:IDENTREQUIREDType"/>
+	
+	<xsd:element name="ANNEEREFERENCE" type="all:ANNEEType"/>
+	<xsd:element name="IDACTIVITE" type="all:IDREQUIREDType"/>
+	<xsd:element name="DATEDEBUT" type="all:DATEREQUIREDType"/>
+	<xsd:element name="DATEDFIN" type="all:DATEREQUIREDType"/>
+	<xsd:element name="FILTRESD" type="filtresd"/>
+			
+	<xsd:element name="ACTIVITE" type="activite"/>
+	<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_EnfantsActivites.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/R_EnfantsActivites.xsd
new file mode 100644
index 00000000..4ed0d95f
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/R_EnfantsActivites.xsd
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:all="urn:AllAxelTypes">
+	
+	<xsd:import schemaLocation="../AllAxelTypes.xsd" namespace="urn:AllAxelTypes"  />
+
+	<xsd:redefine schemaLocation="../R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="EnfantsActivites" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+							<xsd:element ref="DUI" />
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>
+	
+	<xsd:simpleType name="typeactivite">
+		<xsd:restriction base="xsd:string">
+			<xsd:minLength value="1"/>
+			<xsd:maxLength value="4"/>
+		</xsd:restriction>
+	</xsd:simpleType>
+
+	<xsd:complexType name="dui">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<xsd:element ref="NBENFANTINSCRIT" />
+			<xsd:element ref="ENFANT" minOccurs="0" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="enfant">
+		<xsd:sequence>
+			<xsd:element ref="NOM" />
+			<xsd:element ref="PRENOM" />
+			<xsd:element ref="DATENAISSANCE" />
+			<xsd:element ref="IDPERSONNE" />
+			<xsd:element ref="REGIME" />
+			<xsd:element ref="LIBELLEECOLE" />
+			<xsd:element ref="ACTIVITE" minOccurs="0" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="activite">
+		<xsd:sequence>
+			<xsd:element ref="TYPEACTIVITE" />
+			<xsd:element ref="IDACTIVITE" />
+			<xsd:element ref="LIBELLEACTIVITE" />
+			<xsd:element ref="DATEENTREE" />
+			<xsd:element ref="DATESORTIE" />
+			<xsd:element ref="TARIF" />
+			<xsd:element ref="ISPAI" />
+			<xsd:element ref="COUTREVIENT" />
+			<xsd:element ref="DATEDEBUT" />
+			<xsd:element ref="DATEFIN" />
+		</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="NBENFANTINSCRIT" type="xsd:positiveInteger"/>
+	<xsd:element name="NOM" type="all:NOMREQUIREDType"/>
+	<xsd:element name="PRENOM" type="all:PRENOMREQUIREDType"/>
+	<xsd:element name="DATENAISSANCE" type="all:DATEType"/>
+	<xsd:element name="IDPERSONNE" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="REGIME" type="regime" />
+	<xsd:element name="LIBELLEECOLE" type="xsd:string"/>
+		
+	<xsd:element name="TYPEACTIVITE" type="typeactivite"/>
+	<xsd:element name="IDACTIVITE" type="all:IDREQUIREDType"/>
+	<xsd:element name="LIBELLEACTIVITE" type="xsd:string"/>
+	<xsd:element name="DATEENTREE" type="all:DATEType"/>
+	<xsd:element name="DATESORTIE" type="all:DATEType"/>
+	<xsd:element name="TARIF" type="all:MONTANTType"/>
+	<xsd:element name="ISPAI" type="all:OUINONType"/>
+	<xsd:element name="COUTREVIENT" type="all:MONTANTType"/>
+	<xsd:element name="DATEDEBUT" type="all:DATEREQUIREDType"/>
+	<xsd:element name="DATEFIN" type="all:DATEREQUIREDType"/>
+	
+	<xsd:element name="ENFANT" type="enfant" />
+	<xsd:element name="ACTIVITE" type="activite" />
+	<xsd:element name="DUI" type="dui" />
+
+</xsd:schema>
diff --git a/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationPeriode.xsd b/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationPeriode.xsd
new file mode 100644
index 00000000..0baafe1c
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/xsd/Dui/R_ReservationPeriode.xsd
@@ -0,0 +1,59 @@
+<?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:redefine schemaLocation="../R_ShemaResultat.xsd">
+	    <xsd:simpleType name="TYPEType">
+			<xsd:restriction base="TYPEType">
+				<xsd:enumeration value="ReservationPeriode" />
+			</xsd:restriction>
+	    </xsd:simpleType>
+		
+		<xsd:complexType name="PORTAILType">
+			<xsd:complexContent>
+				<xsd:extension base="PORTAILType">
+					<xsd:sequence>
+						<xsd:element ref="DUI" minOccurs="1" maxOccurs="1"/>
+					</xsd:sequence>	
+				</xsd:extension>
+			 </xsd:complexContent>
+		</xsd:complexType>
+	</xsd:redefine>
+	
+		<xsd:complexType name="activite">
+		<xsd:sequence>
+			<xsd:element ref="ANNEEREFERENCE" />
+			<xsd:element ref="IDACTIVITE" />
+			<xsd:element ref="JOUR" />
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="enfant">
+		<xsd:sequence>
+			<xsd:element ref="IDPERSONNE" />
+			<xsd:element ref="ACTIVITE" minOccurs="1" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+	
+	<xsd:complexType name="DUIType">
+		<xsd:sequence>
+			<xsd:element ref="IDDUI" />
+			<xsd:element ref="ENFANT"  minOccurs="1" maxOccurs="unbounded"/>
+		</xsd:sequence> 
+	</xsd:complexType>
+		
+	<xsd:element name="IDDUI" type="all:IDENTREQUIREDType"/>
+	<xsd:element name="ENFANT" type="enfant"/>
+	
+	<xsd:element name="IDPERSONNE" type="all:IDENTREQUIREDType"/>
+	
+	<xsd:element name="ANNEEREFERENCE" type="all:ANNEEType"/>
+	<xsd:element name="IDACTIVITE" type="all:IDREQUIREDType"/>
+	<xsd:element name="JOUR" type="xsd:string"/>
+			
+	<xsd:element name="ACTIVITE" type="activite"/>
+	<xsd:element name="DUI" type="DUIType"/>
+	
+</xsd:schema>
\ No newline at end of file
diff --git a/tests/data/toulouse_axel/booking_info.xml b/tests/data/toulouse_axel/booking_info.xml
new file mode 100644
index 00000000..e8652e6b
--- /dev/null
+++ b/tests/data/toulouse_axel/booking_info.xml
@@ -0,0 +1,28 @@
+<PORTAIL>
+  <DUI>
+    <IDDUI>XXX</IDDUI>
+    <ENFANT>
+      <IDPERSONNE>3535</IDPERSONNE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>A19P1M1</IDACTIVITE>
+        <JOUR>00000</JOUR>
+      </ACTIVITE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>A19P1M2</IDACTIVITE>
+        <JOUR>11111</JOUR>
+      </ACTIVITE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>A19P1M3</IDACTIVITE>
+        <JOUR>11211</JOUR>
+      </ACTIVITE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>A19P1M4</IDACTIVITE>
+        <JOUR>22122</JOUR>
+      </ACTIVITE>
+    </ENFANT>
+  </DUI>
+</PORTAIL>
diff --git a/tests/data/toulouse_axel/child_activities.xml b/tests/data/toulouse_axel/child_activities.xml
new file mode 100644
index 00000000..069f802e
--- /dev/null
+++ b/tests/data/toulouse_axel/child_activities.xml
@@ -0,0 +1,62 @@
+<PORTAIL>
+  <DUI>
+    <IDDUI>XXX</IDDUI>
+    <NBENFANTINSCRIT>1</NBENFANTINSCRIT>
+    <ENFANT>
+      <NOM>foo</NOM>
+      <PRENOM>foo</PRENOM>
+      <DATENAISSANCE>01/01/2019</DATENAISSANCE>
+      <IDPERSONNE>3535</IDPERSONNE>
+      <REGIME>SV</REGIME>
+      <LIBELLEECOLE>MATERNELLE 1</LIBELLEECOLE>
+      <ACTIVITE>
+        <TYPEACTIVITE>MAT</TYPEACTIVITE>
+        <IDACTIVITE>A19P1M1</IDACTIVITE>
+        <LIBELLEACTIVITE>Temps du matin</LIBELLEACTIVITE>
+        <DATEENTREE>01/08/2019</DATEENTREE>
+        <DATESORTIE>31/07/2020</DATESORTIE>
+        <TARIF>0.42</TARIF>
+        <ISPAI>NON</ISPAI>
+        <COUTREVIENT>99999</COUTREVIENT>
+        <DATEDEBUT>01/08/2019</DATEDEBUT>
+        <DATEFIN>31/07/2020</DATEFIN>
+      </ACTIVITE>
+      <ACTIVITE>
+        <TYPEACTIVITE>MIDI</TYPEACTIVITE>
+        <IDACTIVITE>A19P1M2</IDACTIVITE>
+        <LIBELLEACTIVITE>Temps du midi</LIBELLEACTIVITE>
+        <DATEENTREE>01/08/2019</DATEENTREE>
+        <DATESORTIE>31/07/2020</DATESORTIE>
+        <TARIF>0.43</TARIF>
+        <ISPAI>NON</ISPAI>
+        <COUTREVIENT>99999</COUTREVIENT>
+        <DATEDEBUT>01/08/2019</DATEDEBUT>
+        <DATEFIN>31/07/2020</DATEFIN>
+      </ACTIVITE>
+      <ACTIVITE>
+        <TYPEACTIVITE>SOIR</TYPEACTIVITE>
+        <IDACTIVITE>A19P1M3</IDACTIVITE>
+        <LIBELLEACTIVITE>Temps du soir</LIBELLEACTIVITE>
+        <DATEENTREE>01/08/2019</DATEENTREE>
+        <DATESORTIE>31/07/2020</DATESORTIE>
+        <TARIF>0.44</TARIF>
+        <ISPAI>NON</ISPAI>
+        <COUTREVIENT>99999</COUTREVIENT>
+        <DATEDEBUT>01/08/2019</DATEDEBUT>
+        <DATEFIN>31/07/2020</DATEFIN>
+      </ACTIVITE>
+      <ACTIVITE>
+        <TYPEACTIVITE>GARD</TYPEACTIVITE>
+        <IDACTIVITE>A19P1M4</IDACTIVITE>
+        <LIBELLEACTIVITE>Temps mercredi apr&#232;s midi</LIBELLEACTIVITE>
+        <DATEENTREE>01/08/2019</DATEENTREE>
+        <DATESORTIE>31/07/2020</DATESORTIE>
+        <TARIF>0.45</TARIF>
+        <ISPAI>NON</ISPAI>
+        <COUTREVIENT>99999</COUTREVIENT>
+        <DATEDEBUT>01/08/2019</DATEDEBUT>
+        <DATEFIN>31/07/2020</DATEFIN>
+      </ACTIVITE>
+    </ENFANT>
+  </DUI>
+</PORTAIL>
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index 933fb70f..95ab7f4f 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -1,5 +1,6 @@
+# -*- coding: utf-8 -*-
 # passerelle - uniform access to multiple data sources and services
-# Copyright (C) 2019 Entr'ouvert
+# Copyright (C) 2020 Entr'ouvert
 #
 # This program is free software: you can redistribute it and/or modify it
 # under the terms of the GNU Affero General Public License as published
@@ -32,6 +33,7 @@ from passerelle.contrib.toulouse_axel.models import (
     Lock,
     OperationResult,
     ToulouseAxel,
+    enfants_activites,
     form_maj_famille_dui,
     form_paiement_dui,
     list_dui_factures,
@@ -40,6 +42,7 @@ from passerelle.contrib.toulouse_axel.models import (
     ref_facture_a_payer,
     ref_facture_pdf,
     ref_verif_dui,
+    reservation_periode,
 )
 from passerelle.utils.jsonresponse import APIError
 import utils
@@ -93,6 +96,28 @@ def family_data():
     return ref_famille_dui.response_converter.decode(ET.fromstring(resp))['DATA']['PORTAIL']['DUI']
 
 
+@pytest.fixture
+def child_activities_data():
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/child_activities.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    resp = '''
+    <?xml version="1.0"?>
+    <PORTAILSERVICE>
+      <RESULTAT>
+        <TYPE>EnfantsActivites</TYPE>
+        <STATUS>OK</STATUS>
+        <DATE>10/10/2010 10:10:01</DATE>
+        <COMMENTAIRES><![CDATA[]]></COMMENTAIRES>
+      </RESULTAT>
+      <DATA>
+        %s
+      </DATA>
+    </PORTAILSERVICE>
+    '''.strip() % content
+    return enfants_activites.response_converter.decode(ET.fromstring(resp))['DATA']['PORTAIL']['DUI']
+
+
 @pytest.fixture
 def flat_update_params():
     filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/flat_update_family_info.json')
@@ -363,6 +388,36 @@ def test_operation_form_paiement_dui(resource, content):
             })
 
 
+@pytest.mark.parametrize('content', [
+    '<PORTAIL><DUI/></PORTAIL>',
+])
+def test_operation_enfants_activites(resource, content):
+    with mock_getdata(content, 'EnfantsActivites'):
+        with pytest.raises(AxelError):
+            enfants_activites(resource, {
+                'DUI': {
+                    'IDDUI': 'XXX',
+                    'ANNEEREFERENCE': '2042',
+                    'TYPESACTIVITES': 'MAT,MIDI,SOIR,GARD',
+                }
+            })
+
+
+@pytest.mark.parametrize('content', [
+    '<PORTAIL><DUI/></PORTAIL>',
+])
+def test_operation_reservation_periode(resource, content):
+    with mock_getdata(content, 'ReservationPeriode'):
+        with pytest.raises(AxelError):
+            reservation_periode(resource, {
+                'PORTAIL': {
+                    'DUI': {
+                        'IDDUI': 'XXX',
+                    }
+                }
+            })
+
+
 def test_management_dates_endpoint_axel_error(app, resource):
     with mock.patch('passerelle.contrib.toulouse_axel.models.ref_date_gestion_dui') as operation:
         operation.side_effect = AxelError('FooBar')
@@ -1688,3 +1743,205 @@ def test_pay_invoice_endpoint(app, resource):
             }
         }
     }
+
+
+def test_clae_booking_info_endpoint_axel_error(app, resource):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    with mock.patch('passerelle.contrib.toulouse_axel.models.enfants_activites') as operation:
+        operation.side_effect = AxelError('FooBar')
+        resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    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.models.reservation_periode') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    assert resp.json['err_desc'] == "Axel error: FooBar"
+    assert resp.json['err'] == 'error'
+
+
+@pytest.mark.parametrize('value', ['foo', '20/01/2020', '2020'])
+def test_clae_booking_info_endpoint_bad_date_format(app, resource, value):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=%s' % value, status=400)
+    assert resp.json['err_desc'] == "bad date format"
+    assert resp.json['err'] == 'bad-request'
+
+
+def test_clae_booking_info_endpoint_no_result(app, resource, child_activities_data):
+    resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    assert resp.json['err_desc'] == "Person not found"
+    assert resp.json['err'] == 'not-found'
+
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    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.models.reservation_periode') as operation:
+            operation.side_effect = AxelError('FooBar')
+            resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=4242&booking_date=2020-01-20')
+    assert resp.json['err_desc'] == "Child not found"
+    assert resp.json['err'] == 'not-found'
+
+    content = """<PORTAIL>
+  <DUI>
+    <IDDUI>XXX</IDDUI>
+    <ENFANT>
+      <IDPERSONNE>4242</IDPERSONNE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>A19P1M1</IDACTIVITE>
+        <JOUR>00000</JOUR>
+      </ACTIVITE>
+    </ENFANT>
+  </DUI>
+</PORTAIL>"""
+    activities = child_activities_data['ENFANT'][0]
+    with mock_getdata(content, 'ReservationPeriode'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+            resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    assert resp.json['err_desc'] == "Child not found"
+    assert resp.json['err'] == 'not-found'
+
+
+def test_clae_booking_info_endpoint(app, resource, child_activities_data):
+    Link.objects.create(resource=resource, name_id='yyy', dui='XXX', person_id='42')
+    filepath = os.path.join(os.path.dirname(__file__), 'data/toulouse_axel/booking_info.xml')
+    with open(filepath) as xml:
+        content = xml.read()
+    activities = child_activities_data['ENFANT'][0]
+    with mock_getdata(content, 'ReservationPeriode'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+            resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    assert resp.json['err'] == 0
+    assert resp.json['data'] == {
+        'ACTIVITE': [
+            {
+                'COUTREVIENT': '99999',
+                'DATEDEBUT': '2019-08-01',
+                'DATEENTREE': '2019-08-01',
+                'DATEFIN': '2020-07-31',
+                'DATESORTIE': '2020-07-31',
+                'IDACTIVITE': 'A19P1M1',
+                'ISPAI': False,
+                'LIBELLEACTIVITE': 'Temps du matin',
+                'TARIF': '0.42',
+                'TYPEACTIVITE': 'MAT',
+                'booking': {
+                    'days': {
+                        'friday': False,
+                        'monday': False,
+                        'thursday': False,
+                        'tuesday': False,
+                        'wednesday': False
+                    },
+                    'raw_value': '00000'
+                }
+            },
+            {
+                'COUTREVIENT': '99999',
+                'DATEDEBUT': '2019-08-01',
+                'DATEENTREE': '2019-08-01',
+                'DATEFIN': '2020-07-31',
+                'DATESORTIE': '2020-07-31',
+                'IDACTIVITE': 'A19P1M2',
+                'ISPAI': False,
+                'LIBELLEACTIVITE': 'Temps du midi',
+                'TARIF': '0.43',
+                'TYPEACTIVITE': 'MIDI',
+                'booking': {
+                    'days': {
+                        'friday': True,
+                        'monday': True,
+                        'thursday': True,
+                        'tuesday': True,
+                        'wednesday': True
+                    },
+                    'raw_value': '11111'
+                }
+            },
+            {
+                'COUTREVIENT': '99999',
+                'DATEDEBUT': '2019-08-01',
+                'DATEENTREE': '2019-08-01',
+                'DATEFIN': '2020-07-31',
+                'DATESORTIE': '2020-07-31',
+                'IDACTIVITE': 'A19P1M3',
+                'ISPAI': False,
+                'LIBELLEACTIVITE': 'Temps du soir',
+                'TARIF': '0.44',
+                'TYPEACTIVITE': 'SOIR',
+                'booking': {
+                    'days': {
+                        'friday': True,
+                        'monday': True,
+                        'thursday': True,
+                        'tuesday': True,
+                        'wednesday': None
+                    },
+                    'raw_value': '11211'
+                }
+            },
+            {
+                'COUTREVIENT': '99999',
+                'DATEDEBUT': '2019-08-01',
+                'DATEENTREE': '2019-08-01',
+                'DATEFIN': '2020-07-31',
+                'DATESORTIE': '2020-07-31',
+                'IDACTIVITE': 'A19P1M4',
+                'ISPAI': False,
+                'LIBELLEACTIVITE': u'Temps mercredi après midi',
+                'TARIF': '0.45',
+                'TYPEACTIVITE': 'GARD',
+                'booking': {
+                    'days': {
+                        'friday': None,
+                        'monday': None,
+                        'thursday': None,
+                        'tuesday': None,
+                        'wednesday': True
+                    },
+                    'raw_value': '22122'
+                }
+            }
+        ],
+        'DATENAISSANCE': '2019-01-01',
+        'IDPERSONNE': '3535',
+        'LIBELLEECOLE': 'MATERNELLE 1',
+        'NOM': 'foo',
+        'PRENOM': 'foo',
+        'REGIME': 'SV'
+    }
+
+    # test wrong and missing IDACTIVITE
+    content = """<PORTAIL>
+  <DUI>
+    <IDDUI>XXX</IDDUI>
+    <ENFANT>
+      <IDPERSONNE>3535</IDPERSONNE>
+      <ACTIVITE>
+        <ANNEEREFERENCE>2019</ANNEEREFERENCE>
+        <IDACTIVITE>UNKNOWN</IDACTIVITE>
+        <JOUR>00000</JOUR>
+      </ACTIVITE>
+    </ENFANT>
+  </DUI>
+</PORTAIL>"""
+    activities = child_activities_data['ENFANT'][0]
+    with mock_getdata(content, 'ReservationPeriode'):
+        with mock.patch('passerelle.contrib.toulouse_axel.models.ToulouseAxel.get_child_activities', return_value=activities):
+            resp = app.get('/toulouse-axel/test/clae_booking_info?NameID=yyy&idpersonne=3535&booking_date=2020-01-20')
+    assert resp.json['err'] == 0
+    assert resp.json['data']['ACTIVITE'][0]['IDACTIVITE'] == 'A19P1M1'
+    assert resp.json['data']['ACTIVITE'][0]['booking'] == {}
+    assert resp.json['data']['ACTIVITE'][1]['IDACTIVITE'] == 'A19P1M2'
+    assert resp.json['data']['ACTIVITE'][1]['booking'] == {}
+    assert resp.json['data']['ACTIVITE'][2]['IDACTIVITE'] == 'A19P1M3'
+    assert resp.json['data']['ACTIVITE'][2]['booking'] == {}
+    assert resp.json['data']['ACTIVITE'][3]['IDACTIVITE'] == 'A19P1M4'
+    assert resp.json['data']['ACTIVITE'][3]['booking'] == {}
diff --git a/tests/test_toulouse_axel_utils.py b/tests/test_toulouse_axel_utils.py
new file mode 100644
index 00000000..61a2b7db
--- /dev/null
+++ b/tests/test_toulouse_axel_utils.py
@@ -0,0 +1,62 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import datetime
+import pytest
+
+from passerelle.contrib.toulouse_axel.utils import (
+    get_booking,
+    get_reference_year_from_date,
+    get_week_dates_from_date,
+    json_date_format,
+)
+
+
+@pytest.mark.parametrize('value, expected', [
+    ('0', False),
+    ('1', True),
+    ('2', None),
+    ('foo', False),
+])
+def test_get_booking(value, expected):
+    assert get_booking(value) is expected
+
+
+@pytest.mark.parametrize('value, expected', [
+    ('2020-01-01', 2019),
+    ('2020-07-31', 2019),
+    ('2020-08-01', 2020),
+    ('2020-12-31', 2020),
+])
+def test_get_reference_year_from_date(value, expected):
+    in_date = datetime.datetime.strptime(value, json_date_format)
+    assert get_reference_year_from_date(in_date) == expected
+
+
+@pytest.mark.parametrize('in_value, start_value, end_value', [
+    ('2020-01-06', '2020-01-06', '2020-01-10'),  # monday
+    ('2020-01-07', '2020-01-06', '2020-01-10'),  # tuesday
+    ('2020-01-08', '2020-01-06', '2020-01-10'),  # wednesday
+    ('2020-01-09', '2020-01-06', '2020-01-10'),  # thursday
+    ('2020-01-10', '2020-01-06', '2020-01-10'),  # friday
+    ('2020-01-11', '2020-01-06', '2020-01-10'),  # saturday
+    ('2020-01-12', '2020-01-06', '2020-01-10'),  # sunday
+])
+def test_get_week_dates_from_date(in_value, start_value, end_value):
+    in_date = datetime.datetime.strptime(in_value, json_date_format)
+    start_date = datetime.datetime.strptime(start_value, json_date_format)
+    end_date = datetime.datetime.strptime(end_value, json_date_format)
+    assert get_week_dates_from_date(in_date) == (start_date, end_date)
-- 
2.20.1

