{
  "info": {
    "_postman_id": "b7e2d4a1-5c3f-4e8a-9d61-0f2c7a8e4b35",
    "name": "Bertel — API partenaires (parcours essentiel)",
    "description": "Collection distribuable pour les partenaires : parcours thésaurus compact, fiches publiées, sélections, référentiels, suppressions et interopérabilité. Renseignez seulement public_base_url et partner_key dans l’environnement. Les cas négatifs et la recette restent dans la collection de référence.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1. Thésaurus — parcours principal",
      "item": [
        {
          "name": "Découvrir le thésaurus (vue compacte)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/thesaurus?view=compact&lang=fr",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "thesaurus"
              ],
              "query": [
                {
                  "key": "view",
                  "value": "compact"
                },
                {
                  "key": "lang",
                  "value": "fr"
                }
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Découverte compacte conforme', function () {",
                  "    pm.expect(d.meta.contract_version).to.be.a('string').and.not.empty;",
                  "    pm.expect(d.meta.release_id).to.be.a('number').and.at.least(1);",
                  "    pm.expect(d.data.license === null || typeof d.data.license === 'string').to.eql(true);",
                  "    pm.expect(d.data.attribution === null || typeof d.data.attribution === 'string').to.eql(true);",
                  "    pm.expect(d.data.domains).to.be.an('array');",
                  "    for (const domain of d.data.domains) {",
                  "      pm.expect(domain.domain).to.be.a('string').and.not.empty;",
                  "      pm.expect(domain.name === null || typeof domain.name === 'string').to.eql(true);",
                  "      pm.expect(domain.effective_lang === null || typeof domain.effective_lang === 'string').to.eql(true);",
                  "      pm.expect(domain).not.to.have.property('name_i18n');",
                  "    }",
                  "    for (const release of d.data.releases || []) {",
                  "      pm.expect(release).not.to.have.property('domains');",
                  "      pm.expect(release.release_id).to.be.a('number');",
                  "    }",
                  "  });",
                  "  for (const key of ['thesaurus_concept_id', 'thesaurus_historical_concept_id', 'thesaurus_next_cursor', 'thesaurus_changes_cursor', 'thesaurus_compact_etag']) pm.collectionVariables.unset(key);",
                  "  pm.collectionVariables.set('thesaurus_release_id', String(d.meta.release_id));",
                  "  if (d.meta.previous_release_id != null) pm.collectionVariables.set('thesaurus_previous_release_id', String(d.meta.previous_release_id));",
                  "  else pm.collectionVariables.unset('thesaurus_previous_release_id');",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "Exemple synthétique — découverte compacte",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus?view=compact&lang=fr",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus"
                  ],
                  "query": [
                    {
                      "key": "view",
                      "value": "compact"
                    },
                    {
                      "key": "lang",
                      "value": "fr"
                    }
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\"meta\":{\"as_of\":\"2026-11-09T18:21:18Z\",\"previous_release_id\":5,\"contract_version\":\"1.0.0\",\"release_id\":6},\"data\":{\"domains\":[{\"effective_lang\":\"fr\",\"name\":\"Types d’activité\",\"domain\":\"activity_type\"}],\"license\":\"Licence Ouverte / Etalab 2.0\",\"attribution\":\"Office de Tourisme Intercommunal du Sud de La Réunion (OTI du Sud)\",\"releases\":[{\"as_of\":\"2026-11-09T18:21:18Z\",\"previous_release_id\":5,\"release_id\":6},{\"as_of\":\"2026-11-08T08:00:00Z\",\"previous_release_id\":null,\"release_id\":5}]}}"
            }
          ]
        },
        {
          "name": "Parcourir les concepts (vue compacte)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/concepts?view=compact&lang=fr&page_size=2",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "thesaurus",
                "releases",
                "{{thesaurus_release_id}}",
                "concepts"
              ],
              "query": [
                {
                  "key": "view",
                  "value": "compact"
                },
                {
                  "key": "lang",
                  "value": "fr"
                },
                {
                  "key": "page_size",
                  "value": "2"
                }
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Liste compacte conforme', function () {",
                  "    pm.expect(d.meta.contract_version).to.be.a('string').and.not.empty;",
                  "    pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id')));",
                  "    pm.expect(d.meta.total).to.be.a('number');",
                  "    pm.expect(d.meta.next_cursor === null || typeof d.meta.next_cursor === 'string').to.eql(true);",
                  "    pm.expect(d.data).to.be.an('array');",
                  "    for (const row of d.data) {",
                  "      pm.expect(row.concept_id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);",
                  "      pm.expect(row.domain).to.be.a('string').and.not.empty;",
                  "      pm.expect(row.code).to.be.a('string').and.not.empty;",
                  "      pm.expect(row.name === null || typeof row.name === 'string').to.eql(true);",
                  "      pm.expect(row.effective_lang === null || typeof row.effective_lang === 'string').to.eql(true);",
                  "      pm.expect(['active', 'deprecated', 'retired']).to.include(row.lifecycle);",
                  "      pm.expect(row.assignable).to.be.a('boolean');",
                  "      pm.expect(row).not.to.have.property('preferred_labels');",
                  "      pm.expect(row).not.to.have.property('definitions');",
                  "      pm.expect(row).not.to.have.property('source_fields');",
                  "      if (row.description !== undefined) pm.expect(row.definition_lang === null || typeof row.definition_lang === 'string').to.eql(true);",
                  "      if (row.context_key !== undefined) pm.expect(row.context_key).to.be.a('string').and.not.empty;",
                  "      for (const f of ['valid_from', 'valid_to']) if (row[f] !== undefined) pm.expect(row[f]).to.be.a('string').and.not.empty;",
                  "      for (const f of ['broader', 'member_of', 'replaced_by']) if (row[f] !== undefined) pm.expect(row[f]).to.be.an('array').and.not.empty;",
                  "    }",
                  "  });",
                  "  if (d.data.length) pm.collectionVariables.set('thesaurus_concept_id', d.data[0].concept_id);",
                  "  else pm.collectionVariables.unset('thesaurus_concept_id');",
                  "  if (d.meta.next_cursor) pm.collectionVariables.set('thesaurus_next_cursor', d.meta.next_cursor);",
                  "  else pm.collectionVariables.unset('thesaurus_next_cursor');",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "Exemple synthétique — liste compacte",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/concepts?view=compact&lang=fr&page_size=2",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "concepts"
                  ],
                  "query": [
                    {
                      "key": "view",
                      "value": "compact"
                    },
                    {
                      "key": "lang",
                      "value": "fr"
                    },
                    {
                      "key": "page_size",
                      "value": "2"
                    }
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\"meta\":{\"total\":1,\"next_cursor\":null,\"contract_version\":\"1.0.0\",\"release_id\":6},\"data\":[{\"concept_id\":\"11111111-1111-4111-8111-111111111111\",\"lifecycle\":\"active\",\"description\":\"Parcours à pied en milieu naturel.\",\"name\":\"Randonnée\",\"code\":\"randonnee\",\"definition_lang\":\"fr\",\"context_key\":\"activity_type:randonnee\",\"valid_from\":\"2026-01-01\",\"assignable\":true,\"domain\":\"activity_type\",\"effective_lang\":\"fr\"}]}"
            }
          ]
        },
        {
          "name": "Lire un concept (représentation complète)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}",
                "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/concepts/{{thesaurus_concept_id}}?lang=fr",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "thesaurus",
                "releases",
                "{{thesaurus_release_id}}",
                "concepts",
                "{{thesaurus_concept_id}}"
              ],
              "query": [
                {
                  "key": "lang",
                  "value": "fr"
                }
              ]
            },
            "description": "",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) { const d = pm.response.json();",
                  "",
                  "function checkConcept(x) {",
                  "  pm.expect(x.concept_id).to.match(/^[0-9a-f-]{36}$/i);",
                  "  pm.expect(x.preferred_labels).to.be.an('object'); pm.expect(x.definitions).to.be.an('object');",
                  "  pm.expect(['active','deprecated','retired']).to.include(x.lifecycle); pm.expect(x.assignable).to.be.a('boolean');",
                  "  if (x.effective_lang) { if (Object.prototype.hasOwnProperty.call(x.preferred_labels, x.effective_lang)) pm.expect(x.name).to.eql(x.preferred_labels[x.effective_lang]); else { pm.expect(x.effective_lang).to.eql('fr'); pm.expect(x.name).to.be.a('string').and.not.empty; } }",
                  "  if (x.definition_lang) { if (Object.prototype.hasOwnProperty.call(x.definitions, x.definition_lang)) pm.expect(x.description).to.eql(x.definitions[x.definition_lang]); else { pm.expect(x.definition_lang).to.eql('fr'); pm.expect(x.description).to.be.a('string').and.not.empty; } }",
                  "  function checkMembershipField(field) { if (Object.prototype.hasOwnProperty.call(x, field)) { pm.expect(x[field]).to.be.an('array'); pm.expect(new Set(x[field]).size).to.eql(x[field].length); x[field].forEach(id => pm.expect(id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)); } }",
                  "  checkMembershipField('member_of'); checkMembershipField('members');",
                  "  pm.expect(x).not.to.have.property('source_fields');",
                  "}",
                  "",
                  "pm.test('Concept résolu sans fausse traduction', function () { checkConcept(d.data); pm.expect(d.data.concept_id).to.eql(pm.variables.get('thesaurus_concept_id')); }); }"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Fiches et sélections",
      "item": [
        {
          "name": "Lister les fiches publiées",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Page publique et curseur', function () { pm.expect(d.data).to.be.an('array'); pm.expect(d.meta).to.have.property('next_cursor'); });",
                  "  const first = d.data.find(row => row && typeof row.id === 'string');",
                  "  if (first) pm.collectionVariables.set('public_object_id', first.id); else pm.collectionVariables.unset('public_object_id');",
                  "  const itinerary = d.data.find(row => row && row.type === 'ITI' && typeof row.id === 'string');",
                  "  if (itinerary) pm.collectionVariables.set('public_itinerary_id', itinerary.id); else pm.collectionVariables.unset('public_itinerary_id');",
                  "  if (d.meta.next_cursor) pm.collectionVariables.set('public_next_cursor', d.meta.next_cursor); else pm.collectionVariables.unset('public_next_cursor');",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}",
                "description": "Clé partenaire bk_live_…"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects?page_size=20",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "20"
                },
                {
                  "key": "types",
                  "value": "HOT,RES",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "format",
                  "value": "datatourisme",
                  "disabled": true,
                  "description": "jsonld | datatourisme | apidae | tourinsoft — chaque fiche de la page porte son document pivot sous data[i].<profil> (I4c)"
                }
              ]
            },
            "description": "Page de fiches PUBLIÉES (curseur `cursor`, `page_size` 1–200, `types` csv, `search`, `lang`). Avec `format=<profil>` (jsonld/datatourisme/apidae/tourinsoft), chaque fiche de la page porte en plus son document pivot sous une clé additive `data[i].<profil>` — même pagination, un appel par page (I4c). Stocke automatiquement `public_object_id = data[0].id` pour les requêtes suivantes.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Récupérer une fiche publiée",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "{{public_object_id}}"
              ]
            },
            "description": "Ressource complète d'une fiche PUBLIÉE. Un id inconnu ou non publié → 404 (indistinguable, pas de fuite de brouillon).",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "Exemple synthétique abrégé",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects",
                    "{{public_object_id}}"
                  ]
                },
                "description": "Ressource complète d'une fiche PUBLIÉE. Un id inconnu ou non publié → 404 (indistinguable, pas de fuite de brouillon)."
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Bertel-Api-Version",
                  "value": "1.0.0"
                }
              ],
              "cookie": [],
              "body": "{\n  \"meta\": {\n    \"contract_version\": \"1.0.0\",\n    \"vocabulary_release\": 1\n  },\n  \"data\": {\n    \"id\": \"RESZZZ0000000001\",\n    \"type\": \"RES\",\n    \"status\": \"published\",\n    \"name\": \"Restaurant d’exemple\",\n    \"description\": \"Contenu synthétique ; ne décrit aucun établissement réel.\"\n  }\n}"
            }
          ]
        },
        {
          "name": "Fiches et appartenances aux sélections",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Appartenances disponibles : HTTP 200', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Enveloppe du flux général', function () { pm.expect(d.meta).to.be.an('object'); pm.expect(d.data).to.be.an('array'); });",
                  "  pm.test('Chaque objet porte des selection_ids triés et uniques', function () { for (const item of d.data) { pm.expect(item).to.have.property('selection_ids'); pm.expect(item.selection_ids).to.be.an('array'); pm.expect(item.selection_ids.every(id => typeof id === 'string' && id.length > 0)).to.eql(true); pm.expect(item.selection_ids).to.eql([...new Set(item.selection_ids)].sort()); } });",
                  "  if (d.meta.next_cursor) { pm.collectionVariables.set('membership_next_cursor', d.meta.next_cursor); } else { pm.collectionVariables.unset('membership_next_cursor'); }",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}",
                "description": "Même clé partenaire bk_live_…, utilisée côté serveur"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects?view=full&include_selection_ids=true&page_size=25",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects"
              ],
              "query": [
                {
                  "key": "view",
                  "value": "full",
                  "description": "card est aussi disponible"
                },
                {
                  "key": "include_selection_ids",
                  "value": "true",
                  "description": "Absent/false : flux historique, champ absent. Incompatible avec selection_id même false."
                },
                {
                  "key": "page_size",
                  "value": "25"
                },
                {
                  "key": "cursor",
                  "value": "{{membership_next_cursor}}",
                  "disabled": true,
                  "description": "Activer seulement pour la page suivante ; garder true et les mêmes options jusqu’à la dernière page"
                }
              ]
            },
            "description": "Même GET /objects et clé partenaire. Chaque item card/full reçoit selection_ids à sa racine : chaînes triées uniques, [] sans sélection active, aucun nom privé ni règle. Détail et documents pivots inchangés. Absent ou false conserve le traitement historique sans champ. Paramètre vide, répété ou autre que true/false : 400. Toute combinaison avec selection_id, même false : 400. Conserver le flag et les mêmes options pendant la pagination. Pas d'instantané entre pages. Actualiser selection_ids à chaque synchronisation complète même si updated_at est inchangé, car une suspension ou une modification des critères peut changer les appartenances seules. Appliquer les retraits uniquement après toutes les pages réussies ; 503 selection_membership_unavailable conserve l'état précédent et n'est jamais un tableau vide. Le test mémorise membership_next_cursor.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Fiches d’une sélection enregistrée",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Sélection disponible : HTTP 200', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Même enveloppe, contexte sélection et fiches', function () { pm.expect(d.meta.selection_id).to.eql(String(pm.variables.get('public_selection_id'))); pm.expect(d.meta.selection_name).to.be.a('string').and.not.empty; pm.expect(d.meta.selection_revision).to.be.a('number').and.at.least(1); pm.expect(d.data).to.be.an('array'); });",
                  "  if (d.meta.next_cursor) { pm.collectionVariables.set('selection_next_cursor', d.meta.next_cursor); } else { pm.collectionVariables.unset('selection_next_cursor'); }",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}",
                "description": "Même clé partenaire bk_live_…"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects?selection_id={{public_selection_id}}&view=full&page_size=100",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects"
              ],
              "query": [
                {
                  "key": "selection_id",
                  "value": "{{public_selection_id}}",
                  "description": "ID permanent configurable ; nom et révision viennent du registre courant."
                },
                {
                  "key": "view",
                  "value": "full"
                },
                {
                  "key": "page_size",
                  "value": "100"
                },
                {
                  "key": "cursor",
                  "value": "{{selection_next_cursor}}",
                  "disabled": true,
                  "description": "Activer pour la page suivante seulement, puis désactiver pour recommencer"
                }
              ]
            },
            "description": "Sélection enregistrée dans Bertel, avec le même endpoint /objects et les mêmes fiches full. La définition initiale 1006 fusionne producteurs, artisans et marchés sans doublon, sur les quatre communes OTI (INSEE 97403/97412/97417/97422), publié et hors test. meta ajoute selection_id/selection_name/selection_revision (1 initialement). Ne pas combiner avec types/taxonomy/search/include_equivalent_predecessors/include_selection_ids, même false ; seules cursor/page_size/view/lang/track/format/variant sont acceptées. Sélection inconnue ou suspendue : 404 sans retour au catalogue global ; vide/répétée ou conflit : 400. Le test conserve next_cursor dans selection_next_cursor pour une pagination manuelle. Lectures en temps réel sans snapshot : rapprocher les appartenances uniquement après toutes les pages réussies ; une erreur ne justifie aucun retrait. Les identifiants des quinze définitions initiales 1001–1015 sont détaillés dans le guide partenaires.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Fiche au format JSON-LD schema.org",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "pm.test('bloc additif data.jsonld présent', function () { pm.expect(d.data).to.have.property('jsonld'); });",
                  "pm.test('document schema.org', function () { pm.expect(d.data.jsonld['@context']).to.eql('https://schema.org'); pm.expect(d.data.jsonld).to.have.property('@type'); });"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?format=jsonld",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "{{public_object_id}}"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "jsonld"
                }
              ]
            },
            "description": "Ajoute un bloc ADDITIF `data.jsonld` = document JSON-LD schema.org (`@type` selon le type d'objet, crosswalk piloté par table côté serveur `ref_interop_crosswalk`). Directement réutilisable pour le SEO / l'interopérabilité (à injecter dans un `<script type=\"application/ld+json\">`). Orthogonal à `lang` ; les clés de base ne sont jamais mutées. Contacts / médias / réseaux PUBLICS uniquement ; texte propre (Markdown neutralisé).",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "Exemple synthétique abrégé",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?format=jsonld",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects",
                    "{{public_object_id}}"
                  ],
                  "query": [
                    {
                      "key": "format",
                      "value": "jsonld"
                    }
                  ]
                },
                "description": "Ajoute un bloc ADDITIF `data.jsonld` = document JSON-LD schema.org (`@type` selon le type d'objet, crosswalk piloté par table côté serveur `ref_interop_crosswalk`). Directement réutilisable pour le SEO / l'interopérabilité (à injecter dans un `<script type=\"application/ld+json\">`). Orthogonal à `lang` ; les clés de base ne sont jamais mutées. Contacts / médias / réseaux PUBLICS uniquement ; texte propre (Markdown neutralisé)."
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                },
                {
                  "key": "X-Bertel-Api-Version",
                  "value": "1.0.0"
                }
              ],
              "cookie": [],
              "body": "{\n  \"meta\": {\n    \"contract_version\": \"1.0.0\",\n    \"vocabulary_release\": 1\n  },\n  \"data\": {\n    \"id\": \"RESZZZ0000000001\",\n    \"type\": \"RES\",\n    \"status\": \"published\",\n    \"name\": \"Restaurant d’exemple\",\n    \"description\": \"Contenu synthétique ; ne décrit aucun établissement réel.\",\n    \"jsonld\": {\n      \"@context\": \"https://schema.org\",\n      \"@type\": \"Restaurant\",\n      \"@id\": \"urn:example:bertel:restaurant\",\n      \"name\": \"Restaurant d’exemple\"\n    }\n  }\n}"
            }
          ]
        }
      ]
    },
    {
      "name": "3. Catalogues et suppressions",
      "item": [
        {
          "name": "Catalogues de référentiels",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/catalog?domains=amenity,payment_method,language",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "catalog"
              ],
              "query": [
                {
                  "key": "domains",
                  "value": "amenity,payment_method,language"
                },
                {
                  "key": "lang",
                  "value": "fr",
                  "disabled": true
                }
              ]
            },
            "description": "Vocabulaires contrôlés résolus par langue (forme `{code,name,icon_url,parent_code,domain}`). Sans `domains` → tous les catalogues publics.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Suppressions publiées",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/deletions?since={{current_timestamp}}",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "deletions"
              ],
              "query": [
                {
                  "key": "since",
                  "value": "{{current_timestamp}}"
                }
              ]
            },
            "description": "Objets DÉFINITIVEMENT supprimés depuis `since` (pour qu'un miroir partenaire retire ses enregistrements fantômes). Projette `{object_id, type, deleted_at}` uniquement. Reprendre la pagination avec `since = meta.cursor`.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Flux de suppressions', function () { pm.expect(d.data).to.be.an('array'); pm.expect(d.meta.count).to.eql(d.data.length); }); if (d.meta.count > 0 && d.meta.cursor) pm.collectionVariables.set('public_deletions_cursor', d.meta.cursor); else pm.collectionVariables.unset('public_deletions_cursor'); }"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Interopérabilité",
      "item": [
        {
          "name": "Fiche au format DATAtourisme",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "pm.test('bloc data.datatourisme (JSON-LD)', function () { pm.expect(d.data).to.have.property('datatourisme'); pm.expect(d.data.datatourisme['@type']).to.include('PointOfInterest'); });",
                  "pm.test('classe DATAtourisme résolue', function () { const types = d.data.datatourisme['@type']; pm.expect(types).to.be.an('array').with.lengthOf(2); pm.expect(types[1]).to.be.a('string').and.not.empty; });"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?format=datatourisme",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "{{public_object_id}}"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "datatourisme"
                }
              ]
            },
            "description": "Ajoute un bloc ADDITIF `data.datatourisme` = document JSON-LD de l'ontologie nationale DATAtourisme (`@type [\"PointOfInterest\", <classe>]`, `rdfs:label`, `hasDescription`, `isLocatedAt`, `hasContact`). Pour HLO : Chambre d'hôtes → `Guesthouse`, Meublé de tourisme / gîte → `SelfCateringAccommodation`, Hébergement collectif → `GroupLodging`, Refuge et gîte d'étape → `StopOverOrGroupLodge`, puis fallback `Accommodation`. Socle cœur — valider contre l'importeur DATAtourisme avant synchro de prod.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Fiche au format Apidae",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "pm.test('bloc data.apidae', function () { pm.expect(d.data).to.have.property('apidae'); pm.expect(d.data.apidae).to.have.property('type'); });"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?format=apidae",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "{{public_object_id}}"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "apidae"
                }
              ]
            },
            "description": "Ajoute un bloc ADDITIF `data.apidae` = objet JSON au format Apidae (`nom.libelleFr`, `type`, `presentation.descriptifCourt`, `localisation.adresse` + `geolocalisation.geoJson`, `informations.moyensCommunication`). Socle cœur — valider contre la plateforme Apidae avant synchro de prod.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Fiche au format Tourinsoft",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "pm.test('bloc data.tourinsoft', function () { pm.expect(d.data).to.have.property('tourinsoft'); pm.expect(d.data.tourinsoft).to.have.property('SyndObjectID'); });"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?format=tourinsoft",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects",
                "{{public_object_id}}"
              ],
              "query": [
                {
                  "key": "format",
                  "value": "tourinsoft"
                }
              ]
            },
            "description": "Ajoute le bloc historique ADDITIF `data.tourinsoft` au contrat `legacy-v1` (`SyndObjectID`, `type`, `NomOffre` et les 10 autres champs historiques). L'absence de `variant` conserve strictement ce comportement.",
            "auth": {
              "type": "noauth"
            }
          }
        },
        {
          "name": "Liste au format Tourinsoft — régional",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const d = pm.response.json();",
                  "const regional = d.data.filter(x => x.tourinsoft);",
                  "pm.test('page régionale Tourinsoft additive', function () { pm.expect(d.data).to.be.an('array'); regional.forEach(x => { pm.expect(x.tourinsoft).to.have.property('ClassificationType'); if (Object.prototype.hasOwnProperty.call(x.tourinsoft, 'SyndicObjectID')) pm.expect(x.tourinsoft.SyndicObjectID).to.be.a('string').and.not.empty; }); });",
                  "if (regional.length > 0) pm.collectionVariables.set('tourinsoft_regional_object_id', regional[0].id); else pm.collectionVariables.unset('tourinsoft_regional_object_id');"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/objects?page_size=200&format=tourinsoft&variant=reunion-regional-v1",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "objects"
              ],
              "query": [
                {
                  "key": "page_size",
                  "value": "200"
                },
                {
                  "key": "format",
                  "value": "tourinsoft"
                },
                {
                  "key": "variant",
                  "value": "reunion-regional-v1"
                }
              ]
            },
            "description": "Projection régionale opt-in sur les six familles CRT. Un seul RPC set-based traite la page (200 ids maximum) ; les fiches non routées restent dans la page sans bloc `tourinsoft`. Cette requête mémorise le premier id couvert dans `tourinsoft_regional_object_id` pour la requête de détail suivante.",
            "auth": {
              "type": "noauth"
            }
          }
        }
      ]
    },
    {
      "name": "5. Avancé",
      "item": [
        {
          "name": "Lire le manifeste de la publication (vue compacte)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}?view=compact&lang=fr",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "thesaurus",
                "releases",
                "{{thesaurus_release_id}}"
              ],
              "query": [
                {
                  "key": "view",
                  "value": "compact"
                },
                {
                  "key": "lang",
                  "value": "fr"
                }
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const d = pm.response.json();",
                  "  pm.test('Manifeste compact sans historique imbriqué', function () {",
                  "    pm.expect(d.meta.contract_version).to.be.a('string').and.not.empty;",
                  "    pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id')));",
                  "    pm.expect(d.meta.as_of).to.be.a('string').and.not.empty;",
                  "    pm.expect(d.data.domains).to.be.an('array');",
                  "    pm.expect(d.data).not.to.have.property('releases');",
                  "  });",
                  "  pm.collectionVariables.unset('thesaurus_compact_etag');",
                  "  const compactEtag = pm.response.headers.get('ETag');",
                  "  if (compactEtag) pm.collectionVariables.set('thesaurus_compact_etag', compactEtag);",
                  "}"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "Exemple synthétique — manifeste compact",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}?view=compact&lang=fr",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}"
                  ],
                  "query": [
                    {
                      "key": "view",
                      "value": "compact"
                    },
                    {
                      "key": "lang",
                      "value": "fr"
                    }
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "cookie": [],
              "body": "{\"meta\":{\"as_of\":\"2026-11-09T18:21:18Z\",\"previous_release_id\":5,\"contract_version\":\"1.0.0\",\"release_id\":6},\"data\":{\"domains\":[{\"effective_lang\":\"fr\",\"name\":\"Types d’activité\",\"domain\":\"activity_type\"}],\"license\":\"Licence Ouverte / Etalab 2.0\",\"attribution\":\"Office de Tourisme Intercommunal du Sud de La Réunion (OTI du Sud)\"}}"
            }
          ]
        },
        {
          "name": "Manifeste compact : relecture conditionnelle (304)",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{partner_key}}"
              },
              {
                "key": "If-None-Match",
                "value": "{{thesaurus_compact_etag}}",
                "description": "ETag de la vue compacte uniquement ; ne jamais réutiliser thesaurus_etag."
              }
            ],
            "url": {
              "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}?view=compact&lang=fr",
              "host": [
                "{{public_base_url}}"
              ],
              "path": [
                "api",
                "public",
                "thesaurus",
                "releases",
                "{{thesaurus_release_id}}"
              ],
              "query": [
                {
                  "key": "view",
                  "value": "compact"
                },
                {
                  "key": "lang",
                  "value": "fr"
                }
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.variables.set('expected_status', '304');",
                  "const compactEtag = pm.collectionVariables.get('thesaurus_compact_etag');",
                  "if (!compactEtag) {",
                  "  console.info('Relecture 304 compacte ignorée : ETag compact absent ; exécuter le manifeste compact d\\'abord.');",
                  "  pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 304) {",
                  "  pm.test('304 compact sans corps', function () {",
                  "    pm.expect(pm.response.text()).to.eql('');",
                  "  });",
                  "} else {",
                  "  pm.test('Relecture compacte conditionnelle : HTTP 304 attendu', function () {",
                  "    pm.expect(pm.response.code).to.eql(304);",
                  "  });",
                  "}"
                ]
              }
            }
          ],
          "response": []
        },
        {
          "name": "Thésaurus — variantes",
          "item": [
            {
              "name": "Lire la publication précédente",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_previous_release_id}}?lang=fr",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_previous_release_id}}"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "fr"
                    }
                  ]
                },
                "description": "Ignorée sans previous_release_id.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (!pm.collectionVariables.get('thesaurus_previous_release_id')) { console.info('Requête ignorée : aucune édition publique précédente.'); pm.execution.skipRequest(); }"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Édition publique précédente', function () { pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_previous_release_id'))); }); }"
                    ]
                  }
                }
              ],
              "description": "Ignorée tant que l’édition courante n’a pas de previous_release_id (notamment durant le pré-lancement sur l’édition 1)."
            },
            {
              "name": "Découverte en anglais",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}"
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus?lang=en",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "en"
                    }
                  ]
                },
                "description": "Contrôle les champs de nom et de langue du manifeste pour le tag demandé.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) {",
                      " const d = pm.response.json();",
                      " pm.test('Noms réellement traduits et maps préservées', function () {",
                      "   pm.expect(d.data.domains).to.be.an('array').and.not.empty;",
                      "   const requested = \"en\";",
                      "   for (const domain of d.data.domains) {",
                      "     pm.expect(domain.requested_lang).to.eql(requested);",
                      "     const map = domain.name_i18n || {};",
                      "     pm.expect(map).to.be.an('object');",
                      "     const effective = [requested, requested.toLowerCase(), requested.split('-')[0], 'fr', ...Object.keys(map).sort()]",
                      "       .find(lang => typeof map[lang] === 'string' && map[lang].trim()) || 'fr';",
                      "     pm.expect(domain.effective_lang).to.eql(effective);",
                      "     if (map[effective]) pm.expect(domain.name).to.eql(map[effective]);",
                      "     else { pm.expect(effective).to.eql('fr'); pm.expect(domain.name).to.be.a('string').and.not.empty; }",
                      "   }",
                      "   for (const release of d.data.releases || []) {",
                      "     pm.expect(release.is_current).to.eql(release.release_id === d.meta.release_id);",
                      "     pm.expect(release.metadata_scope).to.eql('release_snapshot');",
                      "     for (const domain of release.domains || []) {",
                      "       const map = domain.name_i18n || {};",
                      "       const effective = map[requested] ? requested : map[requested.split('-')[0]] ? requested.split('-')[0] : 'fr';",
                      "       pm.expect(domain.requested_lang).to.eql(requested); pm.expect(domain.effective_lang).to.eql(effective);",
                      "       if (map[effective]) pm.expect(domain.name).to.eql(map[effective]);",
                      "     }",
                      "   }",
                      " });",
                      "}"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Parcourir les concepts compacts — page suivante",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/concepts?view=compact&lang=fr&page_size=2&cursor={{thesaurus_next_cursor}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "concepts"
                  ],
                  "query": [
                    {
                      "key": "view",
                      "value": "compact"
                    },
                    {
                      "key": "lang",
                      "value": "fr"
                    },
                    {
                      "key": "page_size",
                      "value": "2"
                    },
                    {
                      "key": "cursor",
                      "value": "{{thesaurus_next_cursor}}"
                    }
                  ]
                },
                "description": "Mêmes publication, langue, filtres et taille ; ignorée en fin de parcours.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) {",
                      "  const d = pm.response.json();",
                      "  pm.test('Liste compacte conforme', function () {",
                      "    pm.expect(d.meta.contract_version).to.be.a('string').and.not.empty;",
                      "    pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id')));",
                      "    pm.expect(d.meta.total).to.be.a('number');",
                      "    pm.expect(d.meta.next_cursor === null || typeof d.meta.next_cursor === 'string').to.eql(true);",
                      "    pm.expect(d.data).to.be.an('array');",
                      "    for (const row of d.data) {",
                      "      pm.expect(row.concept_id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);",
                      "      pm.expect(row.domain).to.be.a('string').and.not.empty;",
                      "      pm.expect(row.code).to.be.a('string').and.not.empty;",
                      "      pm.expect(row.name === null || typeof row.name === 'string').to.eql(true);",
                      "      pm.expect(row.effective_lang === null || typeof row.effective_lang === 'string').to.eql(true);",
                      "      pm.expect(['active', 'deprecated', 'retired']).to.include(row.lifecycle);",
                      "      pm.expect(row.assignable).to.be.a('boolean');",
                      "      pm.expect(row).not.to.have.property('preferred_labels');",
                      "      pm.expect(row).not.to.have.property('definitions');",
                      "      pm.expect(row).not.to.have.property('source_fields');",
                      "      if (row.description !== undefined) pm.expect(row.definition_lang === null || typeof row.definition_lang === 'string').to.eql(true);",
                      "      if (row.context_key !== undefined) pm.expect(row.context_key).to.be.a('string').and.not.empty;",
                      "      for (const f of ['valid_from', 'valid_to']) if (row[f] !== undefined) pm.expect(row[f]).to.be.a('string').and.not.empty;",
                      "      for (const f of ['broader', 'member_of', 'replaced_by']) if (row[f] !== undefined) pm.expect(row[f]).to.be.an('array').and.not.empty;",
                      "    }",
                      "  });",
                      "  if (d.data.length) pm.collectionVariables.set('thesaurus_concept_id', d.data[0].concept_id);",
                      "  else pm.collectionVariables.unset('thesaurus_concept_id');",
                      "  if (d.meta.next_cursor) pm.collectionVariables.set('thesaurus_next_cursor', d.meta.next_cursor);",
                      "  else pm.collectionVariables.unset('thesaurus_next_cursor');",
                      "}"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Résoudre l’URI pérenne (accès partenaire)",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/concepts/{{thesaurus_concept_id}}?lang={{public_lang}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "concepts",
                    "{{thesaurus_concept_id}}"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "{{public_lang}}"
                    }
                  ]
                },
                "description": "Résout dans la publication courante, sans redirection. Un concept historique connu reste résolvable. Une traduction absente demeure un repli explicite.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json();",
                      "",
                      "function checkConcept(x) {",
                      "  pm.expect(x.concept_id).to.match(/^[0-9a-f-]{36}$/i);",
                      "  pm.expect(x.preferred_labels).to.be.an('object'); pm.expect(x.definitions).to.be.an('object');",
                      "  pm.expect(['active','deprecated','retired']).to.include(x.lifecycle); pm.expect(x.assignable).to.be.a('boolean');",
                      "  if (x.effective_lang) { if (Object.prototype.hasOwnProperty.call(x.preferred_labels, x.effective_lang)) pm.expect(x.name).to.eql(x.preferred_labels[x.effective_lang]); else { pm.expect(x.effective_lang).to.eql('fr'); pm.expect(x.name).to.be.a('string').and.not.empty; } }",
                      "  if (x.definition_lang) { if (Object.prototype.hasOwnProperty.call(x.definitions, x.definition_lang)) pm.expect(x.description).to.eql(x.definitions[x.definition_lang]); else { pm.expect(x.definition_lang).to.eql('fr'); pm.expect(x.description).to.be.a('string').and.not.empty; } }",
                      "  function checkMembershipField(field) { if (Object.prototype.hasOwnProperty.call(x, field)) { pm.expect(x[field]).to.be.an('array'); pm.expect(new Set(x[field]).size).to.eql(x[field].length); x[field].forEach(id => pm.expect(id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)); } }",
                      "  checkMembershipField('member_of'); checkMembershipField('members');",
                      "  pm.expect(x).not.to.have.property('source_fields');",
                      "}",
                      "",
                      "pm.test('Concept résolu sans fausse traduction', function () { checkConcept(d.data); pm.expect(d.data.concept_id).to.eql(pm.variables.get('thesaurus_concept_id')); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Lire les concepts dépréciés et retirés",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/concepts?status=deprecated,retired&lang=fr&page_size=20",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "concepts"
                  ],
                  "query": [
                    {
                      "key": "status",
                      "value": "deprecated,retired"
                    },
                    {
                      "key": "lang",
                      "value": "fr"
                    },
                    {
                      "key": "page_size",
                      "value": "20"
                    }
                  ]
                },
                "description": "Une liste vide est valide ; un ID historique découvert alimente la résolution suivante.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('États historiques', function () { d.data.forEach(x => pm.expect(['deprecated','retired']).to.include(x.lifecycle)); }); if (d.data.length) pm.collectionVariables.set('thesaurus_historical_concept_id', d.data[0].concept_id); else pm.collectionVariables.unset('thesaurus_historical_concept_id'); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Lire les changements — première page",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/changes?lang=fr&page_size=2",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "changes"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "fr"
                    },
                    {
                      "key": "page_size",
                      "value": "2"
                    }
                  ]
                },
                "description": "Delta depuis previous_release_id, pas depuis une date arbitraire. Enchaîner les publications intermédiaires pour rattraper plusieurs versions.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Publication épinglée', function () { pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id'))); pm.expect(d.meta.vocabulary_release).to.eql(d.meta.release_id); });",
                      "  function checkMembershipField(x, field) { if (Object.prototype.hasOwnProperty.call(x, field)) { pm.expect(x[field]).to.be.an('array'); pm.expect(new Set(x[field]).size).to.eql(x[field].length); x[field].forEach(id => pm.expect(id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)); } }",
                      "  pm.test('Delta de la publication précédente', function () { pm.expect(d.data).to.be.an('array'); for (const x of d.data) { for (const snap of [x.before, x.after]) if (snap) { checkMembershipField(snap, 'member_of'); checkMembershipField(snap, 'members'); pm.expect(snap).not.to.have.property('source_fields'); } pm.expect(['added','modified','deprecated','retired']).to.include(x.change_type); if (Number(pm.variables.get('thesaurus_release_id')) === 1) { pm.expect(x.change_type).to.eql('added'); pm.expect(x.before).to.eql(null); } pm.expect(x).to.have.property('before'); pm.expect(x).to.have.property('after'); } });",
                      "  if (d.meta.next_cursor) pm.collectionVariables.set('thesaurus_changes_cursor', d.meta.next_cursor); else pm.collectionVariables.unset('thesaurus_changes_cursor');",
                      "}"
                    ]
                  }
                }
              ],
              "description": "Édition 1 pré-lancement : flux baseline exhaustif en pages, tous les concepts publics sont added et before vaut null. Après le gel, les éditions ultérieures décrivent les changements depuis previous_release_id."
            },
            {
              "name": "Lire les changements — page suivante",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/changes?lang=fr&page_size=2&cursor={{thesaurus_changes_cursor}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "changes"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "fr"
                    },
                    {
                      "key": "page_size",
                      "value": "2"
                    },
                    {
                      "key": "cursor",
                      "value": "{{thesaurus_changes_cursor}}"
                    }
                  ]
                },
                "description": "",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Publication épinglée', function () { pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id'))); pm.expect(d.meta.vocabulary_release).to.eql(d.meta.release_id); });",
                      "  function checkMembershipField(x, field) { if (Object.prototype.hasOwnProperty.call(x, field)) { pm.expect(x[field]).to.be.an('array'); pm.expect(new Set(x[field]).size).to.eql(x[field].length); x[field].forEach(id => pm.expect(id).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)); } }",
                      "  pm.test('Delta de la publication précédente', function () { pm.expect(d.data).to.be.an('array'); for (const x of d.data) { for (const snap of [x.before, x.after]) if (snap) { checkMembershipField(snap, 'member_of'); checkMembershipField(snap, 'members'); pm.expect(snap).not.to.have.property('source_fields'); } pm.expect(['added','modified','deprecated','retired']).to.include(x.change_type); if (Number(pm.variables.get('thesaurus_release_id')) === 1) { pm.expect(x.change_type).to.eql('added'); pm.expect(x.before).to.eql(null); } pm.expect(x).to.have.property('before'); pm.expect(x).to.have.property('after'); } });",
                      "  if (d.meta.next_cursor) pm.collectionVariables.set('thesaurus_changes_cursor', d.meta.next_cursor); else pm.collectionVariables.unset('thesaurus_changes_cursor');",
                      "}"
                    ]
                  }
                }
              ],
              "description": "Poursuit le même snapshot avec le curseur de pagination ; en pré-lancement, recommencer la synchronisation si une retouche invalide le curseur."
            },
            {
              "name": "Exporter la publication en JSON",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/export?lang={{public_lang}}&format=json",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "export"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "{{public_lang}}"
                    },
                    {
                      "key": "format",
                      "value": "json"
                    }
                  ]
                },
                "description": "Export complet non paginé sous {meta,data}. domains/status facultatifs ; toutes les traductions publiées restent présentes.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Publication épinglée', function () { pm.expect(d.meta.release_id).to.eql(Number(pm.variables.get('thesaurus_release_id'))); pm.expect(d.meta.vocabulary_release).to.eql(d.meta.release_id); }); pm.test('Export JSON', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Exporter la publication en JSON-LD SKOS",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/ld+json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/thesaurus/releases/{{thesaurus_release_id}}/export?format=jsonld",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "thesaurus",
                    "releases",
                    "{{thesaurus_release_id}}",
                    "export"
                  ],
                  "query": [
                    {
                      "key": "format",
                      "value": "jsonld"
                    }
                  ]
                },
                "description": "Document RDF autonome application/ld+json, @context/@graph. Nécessite format=jsonld, pas le seul Accept. Version dans X-Bertel-Vocabulary-Release ; pas d’enveloppe meta/data.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "prerequest",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "pm.variables.set('expected_content_type', 'application/ld+json');"
                    ]
                  }
                },
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const doc = pm.response.json(); pm.test('Références de membres Bertel', function () { pm.expect(doc['@graph']).to.be.an('array'); for (const node of doc['@graph']) for (const key of ['bertel:memberOf', 'bertel:member']) if (Object.prototype.hasOwnProperty.call(node, key)) { pm.expect(node[key]).to.be.an('array'); for (const ref of node[key]) pm.expect(ref['@id']).to.be.a('string').and.not.empty; } }); pm.test('Version de l’export RDF', function () { pm.expect(pm.response.headers.get('X-Bertel-Vocabulary-Release')).to.eql(String(pm.variables.get('thesaurus_release_id'))); }); }"
                    ]
                  }
                }
              ]
            }
          ]
        },
        {
          "name": "Parcours complémentaires",
          "item": [
            {
              "name": "Recherche de fiches HLO",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?types=HLO&search={{public_search}}&lang={{public_lang}}&page_size=20",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "types",
                      "value": "HLO"
                    },
                    {
                      "key": "search",
                      "value": "{{public_search}}"
                    },
                    {
                      "key": "lang",
                      "value": "{{public_lang}}"
                    },
                    {
                      "key": "page_size",
                      "value": "20"
                    }
                  ]
                },
                "description": "",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Liste publique', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Fiche en langue demandée",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects/{{public_object_id}}?lang={{public_lang}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects",
                    "{{public_object_id}}"
                  ],
                  "query": [
                    {
                      "key": "lang",
                      "value": "{{public_lang}}"
                    }
                  ]
                },
                "description": "",
                "auth": {
                  "type": "noauth"
                }
              }
            },
            {
              "name": "Fiches — page suivante",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?page_size=20&cursor={{public_next_cursor}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "page_size",
                      "value": "20"
                    },
                    {
                      "key": "cursor",
                      "value": "{{public_next_cursor}}"
                    }
                  ]
                },
                "description": "Mêmes options que la première liste ; ignorée en fin de parcours.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Liste publique', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Sélection — page suivante",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?selection_id={{public_selection_id}}&view=full&page_size=100&cursor={{selection_next_cursor}}",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "selection_id",
                      "value": "{{public_selection_id}}"
                    },
                    {
                      "key": "view",
                      "value": "full"
                    },
                    {
                      "key": "page_size",
                      "value": "100"
                    },
                    {
                      "key": "cursor",
                      "value": "{{selection_next_cursor}}"
                    }
                  ]
                },
                "description": "Mêmes sélection, vue, taille et options que le premier exemple ; ignorée sans curseur.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Liste publique', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Fiches par taxonomie",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?taxonomy={{public_taxonomy_code}}&include_equivalent_predecessors=false&page_size=20",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "taxonomy",
                      "value": "{{public_taxonomy_code}}"
                    },
                    {
                      "key": "include_equivalent_predecessors",
                      "value": "false"
                    },
                    {
                      "key": "page_size",
                      "value": "20"
                    }
                  ]
                },
                "description": "Code découvert dans le thésaurus ; true ajoute seulement les prédécesseurs explicitement équivalents, sans élargissement implicite par replaced_by ou scission.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Liste publique', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Appartenances aux sélections — cartes",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?view=card&include_selection_ids=true&page_size=20",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "view",
                      "value": "card"
                    },
                    {
                      "key": "include_selection_ids",
                      "value": "true"
                    },
                    {
                      "key": "page_size",
                      "value": "20"
                    }
                  ]
                },
                "description": "",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Appartenances sur chaque carte', function () { for (const x of d.data) { pm.expect(x.selection_ids).to.be.an('array'); pm.expect(x.selection_ids).to.eql([...new Set(x.selection_ids)].sort()); } }); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Itinéraire et tracé KML",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects?types=ITI&view=full&page_size=1&track=kml",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects"
                  ],
                  "query": [
                    {
                      "key": "types",
                      "value": "ITI"
                    },
                    {
                      "key": "view",
                      "value": "full"
                    },
                    {
                      "key": "page_size",
                      "value": "1"
                    },
                    {
                      "key": "track",
                      "value": "kml"
                    }
                  ]
                },
                "description": "GeoJSON natif ; KML facultatif. Aucun tracé n’est inventé si la page est vide.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Itinéraires', function () { pm.expect(d.data).to.be.an('array'); d.data.forEach(x => pm.expect(x.type).to.eql('ITI')); }); if (d.data.length) pm.collectionVariables.set('public_itinerary_id', d.data[0].id); else pm.collectionVariables.unset('public_itinerary_id'); }"
                    ]
                  }
                }
              ]
            },
            {
              "name": "Suppressions — reprise",
              "request": {
                "method": "GET",
                "header": [
                  {
                    "key": "Accept",
                    "value": "application/json"
                  },
                  {
                    "key": "Authorization",
                    "value": "Bearer {{partner_key}}",
                    "description": "Clé partenaire personnelle ; garder sa valeur locale et non partagée."
                  }
                ],
                "url": {
                  "raw": "{{public_base_url}}/api/public/objects/deletions?since={{public_deletions_cursor}}&limit=20",
                  "host": [
                    "{{public_base_url}}"
                  ],
                  "path": [
                    "api",
                    "public",
                    "objects",
                    "deletions"
                  ],
                  "query": [
                    {
                      "key": "since",
                      "value": "{{public_deletions_cursor}}"
                    },
                    {
                      "key": "limit",
                      "value": "20"
                    }
                  ]
                },
                "description": "Le curseur des suppressions se repasse comme since, distinct du curseur opaque des listes.",
                "auth": {
                  "type": "noauth"
                }
              },
              "event": [
                {
                  "listen": "test",
                  "script": {
                    "type": "text/javascript",
                    "exec": [
                      "if (pm.response.code === 200) { const d = pm.response.json(); pm.test('Liste publique', function () { pm.expect(d.data).to.be.an('array'); }); }"
                    ]
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "(function () {",
          "  pm.variables.set('expected_status', '200');",
          "  pm.variables.set('expected_content_type', 'application/json');",
          "  if (pm.variables.get('base_url') === undefined) pm.variables.set('base_url', 'https://supabertel.otisud.re/rest/v1');",
          "  if (pm.variables.get('public_base_url') === undefined) pm.variables.set('public_base_url', 'https://bertel.otisud.re');",
          "  const dynamicKeys = [\"object_id\", \"itinerary_id\", \"public_object_id\", \"public_itinerary_id\", \"public_next_cursor\", \"tourinsoft_regional_object_id\", \"selection_next_cursor\", \"membership_next_cursor\", \"public_deletions_cursor\", \"public_taxonomy_code\", \"thesaurus_release_id\", \"thesaurus_previous_release_id\", \"thesaurus_concept_id\", \"thesaurus_historical_concept_id\", \"thesaurus_next_cursor\", \"thesaurus_changes_cursor\", \"thesaurus_etag\", \"thesaurus_compact_etag\"];",
          "  for (const key of dynamicKeys) pm.variables.set(key, pm.collectionVariables.get(key) || '');",
          "  const url = pm.request.url.toString();",
          "  const skip = (reason) => { console.info('Requête ignorée : ' + reason); pm.execution.skipRequest(); };",
          "  const value = (key) => String(pm.variables.get(key) || '').trim();",
          "  if (url.includes('/rpc/')) {",
          "    if (url.includes('/rpc/list_objects_with_validated_changes_since')) {",
          "      skip('exemple réservé à une identité serveur ; aucune clé privilégiée dans cette collection partagée'); return;",
          "    }",
          "    if (/\\/rpc\\/(add_legal_record|request_legal_document)(?:$|\\?)/.test(url) && value('allow_write_requests') !== 'true') {",
          "      skip('écritures désactivées ; allow_write_requests doit être explicitement true'); return;",
          "    }",
          "    const token = value('authorization').replace(/^Bearer\\s+/i, '').trim();",
          "    if (!value('apikey') || /your-|example/i.test(value('apikey')) || !/^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/.test(token)) {",
          "      skip('apikey public et JWT utilisateur requis pour les exemples techniques'); return;",
          "    }",
          "    pm.variables.set('rpc_authorization', 'Bearer ' + token);",
          "  } else if (url.includes('/api/public/') && String(pm.request.headers.get('Authorization') || '').includes('{{partner_key}}')) {",
          "    if (!/^bk_live_[0-9a-f]{48}$/.test(value('partner_key'))) { skip('clé partenaire locale manquante ou mal formée'); return; }",
          "  }",
          "  const raw = url + '\\n' + (pm.request.body && pm.request.body.raw || '') + '\\n' + pm.request.headers.all().filter(h => !h.disabled).map(h => h.value).join('\\n');",
          "  const missing = [...new Set([...raw.matchAll(/\\{\\{([^{}]+)\\}\\}/g)].map(m => m[1]))]",
          "    .filter(key => !key.startsWith('$') && !value(key));",
          "  if (missing.length) skip('renseigner ou produire d’abord : ' + missing.join(', '));",
          "})();"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "(function () {",
          "  const expected = Number(pm.variables.get('expected_status') || 200);",
          "  const contentType = String(pm.variables.get('expected_content_type') || 'application/json');",
          "  pm.test('Statut HTTP attendu : ' + expected, function () { pm.response.to.have.status(expected); });",
          "  if (pm.response.code === 304) {",
          "    pm.test('304 sans corps', function () { pm.expect(pm.response.text()).to.eql(''); });",
          "    return;",
          "  }",
          "  pm.test('Type de contenu attendu', function () { pm.expect(pm.response.headers.get('Content-Type') || '').to.include(contentType); });",
          "  let data;",
          "  pm.test('Corps JSON valide', function () { data = pm.response.json(); });",
          "  if (data === undefined) return;",
          "  if (pm.response.code >= 400) {",
          "    pm.test('Erreur structurée', function () { pm.expect(data).to.be.an('object'); pm.expect(data.error).to.be.a('string').and.not.empty; });",
          "    return;",
          "  }",
          "  const url = pm.request.url.toString();",
          "  if (contentType === 'application/ld+json') {",
          "    pm.test('Document JSON-LD autonome', function () { pm.expect(data).to.have.property('@context'); pm.expect(data['@graph']).to.be.an('array'); pm.expect(data).not.to.have.property('meta'); });",
          "  } else if (url.includes('/api/public/')) {",
          "    pm.test('Enveloppe publique versionnée', function () { pm.expect(data).to.have.property('meta'); pm.expect(data).to.have.property('data'); pm.expect(data.meta.contract_version).to.eql('1.0.0'); });",
          "  } else if (url.includes('/rpc/get_object_resource')) {",
          "    pm.test('Identité canonique de la ressource', function () { pm.expect(data).to.be.an('object'); pm.expect(data.id).to.be.a('string').and.not.empty; pm.expect(data).to.have.property('type'); pm.expect(data).to.have.property('status'); });",
          "  }",
          "  if (url.includes('/api/public/')) {",
          "    pm.test('Version du contrat dans les headers', function () { pm.expect(pm.response.headers.get('X-Bertel-Api-Version')).to.eql('1.0.0'); });",
          "  }",
          "  if (url.includes('/api/public/objects') && !url.includes('/objects/deletions') && data && data.data != null) {",
          "    pm.test('Identités publiques et absence des champs éditeur', function () {",
          "      const rows = Array.isArray(data.data) ? data.data : [data.data];",
          "      rows.forEach(row => { pm.expect(row.id).to.match(/^[A-Z]{3}[A-Z0-9]{3}[0-9A-Z]{10}$/); pm.expect(row).not.to.have.property('canonical_description'); pm.expect(row).not.to.have.property('org_description'); });",
          "    });",
          "  }",
          "  if (url.includes('/rpc/list_object_resources')) {",
          "    pm.test('Enveloppe RPC canonique meta/data', function () { pm.expect(data.meta).to.be.an('object'); pm.expect(data.data).to.be.an('array'); });",
          "  }",
          "  if (url.includes('/rpc/list_object_resources') && data && Array.isArray(data.data)) {",
          "    const first = data.data.find(row => row && typeof row.id === 'string');",
          "    if (first) pm.collectionVariables.set('object_id', first.id);",
          "    const itinerary = data.data.find(row => row && row.type === 'ITI' && typeof row.id === 'string');",
          "    if (itinerary) pm.collectionVariables.set('itinerary_id', itinerary.id);",
          "  }",
          "})();"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "allow_write_requests",
      "value": "false",
      "description": "true autorise les deux exemples d’écriture sur une cible explicitement autorisée ; false les ignore avant envoi."
    },
    {
      "key": "current_timestamp",
      "value": "2024-04-01T00:00:00Z",
      "description": "Point de reprise ISO stable des exemples since. Aucun script ne le remplace par l’heure courante."
    },
    {
      "key": "public_lang",
      "value": "en",
      "description": "Langue de résolution des exemples publics traduits."
    },
    {
      "key": "public_selection_id",
      "value": "1006",
      "description": "Identifiant permanent de la sélection. Les quinze IDs initiaux ne sont pas une liste fermée."
    },
    {
      "key": "public_search",
      "value": "maison",
      "description": "Recherche d’exemple ; une liste vide est valide."
    },
    {
      "key": "thesaurus_query",
      "value": "appartement",
      "description": "Recherche textuelle dans les concepts, distincte de search sur les fiches."
    },
    {
      "key": "public_unknown_object_id",
      "value": "HOTRUNZZZZZZZZZZ",
      "description": "ID bien formé supposé absent pour tester 404 ; adapter si cet ID existe sur votre cible."
    },
    {
      "key": "public_unknown_selection_id",
      "value": "9223372036854775807",
      "description": "ID supposé absent pour tester 404 ; adapter si cet ID existe sur votre cible."
    },
    {
      "key": "thesaurus_unknown_release_id",
      "value": "9007199254740991",
      "description": "Publication bien formée supposée absente pour tester 404."
    },
    {
      "key": "thesaurus_unknown_concept_id",
      "value": "00000000-0000-0000-0000-000000000000",
      "description": "UUID supposé absent pour tester 404."
    },
    {
      "key": "legacy_label_value_id",
      "value": "",
      "description": "UUID réel de valeur de classement ; absence = requête ignorée."
    },
    {
      "key": "legacy_legal_id",
      "value": "",
      "description": "UUID réel du dossier légal ; requis avec opt-in pour request_legal_document."
    },
    {
      "key": "legacy_promotion_code",
      "value": "",
      "description": "Code promotionnel réel ; absence = requête ignorée."
    },
    {
      "key": "legacy_legal_type_code",
      "value": "",
      "description": "Code légal réel à créer avec opt-in."
    },
    {
      "key": "legacy_legal_value",
      "value": "",
      "description": "Objet ou autre valeur JSON valide complète à créer (ex. objet structuré), substitué sans guillemets dans p_value. Vide = écriture ignorée."
    },
    {
      "key": "legacy_legal_valid_from",
      "value": "",
      "description": "Date YYYY-MM-DD de début, à renseigner avant l’écriture."
    },
    {
      "key": "legacy_legal_valid_to",
      "value": "",
      "description": "Date YYYY-MM-DD de fin, à renseigner avant l’écriture."
    },
    {
      "key": "legacy_requested_at",
      "value": "",
      "description": "Horodatage ISO 8601 volontaire, à renseigner avant la demande de document."
    },
    {
      "key": "object_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "itinerary_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "public_object_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "public_itinerary_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "public_next_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "tourinsoft_regional_object_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "selection_next_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "membership_next_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "public_deletions_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "public_taxonomy_code",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_release_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_previous_release_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_concept_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_historical_concept_id",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_next_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_changes_cursor",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_etag",
      "value": "",
      "description": "Résultat de lecture en collection ; ne pas créer une variable d’environnement du même nom."
    },
    {
      "key": "thesaurus_compact_etag",
      "value": ""
    }
  ],
  "auth": {
    "type": "noauth"
  }
}
