From 183be84c7dde4565bfc2422d1efb26d7ef399e83 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Sun, 13 Nov 2022 19:07:33 +0100
Subject: [PATCH] toulouse-maelis: add functional tests (#71647)

---
 functests/toulouse_maelis/.gitignore          |   1 +
 functests/toulouse_maelis/README              |  22 +
 functests/toulouse_maelis/__init__.py         |   0
 functests/toulouse_maelis/conftest.py         | 412 +++++++++++
 functests/toulouse_maelis/conftest.py~        |   9 +
 .../data/test_create_child.json               |  26 +
 .../data/test_create_family.json              | 226 ++++++
 .../toulouse_maelis/data/test_create_rl2.json |  38 +
 .../data/test_read_category_list.json         |  14 +
 .../data/test_read_civility_list.json         |  10 +
 .../data/test_read_csp_list.json              | 110 +++
 .../data/test_read_dietcode_list.json         |  34 +
 .../data/test_read_organ_list.json            | 370 ++++++++++
 .../data/test_read_pai_list.json              |  18 +
 .../data/test_read_quality_list.json          |  70 ++
 .../data/test_read_quotient_list.json         |  26 +
 .../data/test_read_situation_list.json        |  38 +
 .../data/test_read_vaccin_list.json           |  82 +++
 .../data/test_update_child.json               | 108 +++
 .../test_update_child_medical_record.json     |  23 +
 .../data/test_update_child_pai.json           |   7 +
 .../data/test_update_child_person.json        |  41 ++
 .../data/test_update_coordinate.json          |  54 ++
 .../data/test_update_family.json              | 318 ++++++++
 .../data/test_update_family_ras.json          | 260 +++++++
 .../data/test_update_person.json              |  31 +
 .../toulouse_maelis/data/test_update_rl1.json |  38 +
 .../toulouse_maelis/data/test_update_rl2.json |  54 ++
 functests/toulouse_maelis/read_family.py      |  35 +
 functests/toulouse_maelis/test_family.py      | 693 ++++++++++++++++++
 functests/toulouse_maelis/test_misc.py        |  59 ++
 31 files changed, 3227 insertions(+)
 create mode 100644 functests/toulouse_maelis/.gitignore
 create mode 100644 functests/toulouse_maelis/README
 create mode 100644 functests/toulouse_maelis/__init__.py
 create mode 100644 functests/toulouse_maelis/conftest.py
 create mode 100644 functests/toulouse_maelis/conftest.py~
 create mode 100644 functests/toulouse_maelis/data/test_create_child.json
 create mode 100644 functests/toulouse_maelis/data/test_create_family.json
 create mode 100644 functests/toulouse_maelis/data/test_create_rl2.json
 create mode 100644 functests/toulouse_maelis/data/test_read_category_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_civility_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_csp_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_dietcode_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_organ_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_pai_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_quality_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_quotient_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_situation_list.json
 create mode 100644 functests/toulouse_maelis/data/test_read_vaccin_list.json
 create mode 100644 functests/toulouse_maelis/data/test_update_child.json
 create mode 100644 functests/toulouse_maelis/data/test_update_child_medical_record.json
 create mode 100644 functests/toulouse_maelis/data/test_update_child_pai.json
 create mode 100644 functests/toulouse_maelis/data/test_update_child_person.json
 create mode 100644 functests/toulouse_maelis/data/test_update_coordinate.json
 create mode 100644 functests/toulouse_maelis/data/test_update_family.json
 create mode 100644 functests/toulouse_maelis/data/test_update_family_ras.json
 create mode 100644 functests/toulouse_maelis/data/test_update_person.json
 create mode 100644 functests/toulouse_maelis/data/test_update_rl1.json
 create mode 100644 functests/toulouse_maelis/data/test_update_rl2.json
 create mode 100755 functests/toulouse_maelis/read_family.py
 create mode 100644 functests/toulouse_maelis/test_family.py
 create mode 100644 functests/toulouse_maelis/test_misc.py

diff --git a/functests/toulouse_maelis/.gitignore b/functests/toulouse_maelis/.gitignore
new file mode 100644
index 00000000..f89f4eb8
--- /dev/null
+++ b/functests/toulouse_maelis/.gitignore
@@ -0,0 +1 @@
+*.res
diff --git a/functests/toulouse_maelis/README b/functests/toulouse_maelis/README
new file mode 100644
index 00000000..77270a34
--- /dev/null
+++ b/functests/toulouse_maelis/README
@@ -0,0 +1,22 @@
+Functional tests for the Toulouse Maelis connector
+
+Description
+===========
+
+This test suite will use the web API of a passerelle Toulouse Maelis
+connector to link a Publik account and check that the connector is
+behaving as expected.
+
+
+Usage
+=====
+
+You will need a running passerelle instance, whith the Toulouse Maelis connector instance configured to connect to
+a Maelis server.
+Suppose that the Toulouse Mealis connector instance is listening here :
+
+    http://127.0.0.1:8000/toulouse-maelis/test
+
+Then you would start the test suite with the following command:
+
+    $ py.test -sx --url=http://127.0.0.1:8000/toulouse-maelis/test test_toulouse_maelis.py
diff --git a/functests/toulouse_maelis/__init__.py b/functests/toulouse_maelis/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/functests/toulouse_maelis/conftest.py b/functests/toulouse_maelis/conftest.py
new file mode 100644
index 00000000..d3625bee
--- /dev/null
+++ b/functests/toulouse_maelis/conftest.py
@@ -0,0 +1,412 @@
+import copy
+import datetime
+import json
+import os
+import subprocess
+import time
+from uuid import uuid4
+
+import pytest
+import requests
+from django.core.serializers.json import DjangoJSONEncoder
+from zeep.helpers import serialize_object
+
+FAMILY_PAYLOAD = {
+    'category': 'BI',
+    'situation': 'VM',
+    'rl1': {
+        'civility': 'MME',
+        'firstname': 'Marge',
+        'lastname': 'Simpson',
+        'maidenName': 'Bouvier',
+        'quality': 'MERE',
+        'birth': {'dateBirth': '1950-10-01'},
+        'adresse': {'street1': 'Evergreen Terrace', 'town': 'Springfield', 'zipcode': '62701'},
+    },
+    'rl2': {
+        'civility': 'M.',
+        'firstname': 'Homer',
+        'lastname': 'Simpson',
+        'quality': 'PERE',
+        'birth': {'dateBirth': '1956-05-12'},
+        'adresse': {
+            'num': '742',
+            'numComp': None,
+            'street1': 'Evergreen Terrace',
+            'street2': None,
+            'town': 'Springfield',
+            'zipcode': '90701',
+        },
+        'contact': {
+            'isContactMail': True,
+            'isContactSms': True,
+            'isInvoicePdf': True,
+            'mail': 'homer.simpson@example.org.com',
+            'mobile': '0622222222',
+            'phone': '0122222222',
+        },
+        'profession': {
+            'addressPro': {
+                'num': None,
+                'street': None,
+                'town': 'Springfield',
+                'zipcode': '90701',
+            },
+            'codeCSP': '13',
+            'employerName': 'Burns',
+            'phone': '0133333333',
+            'profession': 'Inspecteur de sécurité',
+        },
+        'CAFInfo': {
+            'number': '123',
+            'organ': 'A10007752822',
+        },
+    },
+    'childList': [
+        {
+            'sexe': 'M',
+            'firstname': 'Bart',
+            'lastname': 'Simpson',
+            'birth': {'dateBirth': '1978-04-01'},
+            'bPhoto': True,
+            'bLeaveAlone': True,
+            'dietcode': 'STD',
+            'paiInfoBean': {
+                'code': 'PAIMED',
+                'dateDeb': '2022-09-01',
+                'dateFin': '2023-07-01',
+                'description': 'mischievous, rebellious, misunderstood, disruptive',
+            },
+            'medicalRecord': {
+                'familyDoctor': {
+                    'name': 'MONROE',
+                    'phone': '0612341234',
+                    'address': {
+                        'street1': 'Alameda',
+                        'zipcode': '90701',
+                        'town': 'Springfield',
+                    },
+                },
+                'allergy1': 'butterscotch, imitation butterscotch, glow-in-the-dark monster make-up',
+                'allergy2': 'shrimp and cauliflower',
+                'comment1': "the shrimp allergy isn't fully identified",
+                'comment2': None,
+                'observ1': 'Ay Caramba!',
+                'observ2': 'Eat my shorts!',
+                'isAuthHospital': True,
+                'hospital': 'Springfield General Hospital',
+                'vaccinList': [
+                    {
+                        'code': 'DTC',
+                        'vaccinationDate': '2011-01-11',
+                    },
+                    {
+                        'code': 'ROR',
+                        'vaccinationDate': '2022-02-22',
+                    },
+                ],
+            },
+            'authorizedPersonList': [
+                {
+                    'personInfo': {
+                        'civility': 'M.',
+                        'firstname': 'Abraham Jebediah',
+                        'lastname': 'Simpson',
+                        'dateBirth': '1927-05-24',
+                        'sexe': 'M',
+                        'contact': {
+                            'phone': '0312345678',
+                            'mobile': '',
+                            'mail': 'abe.simpson@example.org',
+                        },
+                    },
+                    'personQuality': {
+                        'code': 'GPP',
+                    },
+                },
+                {
+                    'personInfo': {
+                        'civility': 'MME',
+                        'firstname': 'Mona Penelope',
+                        'lastname': 'Simpson',
+                        'dateBirth': '1929-03-15',
+                        'sexe': 'F',
+                        'contact': {
+                            'phone': '0412345678',
+                            'mobile': '0612345678',
+                            'mail': 'mona.simpson@example.org',
+                        },
+                    },
+                    'personQuality': {
+                        'code': 'GMP',
+                    },
+                },
+            ],
+        },
+        {
+            'sexe': 'F',
+            'firstname': 'Lisa',
+            'lastname': 'Simpson',
+            'birth': {'dateBirth': '1980-05-09'},
+            'dietcode': 'RSV',
+            'paiInfoBean': {
+                'code': 'PAIALI',
+            },
+        },
+        {
+            'sexe': 'F',
+            'firstname': 'Maggie',
+            'lastname': 'Simpson',
+            'birth': {'dateBirth': '1989-12-17'},
+            'dietcode': 'BB',
+        },
+    ],
+    'emergencyPersonList': [
+        {
+            'civility': 'MME',
+            'firstname': 'Patty',
+            'lastname': 'Bouvier',
+            'dateBirth': '1948-08-30',
+            'sexe': 'F',
+            'quality': 'T',
+            'contact': {
+                'phone': '0112345678',
+                'mobile': '0612345678',
+                'mail': 'patty.bouvier@example.org',
+            },
+        },
+        {
+            'civility': 'MME',
+            'firstname': 'Selma',
+            'lastname': 'Bouvier',
+            'dateBirth': '1946-04-29',
+            'sexe': 'F',
+            'quality': 'OS',
+            'contact': {
+                'phone': '0112345678',
+                'mobile': '0612345678',
+                'mail': 'selma.bouvier@example.org',
+            },
+        },
+    ],
+}
+
+
+def pytest_addoption(parser):
+    parser.addoption(
+        '--url',
+        help='Url of a passerelle Toulouse Maelis connector instance',
+        default='https://parsifal-passerelle.dev.publik.love/toulouse-maelis/test',
+    )
+    parser.addoption('--nameid', help='Publik Name ID', default='functest')
+    parser.addoption('--dui', help='DUI number', default='')
+
+
+def unlink(conn, name_id):
+    url = conn + '/unlink?NameID=%s' % name_id
+    resp = requests.post(url)
+    resp.raise_for_status()
+    return resp
+
+
+def link(conn, data):
+    url = conn + '/link?NameID=%s' % data['name_id']
+    payload = {
+        'family_id': data['family_id'],
+        'firstname': data['family_payload']['rl1']['firstname'],
+        'lastname': data['family_payload']['rl1']['lastname'],
+        'dateBirth': data['family_payload']['rl1']['birth']['dateBirth'],
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res == {'data': 'ok', 'err': 0}
+
+
+def read_family(conn, name_id):
+    url = conn + '/read-family?NameID=%s' % name_id
+    resp = requests.get(url)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
+    return res['data']
+
+
+def diff(result, expected_file):
+    class JSONEncoder(DjangoJSONEncoder):
+        def default(self, o):
+            if isinstance(o, time.struct_time):
+                o = datetime.datetime(*tuple(o)[:6])
+            return super().default(o)
+
+    expected_file_path = 'data/' + expected_file
+    assert os.path.isfile(expected_file_path), 'please, touch %s' % expected_file_path
+
+    result_file_path = '/tmp/%s.res' % expected_file
+    with open(result_file_path, 'w') as json_file:
+        json.dump(serialize_object(result), json_file, cls=DjangoJSONEncoder, indent=2)
+        json_file.write('\n')
+    cmd = 'diff %s %s' % (result_file_path, expected_file_path)
+    output = subprocess.run(cmd, shell=True, check=False, stdout=None, stderr=None)
+    assert output.returncode == 0, 'please, %s' % cmd
+    return True
+
+
+def remove_id_on_child(child):
+    child['num'] = 'N/A'
+    child['lastname'] = 'N/A'
+    child['mother'] = 'N/A'  # dont care yet
+    child['father'] = 'N/A'  # dont care yet
+    for person in child['authorizedPersonList']:
+        person['personInfo']['num'] = 'N/A'
+
+
+def remove_id_on_rlg(rlg):
+    if rlg:
+        rlg['num'] = 'N/A'
+        rlg['lastname'] = 'N/A'
+
+
+def remove_id_on_family(family):
+    family['number'] = 'N/A'
+    remove_id_on_rlg(family['RL1'])
+    remove_id_on_rlg(family['RL2'])
+    for child in family['childList']:
+        remove_id_on_child(child)
+    for person in family['emergencyPersonList']:
+        person['numPerson'] = 'N/A'
+
+
+def diff_child(conn, name_id, index, expected_file, key=None):
+    data = read_family(conn, name_id)
+    child = copy.deepcopy(data['childList'][index])
+    remove_id_on_child(child)
+    if not key:
+        assert diff(child, expected_file)
+    else:
+        assert diff(child[key], expected_file)
+    return data
+
+
+def diff_rlg(conn, name_id, index, expected_file):
+    data = read_family(conn, name_id)
+    rlg = copy.deepcopy(data['RL%s' % index])
+    remove_id_on_rlg(rlg)
+    assert diff(rlg, expected_file)
+    return data
+
+
+def diff_family(conn, name_id, expected_file, key=None):
+    data = read_family(conn, name_id)
+    family = copy.deepcopy(data)
+    remove_id_on_family(family)
+    if not key:
+        assert diff(family, expected_file)
+    else:
+        assert diff(family[key], expected_file)
+    return data
+
+
+@pytest.fixture(scope='session')
+def conn(request):
+    return request.config.getoption('--url')
+
+
+@pytest.fixture(scope='session')
+def create_data(request, conn):
+    name_id = request.config.getoption('--nameid')
+    unlink(conn, name_id)
+    lastname = uuid4().hex[0:30]
+
+    # create family
+    create_family_payload = copy.deepcopy(FAMILY_PAYLOAD)
+    del create_family_payload['childList'][1]  # without Lisa
+    del create_family_payload['rl2']  # without Homer
+    create_family_payload['rl1']['lastname'] = lastname
+    for child in create_family_payload['childList']:
+        child['lastname'] = lastname
+
+    url = conn + '/create-family?NameID=%s' % name_id
+    resp = requests.post(url, json=create_family_payload)
+    resp.raise_for_status()
+    create_result = resp.json()
+    assert create_result['err'] == 0
+    print('\ncreate DUI: %s' % str(create_result['data']['number']))
+    data = diff_family(conn, name_id, 'test_create_family.json')
+
+    return {
+        'name_id': name_id,  # linked
+        'family_id': str(create_result['data']['number']),
+        'lastname': lastname,
+        'family_payload': create_family_payload,
+        'data': data,
+    }
+
+
+@pytest.fixture(scope='session')
+def update_data(request, conn):
+    name_id = request.config.getoption('--nameid')
+    unlink(conn, name_id)
+
+    debug = False  # True allow to create then update a new family
+    if debug:
+        lastname = 'something new'
+        FAMILY_PAYLOAD['rl1']['lastname'] = lastname
+        FAMILY_PAYLOAD['rl2']['lastname'] = lastname
+        for child in FAMILY_PAYLOAD['childList']:
+            child['lastname'] = lastname
+
+    # try to re-create test family
+    url = conn + '/create-family?NameID=%s' % name_id
+    resp = requests.post(url, json=FAMILY_PAYLOAD)
+    resp.raise_for_status()
+    create_result = resp.json()
+    if not create_result['err']:
+        # create DUI if it is the first time the test is run
+        family_id = str(create_result['data']['number'])
+        print('\ncreate DUI: %s' % family_id)
+    elif 'E54a' in create_result['err']:
+        # else find DUI number in the error message
+        family_id = str(create_result['err_desc'][-7:-1])
+        print('\nre-use DUI: %s' % family_id)
+        # and link NameID to it
+        url = conn + '/link?NameID=%s' % name_id
+        link_payload = {
+            'family_id': family_id,
+            'firstname': FAMILY_PAYLOAD['rl1']['firstname'],
+            'lastname': FAMILY_PAYLOAD['rl1']['lastname'],
+            'dateBirth': FAMILY_PAYLOAD['rl1']['birth']['dateBirth'],
+        }
+        resp = requests.post(url, json=link_payload)
+        resp.raise_for_status()
+        assert not resp.json()['err']
+    else:
+        # FAMILY_PAYLOAD looks wrong
+        assert False, create_result
+
+    # update DUI
+    data = read_family(conn, name_id)
+    update_family_payload = copy.deepcopy(FAMILY_PAYLOAD)
+    for i, child in enumerate(update_family_payload['childList']):
+        child['num'] = data['childList'][i]['num']  # required for update
+
+    url = conn + '/update-family?NameID=%s' % name_id
+    resp = requests.post(url, json=update_family_payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+
+    if debug:
+        data = read_family(conn, name_id)
+    else:
+        data = diff_family(conn, name_id, 'test_update_family.json')
+
+    return {
+        'name_id': name_id,  # linked
+        'family_id': family_id,
+        'family_payload': update_family_payload,
+        'rl2_num': data['RL2']['num'],
+        'bart_num': data['childList'][0]['num'],
+        'lisa_num': data['childList'][1]['num'],
+        'maggie_num': data['childList'][2]['num'],
+        'data': data,
+    }
diff --git a/functests/toulouse_maelis/conftest.py~ b/functests/toulouse_maelis/conftest.py~
new file mode 100644
index 00000000..f67f2883
--- /dev/null
+++ b/functests/toulouse_maelis/conftest.py~
@@ -0,0 +1,9 @@
+import pytest
+
+
+def pytest_addoption(parser):
+    parser.addoption("--cmis-connector-url", help="Url of a passerelle CMIS connector instance")
+    parser.addoption("--cmis-endpoint", help="Url of a passerelle CMIS endpoint")
+    parser.addoption("--cmis-username", help="Username for the CMIS endpoint")
+    parser.addoption("--cmis-password", help="Password for the CMIS endpoint")
+    parser.addoption("--preserve-tree", action="store_true", default=False, help="Preserve test directory")
diff --git a/functests/toulouse_maelis/data/test_create_child.json b/functests/toulouse_maelis/data/test_create_child.json
new file mode 100644
index 00000000..c36b9f74
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_create_child.json
@@ -0,0 +1,26 @@
+{
+  "num": "N/A",
+  "lastname": "N/A",
+  "firstname": "LISA",
+  "sexe": "M",
+  "birth": {
+    "dateBirth": "1980-05-09T00:00:00+02:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "dietcode": null,
+  "bPhoto": false,
+  "bLeaveAlone": false,
+  "authorizedPersonList": [],
+  "indicatorList": [],
+  "medicalRecord": null,
+  "insurance": null,
+  "paiInfoBean": null,
+  "mother": "N/A",
+  "father": "N/A",
+  "rl": null,
+  "subscribeSchoolList": [],
+  "subscribeActivityList": [],
+  "sexe_text": "Masculin"
+}
diff --git a/functests/toulouse_maelis/data/test_create_family.json b/functests/toulouse_maelis/data/test_create_family.json
new file mode 100644
index 00000000..fb6d95fe
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_create_family.json
@@ -0,0 +1,226 @@
+{
+  "number": "N/A",
+  "category": "BI",
+  "situation": "VM",
+  "flagCom": true,
+  "nbChild": null,
+  "nbTotalChild": null,
+  "nbAES": null,
+  "RL1": {
+    "num": "N/A",
+    "firstname": "MARGE",
+    "lastname": "N/A",
+    "maidenName": "BOUVIER",
+    "quality": "MERE",
+    "civility": "MME",
+    "birth": {
+      "dateBirth": "1950-10-01T00:00:00+01:00",
+      "place": null,
+      "communeCode": null,
+      "countryCode": null
+    },
+    "adresse": {
+      "idStreet": null,
+      "num": 0,
+      "numComp": null,
+      "street1": "Evergreen Terrace",
+      "street2": null,
+      "town": "Springfield",
+      "zipcode": "62701"
+    },
+    "contact": {
+      "phone": null,
+      "mobile": null,
+      "mail": null,
+      "isContactMail": false,
+      "isContactSms": false,
+      "isInvoicePdf": false
+    },
+    "profession": null,
+    "CAFInfo": null,
+    "indicatorList": [],
+    "quotientList": [],
+    "subscribeActivityList": [],
+    "civility_text": "Madame",
+    "quality_text": "MERE"
+  },
+  "RL2": null,
+  "quotientList": [],
+  "childList": [
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "BART",
+      "sexe": "M",
+      "birth": {
+        "dateBirth": "1978-04-01T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "STD",
+      "bPhoto": true,
+      "bLeaveAlone": true,
+      "authorizedPersonList": [
+        {
+          "personInfo": {
+            "num": "N/A",
+            "lastname": "SIMPSON",
+            "firstname": "ABRAHAM JEBEDIAH",
+            "dateBirth": "1927-05-24T00:00:00+01:00",
+            "civility": "M.",
+            "sexe": "M",
+            "contact": {
+              "phone": "0312345678",
+              "mobile": null,
+              "mail": "abe.simpson@example.org"
+            },
+            "civility_text": "Monsieur",
+            "sexe_text": "Masculin"
+          },
+          "personQuality": {
+            "code": "GPP",
+            "libelle": "GRAND-PERE PATERNEL",
+            "code_text": "GRAND-PERE PATERNEL"
+          }
+        },
+        {
+          "personInfo": {
+            "num": "N/A",
+            "lastname": "SIMPSON",
+            "firstname": "MONA PENELOPE",
+            "dateBirth": "1929-03-15T00:00:00Z",
+            "civility": "MME",
+            "sexe": "F",
+            "contact": {
+              "phone": "0412345678",
+              "mobile": "0612345678",
+              "mail": "mona.simpson@example.org"
+            },
+            "civility_text": "Madame",
+            "sexe_text": "F\u00e9minin"
+          },
+          "personQuality": {
+            "code": "GMP",
+            "libelle": "GRAND-MERE PATERNELLE",
+            "code_text": "GRAND-MERE PATERNELLE"
+          }
+        }
+      ],
+      "indicatorList": [],
+      "medicalRecord": {
+        "familyDoctor": {
+          "name": "MONROE",
+          "phone": "0612341234",
+          "address": {
+            "street1": "Alameda",
+            "zipcode": "90701",
+            "town": "Springfield"
+          }
+        },
+        "allergy1": "butterscotch, imitation butterscotch, glow-in-the-dark monster make-up",
+        "allergy2": "shrimp and cauliflower",
+        "comment1": "the shrimp allergy isn't fully identified",
+        "comment2": null,
+        "observ1": "Ay Caramba!",
+        "observ2": "Ay Caramba!",
+        "isAuthHospital": false,
+        "hospital": null,
+        "vaccinList": [
+          {
+            "code": "ROR",
+            "label": "ROUGEOLE-OREILLONS-RUBEOLE",
+            "vaccinationDate": "2022-02-22T00:00:00+01:00"
+          },
+          {
+            "code": "DTC",
+            "label": "DIPHTERIE TETANOS COQUELUCHE",
+            "vaccinationDate": "2011-01-11T00:00:00+01:00"
+          }
+        ]
+      },
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIMED",
+        "dateDeb": "2022-09-01T00:00:00+02:00",
+        "dateFin": "2023-07-01T00:00:00+02:00",
+        "description": "mischievous, rebellious, misunderstood, disruptive",
+        "code_text": "MEDICAL"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "Masculin",
+      "dietcode_text": "1- REPAS STANDARD"
+    },
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "MAGGIE",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1989-12-17T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "BB",
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [],
+      "indicatorList": [],
+      "medicalRecord": null,
+      "insurance": null,
+      "paiInfoBean": null,
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin",
+      "dietcode_text": "REPAS BEBE"
+    }
+  ],
+  "emergencyPersonList": [
+    {
+      "numPerson": "N/A",
+      "civility": "MME",
+      "firstname": "PATTY",
+      "lastname": "BOUVIER",
+      "dateBirth": "1948-08-30T00:00:00+01:00",
+      "sexe": "F",
+      "quality": "T",
+      "contact": {
+        "phone": "0112345678",
+        "mobile": "0612345678",
+        "mail": "patty.bouvier@example.org"
+      },
+      "civility_text": "Madame",
+      "quality_text": "TANTE",
+      "sexe_text": "F\u00e9minin"
+    },
+    {
+      "numPerson": "N/A",
+      "civility": "MME",
+      "firstname": "SELMA",
+      "lastname": "BOUVIER",
+      "dateBirth": "1946-04-29T00:00:00+01:00",
+      "sexe": "F",
+      "quality": "OS",
+      "contact": {
+        "phone": "0112345678",
+        "mobile": "0612345678",
+        "mail": "selma.bouvier@example.org"
+      },
+      "civility_text": "Madame",
+      "quality_text": "ORGANISME SOCIAL",
+      "sexe_text": "F\u00e9minin"
+    }
+  ],
+  "indicatorList": [],
+  "childErrorList": [],
+  "category_text": "BIPARENTALE",
+  "situation_text": "Vivant maritalement"
+}
diff --git a/functests/toulouse_maelis/data/test_create_rl2.json b/functests/toulouse_maelis/data/test_create_rl2.json
new file mode 100644
index 00000000..9735c6ad
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_create_rl2.json
@@ -0,0 +1,38 @@
+{
+  "num": "N/A",
+  "firstname": "HOMER",
+  "lastname": "N/A",
+  "maidenName": null,
+  "quality": "PERE",
+  "civility": "M.",
+  "birth": {
+    "dateBirth": "1956-05-12T00:00:00+01:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "adresse": {
+    "idStreet": null,
+    "num": 742,
+    "numComp": null,
+    "street1": "Evergreen Terrace",
+    "street2": null,
+    "town": "Springfield",
+    "zipcode": "90701"
+  },
+  "contact": {
+    "phone": null,
+    "mobile": null,
+    "mail": null,
+    "isContactMail": false,
+    "isContactSms": false,
+    "isInvoicePdf": false
+  },
+  "profession": null,
+  "CAFInfo": null,
+  "indicatorList": [],
+  "quotientList": [],
+  "subscribeActivityList": [],
+  "civility_text": "Monsieur",
+  "quality_text": "PERE"
+}
diff --git a/functests/toulouse_maelis/data/test_read_category_list.json b/functests/toulouse_maelis/data/test_read_category_list.json
new file mode 100644
index 00000000..8a48b430
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_category_list.json
@@ -0,0 +1,14 @@
+[
+  {
+    "id": "BI",
+    "text": "BIPARENTALE"
+  },
+  {
+    "id": "ACCEUI",
+    "text": "FAMILLE D'ACCUEIL"
+  },
+  {
+    "id": "MONO",
+    "text": "MONOPARENTALE"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_civility_list.json b/functests/toulouse_maelis/data/test_read_civility_list.json
new file mode 100644
index 00000000..5894caa8
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_civility_list.json
@@ -0,0 +1,10 @@
+[
+  {
+    "id": "MME",
+    "text": "Madame"
+  },
+  {
+    "id": "M.",
+    "text": "Monsieur"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_csp_list.json b/functests/toulouse_maelis/data/test_read_csp_list.json
new file mode 100644
index 00000000..475cc45b
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_csp_list.json
@@ -0,0 +1,110 @@
+[
+  {
+    "id": "14",
+    "text": "AGENT DE MAITRISE"
+  },
+  {
+    "id": "1",
+    "text": "AGRICULTEUR"
+  },
+  {
+    "id": "ART",
+    "text": "ARTISAN"
+  },
+  {
+    "id": "2",
+    "text": "ARTISAN-COMMERCANT"
+  },
+  {
+    "id": "15",
+    "text": "AUTRES"
+  },
+  {
+    "id": "CADR",
+    "text": "CADRE"
+  },
+  {
+    "id": "13",
+    "text": "CADRE SUPERIEUR"
+  },
+  {
+    "id": "3",
+    "text": "CHEF D'ENTREPRISE"
+  },
+  {
+    "id": "CHOM",
+    "text": "CHOMEUR"
+  },
+  {
+    "id": "COMM",
+    "text": "COMMERCANT"
+  },
+  {
+    "id": "10",
+    "text": "DEMANDEUR D'EMPLOI"
+  },
+  {
+    "id": "DIV",
+    "text": "DIVERS"
+  },
+  {
+    "id": "5",
+    "text": "EMPLOYE"
+  },
+  {
+    "id": "17",
+    "text": "ENSEIGNANT"
+  },
+  {
+    "id": "8",
+    "text": "ETUDIANT"
+  },
+  {
+    "id": "11",
+    "text": "FONCTIONNAIRE"
+  },
+  {
+    "id": "MAIR",
+    "text": "MAIRIE DE NICE"
+  },
+  {
+    "id": "6",
+    "text": "OUVRIER"
+  },
+  {
+    "id": "PERENS",
+    "text": "PERISCO ENSEIGNANT"
+  },
+  {
+    "id": "PEREXT",
+    "text": "PERISCO EXTERNE"
+  },
+  {
+    "id": "PERMAI",
+    "text": "PERISCO MAIRIE DE NICE"
+  },
+  {
+    "id": "PERANI",
+    "text": "PERISCO S.ANIMATION"
+  },
+  {
+    "id": "LIB",
+    "text": "PROFESSION LIBERALE"
+  },
+  {
+    "id": "12",
+    "text": "RETRAITE"
+  },
+  {
+    "id": "RMI",
+    "text": "REVENU MINIMUM D'INSERTION"
+  },
+  {
+    "id": "SANPRO",
+    "text": "SANS PROFESSION"
+  },
+  {
+    "id": "SEC",
+    "text": "SECRETAIRE"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_dietcode_list.json b/functests/toulouse_maelis/data/test_read_dietcode_list.json
new file mode 100644
index 00000000..602943ce
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_dietcode_list.json
@@ -0,0 +1,34 @@
+[
+  {
+    "id": "BB",
+    "text": "REPAS BEBE"
+  },
+  {
+    "id": "MSP",
+    "text": "REPAS MOYEN SANS PORC"
+  },
+  {
+    "id": "MSV",
+    "text": "REPAS MOYEN SANS VIANDE"
+  },
+  {
+    "id": "MST",
+    "text": "REPAS MOYEN STANDARD"
+  },
+  {
+    "id": "STD",
+    "text": "1- REPAS STANDARD"
+  },
+  {
+    "id": "RSP",
+    "text": "2- R\u00c9GIME SANS PORC"
+  },
+  {
+    "id": "RSV",
+    "text": "3- R\u00c9GIME SANS VIANDE"
+  },
+  {
+    "id": "PAI",
+    "text": "4- PROTOCOLE D'ACCUEIL INDIVIDUALIS\u00c9"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_organ_list.json b/functests/toulouse_maelis/data/test_read_organ_list.json
new file mode 100644
index 00000000..f3eefe74
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_organ_list.json
@@ -0,0 +1,370 @@
+[
+  {
+    "id": "A10004460232",
+    "text": "LEVENS"
+  },
+  {
+    "id": "A10007752822",
+    "text": "LA COLLE SUR LOUP"
+  },
+  {
+    "id": "A10001133770",
+    "text": "ASSIM"
+  },
+  {
+    "id": "A10007751483",
+    "text": "EZE"
+  },
+  {
+    "id": "A10008152785",
+    "text": "LUCERAM"
+  },
+  {
+    "id": "A10008170056",
+    "text": "SAINT ANDRE LE MANOIR"
+  },
+  {
+    "id": "A10008425613",
+    "text": "LA TRINITE ENFANTS PLACES"
+  },
+  {
+    "id": "A10001496896",
+    "text": "MONTJOYE"
+  },
+  {
+    "id": "A10001715627",
+    "text": "FOYER DE L'ENFANCE DES AM"
+  },
+  {
+    "id": "A10007182676",
+    "text": "LA TRINITE"
+  },
+  {
+    "id": "A10010030250",
+    "text": "PUGET SUR ARGENS"
+  },
+  {
+    "id": "A10001481849",
+    "text": "BEAUSOLEIL"
+  },
+  {
+    "id": "A10000980674",
+    "text": "SAINT JEAN CAP FERRAT"
+  },
+  {
+    "id": "A10000980692",
+    "text": "BERRE LES ALPES"
+  },
+  {
+    "id": "A10001314895",
+    "text": "VILLE DE NICE"
+  },
+  {
+    "id": "A10001474792",
+    "text": "PEP 06 - CORNICHE"
+  },
+  {
+    "id": "A10001474782",
+    "text": "PEP 06 MADELEINE"
+  },
+  {
+    "id": "A10007182941",
+    "text": "VALBONNE SOPHIA ANTIPOLIS"
+  },
+  {
+    "id": "A10007182961",
+    "text": "GATTIERES"
+  },
+  {
+    "id": "A10010601009",
+    "text": "FONDATION DE NICE"
+  },
+  {
+    "id": "A10001427675",
+    "text": "CANNES"
+  },
+  {
+    "id": "A10000802907",
+    "text": "VILLEFRANCHE SUR MER"
+  },
+  {
+    "id": "A10000802910",
+    "text": "SAINT ANDRE DE LA ROCHE"
+  },
+  {
+    "id": "A10007751693",
+    "text": "SAINT LAURENT DU VAR"
+  },
+  {
+    "id": "A10007751913",
+    "text": "LA TOUR"
+  },
+  {
+    "id": "A10001039676",
+    "text": "IME CORNICHE FLEURIE"
+  },
+  {
+    "id": "A10001612084",
+    "text": "VENCE"
+  },
+  {
+    "id": "A10000980566",
+    "text": "ANTIBES"
+  },
+  {
+    "id": "A10000980567",
+    "text": "ASPREMONT"
+  },
+  {
+    "id": "A10000980568",
+    "text": "CAGNES SUR MER"
+  },
+  {
+    "id": "A10000980569",
+    "text": "CASTAGNIERS"
+  },
+  {
+    "id": "A10000980570",
+    "text": "CONTES"
+  },
+  {
+    "id": "A10000980571",
+    "text": "FALICON"
+  },
+  {
+    "id": "A10000980572",
+    "text": "GILETTE"
+  },
+  {
+    "id": "A10000980573",
+    "text": "TOUET DE L ESCARENE"
+  },
+  {
+    "id": "A10001715572",
+    "text": "CAP D AIL"
+  },
+  {
+    "id": "A10000980575",
+    "text": "MENTON"
+  },
+  {
+    "id": "A10000980577",
+    "text": "ROQUEBRUNE CAP MARTIN"
+  },
+  {
+    "id": "A10000980584",
+    "text": "VALLAURIS"
+  },
+  {
+    "id": "A10001715581",
+    "text": "TOURRETTE LEVENS"
+  },
+  {
+    "id": "A10004071438",
+    "text": "ASSOCIATION LOU MERILHOUN LEVENS"
+  },
+  {
+    "id": "A10007179260",
+    "text": "BEAULIEU SUR MER"
+  },
+  {
+    "id": "A10000980388",
+    "text": "APAJH"
+  },
+  {
+    "id": "A10007751440",
+    "text": "CARROS"
+  },
+  {
+    "id": "A10007751441",
+    "text": "BLAUSASC"
+  },
+  {
+    "id": "A10000979354",
+    "text": "ATIAM"
+  },
+  {
+    "id": "A10000979370",
+    "text": "UDAF"
+  },
+  {
+    "id": "A10000979372",
+    "text": "ALVA 06"
+  },
+  {
+    "id": "A10000979373",
+    "text": "IES CLEMENT ADER"
+  },
+  {
+    "id": "A10000979374",
+    "text": "ALC L OLIVIER"
+  },
+  {
+    "id": "A10000979375",
+    "text": "IESEDA LES CHANTERELLES"
+  },
+  {
+    "id": "A10000979376",
+    "text": "IME TERRASSES"
+  },
+  {
+    "id": "A10007751879",
+    "text": "SAINT PAUL DE VENCE"
+  },
+  {
+    "id": "A10007753594",
+    "text": "VILLENEUVE LOUBET"
+  },
+  {
+    "id": "A10013129309",
+    "text": "TOURETTES-SUR-LOUP"
+  },
+  {
+    "id": "A10013447888",
+    "text": "COLOMARS"
+  },
+  {
+    "id": "A10015115495",
+    "text": "BREIL SUR ROYA"
+  },
+  {
+    "id": "A10015255908",
+    "text": "GRASSE"
+  },
+  {
+    "id": "A10016957436",
+    "text": "SAINT BLAISE"
+  },
+  {
+    "id": "A10013128210",
+    "text": "BONSON"
+  },
+  {
+    "id": "A10013128217",
+    "text": "CANTARON"
+  },
+  {
+    "id": "A10015126217",
+    "text": "MANDELIEU"
+  },
+  {
+    "id": "A10015528233",
+    "text": "THEOULE SUR MER"
+  },
+  {
+    "id": "A10016401771",
+    "text": "ASCROS"
+  },
+  {
+    "id": "A10016990309",
+    "text": "CANNES LA BOCCA"
+  },
+  {
+    "id": "A10012498607",
+    "text": "LA GAUDE"
+  },
+  {
+    "id": "A10013123067",
+    "text": "DRAP"
+  },
+  {
+    "id": "A10014621894",
+    "text": "MALAUSSENE"
+  },
+  {
+    "id": "A10014922597",
+    "text": "ROQUEFORT LES PINS"
+  },
+  {
+    "id": "A10014927854",
+    "text": "L ESCARENE"
+  },
+  {
+    "id": "A10014938988",
+    "text": "MOUGINS"
+  },
+  {
+    "id": "A10013129048",
+    "text": "SOSPEL"
+  },
+  {
+    "id": "A10013459160",
+    "text": "LE CANNET"
+  },
+  {
+    "id": "A10015677583",
+    "text": "SAINT MARTIN DU VAR"
+  },
+  {
+    "id": "A10017265908",
+    "text": "BIOT"
+  },
+  {
+    "id": "A10000980576",
+    "text": "PEILLON"
+  },
+  {
+    "id": "A10024933344",
+    "text": "LES ARCS"
+  },
+  {
+    "id": "A10024933368",
+    "text": "LA TURBIE"
+  },
+  {
+    "id": "A10024933442",
+    "text": "SAINT JULIEN"
+  },
+  {
+    "id": "A10021971554",
+    "text": "SAINT JEANNET"
+  },
+  {
+    "id": "A10019717953",
+    "text": "PEILLE"
+  },
+  {
+    "id": "g",
+    "text": "REGIME GENERAL"
+  },
+  {
+    "id": "a",
+    "text": "REGIMES AUTRES"
+  },
+  {
+    "id": "a11003995450",
+    "text": "REGIME MSA"
+  },
+  {
+    "id": "a11013011612",
+    "text": "REGIME SNCF"
+  },
+  {
+    "id": "a11017891473",
+    "text": "REGIME GENERAL MONACO"
+  },
+  {
+    "id": "a11015961838",
+    "text": "REGIME EDF/GDF - TITULAIRE"
+  },
+  {
+    "id": "a11015961839",
+    "text": "REGIME MONACO"
+  },
+  {
+    "id": "a11025419517",
+    "text": "TRIBUNAL DE GRANDE INSTANCE DE NICE"
+  },
+  {
+    "id": "A10026769374",
+    "text": "LA ROQUETTE SUR VAR"
+  },
+  {
+    "id": "A10027856948",
+    "text": "BAR-SUR-LOUP"
+  },
+  {
+    "id": "A10029519260",
+    "text": "TOUET SUR VAR"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_pai_list.json b/functests/toulouse_maelis/data/test_read_pai_list.json
new file mode 100644
index 00000000..b952068f
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_pai_list.json
@@ -0,0 +1,18 @@
+[
+  {
+    "id": "PAIALI",
+    "text": "ALIMENTAIRE"
+  },
+  {
+    "id": "PAIO",
+    "text": "AUTRE"
+  },
+  {
+    "id": "PAIMED",
+    "text": "MEDICAL"
+  },
+  {
+    "id": "PAI2",
+    "text": "MEDICAL ET ALIMENTAIRE"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_quality_list.json b/functests/toulouse_maelis/data/test_read_quality_list.json
new file mode 100644
index 00000000..bdb2f12b
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_quality_list.json
@@ -0,0 +1,70 @@
+[
+  {
+    "id": "AU",
+    "text": "AUTRE"
+  },
+  {
+    "id": "BP",
+    "text": "BEAU PERE"
+  },
+  {
+    "id": "BM",
+    "text": "BELLE MERE"
+  },
+  {
+    "id": "CONSO",
+    "text": "CONSOMMATEUR"
+  },
+  {
+    "id": "EN",
+    "text": "ENFANT"
+  },
+  {
+    "id": "FS",
+    "text": "FRERES ET SOEURS"
+  },
+  {
+    "id": "GM",
+    "text": "GRAND MERE MATERNELLE"
+  },
+  {
+    "id": "GP",
+    "text": "GRAND PERE MATERNEL"
+  },
+  {
+    "id": "GMP",
+    "text": "GRAND-MERE PATERNELLE"
+  },
+  {
+    "id": "GPP",
+    "text": "GRAND-PERE PATERNEL"
+  },
+  {
+    "id": "MAIRIE",
+    "text": "MAIRIE"
+  },
+  {
+    "id": "MERE",
+    "text": "MERE"
+  },
+  {
+    "id": "O",
+    "text": "ONCLE"
+  },
+  {
+    "id": "OS",
+    "text": "ORGANISME SOCIAL"
+  },
+  {
+    "id": "PERE",
+    "text": "PERE"
+  },
+  {
+    "id": "T",
+    "text": "TANTE"
+  },
+  {
+    "id": "TUTEUR",
+    "text": "TUTEUR"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_quotient_list.json b/functests/toulouse_maelis/data/test_read_quotient_list.json
new file mode 100644
index 00000000..14b1cfc7
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_quotient_list.json
@@ -0,0 +1,26 @@
+[
+  {
+    "id": "QF",
+    "text": "GRILLE DE QUOTIENT"
+  },
+  {
+    "id": "QCLSH",
+    "text": "QUOTIENT CENTRE AERE"
+  },
+  {
+    "id": "QJEU",
+    "text": "QUOTIENT JEUNESSE"
+  },
+  {
+    "id": "QS",
+    "text": "QUOTIENT SCOLAIRE"
+  },
+  {
+    "id": "QSPORT",
+    "text": "QUOTIENT SPORT"
+  },
+  {
+    "id": "MOY ECO",
+    "text": "REVENU MOYEN ( MENSUEL OU ANNUEL)"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_situation_list.json b/functests/toulouse_maelis/data/test_read_situation_list.json
new file mode 100644
index 00000000..b512eec1
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_situation_list.json
@@ -0,0 +1,38 @@
+[
+  {
+    "id": "C",
+    "text": "C\u00e9libataire"
+  },
+  {
+    "id": "D",
+    "text": "Divorc\u00e9 (e)"
+  },
+  {
+    "id": "CS",
+    "text": "EN COURS DE SEPARATION"
+  },
+  {
+    "id": "M",
+    "text": "Mari\u00e9 (e)"
+  },
+  {
+    "id": "P",
+    "text": "Pacs\u00e9 (e)"
+  },
+  {
+    "id": "S",
+    "text": "S\u00e9par\u00e9 (e)"
+  },
+  {
+    "id": "UL",
+    "text": "UNION LIBRE"
+  },
+  {
+    "id": "V",
+    "text": "Veuf (ve)"
+  },
+  {
+    "id": "VM",
+    "text": "Vivant maritalement"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_read_vaccin_list.json b/functests/toulouse_maelis/data/test_read_vaccin_list.json
new file mode 100644
index 00000000..0be1dca7
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_read_vaccin_list.json
@@ -0,0 +1,82 @@
+[
+  {
+    "id": "BCG",
+    "text": "BCG"
+  },
+  {
+    "id": "CIB",
+    "text": "CONTRE-INDICATION  TEMPORAIRE AU BCG"
+  },
+  {
+    "id": "MONO",
+    "text": "CONTROLE DU BCG (+)"
+  },
+  {
+    "id": "MONON",
+    "text": "CONTROLE DU BCG (-)"
+  },
+  {
+    "id": "DTC",
+    "text": "DIPHTERIE TETANOS COQUELUCHE"
+  },
+  {
+    "id": "DTCP",
+    "text": "DIPHTERIE-TETANOS-COQUELUCHE-POLIO"
+  },
+  {
+    "id": "DTP",
+    "text": "DIPHTERIE-TETANOS-POLIO"
+  },
+  {
+    "id": "HEP",
+    "text": "HEPATITE"
+  },
+  {
+    "id": "HEPA B",
+    "text": "HEPATITE B"
+  },
+  {
+    "id": "IN",
+    "text": "INFANRIX"
+  },
+  {
+    "id": "HIB",
+    "text": "MENINGITE"
+  },
+  {
+    "id": "PENT",
+    "text": "PENTACOQ"
+  },
+  {
+    "id": "PENTH",
+    "text": "PENT'HIBEST"
+  },
+  {
+    "id": "P",
+    "text": "POLIO"
+  },
+  {
+    "id": "ROR",
+    "text": "ROUGEOLE-OREILLONS-RUBEOLE"
+  },
+  {
+    "id": "ROUX",
+    "text": "ROUVAX"
+  },
+  {
+    "id": "TETANOS",
+    "text": "TETANOS"
+  },
+  {
+    "id": "TPOLIO",
+    "text": "TETANOS POLIO"
+  },
+  {
+    "id": "TETR",
+    "text": "TETRACOQ"
+  },
+  {
+    "id": "T.POLIO",
+    "text": "T.POLIO"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_update_child.json b/functests/toulouse_maelis/data/test_update_child.json
new file mode 100644
index 00000000..cdeaa499
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_child.json
@@ -0,0 +1,108 @@
+{
+  "num": "N/A",
+  "lastname": "N/A",
+  "firstname": "BARTOLOME",
+  "sexe": "F",
+  "birth": {
+    "dateBirth": "1970-01-01T00:00:00+01:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "dietcode": "STD",
+  "bPhoto": false,
+  "bLeaveAlone": false,
+  "authorizedPersonList": [
+    {
+      "personInfo": {
+        "num": "N/A",
+        "lastname": "SIMPSON",
+        "firstname": "ABRAHAM JEBEDIAH",
+        "dateBirth": "1927-05-24T00:00:00+01:00",
+        "civility": "M.",
+        "sexe": "M",
+        "contact": {
+          "phone": "0312345678",
+          "mobile": null,
+          "mail": "abe.simpson@example.org"
+        },
+        "civility_text": "Monsieur",
+        "sexe_text": "Masculin"
+      },
+      "personQuality": {
+        "code": "GPP",
+        "libelle": "GRAND-PERE PATERNEL",
+        "code_text": "GRAND-PERE PATERNEL"
+      }
+    },
+    {
+      "personInfo": {
+        "num": "N/A",
+        "lastname": "SIMPSON",
+        "firstname": "MONA PENELOPE",
+        "dateBirth": "1929-03-15T00:00:00Z",
+        "civility": "MME",
+        "sexe": "F",
+        "contact": {
+          "phone": "0412345678",
+          "mobile": "0612345678",
+          "mail": "mona.simpson@example.org"
+        },
+        "civility_text": "Madame",
+        "sexe_text": "F\u00e9minin"
+      },
+      "personQuality": {
+        "code": "GMP",
+        "libelle": "GRAND-MERE PATERNELLE",
+        "code_text": "GRAND-MERE PATERNELLE"
+      }
+    }
+  ],
+  "indicatorList": [],
+  "medicalRecord": {
+    "familyDoctor": {
+      "name": "MONROE",
+      "phone": "0612341234",
+      "address": {
+        "street1": "Alameda",
+        "zipcode": "90701",
+        "town": "Springfield"
+      }
+    },
+    "allergy1": "butterscotch, imitation butterscotch, glow-in-the-dark monster make-up",
+    "allergy2": "shrimp and cauliflower",
+    "comment1": "the shrimp allergy isn't fully identified",
+    "comment2": null,
+    "observ1": "Ay Caramba!",
+    "observ2": "Ay Caramba!",
+    "isAuthHospital": true,
+    "hospital": "Springfield General Hospital",
+    "vaccinList": [
+      {
+        "code": "ROR",
+        "label": "ROUGEOLE-OREILLONS-RUBEOLE",
+        "vaccinationDate": "2022-02-22T00:00:00+01:00"
+      },
+      {
+        "code": "DTC",
+        "label": "DIPHTERIE TETANOS COQUELUCHE",
+        "vaccinationDate": "2011-01-11T00:00:00+01:00"
+      }
+    ]
+  },
+  "insurance": null,
+  "paiInfoBean": {
+    "code": "PAIMED",
+    "dateDeb": "2022-09-01T00:00:00+02:00",
+    "dateFin": "2023-07-01T00:00:00+02:00",
+    "description": "mischievous, rebellious, misunderstood, disruptive",
+    "code_text": "MEDICAL"
+  },
+  "mother": "N/A",
+  "father": "N/A",
+  "rl": null,
+  "subscribeSchoolList": [],
+  "subscribeActivityList": [],
+  "sexe_text": "F\u00e9minin",
+  "dietcode_text": "1- REPAS STANDARD"
+}
diff --git a/functests/toulouse_maelis/data/test_update_child_medical_record.json b/functests/toulouse_maelis/data/test_update_child_medical_record.json
new file mode 100644
index 00000000..7993c70d
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_child_medical_record.json
@@ -0,0 +1,23 @@
+{
+  "familyDoctor": null,
+  "allergy1": null,
+  "allergy2": null,
+  "comment1": null,
+  "comment2": null,
+  "observ1": null,
+  "observ2": null,
+  "isAuthHospital": false,
+  "hospital": null,
+  "vaccinList": [
+    {
+      "code": "ROR",
+      "label": "ROUGEOLE-OREILLONS-RUBEOLE",
+      "vaccinationDate": "2022-02-22T00:00:00+01:00"
+    },
+    {
+      "code": "DTC",
+      "label": "DIPHTERIE TETANOS COQUELUCHE",
+      "vaccinationDate": "2011-01-11T00:00:00+01:00"
+    }
+  ]
+}
diff --git a/functests/toulouse_maelis/data/test_update_child_pai.json b/functests/toulouse_maelis/data/test_update_child_pai.json
new file mode 100644
index 00000000..c9326f37
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_child_pai.json
@@ -0,0 +1,7 @@
+{
+  "code": "PAIO",
+  "dateDeb": "1970-01-01T00:00:00+01:00",
+  "dateFin": "1970-01-01T00:00:00+01:00",
+  "description": null,
+  "code_text": "AUTRE"
+}
diff --git a/functests/toulouse_maelis/data/test_update_child_person.json b/functests/toulouse_maelis/data/test_update_child_person.json
new file mode 100644
index 00000000..21edaf98
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_child_person.json
@@ -0,0 +1,41 @@
+[
+  {
+    "personInfo": {
+      "num": "N/A",
+      "lastname": "RESET",
+      "firstname": "RESET",
+      "dateBirth": "1970-01-01T00:00:00+01:00",
+      "civility": null,
+      "sexe": "M",
+      "contact": null,
+      "sexe_text": "Masculin"
+    },
+    "personQuality": {
+      "code": "AU",
+      "libelle": "AUTRE",
+      "code_text": "AUTRE"
+    }
+  },
+  {
+    "personInfo": {
+      "num": "N/A",
+      "lastname": "SIMPSON",
+      "firstname": "MONA PENELOPE",
+      "dateBirth": "1929-03-15T00:00:00Z",
+      "civility": "MME",
+      "sexe": "F",
+      "contact": {
+        "phone": "0412345678",
+        "mobile": "0612345678",
+        "mail": "mona.simpson@example.org"
+      },
+      "civility_text": "Madame",
+      "sexe_text": "F\u00e9minin"
+    },
+    "personQuality": {
+      "code": "GMP",
+      "libelle": "GRAND-MERE PATERNELLE",
+      "code_text": "GRAND-MERE PATERNELLE"
+    }
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_update_coordinate.json b/functests/toulouse_maelis/data/test_update_coordinate.json
new file mode 100644
index 00000000..bc5b0040
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_coordinate.json
@@ -0,0 +1,54 @@
+{
+  "num": "N/A",
+  "firstname": "HOMER",
+  "lastname": "N/A",
+  "maidenName": null,
+  "quality": "PERE",
+  "civility": "M.",
+  "birth": {
+    "dateBirth": "1956-05-12T00:00:00+01:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "adresse": {
+    "idStreet": null,
+    "num": 42,
+    "numComp": "Q",
+    "street1": "reset",
+    "street2": null,
+    "town": "reset",
+    "zipcode": "reset",
+    "numComp_text": "quater"
+  },
+  "contact": {
+    "phone": null,
+    "mobile": null,
+    "mail": null,
+    "isContactMail": false,
+    "isContactSms": false,
+    "isInvoicePdf": false
+  },
+  "profession": {
+    "codeCSP": "15",
+    "profession": null,
+    "employerName": null,
+    "phone": null,
+    "addressPro": {
+      "num": 42,
+      "street": null,
+      "zipcode": null,
+      "town": null
+    },
+    "codeCSP_text": "AUTRES"
+  },
+  "CAFInfo": {
+    "number": "reset",
+    "organ": null
+  },
+  "indicatorList": [],
+  "quotientList": [],
+  "subscribeActivityList": [],
+  "civility_text": "Monsieur",
+  "quality_text": "PERE"
+}
diff --git a/functests/toulouse_maelis/data/test_update_family.json b/functests/toulouse_maelis/data/test_update_family.json
new file mode 100644
index 00000000..c52696af
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_family.json
@@ -0,0 +1,318 @@
+{
+  "number": "N/A",
+  "category": "BI",
+  "situation": "VM",
+  "flagCom": true,
+  "nbChild": null,
+  "nbTotalChild": null,
+  "nbAES": null,
+  "RL1": {
+    "num": "N/A",
+    "firstname": "MARGE",
+    "lastname": "N/A",
+    "maidenName": "BOUVIER",
+    "quality": "MERE",
+    "civility": "MME",
+    "birth": {
+      "dateBirth": "1950-10-01T00:00:00+01:00",
+      "place": null,
+      "communeCode": null,
+      "countryCode": null
+    },
+    "adresse": {
+      "idStreet": null,
+      "num": 0,
+      "numComp": null,
+      "street1": "Evergreen Terrace",
+      "street2": null,
+      "town": "Springfield",
+      "zipcode": "62701"
+    },
+    "contact": {
+      "phone": null,
+      "mobile": null,
+      "mail": null,
+      "isContactMail": false,
+      "isContactSms": false,
+      "isInvoicePdf": false
+    },
+    "profession": null,
+    "CAFInfo": null,
+    "indicatorList": [],
+    "quotientList": [],
+    "subscribeActivityList": [],
+    "civility_text": "Madame",
+    "quality_text": "MERE"
+  },
+  "RL2": {
+    "num": "N/A",
+    "firstname": "HOMER",
+    "lastname": "N/A",
+    "maidenName": null,
+    "quality": "PERE",
+    "civility": "M.",
+    "birth": {
+      "dateBirth": "1956-05-12T00:00:00+01:00",
+      "place": null,
+      "communeCode": null,
+      "countryCode": null
+    },
+    "adresse": {
+      "idStreet": null,
+      "num": 742,
+      "numComp": null,
+      "street1": "Evergreen Terrace",
+      "street2": null,
+      "town": "Springfield",
+      "zipcode": "90701"
+    },
+    "contact": {
+      "phone": "0122222222",
+      "mobile": "0622222222",
+      "mail": "homer.simpson@example.org.com",
+      "isContactMail": true,
+      "isContactSms": true,
+      "isInvoicePdf": true
+    },
+    "profession": {
+      "codeCSP": "13",
+      "profession": "Inspecteur de s\u00e9curit\u00e9",
+      "employerName": "Burns",
+      "phone": "0133333333",
+      "addressPro": {
+        "num": null,
+        "street": null,
+        "zipcode": "90701",
+        "town": "Springfield"
+      },
+      "codeCSP_text": "CADRE SUPERIEUR"
+    },
+    "CAFInfo": {
+      "number": "123",
+      "organ": "A10007752822",
+      "organ_text": "LA COLLE SUR LOUP"
+    },
+    "indicatorList": [],
+    "quotientList": [],
+    "subscribeActivityList": [],
+    "civility_text": "Monsieur",
+    "quality_text": "PERE"
+  },
+  "quotientList": [],
+  "childList": [
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "BART",
+      "sexe": "M",
+      "birth": {
+        "dateBirth": "1978-04-01T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "STD",
+      "bPhoto": true,
+      "bLeaveAlone": true,
+      "authorizedPersonList": [
+        {
+          "personInfo": {
+            "num": "N/A",
+            "lastname": "SIMPSON",
+            "firstname": "ABRAHAM JEBEDIAH",
+            "dateBirth": "1927-05-24T00:00:00+01:00",
+            "civility": "M.",
+            "sexe": "M",
+            "contact": {
+              "phone": "0312345678",
+              "mobile": null,
+              "mail": "abe.simpson@example.org"
+            },
+            "civility_text": "Monsieur",
+            "sexe_text": "Masculin"
+          },
+          "personQuality": {
+            "code": "GPP",
+            "libelle": "GRAND-PERE PATERNEL",
+            "code_text": "GRAND-PERE PATERNEL"
+          }
+        },
+        {
+          "personInfo": {
+            "num": "N/A",
+            "lastname": "SIMPSON",
+            "firstname": "MONA PENELOPE",
+            "dateBirth": "1929-03-15T00:00:00Z",
+            "civility": "MME",
+            "sexe": "F",
+            "contact": {
+              "phone": "0412345678",
+              "mobile": "0612345678",
+              "mail": "mona.simpson@example.org"
+            },
+            "civility_text": "Madame",
+            "sexe_text": "F\u00e9minin"
+          },
+          "personQuality": {
+            "code": "GMP",
+            "libelle": "GRAND-MERE PATERNELLE",
+            "code_text": "GRAND-MERE PATERNELLE"
+          }
+        }
+      ],
+      "indicatorList": [],
+      "medicalRecord": {
+        "familyDoctor": {
+          "name": "MONROE",
+          "phone": "0612341234",
+          "address": {
+            "street1": "Alameda",
+            "zipcode": "90701",
+            "town": "Springfield"
+          }
+        },
+        "allergy1": "butterscotch, imitation butterscotch, glow-in-the-dark monster make-up",
+        "allergy2": "shrimp and cauliflower",
+        "comment1": "the shrimp allergy isn't fully identified",
+        "comment2": null,
+        "observ1": "Ay Caramba!",
+        "observ2": "Ay Caramba!",
+        "isAuthHospital": true,
+        "hospital": "Springfield General Hospital",
+        "vaccinList": [
+          {
+            "code": "ROR",
+            "label": "ROUGEOLE-OREILLONS-RUBEOLE",
+            "vaccinationDate": "2022-02-22T00:00:00+01:00"
+          },
+          {
+            "code": "DTC",
+            "label": "DIPHTERIE TETANOS COQUELUCHE",
+            "vaccinationDate": "2011-01-11T00:00:00+01:00"
+          }
+        ]
+      },
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIMED",
+        "dateDeb": "2022-09-01T00:00:00+02:00",
+        "dateFin": "2023-07-01T00:00:00+02:00",
+        "description": "mischievous, rebellious, misunderstood, disruptive",
+        "code_text": "MEDICAL"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "Masculin",
+      "dietcode_text": "1- REPAS STANDARD"
+    },
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "LISA",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1980-05-09T00:00:00+02:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "RSV",
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [],
+      "indicatorList": [],
+      "medicalRecord": null,
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIALI",
+        "dateDeb": null,
+        "dateFin": null,
+        "description": null,
+        "code_text": "ALIMENTAIRE"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin",
+      "dietcode_text": "3- R\u00c9GIME SANS VIANDE"
+    },
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "MAGGIE",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1989-12-17T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "BB",
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [],
+      "indicatorList": [],
+      "medicalRecord": null,
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIALI",
+        "dateDeb": null,
+        "dateFin": null,
+        "description": null,
+        "code_text": "ALIMENTAIRE"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin",
+      "dietcode_text": "REPAS BEBE"
+    }
+  ],
+  "emergencyPersonList": [
+    {
+      "numPerson": "N/A",
+      "civility": "MME",
+      "firstname": "PATTY",
+      "lastname": "BOUVIER",
+      "dateBirth": "1948-08-30T00:00:00+01:00",
+      "sexe": "F",
+      "quality": "T",
+      "contact": {
+        "phone": "0112345678",
+        "mobile": "0612345678",
+        "mail": "patty.bouvier@example.org"
+      },
+      "civility_text": "Madame",
+      "quality_text": "TANTE",
+      "sexe_text": "F\u00e9minin"
+    },
+    {
+      "numPerson": "N/A",
+      "civility": "MME",
+      "firstname": "SELMA",
+      "lastname": "BOUVIER",
+      "dateBirth": "1946-04-29T00:00:00+01:00",
+      "sexe": "F",
+      "quality": "OS",
+      "contact": {
+        "phone": "0112345678",
+        "mobile": "0612345678",
+        "mail": "selma.bouvier@example.org"
+      },
+      "civility_text": "Madame",
+      "quality_text": "ORGANISME SOCIAL",
+      "sexe_text": "F\u00e9minin"
+    }
+  ],
+  "indicatorList": [],
+  "childErrorList": [],
+  "category_text": "BIPARENTALE",
+  "situation_text": "Vivant maritalement"
+}
diff --git a/functests/toulouse_maelis/data/test_update_family_ras.json b/functests/toulouse_maelis/data/test_update_family_ras.json
new file mode 100644
index 00000000..8e943b4c
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_family_ras.json
@@ -0,0 +1,260 @@
+{
+  "number": "N/A",
+  "category": "MONO",
+  "situation": "UL",
+  "flagCom": true,
+  "nbChild": null,
+  "nbTotalChild": null,
+  "nbAES": null,
+  "RL1": {
+    "num": "N/A",
+    "firstname": "MARGE",
+    "lastname": "N/A",
+    "maidenName": "BOUVIER",
+    "quality": "AU",
+    "civility": "MME",
+    "birth": {
+      "dateBirth": "1950-10-01T00:00:00+01:00",
+      "place": null,
+      "communeCode": null,
+      "countryCode": null
+    },
+    "adresse": {
+      "idStreet": null,
+      "num": 0,
+      "numComp": null,
+      "street1": "reset",
+      "street2": null,
+      "town": "reset",
+      "zipcode": "reset"
+    },
+    "contact": {
+      "phone": null,
+      "mobile": null,
+      "mail": null,
+      "isContactMail": false,
+      "isContactSms": false,
+      "isInvoicePdf": false
+    },
+    "profession": null,
+    "CAFInfo": null,
+    "indicatorList": [],
+    "quotientList": [],
+    "subscribeActivityList": [],
+    "civility_text": "Madame",
+    "quality_text": "AUTRE"
+  },
+  "RL2": {
+    "num": "N/A",
+    "firstname": "HOMER",
+    "lastname": "N/A",
+    "maidenName": null,
+    "quality": "AU",
+    "civility": "M.",
+    "birth": {
+      "dateBirth": "1956-05-12T00:00:00+01:00",
+      "place": null,
+      "communeCode": null,
+      "countryCode": null
+    },
+    "adresse": {
+      "idStreet": null,
+      "num": 42,
+      "numComp": "Q",
+      "street1": "reset",
+      "street2": null,
+      "town": "reset",
+      "zipcode": "reset",
+      "numComp_text": "quater"
+    },
+    "contact": {
+      "phone": null,
+      "mobile": null,
+      "mail": null,
+      "isContactMail": false,
+      "isContactSms": false,
+      "isInvoicePdf": false
+    },
+    "profession": {
+      "codeCSP": "15",
+      "profession": null,
+      "employerName": null,
+      "phone": null,
+      "addressPro": {
+        "num": 42,
+        "street": null,
+        "zipcode": null,
+        "town": null
+      },
+      "codeCSP_text": "AUTRES"
+    },
+    "CAFInfo": {
+      "number": "reset",
+      "organ": null
+    },
+    "indicatorList": [],
+    "quotientList": [],
+    "subscribeActivityList": [],
+    "civility_text": "Monsieur",
+    "quality_text": "AUTRE"
+  },
+  "quotientList": [],
+  "childList": [
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "BARTOLOME",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1970-01-01T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": null,
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [
+        {
+          "personInfo": {
+            "num": "N/A",
+            "lastname": "RESET",
+            "firstname": "RESET",
+            "dateBirth": "1970-01-01T00:00:00+01:00",
+            "civility": null,
+            "sexe": "M",
+            "contact": null,
+            "sexe_text": "Masculin"
+          },
+          "personQuality": {
+            "code": "AU",
+            "libelle": "AUTRE",
+            "code_text": "AUTRE"
+          }
+        }
+      ],
+      "indicatorList": [],
+      "medicalRecord": {
+        "familyDoctor": null,
+        "allergy1": null,
+        "allergy2": null,
+        "comment1": null,
+        "comment2": null,
+        "observ1": null,
+        "observ2": null,
+        "isAuthHospital": false,
+        "hospital": null,
+        "vaccinList": [
+          {
+            "code": "ROR",
+            "label": "ROUGEOLE-OREILLONS-RUBEOLE",
+            "vaccinationDate": "2022-02-22T00:00:00+01:00"
+          },
+          {
+            "code": "DTC",
+            "label": "DIPHTERIE TETANOS COQUELUCHE",
+            "vaccinationDate": "2011-01-11T00:00:00+01:00"
+          }
+        ]
+      },
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIO",
+        "dateDeb": "1970-01-01T00:00:00+01:00",
+        "dateFin": "1970-01-01T00:00:00+01:00",
+        "description": null,
+        "code_text": "AUTRE"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin"
+    },
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "LISA",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1980-05-09T00:00:00+02:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "RSV",
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [],
+      "indicatorList": [],
+      "medicalRecord": null,
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIALI",
+        "dateDeb": null,
+        "dateFin": null,
+        "description": null,
+        "code_text": "ALIMENTAIRE"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin",
+      "dietcode_text": "3- R\u00c9GIME SANS VIANDE"
+    },
+    {
+      "num": "N/A",
+      "lastname": "N/A",
+      "firstname": "MAGGIE",
+      "sexe": "F",
+      "birth": {
+        "dateBirth": "1989-12-17T00:00:00+01:00",
+        "place": null,
+        "communeCode": null,
+        "countryCode": null
+      },
+      "dietcode": "BB",
+      "bPhoto": false,
+      "bLeaveAlone": false,
+      "authorizedPersonList": [],
+      "indicatorList": [],
+      "medicalRecord": null,
+      "insurance": null,
+      "paiInfoBean": {
+        "code": "PAIALI",
+        "dateDeb": null,
+        "dateFin": null,
+        "description": null,
+        "code_text": "ALIMENTAIRE"
+      },
+      "mother": "N/A",
+      "father": "N/A",
+      "rl": null,
+      "subscribeSchoolList": [],
+      "subscribeActivityList": [],
+      "sexe_text": "F\u00e9minin",
+      "dietcode_text": "REPAS BEBE"
+    }
+  ],
+  "emergencyPersonList": [
+    {
+      "numPerson": "N/A",
+      "civility": null,
+      "firstname": "RESET",
+      "lastname": "RESET",
+      "dateBirth": "1970-01-01T00:00:00+01:00",
+      "sexe": "M",
+      "quality": "AU",
+      "contact": null,
+      "quality_text": "AUTRE",
+      "sexe_text": "Masculin"
+    }
+  ],
+  "indicatorList": [],
+  "childErrorList": [],
+  "category_text": "MONOPARENTALE",
+  "situation_text": "UNION LIBRE"
+}
diff --git a/functests/toulouse_maelis/data/test_update_person.json b/functests/toulouse_maelis/data/test_update_person.json
new file mode 100644
index 00000000..96fae7c8
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_person.json
@@ -0,0 +1,31 @@
+[
+  {
+    "numPerson": "N/A",
+    "civility": "MME",
+    "firstname": "PATTY",
+    "lastname": "BOUVIER",
+    "dateBirth": "1948-08-30T00:00:00+01:00",
+    "sexe": "F",
+    "quality": "T",
+    "contact": {
+      "phone": "0112345678",
+      "mobile": "0612345678",
+      "mail": "patty.bouvier@example.org"
+    },
+    "civility_text": "Madame",
+    "quality_text": "TANTE",
+    "sexe_text": "F\u00e9minin"
+  },
+  {
+    "numPerson": "N/A",
+    "civility": null,
+    "firstname": "RESET",
+    "lastname": "RESET",
+    "dateBirth": "1970-01-01T00:00:00+01:00",
+    "sexe": "M",
+    "quality": "AU",
+    "contact": null,
+    "quality_text": "AUTRE",
+    "sexe_text": "Masculin"
+  }
+]
diff --git a/functests/toulouse_maelis/data/test_update_rl1.json b/functests/toulouse_maelis/data/test_update_rl1.json
new file mode 100644
index 00000000..5b216b43
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_rl1.json
@@ -0,0 +1,38 @@
+{
+  "num": "N/A",
+  "firstname": "MARGE",
+  "lastname": "N/A",
+  "maidenName": "BOUVIER",
+  "quality": "AU",
+  "civility": "MME",
+  "birth": {
+    "dateBirth": "1950-10-01T00:00:00+01:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "adresse": {
+    "idStreet": null,
+    "num": 0,
+    "numComp": null,
+    "street1": "Evergreen Terrace",
+    "street2": null,
+    "town": "Springfield",
+    "zipcode": "62701"
+  },
+  "contact": {
+    "phone": null,
+    "mobile": null,
+    "mail": null,
+    "isContactMail": false,
+    "isContactSms": false,
+    "isInvoicePdf": false
+  },
+  "profession": null,
+  "CAFInfo": null,
+  "indicatorList": [],
+  "quotientList": [],
+  "subscribeActivityList": [],
+  "civility_text": "Madame",
+  "quality_text": "AUTRE"
+}
diff --git a/functests/toulouse_maelis/data/test_update_rl2.json b/functests/toulouse_maelis/data/test_update_rl2.json
new file mode 100644
index 00000000..da100193
--- /dev/null
+++ b/functests/toulouse_maelis/data/test_update_rl2.json
@@ -0,0 +1,54 @@
+{
+  "num": "N/A",
+  "firstname": "HOMER",
+  "lastname": "N/A",
+  "maidenName": null,
+  "quality": "AU",
+  "civility": "M.",
+  "birth": {
+    "dateBirth": "1956-05-12T00:00:00+01:00",
+    "place": null,
+    "communeCode": null,
+    "countryCode": null
+  },
+  "adresse": {
+    "idStreet": null,
+    "num": 742,
+    "numComp": null,
+    "street1": "Evergreen Terrace",
+    "street2": null,
+    "town": "Springfield",
+    "zipcode": "90701"
+  },
+  "contact": {
+    "phone": "0122222222",
+    "mobile": "0622222222",
+    "mail": "homer.simpson@example.org.com",
+    "isContactMail": true,
+    "isContactSms": true,
+    "isInvoicePdf": true
+  },
+  "profession": {
+    "codeCSP": "13",
+    "profession": "Inspecteur de s\u00e9curit\u00e9",
+    "employerName": "Burns",
+    "phone": "0133333333",
+    "addressPro": {
+      "num": null,
+      "street": null,
+      "zipcode": "90701",
+      "town": "Springfield"
+    },
+    "codeCSP_text": "CADRE SUPERIEUR"
+  },
+  "CAFInfo": {
+    "number": "123",
+    "organ": "A10007752822",
+    "organ_text": "LA COLLE SUR LOUP"
+  },
+  "indicatorList": [],
+  "quotientList": [],
+  "subscribeActivityList": [],
+  "civility_text": "Monsieur",
+  "quality_text": "AUTRE"
+}
diff --git a/functests/toulouse_maelis/read_family.py b/functests/toulouse_maelis/read_family.py
new file mode 100755
index 00000000..7ab6ae1e
--- /dev/null
+++ b/functests/toulouse_maelis/read_family.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python3
+
+import argparse
+
+import requests
+import zeep
+from zeep.transports import Transport
+from zeep.wsse.username import UsernameToken
+
+WSSE = UsernameToken('maelis-webservice', 'maelis-password')
+WSDL_URL = 'https://demo-toulouse.sigec.fr/maelisws-toulouse/services/FamilyService?wsdl'
+
+
+def read_family(family_id, verbose):
+    session = requests.Session()
+    session.verify = False
+    transport = Transport(session=session)
+    settings = zeep.Settings(strict=False, xsd_ignore_sequence_order=True)
+    client = zeep.Client(WSDL_URL, transport=transport, wsse=WSSE, settings=settings)
+
+    result = client.service.readFamily(
+        dossierNumber=family_id,
+        # schoolYear=
+        # incomeYear=2020,  # <-- pour avoir les quotients
+        # referenceYear=2020,
+    )
+    print(result)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--verbose', '-v', type=int, default=2, help='display errors')
+    parser.add_argument('family_id', help='196544', nargs='?', default='196544')
+    args = parser.parse_args()
+    read_family(args.family_id, verbose=args.verbose)
diff --git a/functests/toulouse_maelis/test_family.py b/functests/toulouse_maelis/test_family.py
new file mode 100644
index 00000000..9713cc4f
--- /dev/null
+++ b/functests/toulouse_maelis/test_family.py
@@ -0,0 +1,693 @@
+import copy
+
+import pytest
+import requests
+
+from .conftest import diff, diff_child, diff_family, diff_rlg, link, read_family, unlink
+
+FAMILY_RESET_PAYLOAD = {
+    'category': 'MONO',
+    'situation': 'UL',
+    'rl1': {
+        'civility': 'M.',  # no effect
+        'firstname': 'Marge',  # must be
+        'lastname': 'Simpson',  # must be
+        'maidenName': 'reset',  # no effect
+        'quality': 'AU',
+        'birth': {'dateBirth': '1950-10-01'},  # must be
+        'adresse': {'street1': 'reset', 'town': 'reset', 'zipcode': 'reset'},
+    },
+    'rl2': {
+        'civility': 'MME',  # no effect
+        'firstname': 'Homer',  # must be
+        'lastname': 'Simpson',  # must be
+        'quality': 'AU',
+        'birth': {'dateBirth': '1956-05-12'},  # must be
+        'adresse': {
+            'num': '42',
+            'numComp': 'Q',
+            'street1': 'reset',  # E19 : La voie est obligatoire
+            'street2': '',
+            'town': 'reset',  # E17 : Le nom de la commune est obligatoire'
+            'zipcode': 'reset',  # E16 : Le code postal est obligatoire
+        },
+        'contact': {  # contact is removed (set to None) by the bellow content
+            'isContactMail': False,
+            'isContactSms': False,
+            'isInvoicePdf': False,
+            'mail': '',
+            'mobile': '',
+            'phone': '',
+        },
+        'profession': {
+            'addressPro': {'num': '42', 'street': '', 'town': '', 'zipcode': ''},
+            'codeCSP': '15',
+            'employerName': '',
+            'phone': '',
+            'profession': '',
+        },
+        'CAFInfo': {
+            'number': 'reset',  # cannot be removed
+            'organ': '',
+        },
+    },
+    'childList': [
+        {
+            'num': 'place holder',  # required for update
+            'sexe': 'F',
+            'firstname': 'Bartolome',  # some side effects, cf test_update_child
+            'lastname': 'Simps',
+            'birth': {'dateBirth': '1970-01-01'},
+            'bPhoto': False,
+            'bLeaveAlone': False,
+            'dietcode': '',
+            'paiInfoBean': {
+                'code': 'PAIO',
+                'dateDeb': '1970-01-01',
+                'dateFin': '1970-01-01',
+                'description': '',
+            },
+            'medicalRecord': {
+                'familyDoctor': {  # familyDoctor is removed (set to None) by the bellow content
+                    'name': '',
+                    'phone': '',
+                    'address': {
+                        'street1': '',
+                        'zipcode': '',
+                        'town': '',
+                    },
+                },
+                'allergy1': '',
+                'allergy2': '',
+                'comment1': '',
+                'comment2': '',
+                'observ1': '',
+                'observ2': '',
+                'isAuthHospital': False,
+                'hospital': '',
+                'vaccinList': [],
+            },
+            # setting authorizedPersonList to None will remove the list
+            'authorizedPersonList': [
+                {
+                    'personInfo': {
+                        'civility': None,
+                        'firstname': 'reset',  # E704 : Le prénom de la personne est obligatoire
+                        'lastname': 'reset',  # E705 : Le nom de la personne est obligatoire
+                        'dateBirth': '1970-01-01',
+                        'sexe': 'M',
+                        'contact': {  # contact is removed (set to None) by the bellow content
+                            'phone': '',
+                            'mobile': '',
+                            'mail': '',
+                        },
+                    },
+                    'personQuality': {
+                        'code': 'AU',
+                    },
+                },
+            ],
+        },
+    ],
+    # setting emergencyPersonList to None will keep the list
+    'emergencyPersonList': [
+        {
+            'civility': None,
+            'firstname': 'reset',  # ORA-01400: impossible d'insérer NULL dans ("MAELIS"."H_PERS"."ER_PNOM")
+            'lastname': 'reset',  # ORA-01400: impossible d'insérer NULL dans ("MAELIS"."H_PERS"."ER_NOM")
+            'dateBirth': '1970-01-01',
+            'sexe': 'M',
+            'quality': 'AU',
+            'contact': {
+                'phone': '',
+                'mobile': '',
+                'mail': '',
+            },
+        },
+    ],
+}
+
+
+# @pytest.mark.xfail(run=False)
+@pytest.mark.parametrize(
+    "ref",
+    [
+        'category',
+        'civility',  # 'country', 'county',
+        'csp',
+        'dietcode',
+        'organ',
+        'pai',
+        'quality',
+        'quotient',
+        'situation',
+        # 'street','town',
+        'vaccin',
+    ],
+)
+def test_referentials(conn, ref):
+    url = conn + '/read-%s-list' % ref
+    resp = requests.get(url)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
+    assert len(res['data']) > 1
+    for item in res['data']:
+        assert 'id' in item
+        assert 'text' in item
+    assert diff(res['data'], 'test_read_%s_list.json' % ref)
+
+
+def test_update_family(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    url = conn + '/update-family?NameID=%s' % update_data['name_id']
+
+    # reset fields
+    family_reset_payload = copy.deepcopy(FAMILY_RESET_PAYLOAD)
+    family_reset_payload['childList'][0]['num'] = update_data['family_payload']['childList'][0]['num']
+    resp = requests.post(url, json=family_reset_payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = diff_family(conn, update_data['name_id'], 'test_update_family_ras.json')
+
+    # unchanged values
+    assert data['RL1']['civility'] != family_reset_payload['rl1']['civility']
+    assert data['RL2']['civility'] != family_reset_payload['rl2']['civility']
+    assert data['RL1']['maidenName'] != family_reset_payload['rl1']['maidenName']
+
+    # changed values
+    assert data['RL1']['quality'] == family_reset_payload['rl1']['quality']
+    assert data['RL2']['quality'] == family_reset_payload['rl2']['quality']
+
+    # without passing emergencyPersonList the list is keept
+    assert len(data['emergencyPersonList']) == 1
+    del family_reset_payload['emergencyPersonList']
+    resp = requests.post(url, json=family_reset_payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert len(data['emergencyPersonList']) == 1
+
+    # without passing authorizedPersonList the list is removed
+    assert len(data['childList'][0]['authorizedPersonList']) == 1
+    del family_reset_payload['childList'][0]['authorizedPersonList']
+    resp = requests.post(url, json=family_reset_payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][0]['authorizedPersonList'] == []
+
+    # update root fields
+    payload = {
+        'category': 'BI',
+        'situation': 'VM',
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['category'] == 'BI'
+    assert data['situation'] == 'VM'
+    assert data['category_text'] == 'BIPARENTALE'
+    assert data['situation_text'] == 'Vivant maritalement'
+
+    # restore family
+    resp = requests.post(url, json=update_data['family_payload'])
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_create_family(conn, create_data, update_data):
+    unlink(conn, create_data['name_id'])
+    link(conn, create_data)
+    url = conn + '/create-family?NameID=%s' % create_data['name_id']
+
+    # RL1 already exists (on update_data) error
+    unlink(conn, create_data['name_id'])
+    payload = copy.deepcopy(create_data['family_payload'])
+    payload['rl1']['lastname'] = 'Simpson'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert 'E54a' in res['err']
+    assert 'Il existe déjà un Responsable Légal correspondant' in res['err_desc']
+    assert res['err_class'] == 'passerelle.utils.jsonresponse.APIError'
+
+    # RL1 already exists (on update_data, as RL2) error
+    payload['rl1']['firstname'] = 'Homer'
+    payload['rl1']['birth']['dateBirth'] = '1956-05-12'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert 'E54a' in res['err']
+    assert 'Il existe déjà un Responsable Légal correspondant' in res['err_desc']
+    assert res['err_class'] == 'passerelle.utils.jsonresponse.APIError'
+
+
+def test_is_rl_exists(conn, update_data):
+    url = conn + '/is-rl-exists'
+    payload = {'firstname': 'Marge', 'lastname': 'Simpson', 'dateBirth': '1950-10-01'}
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': True}
+
+    payload['firstname'] = payload['firstname'].upper()
+    payload['lastname'] = payload['lastname'].lower()
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': True}
+
+    payload['dateBirth'] = '1970-01-01'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': False}
+
+    # test on rl2
+    payload = {'firstname': 'Homer', 'lastname': 'Simpson', 'dateBirth': '1956-05-12'}
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': True}
+
+
+def test_create_rl2(conn, create_data, update_data):
+    unlink(conn, create_data['name_id'])
+    link(conn, create_data)
+
+    data = read_family(conn, create_data['name_id'])
+    assert data['RL2'] is None
+
+    # no unicity restriction on RL2 (duplicate RL2 from update_data)
+    url = conn + '/create-rl2?NameID=%s' % create_data['name_id']
+    payload = copy.deepcopy(update_data['family_payload']['rl2'])
+    for key in 'contact', 'profession', 'CAFInfo':
+        del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_rlg(conn, create_data['name_id'], 2, 'test_create_rl2.json')
+
+
+@pytest.mark.parametrize("rl", ['1', '2'])
+def test_update_rlg(conn, update_data, rl):
+    rlg = 'rl' + rl
+    RLG = 'RL' + rl
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    url = conn + '/update-rl%s?NameID=%s' % (rl, update_data['name_id'])
+
+    # reset responsable legal
+    payload = copy.deepcopy(FAMILY_RESET_PAYLOAD[rlg])
+    for key in 'adresse', 'contact', 'profession', 'CAFInfo':
+        if key in payload:
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = diff_rlg(conn, update_data['name_id'], rl, 'test_update_rl%s.json' % rl)
+
+    # unchanged values
+    assert data[RLG]['civility'] != FAMILY_RESET_PAYLOAD[rlg]['civility']
+    if rl == 1:
+        assert data[RLG]['maidenName'] != FAMILY_RESET_PAYLOAD[rlg]['maidenName']
+
+    # changed values
+    assert data[RLG]['quality'] == FAMILY_RESET_PAYLOAD[rlg]['quality']
+
+    # update firstname is refused
+    payload['firstname'] = 'plop'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 'Family-updateFamily-soap:Server'
+    assert (
+        "Le Responsable légal %s transmis n'est pas celui qui existe pour la famille" % rl in res['err_desc']
+    )
+
+    # update lastname is refused
+    payload['firstname'] = update_data['family_payload'][rlg]['firstname']
+    payload['lastname'] = 'plop'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 'Family-updateFamily-soap:Server'
+    assert (
+        "Le Responsable légal %s transmis n'est pas celui qui existe pour la famille" % rl in res['err_desc']
+    )
+
+    # update birtday is refused
+    payload['lastname'] = 'Simpson'
+    payload['birth']['dateBirth'] = '1970-01-01'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 'Family-updateFamily-soap:Server'
+    if rl == 1:
+        assert (
+            "Le Responsable légal %s transmis n'est pas celui qui existe pour la famille" % rl
+            in res['err_desc']
+        )
+    else:
+        assert "La date de naissance ne peut pas être modifiée" in res['err_desc']
+
+    # restore RL1
+    payload = copy.deepcopy(update_data['family_payload'][rlg])
+    for key in 'adresse', 'contact', 'profession', 'CAFInfo':
+        if payload.get(key):
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_update_coordinate(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+
+    # reset RL2 coordinates
+    url = conn + '/update-coordinate?NameID=%s&rl_id=%s' % (update_data['name_id'], update_data['rl2_num'])
+    payload = {}
+    for key in 'adresse', 'contact', 'profession', 'CAFInfo':
+        payload[key] = FAMILY_RESET_PAYLOAD['rl2'][key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_rlg(conn, update_data['name_id'], 2, 'test_update_coordinate.json')
+
+    # restore RL2 coordinates
+    payload = {}
+    for key in 'adresse', 'contact', 'profession', 'CAFInfo':
+        payload[key] = update_data['family_payload']['rl2'][key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_is_child_exists(conn, create_data):
+    url = conn + '/is-child-exists'
+    payload = {
+        'firstname': 'Maggie',
+        'lastname': create_data['lastname'],
+        'dateBirth': '1989-12-17',
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': True}
+
+    payload['firstname'] = payload['firstname'].upper()
+    payload['lastname'] = payload['lastname'].lower()
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': True}
+
+    payload['firstname'] = 'plop'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json() == {'err': 0, 'data': False}
+
+
+def test_create_child(conn, create_data, update_data):
+    unlink(conn, create_data['name_id'])
+    link(conn, create_data)
+
+    data = read_family(conn, create_data['name_id'])
+    assert len(data['childList']) == 2
+
+    url = conn + '/create-child?NameID=%s' % create_data['name_id']
+    payload = {
+        'sexe': 'M',
+        'firstname': 'Lisa',
+        'lastname': create_data['lastname'],
+        'birth': {'dateBirth': '1980-05-09'},
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_child(conn, create_data['name_id'], 2, 'test_create_child.json')
+
+    # child already exists error (Lisa form same family)
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert 'Il existe déjà un enfant correspondant' in res['err_desc']
+    res['err_desc'] = 'N/A'
+    assert res == {
+        'err': 'Family-createChild-soap:Server',
+        'err_class': 'passerelle.utils.jsonresponse.APIError',
+        'err_desc': 'N/A',
+        'data': None,
+    }
+
+    # child already exists error (Lisa form update_data)
+    payload['lastname'] = 'Simpson'
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert 'Il existe déjà un enfant correspondant' in res['err_desc']
+    res['err_desc'] = 'N/A'
+    assert res == {
+        'err': 'already-child',  # error is return into childErrorList
+        'err_class': 'passerelle.utils.jsonresponse.APIError',
+        'err_desc': 'N/A',
+        'data': None,
+    }
+
+
+def test_update_child(conn, update_data, create_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+
+    # renaming using existing child names on same family will in fact target the existing child
+    # side effect: the authorized person list is copied by the connector (from Lisa to Maggie)
+    url = conn + '/update-child?NameID=%s&child_id=%s' % (update_data['name_id'], update_data['lisa_num'])
+    payload = copy.deepcopy(update_data['family_payload']['childList'][2])  # Maggie content
+    for key in 'dietcode', 'paiInfoBean', 'medicalRecord', 'authorizedPersonList':
+        if key in payload:
+            del payload[key]
+    assert payload['firstname'] == 'Maggie'
+    payload['sexe'] = 'M'
+    resp = requests.post(url, json=payload)  # use Lisa id in url
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][1]['num'] == update_data['lisa_num']
+    assert data['childList'][1]['firstname'] == 'LISA'  # Lisa unchanged
+    assert data['childList'][2]['sexe'] == 'M'  # Maggie updated
+
+    # restore Maggie
+    url = conn + '/update-child?NameID=%s&child_id=%s' % (update_data['name_id'], update_data['maggie_num'])
+    payload = copy.deepcopy(update_data['family_payload']['childList'][2])
+    for key in 'dietcode', 'paiInfoBean', 'medicalRecord', 'authorizedPersonList':
+        if key in payload:
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+    # rename to an existing child on other family
+    url = conn + '/update-child?NameID=%s&child_id=%s' % (update_data['name_id'], update_data['bart_num'])
+    payload = copy.deepcopy(create_data['family_payload']['childList'][1])
+    for key in 'dietcode', 'paiInfoBean', 'medicalRecord', 'authorizedPersonList':
+        if key in payload:
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][0]['num'] == update_data['bart_num']
+    assert data['childList'][0]['firstname'] == 'MAGGIE'
+
+    # reset Bart
+    payload = copy.deepcopy(FAMILY_RESET_PAYLOAD['childList'][0])
+    for key in 'dietcode', 'paiInfoBean', 'medicalRecord', 'authorizedPersonList':
+        if key in payload:
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_child(conn, update_data['name_id'], 0, 'test_update_child.json')
+
+    # restore Bart
+    payload = copy.deepcopy(update_data['family_payload']['childList'][0])
+    for key in 'dietcode', 'paiInfoBean', 'medicalRecord', 'authorizedPersonList':
+        if key in payload:
+            del payload[key]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_update_child_dietcode(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    url = conn + '/update-child-dietcode?NameID=%s&child_id=%s&dietcode=' % (
+        update_data['name_id'],
+        update_data['bart_num'],
+    )
+
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][0]['dietcode'] == 'STD'
+    assert data['childList'][0]['dietcode_text'] == '1- REPAS STANDARD'
+
+    # change dietcode
+    resp = requests.post(url + 'RSP')
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][0]['dietcode'] == 'RSP'
+    assert data['childList'][0]['dietcode_text'] == '2- R\u00c9GIME SANS PORC'
+
+    # empty dietcode
+    resp = requests.post(url + '')
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert data['childList'][0]['dietcode'] == None
+    assert 'dietcode_text' not in data['childList'][0]
+
+    # restore dietcode
+    resp = requests.post(url + 'STD')
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_update_child_pai(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    url = conn + '/update-child-pai?NameID=%s&child_id=%s' % (update_data['name_id'], update_data['bart_num'])
+
+    # reset PAI
+    payload = FAMILY_RESET_PAYLOAD['childList'][0]['paiInfoBean']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_child(conn, update_data['name_id'], 0, 'test_update_child_pai.json', key='paiInfoBean')
+
+    # restore PAI
+    payload = update_data['family_payload']['childList'][0]['paiInfoBean']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_update_child_medical_record(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    url = conn + '/update-child-medical-record?NameID=%s&child_id=%s' % (
+        update_data['name_id'],
+        update_data['bart_num'],
+    )
+
+    # observation 2 is wrong
+    assert (
+        update_data['data']['childList'][0]['medicalRecord']['observ2']
+        != update_data['family_payload']['childList'][0]['medicalRecord']['observ2']
+    )
+    assert (
+        update_data['data']['childList'][0]['medicalRecord']['observ2']
+        == update_data['family_payload']['childList'][0]['medicalRecord']['observ1']
+    )
+
+    # reset medical record
+    payload = FAMILY_RESET_PAYLOAD['childList'][0]['medicalRecord']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_child(
+        conn, update_data['name_id'], 0, 'test_update_child_medical_record.json', key='medicalRecord'
+    )
+
+    # restore medical record
+    payload = update_data['family_payload']['childList'][0]['medicalRecord']
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_person(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    data = read_family(conn, update_data['name_id'])
+    assert len(data['emergencyPersonList']) == 2
+
+    # delete Patty
+    selma_num = data['emergencyPersonList'][1]['numPerson']
+    url = conn + '/delete-person?NameID=%s&person_id=%s' % (update_data['name_id'], selma_num)
+    resp = requests.post(url)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert len(data['emergencyPersonList']) == 1
+
+    # re-create Selma
+    # last inserted person is added on the tail of the list
+    url = conn + '/create-person?NameID=%s' % (update_data['name_id'])
+    payload = FAMILY_RESET_PAYLOAD['emergencyPersonList'][0]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = diff_family(conn, update_data['name_id'], 'test_update_person.json', 'emergencyPersonList')
+
+    # update Selma (and restore family person list)
+    selma_num = data['emergencyPersonList'][1]['numPerson']
+    url = conn + '/update-person?NameID=%s&person_id=%s' % (update_data['name_id'], selma_num)
+    payload = update_data['family_payload']['emergencyPersonList'][1]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
+
+
+def test_child_person(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    data = read_family(conn, update_data['name_id'])
+    assert len(data['childList'][0]['authorizedPersonList']) == 2
+
+    # delete Abi
+    abi_num = data['childList'][0]['authorizedPersonList'][0]['personInfo']['num']
+    url = conn + '/delete-child-person?NameID=%s&child_id=%s&person_id=%s' % (
+        update_data['name_id'],
+        update_data['bart_num'],
+        abi_num,
+    )
+    resp = requests.post(url)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = read_family(conn, update_data['name_id'])
+    assert len(data['childList'][0]['authorizedPersonList']) == 1
+
+    # re-create Abi
+    # last inserted person is added on the head of the list
+    url = conn + '/create-child-person?NameID=%s&child_id=%s' % (
+        update_data['name_id'],
+        update_data['bart_num'],
+    )
+    payload = FAMILY_RESET_PAYLOAD['childList'][0]['authorizedPersonList'][0]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    data = diff_child(
+        conn, update_data['name_id'], 0, 'test_update_child_person.json', 'authorizedPersonList'
+    )
+
+    # update Abi (and restore child person list)
+    abi_num = data['childList'][0]['authorizedPersonList'][0]['personInfo']['num']
+    url = conn + '/update-child-person?NameID=%s&child_id=%s&person_id=%s' % (
+        update_data['name_id'],
+        update_data['bart_num'],
+        abi_num,
+    )
+    payload = update_data['family_payload']['childList'][0]['authorizedPersonList'][0]
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    assert resp.json()['err'] == 0
+    assert diff_family(conn, update_data['name_id'], 'test_update_family.json')
diff --git a/functests/toulouse_maelis/test_misc.py b/functests/toulouse_maelis/test_misc.py
new file mode 100644
index 00000000..f6b9fa14
--- /dev/null
+++ b/functests/toulouse_maelis/test_misc.py
@@ -0,0 +1,59 @@
+import requests
+
+from .conftest import link, unlink
+
+
+def test_up(conn):
+    url = conn + '/up'
+    resp = requests.get(url)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res['err'] == 0
+
+
+def test_unlink(conn, update_data):
+    resp = unlink(conn, update_data['name_id'])
+    resp = unlink(conn, update_data['name_id'])
+    res = resp.json()
+    assert res['err'] == 'not-linked'
+
+
+def test_link(conn, update_data):
+    unlink(conn, update_data['name_id'])
+    link(conn, update_data)
+    link(conn, update_data)
+
+    # wrong DUI number
+    url = conn + '/link?NameID=%s' % update_data['name_id']
+    payload = {
+        'family_id': '999999',
+        'firstname': update_data['family_payload']['rl1']['firstname'],
+        'lastname': update_data['family_payload']['rl1']['lastname'],
+        'dateBirth': update_data['family_payload']['rl1']['birth']['dateBirth'],
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res == {
+        'err': 'Family-readFamily-soap:Server',
+        'err_class': 'passerelle.utils.jsonresponse.APIError',
+        'err_desc': 'E02 : Le dossier numéro [999999] ne correspond à aucune famille',
+        'data': None,
+    }
+
+    # wrong DUI firstname
+    payload = {
+        'family_id': update_data['family_id'],
+        'firstname': 'plop',
+        'lastname': update_data['family_payload']['rl1']['lastname'],
+        'dateBirth': update_data['family_payload']['rl1']['birth']['dateBirth'],
+    }
+    resp = requests.post(url, json=payload)
+    resp.raise_for_status()
+    res = resp.json()
+    assert res == {
+        'err': 'not-found',
+        'err_class': 'passerelle.utils.jsonresponse.APIError',
+        'err_desc': "RL1 does not match '196947' family",
+        'data': None,
+    }
-- 
2.30.2

